@opengsd/gsd-core 1.3.1 → 1.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/gsd-advisor-researcher.md +1 -20
- package/agents/gsd-ai-researcher.md +1 -20
- package/agents/gsd-domain-researcher.md +1 -20
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-phase-researcher.md +92 -166
- package/agents/gsd-planner.md +9 -36
- package/agents/gsd-project-researcher.md +62 -141
- package/agents/gsd-ui-researcher.md +2 -21
- package/agents/gsd-verifier.md +8 -2
- package/bin/install.js +85 -4
- package/commands/gsd/graphify.md +11 -6
- package/commands/gsd/import.md +6 -2
- package/commands/gsd/plan-phase.md +2 -2
- package/gsd-core/bin/check-latest-version.cjs +3 -2
- package/gsd-core/bin/gsd-tools.cjs +238 -32
- package/gsd-core/bin/lib/check-command-router.cjs +1 -0
- package/gsd-core/bin/lib/cli-exit.cjs +42 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
- package/gsd-core/bin/lib/commands.cjs +5 -4
- package/gsd-core/bin/lib/config.cjs +28 -4
- package/gsd-core/bin/lib/core.cjs +72 -28
- package/gsd-core/bin/lib/graphify.cjs +2 -2
- package/gsd-core/bin/lib/init-command-router.cjs +2 -2
- package/gsd-core/bin/lib/init.cjs +19 -3
- package/gsd-core/bin/lib/intel.cjs +3 -20
- package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
- package/gsd-core/bin/lib/phase.cjs +3 -3
- package/gsd-core/bin/lib/research-provider.cjs +137 -0
- package/gsd-core/bin/lib/research-store.cjs +167 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
- package/gsd-core/bin/lib/security.cjs +73 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +3 -0
- package/gsd-core/bin/lib/validate.cjs +2 -2
- package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
- package/gsd-core/bin/lib/verification.cjs +193 -0
- package/gsd-core/bin/lib/verify.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
- package/gsd-core/references/planner-load-graph-context.md +36 -0
- package/gsd-core/references/planning-config.md +3 -1
- package/gsd-core/references/research-documentation-lookup.md +29 -0
- package/gsd-core/references/research-philosophy.md +29 -0
- package/gsd-core/references/research-verification-protocol.md +27 -0
- package/gsd-core/workflows/execute-phase.md +19 -8
- package/gsd-core/workflows/help/modes/full.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -2
- package/gsd-core/workflows/plan-phase.md +14 -10
- package/gsd-core/workflows/plan-review-convergence.md +3 -3
- package/gsd-core/workflows/review.md +22 -5
- package/gsd-core/workflows/ship.md +5 -8
- package/gsd-core/workflows/spec-phase.md +2 -1
- package/gsd-core/workflows/update.md +2 -1
- package/hooks/dist/gsd-context-monitor.js +1 -1
- package/hooks/dist/gsd-workflow-guard.js +1 -0
- package/hooks/dist/gsd-worktree-path-guard.js +1 -1
- package/hooks/gsd-context-monitor.js +1 -1
- package/hooks/gsd-workflow-guard.js +1 -0
- package/hooks/gsd-worktree-path-guard.js +1 -1
- package/package.json +4 -1
- package/scripts/affected-tests-lib.cjs +3 -2
- package/scripts/changeset/cli.cjs +183 -28
- package/scripts/changeset/lint.cjs +5 -4
- package/scripts/changeset/new.cjs +4 -4
- package/scripts/check-alias-drift.cjs +77 -71
- package/scripts/check-env.cjs +185 -179
- package/scripts/check-npm-integrity.cjs +115 -109
- package/scripts/ci-guard-runner.cjs +11 -5
- package/scripts/ci-prepare-test-scope.cjs +27 -22
- package/scripts/ci-rebase-check.cjs +46 -45
- package/scripts/ci-test-scope.cjs +6 -4
- package/scripts/diff-touches-shipped-paths.cjs +52 -44
- package/scripts/gen-inventory-manifest.cjs +38 -32
- package/scripts/gen-research-agents.cjs +276 -0
- package/scripts/lib/cli-exit.cjs +56 -0
- package/scripts/lint-command-contract.cjs +28 -22
- package/scripts/lint-descriptions.cjs +32 -28
- package/scripts/lint-docs-required.cjs +4 -4
- package/scripts/lint-legacy-dir-name.cjs +56 -52
- package/scripts/lint-pr-check-project-dir.cjs +3 -1
- package/scripts/lint-shell-command-projection-drift.cjs +27 -22
- package/scripts/lint-skill-deps.cjs +31 -26
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-test-file-count.cjs +5 -4
- package/scripts/mutation-matrix.cjs +6 -3
- package/scripts/prompt-injection-scan.sh +1 -1
- package/scripts/release-notes/format-github-release-notes.cjs +8 -3
- package/scripts/release-tarball-smoke.cjs +6 -4
- package/scripts/research-profiles.cjs +149 -0
- package/scripts/run-affected-tests.cjs +2 -1
- package/scripts/run-cross-platform-tests.cjs +11 -7
- package/scripts/run-tests.cjs +8 -7
- package/scripts/strip-prose-atrefs.cjs +1 -1
- package/scripts/sync-runtime-launcher.cjs +0 -3
- package/scripts/verify-npm-publish.cjs +14 -26
|
@@ -16,62 +16,7 @@
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let ignoreExtraneous = false;
|
|
22
|
-
|
|
23
|
-
for (const arg of process.argv.slice(2)) {
|
|
24
|
-
if (arg === '--ignore-extraneous') {
|
|
25
|
-
ignoreExtraneous = true;
|
|
26
|
-
} else if (arg === '--help' || arg === '-h') {
|
|
27
|
-
process.stdout.write(
|
|
28
|
-
'Usage: node scripts/check-npm-integrity.cjs [--ignore-extraneous]\n'
|
|
29
|
-
);
|
|
30
|
-
process.exit(0);
|
|
31
|
-
} else {
|
|
32
|
-
process.stderr.write(`ERROR: Unknown argument: ${arg}\n`);
|
|
33
|
-
process.exit(2);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ---- Locate lockfile --------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
const lockfilePath = path.join(process.cwd(), 'package-lock.json');
|
|
40
|
-
|
|
41
|
-
if (!fs.existsSync(lockfilePath)) {
|
|
42
|
-
process.stderr.write(`ERROR: package-lock.json not found in ${process.cwd()}\n`);
|
|
43
|
-
process.exit(2);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---- Parse lockfile ---------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
let lock;
|
|
49
|
-
try {
|
|
50
|
-
lock = JSON.parse(fs.readFileSync(lockfilePath, 'utf-8'));
|
|
51
|
-
} catch (e) {
|
|
52
|
-
process.stderr.write(`ERROR: Failed to parse package-lock.json: ${e.message}\n`);
|
|
53
|
-
process.exit(2);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const lockVersion = lock.lockfileVersion || 1;
|
|
57
|
-
if (lockVersion < 2) {
|
|
58
|
-
process.stderr.write(
|
|
59
|
-
`ERROR: package-lock.json lockfileVersion ${lockVersion} is not supported. ` +
|
|
60
|
-
'Run `npm install` to upgrade to v3.\n'
|
|
61
|
-
);
|
|
62
|
-
process.exit(2);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const packages = lock.packages || {};
|
|
66
|
-
const rootEntry = packages[''] || {};
|
|
67
|
-
|
|
68
|
-
// Collect declared dependency ranges from root entry.
|
|
69
|
-
const declaredRanges = {};
|
|
70
|
-
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
71
|
-
for (const [name, range] of Object.entries(rootEntry[field] || {})) {
|
|
72
|
-
if (!declaredRanges[name]) declaredRanges[name] = range;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
19
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
75
20
|
|
|
76
21
|
// ---- Minimal semver satisfies -----------------------------------------------
|
|
77
22
|
|
|
@@ -132,78 +77,139 @@ function satisfies(installed, range) {
|
|
|
132
77
|
return installed === range;
|
|
133
78
|
}
|
|
134
79
|
|
|
135
|
-
|
|
80
|
+
function main() {
|
|
81
|
+
// ---- Argument parsing -------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
let ignoreExtraneous = false;
|
|
84
|
+
|
|
85
|
+
for (const arg of process.argv.slice(2)) {
|
|
86
|
+
if (arg === '--ignore-extraneous') {
|
|
87
|
+
ignoreExtraneous = true;
|
|
88
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
89
|
+
process.stdout.write(
|
|
90
|
+
'Usage: node scripts/check-npm-integrity.cjs [--ignore-extraneous]\n'
|
|
91
|
+
);
|
|
92
|
+
return 0;
|
|
93
|
+
} else {
|
|
94
|
+
process.stderr.write(`ERROR: Unknown argument: ${arg}\n`);
|
|
95
|
+
throw new ExitError(2);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
136
98
|
|
|
137
|
-
|
|
138
|
-
const missings = [];
|
|
139
|
-
const extraneousFound = [];
|
|
99
|
+
// ---- Locate lockfile --------------------------------------------------------
|
|
140
100
|
|
|
141
|
-
|
|
142
|
-
if (!key.startsWith('node_modules/')) continue;
|
|
143
|
-
const rest = key.slice('node_modules/'.length);
|
|
144
|
-
const isScoped = rest[0] === '@';
|
|
145
|
-
const slashCount = (rest.match(/\//g) || []).length;
|
|
146
|
-
if (isScoped && slashCount > 1) continue;
|
|
147
|
-
if (!isScoped && slashCount > 0) continue;
|
|
101
|
+
const lockfilePath = path.join(process.cwd(), 'package-lock.json');
|
|
148
102
|
|
|
149
|
-
|
|
150
|
-
|
|
103
|
+
if (!fs.existsSync(lockfilePath)) {
|
|
104
|
+
process.stderr.write(`ERROR: package-lock.json not found in ${process.cwd()}\n`);
|
|
105
|
+
throw new ExitError(2);
|
|
106
|
+
}
|
|
151
107
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
108
|
+
// ---- Parse lockfile ---------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
let lock;
|
|
111
|
+
try {
|
|
112
|
+
lock = JSON.parse(fs.readFileSync(lockfilePath, 'utf-8'));
|
|
113
|
+
} catch (e) {
|
|
114
|
+
process.stderr.write(`ERROR: Failed to parse package-lock.json: ${e.message}\n`);
|
|
115
|
+
throw new ExitError(2);
|
|
155
116
|
}
|
|
156
117
|
|
|
157
|
-
|
|
118
|
+
const lockVersion = lock.lockfileVersion || 1;
|
|
119
|
+
if (lockVersion < 2) {
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
`ERROR: package-lock.json lockfileVersion ${lockVersion} is not supported. ` +
|
|
122
|
+
'Run `npm install` to upgrade to v3.\n'
|
|
123
|
+
);
|
|
124
|
+
throw new ExitError(2);
|
|
125
|
+
}
|
|
158
126
|
|
|
159
|
-
|
|
160
|
-
|
|
127
|
+
const packages = lock.packages || {};
|
|
128
|
+
const rootEntry = packages[''] || {};
|
|
129
|
+
|
|
130
|
+
// Collect declared dependency ranges from root entry.
|
|
131
|
+
const declaredRanges = {};
|
|
132
|
+
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
133
|
+
for (const [name, range] of Object.entries(rootEntry[field] || {})) {
|
|
134
|
+
if (!declaredRanges[name]) declaredRanges[name] = range;
|
|
135
|
+
}
|
|
161
136
|
}
|
|
162
|
-
}
|
|
163
137
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
138
|
+
// ---- Walk packages map ------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
const invalids = [];
|
|
141
|
+
const missings = [];
|
|
142
|
+
const extraneousFound = [];
|
|
143
|
+
|
|
144
|
+
for (const [key, entry] of Object.entries(packages)) {
|
|
145
|
+
if (!key.startsWith('node_modules/')) continue;
|
|
146
|
+
const rest = key.slice('node_modules/'.length);
|
|
147
|
+
const isScoped = rest[0] === '@';
|
|
148
|
+
const slashCount = (rest.match(/\//g) || []).length;
|
|
149
|
+
if (isScoped && slashCount > 1) continue;
|
|
150
|
+
if (!isScoped && slashCount > 0) continue;
|
|
151
|
+
|
|
152
|
+
const pkgName = rest;
|
|
153
|
+
const installedVersion = entry.version || '';
|
|
154
|
+
|
|
155
|
+
if (entry.extraneous) {
|
|
156
|
+
extraneousFound.push({ name: pkgName, version: installedVersion });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!declaredRanges[pkgName]) continue;
|
|
161
|
+
|
|
162
|
+
if (!satisfies(installedVersion, declaredRanges[pkgName])) {
|
|
163
|
+
invalids.push({ name: pkgName, version: installedVersion, declared: declaredRanges[pkgName] });
|
|
164
|
+
}
|
|
167
165
|
}
|
|
168
|
-
}
|
|
169
166
|
|
|
170
|
-
|
|
167
|
+
for (const name of Object.keys(declaredRanges)) {
|
|
168
|
+
if (!packages[`node_modules/${name}`]) {
|
|
169
|
+
missings.push({ name, required: declaredRanges[name] });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
const failMissing = missings.length > 0;
|
|
174
|
-
const failExtra = !ignoreExtraneous && extraneousFound.length > 0;
|
|
173
|
+
// ---- Verdict ----------------------------------------------------------------
|
|
175
174
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
175
|
+
const failInvalid = invalids.length > 0;
|
|
176
|
+
const failMissing = missings.length > 0;
|
|
177
|
+
const failExtra = !ignoreExtraneous && extraneousFound.length > 0;
|
|
180
178
|
|
|
181
|
-
|
|
179
|
+
if (!failInvalid && !failMissing && !failExtra) {
|
|
180
|
+
process.stderr.write('check-npm-integrity.cjs: clean\n');
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
lines.push(
|
|
184
|
+
const lines = ['FAIL: dependency integrity drift detected', ''];
|
|
185
|
+
|
|
186
|
+
if (failInvalid) {
|
|
187
|
+
lines.push(' INVALID (installed version does not satisfy declared range):');
|
|
188
|
+
for (const { name, declared, version } of invalids) {
|
|
189
|
+
lines.push(` ${name}: declared=${declared} installed=${version}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push('');
|
|
187
192
|
}
|
|
188
|
-
lines.push('');
|
|
189
|
-
}
|
|
190
193
|
|
|
191
|
-
if (failMissing) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
if (failMissing) {
|
|
195
|
+
lines.push(' MISSING (declared but absent from lockfile packages map):');
|
|
196
|
+
for (const { name, required } of missings) {
|
|
197
|
+
lines.push(` ${name}@${required}`);
|
|
198
|
+
}
|
|
199
|
+
lines.push('');
|
|
195
200
|
}
|
|
196
|
-
lines.push('');
|
|
197
|
-
}
|
|
198
201
|
|
|
199
|
-
if (failExtra) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
if (failExtra) {
|
|
203
|
+
lines.push(' EXTRANEOUS (in lockfile but not declared as a dependency):');
|
|
204
|
+
for (const { name, version } of extraneousFound) {
|
|
205
|
+
lines.push(` ${name}@${version}`);
|
|
206
|
+
}
|
|
207
|
+
lines.push('');
|
|
203
208
|
}
|
|
204
|
-
|
|
209
|
+
|
|
210
|
+
lines.push('Remediation: rm -rf node_modules && npm ci');
|
|
211
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
212
|
+
throw new ExitError(1);
|
|
205
213
|
}
|
|
206
214
|
|
|
207
|
-
|
|
208
|
-
process.stderr.write(lines.join('\n') + '\n');
|
|
209
|
-
process.exit(1);
|
|
215
|
+
runMain(main);
|
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
// Exit 0 = github-hosted runner confirmed.
|
|
7
7
|
// Exit 1 = not a github-hosted runner (emits GitHub Actions error annotation).
|
|
8
8
|
|
|
9
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
10
|
+
|
|
9
11
|
const env = process.env.RUNNER_ENVIRONMENT || '';
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
function main() {
|
|
14
|
+
if (env !== 'github-hosted') {
|
|
15
|
+
throw new ExitError(
|
|
16
|
+
1,
|
|
17
|
+
`::error::Expected github-hosted runner. RUNNER_ENVIRONMENT=${env || 'unset'}`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
16
20
|
}
|
|
21
|
+
|
|
22
|
+
runMain(main);
|
|
@@ -14,33 +14,38 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
|
|
17
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
18
|
+
|
|
17
19
|
const scope = process.env.TEST_SCOPE || '';
|
|
18
20
|
const targeted = process.env.TARGETED_TESTS || '';
|
|
19
21
|
const windows = process.env.WINDOWS_TESTS || '';
|
|
20
22
|
|
|
21
23
|
const FALLBACK = 'tests/command-contract.test.cjs tests/commands.test.cjs tests/core.test.cjs tests/package-manifest.test.cjs';
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Trim and fall back to default set if empty.
|
|
34
|
-
if (!selected.trim()) {
|
|
35
|
-
|
|
25
|
+
function main() {
|
|
26
|
+
let selected;
|
|
27
|
+
if (scope === 'windows') {
|
|
28
|
+
selected = windows;
|
|
29
|
+
} else if (scope === 'targeted') {
|
|
30
|
+
selected = targeted;
|
|
31
|
+
} else {
|
|
32
|
+
throw new ExitError(1, `::error::Unknown test scope: ${scope}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Trim and fall back to default set if empty.
|
|
36
|
+
if (!selected.trim()) {
|
|
37
|
+
selected = FALLBACK;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Split on whitespace, filter blanks, join with newlines.
|
|
41
|
+
const lines = selected.split(/\s+/).filter(Boolean);
|
|
42
|
+
const content = lines.join('\n') + '\n';
|
|
43
|
+
|
|
44
|
+
const outPath = path.join(process.cwd(), '.ci-selected-tests.txt');
|
|
45
|
+
fs.writeFileSync(outPath, content, 'utf-8');
|
|
46
|
+
|
|
47
|
+
process.stdout.write('Scoped tests:\n');
|
|
48
|
+
process.stdout.write(content);
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
const lines = selected.split(/\s+/).filter(Boolean);
|
|
40
|
-
const content = lines.join('\n') + '\n';
|
|
41
|
-
|
|
42
|
-
const outPath = path.join(process.cwd(), '.ci-selected-tests.txt');
|
|
43
|
-
fs.writeFileSync(outPath, content, 'utf-8');
|
|
44
|
-
|
|
45
|
-
process.stdout.write('Scoped tests:\n');
|
|
46
|
-
process.stdout.write(content);
|
|
51
|
+
runMain(main);
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
// Exit 0 = merged cleanly (or merge was a no-op).
|
|
12
12
|
// Exit 1 = merge conflict or fetch failure.
|
|
13
13
|
|
|
14
|
-
const { execFileSync
|
|
15
|
-
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
16
17
|
|
|
17
18
|
function run(cmd, args, opts) {
|
|
18
19
|
try {
|
|
@@ -27,8 +28,7 @@ function runOrThrow(cmd, args, label) {
|
|
|
27
28
|
try {
|
|
28
29
|
execFileSync(cmd, args, { stdio: 'inherit' });
|
|
29
30
|
} catch (e) {
|
|
30
|
-
|
|
31
|
-
process.exit(1);
|
|
31
|
+
throw new ExitError(1, `::error::${label} failed`);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -36,50 +36,51 @@ const token = process.env.GITHUB_TOKEN || '';
|
|
|
36
36
|
const baseBranch = process.env.GITHUB_BASE_REF || 'main';
|
|
37
37
|
const repo = process.env.GITHUB_REPOSITORY || '';
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
runOrThrow('git', ['config', 'user.
|
|
42
|
-
|
|
43
|
-
// Set authenticated remote URL.
|
|
44
|
-
if (token && repo) {
|
|
45
|
-
runOrThrow(
|
|
46
|
-
'git',
|
|
47
|
-
['remote', 'set-url', 'origin', `https://x-access-token:${token}@github.com/${repo}.git`],
|
|
48
|
-
'git remote set-url'
|
|
49
|
-
);
|
|
50
|
-
}
|
|
39
|
+
function main() {
|
|
40
|
+
// Configure git identity (needed for merge commit).
|
|
41
|
+
runOrThrow('git', ['config', 'user.email', 'ci@gsd-redux'], 'git config user.email');
|
|
42
|
+
runOrThrow('git', ['config', 'user.name', 'CI Rebase Check'], 'git config user.name');
|
|
51
43
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
// Set authenticated remote URL.
|
|
45
|
+
if (token && repo) {
|
|
46
|
+
runOrThrow(
|
|
47
|
+
'git',
|
|
48
|
+
['remote', 'set-url', 'origin', `https://x-access-token:${token}@github.com/${repo}.git`],
|
|
49
|
+
'git remote set-url'
|
|
50
|
+
);
|
|
59
51
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
|
|
53
|
+
// Fetch base branch with retry.
|
|
54
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
55
|
+
const result = run('git', ['fetch', 'origin', baseBranch]);
|
|
56
|
+
if (result) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (attempt === 3) {
|
|
60
|
+
throw new ExitError(1, `::error::git fetch origin ${baseBranch} failed after 3 attempts.`);
|
|
61
|
+
}
|
|
62
|
+
// Wait before retry: attempt * 4 seconds.
|
|
63
|
+
const waitMs = attempt * 4000;
|
|
64
|
+
const deadline = Date.now() + waitMs;
|
|
65
|
+
while (Date.now() < deadline) { /* busy wait, acceptable in CI */ }
|
|
63
66
|
}
|
|
64
|
-
// Wait before retry: attempt * 4 seconds.
|
|
65
|
-
const waitMs = attempt * 4000;
|
|
66
|
-
const deadline = Date.now() + waitMs;
|
|
67
|
-
while (Date.now() < deadline) { /* busy wait, acceptable in CI */ }
|
|
68
|
-
}
|
|
69
67
|
|
|
70
|
-
// Attempt merge.
|
|
71
|
-
try {
|
|
72
|
-
execFileSync('git', ['merge', '--no-edit', '--no-ff', `origin/${baseBranch}`], { stdio: 'inherit' });
|
|
73
|
-
} catch (e) {
|
|
74
|
-
process.stderr.write(
|
|
75
|
-
`::error::This PR cannot cleanly merge origin/${baseBranch}. Rebase your branch onto current ${baseBranch} and push again.\n`
|
|
76
|
-
);
|
|
77
|
-
process.stderr.write('::error::Conflicting files:\n');
|
|
68
|
+
// Attempt merge.
|
|
78
69
|
try {
|
|
79
|
-
execFileSync('git', ['
|
|
80
|
-
} catch (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
70
|
+
execFileSync('git', ['merge', '--no-edit', '--no-ff', `origin/${baseBranch}`], { stdio: 'inherit' });
|
|
71
|
+
} catch (e) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
`::error::This PR cannot cleanly merge origin/${baseBranch}. Rebase your branch onto current ${baseBranch} and push again.\n`
|
|
74
|
+
);
|
|
75
|
+
process.stderr.write('::error::Conflicting files:\n');
|
|
76
|
+
try {
|
|
77
|
+
execFileSync('git', ['diff', '--name-only', '--diff-filter=U'], { stdio: 'inherit' });
|
|
78
|
+
} catch (_) { /* ignore */ }
|
|
79
|
+
try {
|
|
80
|
+
execFileSync('git', ['merge', '--abort'], { stdio: 'inherit' });
|
|
81
|
+
} catch (_) { /* ignore */ }
|
|
82
|
+
throw new ExitError(1);
|
|
83
|
+
}
|
|
85
84
|
}
|
|
85
|
+
|
|
86
|
+
runMain(main);
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
const { execFileSync } = require('child_process');
|
|
5
5
|
const { existsSync, readdirSync, appendFileSync } = require('fs');
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
7
8
|
|
|
8
9
|
const RULES = [
|
|
9
10
|
{
|
|
@@ -198,7 +199,7 @@ function parseArgs(argv) {
|
|
|
198
199
|
if (!out.files) throw new Error('--files requires a value');
|
|
199
200
|
} else if (arg === '--help' || arg === '-h') {
|
|
200
201
|
console.log(usage());
|
|
201
|
-
|
|
202
|
+
throw new ExitError(0);
|
|
202
203
|
} else {
|
|
203
204
|
throw new Error(`unknown argument: ${arg}`);
|
|
204
205
|
}
|
|
@@ -318,10 +319,11 @@ function main() {
|
|
|
318
319
|
writeOutputs(result);
|
|
319
320
|
console.log(JSON.stringify(result, null, 2));
|
|
320
321
|
} catch (error) {
|
|
322
|
+
if (error instanceof ExitError) throw error;
|
|
321
323
|
console.error(`ci-test-scope: ${error.message}`);
|
|
322
324
|
console.error(usage());
|
|
323
|
-
|
|
325
|
+
throw new ExitError(2);
|
|
324
326
|
}
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
main
|
|
329
|
+
runMain(main);
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
const fs = require('node:fs');
|
|
45
45
|
const path = require('node:path');
|
|
46
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
46
47
|
|
|
47
48
|
const EXIT_SHIPPED = 0;
|
|
48
49
|
const EXIT_NOT_SHIPPED = 1;
|
|
@@ -88,60 +89,67 @@ function isPushBlocking(diffPath) {
|
|
|
88
89
|
return diffPath.replace(/\\/g, '/').startsWith('.github/workflows/');
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
function readStdin() {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
let data = '';
|
|
95
|
+
process.stdin.setEncoding('utf8');
|
|
96
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
97
|
+
process.stdin.on('end', () => resolve(data));
|
|
98
|
+
process.stdin.on('error', reject);
|
|
99
|
+
});
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
function main() {
|
|
98
|
-
// Surface ANY uncaught failure as exit 2 (classifier error) rather
|
|
99
|
-
// than letting Node's default-1 shadow the legitimate
|
|
100
|
-
// "no shipped paths" result. Bug #2983.
|
|
101
|
-
process.on('uncaughtException', (err) => fail('uncaught exception', err));
|
|
102
|
-
process.on('unhandledRejection', (err) => fail('unhandled rejection', err));
|
|
103
|
-
|
|
104
|
-
let shipPrefixes;
|
|
102
|
+
async function main() {
|
|
105
103
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
let shipPrefixes;
|
|
105
|
+
try {
|
|
106
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
107
|
+
shipPrefixes = loadShipPrefixes(pkgPath);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
process.stderr.write(`diff-touches-shipped-paths: failed to read package.json from ${process.cwd()}\n`);
|
|
110
|
+
if (err && err.stack) process.stderr.write(`${err.stack}\n`);
|
|
111
|
+
throw new ExitError(EXIT_ERROR);
|
|
112
|
+
}
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
process.stdin.setEncoding('utf8');
|
|
114
|
-
process.stdin.on('error', (err) => fail('stdin read error', err));
|
|
115
|
-
process.stdin.on('data', (chunk) => {
|
|
116
|
-
buf += chunk;
|
|
117
|
-
});
|
|
118
|
-
process.stdin.on('end', () => {
|
|
114
|
+
let buf;
|
|
119
115
|
try {
|
|
120
|
-
|
|
121
|
-
// #2980 still wins over #3621: any commit touching .github/workflows/*
|
|
122
|
-
// is unpickable regardless of other content because the push step
|
|
123
|
-
// fails on workflow scope rejection. Check this first.
|
|
124
|
-
if (paths.some(isPushBlocking)) {
|
|
125
|
-
process.exit(EXIT_NOT_SHIPPED);
|
|
126
|
-
}
|
|
127
|
-
if (paths.some((p) => isShipped(p, shipPrefixes))) {
|
|
128
|
-
process.exit(EXIT_SHIPPED);
|
|
129
|
-
}
|
|
130
|
-
// #3621: a commit whose only relevant paths are CI-gating tests is
|
|
131
|
-
// still pickable — it can change whether the hotfix CI passes even
|
|
132
|
-
// though it doesn't change what the npm tarball ships.
|
|
133
|
-
if (paths.some(isCiGating)) {
|
|
134
|
-
process.exit(EXIT_SHIPPED);
|
|
135
|
-
}
|
|
136
|
-
process.exit(EXIT_NOT_SHIPPED);
|
|
116
|
+
buf = await readStdin();
|
|
137
117
|
} catch (err) {
|
|
138
|
-
|
|
118
|
+
process.stderr.write(`diff-touches-shipped-paths: stdin read error\n`);
|
|
119
|
+
if (err && err.stack) process.stderr.write(`${err.stack}\n`);
|
|
120
|
+
throw new ExitError(EXIT_ERROR);
|
|
139
121
|
}
|
|
140
|
-
|
|
122
|
+
|
|
123
|
+
const paths = buf.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
124
|
+
// #2980 still wins over #3621: any commit touching .github/workflows/*
|
|
125
|
+
// is unpickable regardless of other content because the push step
|
|
126
|
+
// fails on workflow scope rejection. Check this first.
|
|
127
|
+
if (paths.some(isPushBlocking)) {
|
|
128
|
+
return EXIT_NOT_SHIPPED;
|
|
129
|
+
}
|
|
130
|
+
if (paths.some((p) => isShipped(p, shipPrefixes))) {
|
|
131
|
+
return EXIT_SHIPPED;
|
|
132
|
+
}
|
|
133
|
+
// #3621: a commit whose only relevant paths are CI-gating tests is
|
|
134
|
+
// still pickable — it can change whether the hotfix CI passes even
|
|
135
|
+
// though it doesn't change what the npm tarball ships.
|
|
136
|
+
if (paths.some(isCiGating)) {
|
|
137
|
+
return EXIT_SHIPPED;
|
|
138
|
+
}
|
|
139
|
+
return EXIT_NOT_SHIPPED;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// Re-throw ExitError unchanged; map any unexpected error to EXIT_ERROR=2
|
|
142
|
+
// (Node's default uncaught-exception code is 1, which is indistinguishable
|
|
143
|
+
// from the legitimate EXIT_NOT_SHIPPED result — bug #2983).
|
|
144
|
+
if (e instanceof ExitError) throw e;
|
|
145
|
+
process.stderr.write(`diff-touches-shipped-paths: classification failed\n`);
|
|
146
|
+
if (e && e.stack) process.stderr.write(`${e.stack}\n`);
|
|
147
|
+
throw new ExitError(EXIT_ERROR);
|
|
148
|
+
}
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
if (require.main === module) {
|
|
144
|
-
main
|
|
152
|
+
runMain(main);
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
module.exports = { loadShipPrefixes, isShipped, isCiGating, isPushBlocking, EXIT_SHIPPED, EXIT_NOT_SHIPPED, EXIT_ERROR };
|