Skip to content

Commit a5a734f

Browse files
committed
Add pnpm pre-ci:affected fast mode
1 parent fb13f3d commit a5a734f

4 files changed

Lines changed: 74 additions & 23 deletions

File tree

bin/ci-gates.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
//
66
// `job` is the workflow job id (the key under `jobs:`), which is stable, unlike
77
// the rendered display name (matrix jobs interpolate `${{ ... }}`).
8+
//
9+
// pre-ci gates may declare:
10+
// command — full-parity command (run by `pnpm pre-ci`)
11+
// affected — faster command for `pnpm pre-ci:affected` (defaults to `command`)
12+
// affectedWhen: 'codegen' — in affected mode, run only when the diff plausibly
13+
// changes generated output; otherwise skip with a reminder.
814

915
export const CI_GATES = [
1016
// --- gates a contributor can reproduce locally before pushing ---
1117
// Ordered as pre-ci should run them: build precedes the oclif codegen check,
1218
// and the graphql check precedes the oclif check (their whole-repo `git status`
1319
// asserts otherwise cross-contaminate in a single working tree).
14-
{job: 'type-check', kind: 'pre-ci', command: 'pnpm type-check'},
15-
{job: 'lint', kind: 'pre-ci', command: 'pnpm lint'},
16-
{job: 'bundle', kind: 'pre-ci', command: 'pnpm build'},
20+
{job: 'type-check', kind: 'pre-ci', command: 'pnpm type-check', affected: 'pnpm type-check:affected'},
21+
{job: 'lint', kind: 'pre-ci', command: 'pnpm lint', affected: 'pnpm lint:affected'},
22+
{job: 'bundle', kind: 'pre-ci', command: 'pnpm build', affected: 'pnpm build:affected'},
1723
{job: 'knip', kind: 'pre-ci', command: 'pnpm knip'},
18-
{job: 'graphql-schema', kind: 'pre-ci', command: 'pnpm codegen:check:graphql'},
19-
{job: 'oclif-checks', kind: 'pre-ci', command: 'pnpm codegen:check:oclif'},
20-
{job: 'unit-tests', kind: 'pre-ci', command: 'pnpm test'},
24+
{job: 'graphql-schema', kind: 'pre-ci', command: 'pnpm codegen:check:graphql', affectedWhen: 'codegen'},
25+
{job: 'oclif-checks', kind: 'pre-ci', command: 'pnpm codegen:check:oclif', affectedWhen: 'codegen'},
26+
{job: 'unit-tests', kind: 'pre-ci', command: 'pnpm test', affected: 'pnpm vitest run --changed origin/main'},
2127

2228
// --- CI-only jobs, with the reason they are not part of pre-ci ---
2329
{

bin/pre-ci.js

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,58 @@
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.
710
import {execSync} from 'node:child_process'
811

912
import {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') || /graphql/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

1653
const results = []
1754
for (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

3275
console.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

3778
const failed = results.filter((result) => !result.ok)
3879
if (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.')

dev.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ commands:
6969
pre-ci:
7070
desc: 'Run the local subset of PR CI gates (run what CI runs) before pushing'
7171
run: pnpm run pre-ci
72+
pre-ci:affected:
73+
desc: 'Faster pre-ci limited to what your diff touches'
74+
run: pnpm run pre-ci:affected
7275

7376
check:
7477
type-check: pnpm nx affected --target=type-check

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"release": "./bin/release",
3232
"post-release": "./bin/post-release",
3333
"pre-ci": "node bin/pre-ci.js",
34+
"pre-ci:affected": "node bin/pre-ci.js --affected",
3435
"update-observe": "node bin/update-observe.js",
3536
"shopify:run": "node packages/cli/bin/dev.js",
3637
"shopify": "nx build cli && node packages/cli/bin/dev.js",

0 commit comments

Comments
 (0)