1- // Runs the local subset of PR CI gates ("run what CI runs") so contributors can
2- // catch failures before pushing. The gate list and its parity with the workflow
3- // are defined in bin/ci-gates.js and enforced by bin/check-ci-gates.js.
1+ // Runs the local PR CI gates ("run what CI runs") so contributors can catch
2+ // failures before pushing. The gate list and its parity with the workflow are
3+ // defined in bin/ci-gates.js and enforced by bin/check-ci-gates.js.
44//
5- // pre-ci mirrors CI's full (`--all`) targets so that green locally implies green
6- // in CI. It is intentionally slower than the affected-only `dev check`.
5+ // pnpm pre-ci full parity with CI's `--all` targets (slower)
6+ // pnpm pre-ci:affected only what your diff touches (faster inner loop)
7+ //
8+ // Affected mode runs the nx/vitest affected variants and skips the codegen
9+ // freshness checks unless the diff plausibly changes generated output.
710import { execSync } from 'node:child_process'
811
912import { PRE_CI_GATES , CI_ONLY_GATES } from './ci-gates.js'
1013
11- const steps = [
12- { label : 'CI gate manifest in sync' , command : 'pnpm check-ci-gates' } ,
13- ...PRE_CI_GATES . map ( ( gate ) => ( { label : gate . job , command : gate . command } ) ) ,
14- ]
14+ const affected = process . argv . includes ( '--affected' )
15+
16+ // Changed files vs the merge-base with origin/main, plus the working tree.
17+ // Returns null if detection fails, so callers can fail safe (assume relevant).
18+ function changedFiles ( ) {
19+ try {
20+ const base = execSync ( 'git merge-base HEAD origin/main' , { encoding : 'utf8' } ) . trim ( )
21+ const committed = execSync ( `git diff --name-only ${ base } ...HEAD` , { encoding : 'utf8' } )
22+ const working = execSync ( 'git status --porcelain' , { encoding : 'utf8' } )
23+ const files = new Set ( )
24+ for ( const line of committed . split ( '\n' ) ) if ( line . trim ( ) ) files . add ( line . trim ( ) )
25+ for ( const line of working . split ( '\n' ) ) if ( line . slice ( 3 ) . trim ( ) ) files . add ( line . slice ( 3 ) . trim ( ) )
26+ return [ ...files ]
27+ } catch {
28+ return null
29+ }
30+ }
31+
32+ function touchesGeneratedOutput ( files ) {
33+ if ( files === null ) return true
34+ return files . some (
35+ ( file ) => file . includes ( '/commands/' ) || file . endsWith ( '.graphql' ) || / g r a p h q l / i. test ( file ) || file . startsWith ( 'docs-shopify.dev/' ) ,
36+ )
37+ }
38+
39+ const diff = affected ? changedFiles ( ) : null
40+ const codegenRelevant = affected ? touchesGeneratedOutput ( diff ) : true
41+
42+ const steps = [ { label : 'CI gate manifest in sync' , command : 'pnpm check-ci-gates' } ]
43+ const skipped = [ ]
44+ for ( const gate of PRE_CI_GATES ) {
45+ if ( affected && gate . affectedWhen === 'codegen' && ! codegenRelevant ) {
46+ skipped . push ( { job : gate . job , reason : 'affected mode: diff does not touch commands, flags, or GraphQL' } )
47+ continue
48+ }
49+ const command = affected ? gate . affected ?? gate . command : gate . command
50+ steps . push ( { label : affected ? `${ gate . job } (affected)` : gate . job , command} )
51+ }
1552
1653const results = [ ]
1754for ( const step of steps ) {
18- process . stdout . write ( `\n▶ ${ step . label } : ${ step . command } \n` )
55+ process . stdout . write ( `\n\u25b6 ${ step . label } : ${ step . command } \n` )
1956 try {
2057 execSync ( step . command , { stdio : 'inherit' } )
2158 results . push ( { ...step , ok : true } )
@@ -24,19 +61,23 @@ for (const step of steps) {
2461 }
2562}
2663
27- console . log ( '\n──────── pre-ci summary ────────' )
28- for ( const result of results ) {
29- console . log ( `${ result . ok ? '✓' : '✗' } ${ result . label } ` )
64+ console . log ( `\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 pre-ci${ affected ? ' (affected)' : '' } summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500` )
65+ for ( const result of results ) console . log ( `${ result . ok ? '\u2713' : '\u2717' } ${ result . label } ` )
66+ for ( const gate of skipped ) console . log ( `\u00b7 ${ gate . job } \u2014 skipped (${ gate . reason } )` )
67+
68+ if ( affected ) {
69+ console . log ( '\nAffected mode is a fast pre-push check, not full CI parity. Run `pnpm pre-ci` before a high-risk push.' )
70+ if ( skipped . length > 0 ) {
71+ console . log ( 'If you changed commands, flags, or GraphQL queries, run `pnpm codegen` and commit the result.' )
72+ }
3073}
3174
3275console . log ( '\nNot run locally (CI-only):' )
33- for ( const gate of CI_ONLY_GATES ) {
34- console . log ( `· ${ gate . job } — ${ gate . reason } ` )
35- }
76+ for ( const gate of CI_ONLY_GATES ) console . log ( `\u00b7 ${ gate . job } \u2014 ${ gate . reason } ` )
3677
3778const failed = results . filter ( ( result ) => ! result . ok )
3879if ( failed . length > 0 ) {
3980 console . error ( `\npre-ci failed: ${ failed . map ( ( result ) => result . label ) . join ( ', ' ) } ` )
4081 process . exit ( 1 )
4182}
42- console . log ( '\npre-ci passed. Note: codegen checks regenerate files — review `git status` for any uncommitted generated changes.' )
83+ console . log ( '\npre-ci passed. Codegen checks regenerate files — review `git status` for uncommitted generated changes.' )
0 commit comments