@opengsd/gsd-core 1.4.4 → 1.5.0-rc.2
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/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/agents/gsd-code-fixer.md +3 -2
- package/agents/gsd-debug-session-manager.md +2 -1
- package/agents/gsd-debugger.md +4 -3
- package/agents/gsd-executor.md +17 -16
- package/agents/gsd-intel-updater.md +38 -41
- package/agents/gsd-phase-researcher.md +8 -8
- package/agents/gsd-plan-checker.md +23 -13
- package/agents/gsd-planner.md +32 -188
- package/agents/gsd-project-researcher.md +5 -4
- package/agents/gsd-research-synthesizer.md +2 -1
- package/agents/gsd-ui-researcher.md +2 -1
- package/agents/gsd-verifier.md +12 -11
- package/bin/install.js +965 -1486
- package/commands/gsd/autonomous.md +5 -1
- package/commands/gsd/ns-manage.md +8 -1
- package/commands/gsd/ns-project.md +5 -0
- package/commands/gsd/ns-review.md +4 -1
- package/commands/gsd/ns-workflow.md +7 -1
- package/commands/gsd/plan-review-convergence.md +5 -4
- package/commands/gsd/surface.md +12 -5
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +198 -101
- package/gsd-core/bin/gsd_run +20 -0
- package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
- package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
- package/gsd-core/bin/lib/capability-state.cjs +336 -0
- package/gsd-core/bin/lib/check-command-router.cjs +133 -2
- package/gsd-core/bin/lib/cli-exit.cjs +22 -3
- package/gsd-core/bin/lib/config-loader.cjs +716 -0
- package/gsd-core/bin/lib/configuration.cjs +4 -34
- package/gsd-core/bin/lib/core-utils.cjs +198 -0
- package/gsd-core/bin/lib/core.cjs +107 -1817
- package/gsd-core/bin/lib/edge-probe.cjs +173 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
- package/gsd-core/bin/lib/federated-config.cjs +182 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
- package/gsd-core/bin/lib/init.cjs +58 -12
- package/gsd-core/bin/lib/install-profiles.cjs +157 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
- package/gsd-core/bin/lib/intel.cjs +3 -3
- package/gsd-core/bin/lib/io.cjs +222 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
- package/gsd-core/bin/lib/model-resolver.cjs +426 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
- package/gsd-core/bin/lib/phase-id.cjs +215 -0
- package/gsd-core/bin/lib/phase-locator.cjs +148 -0
- package/gsd-core/bin/lib/phase.cjs +17 -0
- package/gsd-core/bin/lib/probe-core.cjs +257 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
- package/gsd-core/bin/lib/roadmap.cjs +44 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
- package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
- package/gsd-core/bin/lib/state-document.cjs +8 -0
- package/gsd-core/bin/lib/state.cjs +114 -2
- package/gsd-core/bin/lib/surface.cjs +66 -14
- package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
- package/gsd-core/bin/lib/update-context.cjs +4 -1
- package/gsd-core/bin/lib/verify.cjs +104 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
- package/gsd-core/bin/shared/model-catalog.json +11 -6
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
- package/gsd-core/references/edge-probe.md +261 -0
- package/gsd-core/references/planner-antipatterns.md +41 -0
- package/gsd-core/references/planner-guidance.md +186 -0
- package/gsd-core/references/planner-reviews.md +5 -2
- package/gsd-core/templates/phase-prompt.md +7 -7
- package/gsd-core/templates/project.md +19 -2
- package/gsd-core/templates/spec.md +12 -0
- package/gsd-core/templates/summary-complex.md +1 -0
- package/gsd-core/templates/summary-minimal.md +1 -0
- package/gsd-core/templates/summary-standard.md +1 -0
- package/gsd-core/templates/summary.md +1 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +111 -51
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +6 -4
- package/gsd-core/workflows/code-review.md +53 -17
- package/gsd-core/workflows/complete-milestone.md +11 -5
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +8 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +8 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +9 -1
- package/gsd-core/workflows/new-project.md +9 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +65 -28
- package/gsd-core/workflows/plan-review-convergence.md +60 -33
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -7
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ship.md +8 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +130 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +2 -2
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +8 -1
- package/package.json +11 -3
- package/scripts/base64-scan.sh +1 -1
- package/scripts/changeset/cli.cjs +8 -1
- package/scripts/changeset/lint.cjs +38 -2
- package/scripts/ci-test-scope.cjs +21 -10
- package/scripts/gen-capability-registry.cjs +2293 -0
- package/scripts/gen-loop-host-contract.cjs +471 -0
- package/scripts/lib/allowlist-ratchet.cjs +101 -1
- package/scripts/lint-regression-test-names.allowlist.json +269 -0
- package/scripts/lint-regression-test-names.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +25 -4
- package/scripts/lint-windows-test-portability.cjs +178 -0
- package/scripts/prompt-injection-scan.sh +4 -4
- package/scripts/research-profiles.cjs +10 -10
- package/scripts/run-tests.cjs +133 -29
- package/scripts/secret-scan.sh +3 -3
- package/scripts/sync-next-version.cjs +133 -0
- package/scripts/sync-runtime-launcher.cjs +21 -5
- package/scripts/update-size-baseline.cjs +68 -0
- package/scripts/workflow-policy.cjs +42 -9
- package/scripts/workflow-size.cjs +90 -0
- package/scripts/run-cross-platform-tests.cjs +0 -67
|
@@ -69,15 +69,15 @@ ALLOWLIST=(
|
|
|
69
69
|
'scripts/prompt-injection-scan.sh'
|
|
70
70
|
'scripts/base64-scan.sh'
|
|
71
71
|
'scripts/secret-scan.sh'
|
|
72
|
-
'tests/security-scan.test.cjs'
|
|
72
|
+
'tests/security-scan.security.test.cjs'
|
|
73
73
|
'tests/security.test.cjs'
|
|
74
|
-
'tests/prompt-injection-scan.test.cjs'
|
|
74
|
+
'tests/prompt-injection-scan.security.test.cjs'
|
|
75
75
|
'tests/verify.test.cjs'
|
|
76
76
|
'gsd-core/bin/lib/security.cjs'
|
|
77
77
|
'hooks/gsd-prompt-guard.js'
|
|
78
78
|
'hooks/gsd-read-injection-scanner.js'
|
|
79
|
-
'tests/read-injection-scanner.test.cjs'
|
|
80
|
-
'tests/security-prompt-injection.test.cjs'
|
|
79
|
+
'tests/read-injection-scanner.security.test.cjs'
|
|
80
|
+
'tests/security-prompt-injection.security.test.cjs'
|
|
81
81
|
'tests/fixtures/adversarial/security/'
|
|
82
82
|
'SECURITY.md'
|
|
83
83
|
# These files contain intentional injection examples / security-model prose
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* color — verbatim frontmatter `color:` value
|
|
14
14
|
* tools — verbatim frontmatter `tools:` value (single string, comma-separated)
|
|
15
15
|
* requiredIncludes — @~/.claude/gsd-core/references/<file>.md strings the body MUST contain
|
|
16
|
-
* requiredSeamCalls — `
|
|
16
|
+
* requiredSeamCalls — `gsd_run query <cmd>` strings the body MUST contain
|
|
17
17
|
* outputContract — strings the body MUST contain (output path, return marker, etc.)
|
|
18
18
|
*/
|
|
19
19
|
|
|
@@ -31,9 +31,9 @@ const PROFILES = [
|
|
|
31
31
|
'@~/.claude/gsd-core/references/research-verification-protocol.md',
|
|
32
32
|
],
|
|
33
33
|
requiredSeamCalls: [
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
34
|
+
'gsd_run query research-plan',
|
|
35
|
+
'gsd_run query research-store put',
|
|
36
|
+
'gsd_run query classify-confidence',
|
|
37
37
|
],
|
|
38
38
|
outputContract: [
|
|
39
39
|
'.planning/research/',
|
|
@@ -53,10 +53,10 @@ const PROFILES = [
|
|
|
53
53
|
'@~/.claude/gsd-core/references/research-verification-protocol.md',
|
|
54
54
|
],
|
|
55
55
|
requiredSeamCalls: [
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
56
|
+
'gsd_run query research-plan',
|
|
57
|
+
'gsd_run query research-store put',
|
|
58
|
+
'gsd_run query classify-confidence',
|
|
59
|
+
'gsd_run query package-legitimacy check',
|
|
60
60
|
],
|
|
61
61
|
outputContract: [
|
|
62
62
|
'.planning/phases/XX-name/{phase_num}-RESEARCH.md',
|
|
@@ -122,7 +122,7 @@ const PROFILES = [
|
|
|
122
122
|
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
|
123
123
|
],
|
|
124
124
|
requiredSeamCalls: [
|
|
125
|
-
'
|
|
125
|
+
'gsd_run query commit',
|
|
126
126
|
],
|
|
127
127
|
outputContract: [
|
|
128
128
|
'UI-SPEC.md',
|
|
@@ -137,7 +137,7 @@ const PROFILES = [
|
|
|
137
137
|
tools: 'Read, Write, Bash',
|
|
138
138
|
requiredIncludes: [],
|
|
139
139
|
requiredSeamCalls: [
|
|
140
|
-
'
|
|
140
|
+
'gsd_run query commit',
|
|
141
141
|
],
|
|
142
142
|
outputContract: [
|
|
143
143
|
'.planning/research/SUMMARY.md',
|
package/scripts/run-tests.cjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// See docs/TESTING-SUITES.md for full grouping policy.
|
|
21
21
|
'use strict';
|
|
22
22
|
|
|
23
|
-
const { readdirSync
|
|
23
|
+
const { readdirSync } = require('fs');
|
|
24
24
|
const { join } = require('path');
|
|
25
25
|
const { execFileSync } = require('child_process');
|
|
26
26
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
@@ -31,21 +31,84 @@ const SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow'];
|
|
|
31
31
|
// src/*.cts and gitignored, so on a clean checkout (fresh CI, before any build)
|
|
32
32
|
// the artifact is absent — yet test files require it. This is the universal
|
|
33
33
|
// chokepoint every test path funnels through (test:unit, --files-from, direct
|
|
34
|
-
// invocation), so build the artifact here
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
// invocation), so build the artifact here.
|
|
35
|
+
//
|
|
36
|
+
// Strategy (incremental + re-emit-on-missing, closes both #969 failure modes):
|
|
37
|
+
// 1. Run tsc incrementally (fast ~380ms no-op when sources unchanged).
|
|
38
|
+
// 2. Verify every src/*.cts (non-.d.cts) maps to a non-empty gsd-core/bin/lib/*.cjs.
|
|
39
|
+
// 3. If any expected .cjs is missing or zero-bytes (persistent-mirror scenario:
|
|
40
|
+
// tsc no-ops because tsbuildinfo looks current even though the file was deleted),
|
|
41
|
+
// delete the tsbuildinfo and run tsc ONCE MORE (clean re-emit), then re-verify.
|
|
42
|
+
//
|
|
43
|
+
// Common case: fast incremental no-op. Stale/deleted-output case: detected by
|
|
44
|
+
// the cheap existsSync loop and force-rebuilt. Paths resolve from __dirname so
|
|
45
|
+
// it works regardless of GSD_TEST_DIR / temp-dir cwd.
|
|
46
|
+
function ensureBuiltArtifacts(overrides = {}) {
|
|
47
|
+
const { existsSync, readdirSync, statSync, unlinkSync } = require('fs');
|
|
48
|
+
const root = overrides.root || join(__dirname, '..');
|
|
49
|
+
const srcDir = overrides.srcDir || join(root, 'src');
|
|
50
|
+
const outDir = overrides.outDir || join(root, 'gsd-core', 'bin', 'lib');
|
|
51
|
+
const tsBuildInfoPath = overrides.tsBuildInfoPath || join(root, 'tsconfig.build.tsbuildinfo');
|
|
52
|
+
const tsconfigPath = overrides.tsconfigPath || join(root, 'tsconfig.build.json');
|
|
44
53
|
const tscBin = require.resolve('typescript/bin/tsc');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
const tscArgs = [tscBin, '-p', tsconfigPath];
|
|
55
|
+
|
|
56
|
+
// Build the 1:1 map of expected output paths from src/*.cts sources.
|
|
57
|
+
// Excludes *.d.cts (declaration-only files that produce no output).
|
|
58
|
+
// Handles subdirectories (e.g. src/installer-migrations/*.cts → gsd-core/bin/lib/installer-migrations/*.cjs).
|
|
59
|
+
function gatherExpectedOutputs() {
|
|
60
|
+
const expected = [];
|
|
61
|
+
function scan(dir, relBase) {
|
|
62
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
scan(join(dir, entry.name), relBase ? `${relBase}/${entry.name}` : entry.name);
|
|
65
|
+
} else if (entry.name.endsWith('.cts') && !entry.name.endsWith('.d.cts')) {
|
|
66
|
+
const stem = entry.name.slice(0, -'.cts'.length);
|
|
67
|
+
const rel = relBase ? `${relBase}/${stem}.cjs` : `${stem}.cjs`;
|
|
68
|
+
expected.push(join(outDir, rel));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
scan(srcDir, '');
|
|
73
|
+
return expected;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkMissingOutputs(expectedPaths) {
|
|
77
|
+
return expectedPaths.filter(p => !existsSync(p) || statSync(p).size === 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// #996 placed the tsbuildinfo inside gsd-core/bin/ (a copied/shipped tree), which
|
|
81
|
+
// raced install-test copies. It now lives at the repo root. Best-effort purge any
|
|
82
|
+
// stale bin-local copy so persistent workspaces/mirrors self-heal (no-op on a temp
|
|
83
|
+
// override root or a clean checkout).
|
|
84
|
+
const legacyTsBuildInfo = join(root, 'gsd-core', 'bin', 'tsconfig.build.tsbuildinfo');
|
|
85
|
+
try { if (existsSync(legacyTsBuildInfo)) unlinkSync(legacyTsBuildInfo); } catch { /* best-effort */ }
|
|
86
|
+
|
|
87
|
+
// Step 1: incremental build (fast no-op when sources unchanged).
|
|
88
|
+
execFileSync(process.execPath, tscArgs, { cwd: root, stdio: 'inherit' });
|
|
89
|
+
|
|
90
|
+
// Step 2: verify expected outputs.
|
|
91
|
+
const expected = gatherExpectedOutputs();
|
|
92
|
+
const missing = checkMissingOutputs(expected);
|
|
93
|
+
|
|
94
|
+
// Step 3: if any output is missing/zero-bytes, force a clean re-emit.
|
|
95
|
+
// This handles the persistent-mirror case where tsc's incremental no-op left
|
|
96
|
+
// a deleted .cjs unregenerated (tsbuildinfo recorded it as up-to-date).
|
|
97
|
+
if (missing.length > 0) {
|
|
98
|
+
if (existsSync(tsBuildInfoPath)) {
|
|
99
|
+
unlinkSync(tsBuildInfoPath);
|
|
100
|
+
}
|
|
101
|
+
execFileSync(process.execPath, tscArgs, { cwd: root, stdio: 'inherit' });
|
|
102
|
+
// Re-verify after clean re-emit; surface any remaining gaps loudly.
|
|
103
|
+
const stillMissing = checkMissingOutputs(expected);
|
|
104
|
+
if (stillMissing.length > 0) {
|
|
105
|
+
const names = stillMissing.map(p => require('path').basename(p)).join(', ');
|
|
106
|
+
throw new Error(
|
|
107
|
+
`ensureBuiltArtifacts: tsc clean re-emit still missing outputs: ${names}. ` +
|
|
108
|
+
`Check src/ for compilation errors.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
49
112
|
}
|
|
50
113
|
const MARKED_SUITES = ['integration', 'install', 'security', 'slow'];
|
|
51
114
|
|
|
@@ -256,14 +319,13 @@ function main() {
|
|
|
256
319
|
// Default concurrency: 4 on Linux/macOS, 2 on Windows.
|
|
257
320
|
//
|
|
258
321
|
// Windows has significantly higher per-subprocess overhead than Linux/macOS:
|
|
259
|
-
// - Windows Defender scans each spawned process
|
|
260
|
-
//
|
|
261
|
-
// -
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
// they could flush. Reducing to 2 halves the peak concurrent worker count.
|
|
322
|
+
// - Windows Defender scans each spawned process on first execution, adding
|
|
323
|
+
// latency proportional to the number of concurrent spawns.
|
|
324
|
+
// - NTFS has higher file-system latency under concurrent access compared to
|
|
325
|
+
// ext4/APFS, which amplifies contention when multiple test chunks run in
|
|
326
|
+
// parallel and all read/write the same fixture directories.
|
|
327
|
+
// Reducing to 2 halves the peak concurrent subprocess count on Windows and
|
|
328
|
+
// keeps per-chunk wall-clock time well within the 20m CI job cap.
|
|
267
329
|
//
|
|
268
330
|
// Operator override via TEST_CONCURRENCY env var for local debugging.
|
|
269
331
|
const defaultConcurrency = process.platform === 'win32' ? 2 : 4;
|
|
@@ -280,7 +342,21 @@ function main() {
|
|
|
280
342
|
const MAX_CMDLINE_CHARS = process.env.RUN_TESTS_MAX_CMDLINE_CHARS
|
|
281
343
|
? Number(process.env.RUN_TESTS_MAX_CMDLINE_CHARS)
|
|
282
344
|
: 28000; // headroom below the 32,767 Windows ceiling
|
|
283
|
-
|
|
345
|
+
|
|
346
|
+
// node:test does not exit until the event loop drains. A unit test that leaks
|
|
347
|
+
// an open handle (un-terminated Worker, un-killed child_process, ref'd timer)
|
|
348
|
+
// makes a chunk's `node --test` child hang ~150s on Windows AFTER its last test
|
|
349
|
+
// prints; two such stalls push the windows full lane past its 20m cap and the
|
|
350
|
+
// job is CANCELLED with no failed step — a false-negative gate (#1051, recurrence
|
|
351
|
+
// of #869). --test-force-exit (Node >=22; engines requires >=22.0.0) exits the
|
|
352
|
+
// runner once all tests finish regardless of lingering handles. The leaking
|
|
353
|
+
// tests are also fixed at the source; this is the defensive backstop.
|
|
354
|
+
// RUN_TESTS_NO_FORCE_EXIT=1 disables it (used by the harness regression test to
|
|
355
|
+
// observe the pre-fix hang).
|
|
356
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
357
|
+
const forceExit = nodeMajor >= 22 && !process.env.RUN_TESTS_NO_FORCE_EXIT;
|
|
358
|
+
|
|
359
|
+
const FIXED_OVERHEAD = process.execPath.length + '--test'.length + concurrency.length + (forceExit ? '--test-force-exit'.length + 1 : 0) + 8;
|
|
284
360
|
const chunks = [];
|
|
285
361
|
let current = [];
|
|
286
362
|
let currentLen = FIXED_OVERHEAD;
|
|
@@ -296,17 +372,45 @@ function main() {
|
|
|
296
372
|
}
|
|
297
373
|
if (current.length > 0) chunks.push(current);
|
|
298
374
|
|
|
375
|
+
// A chunk that still hangs (a leak the backstop somehow misses, or a wedged
|
|
376
|
+
// subprocess) must fail loudly rather than silently burn the job's wall-clock
|
|
377
|
+
// budget until the CI runner cancels the whole job. Default 10 min per chunk:
|
|
378
|
+
// well above a healthy chunk (~4-5 min on the windows lane) but below the 20m
|
|
379
|
+
// job cap. Operator/test override via RUN_TESTS_CHUNK_TIMEOUT_MS.
|
|
380
|
+
const chunkTimeoutMs = process.env.RUN_TESTS_CHUNK_TIMEOUT_MS
|
|
381
|
+
? Number(process.env.RUN_TESTS_CHUNK_TIMEOUT_MS)
|
|
382
|
+
: 600000;
|
|
383
|
+
|
|
299
384
|
let firstFailureExit = 0;
|
|
300
385
|
for (let i = 0; i < chunks.length; i++) {
|
|
301
386
|
if (chunks.length > 1) {
|
|
302
387
|
console.error(`run-tests: chunk ${i + 1}/${chunks.length} — ${chunks[i].length} files`);
|
|
303
388
|
}
|
|
304
389
|
try {
|
|
305
|
-
execFileSync(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
390
|
+
execFileSync(
|
|
391
|
+
process.execPath,
|
|
392
|
+
['--test', ...(forceExit ? ['--test-force-exit'] : []), concurrency, ...chunks[i]],
|
|
393
|
+
{
|
|
394
|
+
stdio: 'inherit',
|
|
395
|
+
env: { ...process.env },
|
|
396
|
+
timeout: chunkTimeoutMs,
|
|
397
|
+
},
|
|
398
|
+
);
|
|
309
399
|
} catch (err) {
|
|
400
|
+
// When the per-chunk timeout fires, execFileSync kills the child and
|
|
401
|
+
// surfaces it as err.code === 'ETIMEDOUT' (POSIX) and/or err.killed === true
|
|
402
|
+
// (platform-dependent). Check both so detection holds on Windows and POSIX.
|
|
403
|
+
const timedOut = err.killed === true || err.code === 'ETIMEDOUT';
|
|
404
|
+
if (timedOut) {
|
|
405
|
+
console.error(
|
|
406
|
+
`run-tests: chunk ${i + 1}/${chunks.length} exceeded the per-chunk timeout ` +
|
|
407
|
+
`of ${chunkTimeoutMs}ms and was killed — a test in this chunk is likely leaking ` +
|
|
408
|
+
`an open handle (un-terminated Worker, un-killed child process, or ref'd timer) ` +
|
|
409
|
+
`so node --test never exits. Files: ${chunks[i]
|
|
410
|
+
.map(f => f.split(/[\\/]/).pop())
|
|
411
|
+
.join(' ')}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
310
414
|
const code = err.status || 1;
|
|
311
415
|
// Run every chunk so the operator sees all failures in one pass; report
|
|
312
416
|
// the first non-zero exit at the end.
|
|
@@ -320,4 +424,4 @@ if (require.main === module) {
|
|
|
320
424
|
runMain(main);
|
|
321
425
|
}
|
|
322
426
|
|
|
323
|
-
module.exports = { suiteOf };
|
|
427
|
+
module.exports = { suiteOf, ensureBuiltArtifacts };
|
package/scripts/secret-scan.sh
CHANGED
|
@@ -218,9 +218,9 @@ should_skip_file() {
|
|
|
218
218
|
# Skip the scan scripts themselves and test files
|
|
219
219
|
case "$file" in
|
|
220
220
|
*/secret-scan.sh) return 0 ;;
|
|
221
|
-
*/secret-scan-lint.test.cjs) return 0 ;;
|
|
222
|
-
*/security-scan.test.cjs) return 0 ;;
|
|
223
|
-
*/security-prompt-injection.test.cjs) return 0 ;;
|
|
221
|
+
*/secret-scan-lint.security.test.cjs) return 0 ;;
|
|
222
|
+
*/security-scan.security.test.cjs) return 0 ;;
|
|
223
|
+
*/security-prompt-injection.security.test.cjs) return 0 ;;
|
|
224
224
|
tests/fixtures/adversarial/security/*|*/tests/fixtures/adversarial/security/*) return 0 ;;
|
|
225
225
|
esac
|
|
226
226
|
return 1
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Sync the `next` branch's package.json version to a published release version.
|
|
4
|
+
*
|
|
5
|
+
* The release pipeline bumps the version only on the release/X.Y.Z branch at
|
|
6
|
+
* publish time, so `next` drifts and can carry a never-published placeholder
|
|
7
|
+
* (e.g. 1.3.1-dev.0) that leaks to source/dev installs reporting package.json
|
|
8
|
+
* version. This keeps `next` equal to the last published release for every
|
|
9
|
+
* release type (rc / hotfix / final). See issue #1104.
|
|
10
|
+
*
|
|
11
|
+
* Modes:
|
|
12
|
+
* default — open + admin-merge a `chore: sync next version` PR (used by
|
|
13
|
+
* release.yml's rc job, which has no next-targeting PR of its
|
|
14
|
+
* own). Idempotent: a no-op when `next` is already at the target.
|
|
15
|
+
* --in-place — set the version (+ manifests, via the npm `version` hook) in
|
|
16
|
+
* the current working tree only; the caller commits/pushes (used
|
|
17
|
+
* by auto-backmerge.yml, which folds it into its existing
|
|
18
|
+
* main->next PR).
|
|
19
|
+
*
|
|
20
|
+
* Subprocesses are bounded (repo convention) and the exec seam is injectable so
|
|
21
|
+
* the orchestration is unit-testable without git/gh/npm.
|
|
22
|
+
*/
|
|
23
|
+
const { execFileSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta)\.[0-9]+)?$/;
|
|
26
|
+
|
|
27
|
+
/** True iff `v` is a publishable release version: X.Y.Z optionally -rc.N / -beta.N. Rejects -dev and anything else. */
|
|
28
|
+
function isReleaseVersion(v) {
|
|
29
|
+
return typeof v === 'string' && RELEASE_VERSION.test(v);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read the `version` field out of a package.json text blob. Throws on a missing/non-string version. */
|
|
33
|
+
function versionFromPackageJson(text) {
|
|
34
|
+
const parsed = JSON.parse(text);
|
|
35
|
+
if (typeof parsed.version !== 'string') throw new Error('package.json has no string version');
|
|
36
|
+
return parsed.version;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultRun(cmd, args, opts = {}) {
|
|
40
|
+
return execFileSync(cmd, args, {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
timeout: opts.timeout ?? 30000,
|
|
43
|
+
stdio: opts.stdio ?? ['ignore', 'pipe', 'inherit'],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Set package.json version (and manifests, via the npm `version` lifecycle hook) in the current tree. */
|
|
48
|
+
function applyVersion(version, { run = defaultRun } = {}) {
|
|
49
|
+
if (!isReleaseVersion(version)) {
|
|
50
|
+
throw new Error(`refusing to sync next to invalid/non-release version '${version}'`);
|
|
51
|
+
}
|
|
52
|
+
run('npm', ['version', version, '--no-git-tag-version', '--allow-same-version'], { timeout: 60000 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Full PR-based sync to the `next` branch. Returns 'noop' | 'synced'. */
|
|
56
|
+
function syncViaPr(version, { run = defaultRun } = {}) {
|
|
57
|
+
if (!isReleaseVersion(version)) {
|
|
58
|
+
throw new Error(`refusing to sync next to invalid/non-release version '${version}'`);
|
|
59
|
+
}
|
|
60
|
+
run('git', ['fetch', 'origin', 'next'], { timeout: 30000 });
|
|
61
|
+
const current = versionFromPackageJson(run('git', ['show', 'origin/next:package.json'], { timeout: 15000 }));
|
|
62
|
+
if (current === version) {
|
|
63
|
+
process.stdout.write(`next already at ${version} — nothing to sync\n`);
|
|
64
|
+
return 'noop';
|
|
65
|
+
}
|
|
66
|
+
const branch = `chore/sync-next-version-${version}`;
|
|
67
|
+
run('git', ['checkout', '-B', branch, 'origin/next'], { timeout: 15000 });
|
|
68
|
+
applyVersion(version, { run });
|
|
69
|
+
run('git', ['add', '-u'], { timeout: 15000 });
|
|
70
|
+
// `git diff --cached --quiet` exits 0 when there is NO staged diff (execFileSync
|
|
71
|
+
// returns), exits 1 when there IS one (execFileSync throws). Proceed only on a diff.
|
|
72
|
+
let hasDiff = false;
|
|
73
|
+
try {
|
|
74
|
+
run('git', ['diff', '--cached', '--quiet'], { timeout: 15000 });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// exit 1 == there is a staged diff (expected); anything else is a real failure.
|
|
77
|
+
if (err && err.status === 1) hasDiff = true;
|
|
78
|
+
else throw err;
|
|
79
|
+
}
|
|
80
|
+
if (!hasDiff) {
|
|
81
|
+
process.stdout.write('no changes to commit — nothing to sync\n');
|
|
82
|
+
return 'noop';
|
|
83
|
+
}
|
|
84
|
+
run('git', ['commit', '-m', `chore: sync next package version to ${version}`], { timeout: 15000 });
|
|
85
|
+
run('git', ['push', '--force-with-lease', 'origin', branch], { timeout: 60000 });
|
|
86
|
+
let prUrl = run('gh', ['pr', 'list', '--head', branch, '--base', 'next', '--state', 'open',
|
|
87
|
+
'--json', 'url', '--jq', '.[0].url // ""'], { timeout: 30000 }).trim();
|
|
88
|
+
if (!prUrl) {
|
|
89
|
+
prUrl = run('gh', ['pr', 'create', '--base', 'next', '--head', branch,
|
|
90
|
+
'--title', `chore: sync next package version to ${version}`,
|
|
91
|
+
'--body', `Automated: keep \`next\`'s package.json at the last published release (\`${version}\`) so the default branch never carries a never-published version. Generated by the release pipeline (#1104).`,
|
|
92
|
+
], { timeout: 60000 }).trim();
|
|
93
|
+
}
|
|
94
|
+
const m = prUrl.match(/\/pull\/(\d+)\b/);
|
|
95
|
+
if (!m) throw new Error(`could not parse PR number from gh output: ${prUrl}`);
|
|
96
|
+
const prNum = m[1];
|
|
97
|
+
try {
|
|
98
|
+
run('gh', ['pr', 'edit', prNum, '--add-label', 'automation', '--add-label', 'no-changelog'], { timeout: 30000 });
|
|
99
|
+
} catch {
|
|
100
|
+
/* labels are best-effort; admin-merge below still lands the PR */
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
run('gh', ['pr', 'merge', '--admin', '--merge', prNum], { timeout: 60000 });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// tolerate "already merged"; rethrow anything else
|
|
106
|
+
const msg = String((err && (err.stderr || err.message)) || '');
|
|
107
|
+
if (!/already merged|not mergeable|Merged/i.test(msg)) throw err;
|
|
108
|
+
}
|
|
109
|
+
process.stdout.write(`synced next -> ${version} via PR #${prNum}\n`);
|
|
110
|
+
return 'synced';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function main(argv = process.argv.slice(2), deps = {}) {
|
|
114
|
+
const inPlace = argv.includes('--in-place');
|
|
115
|
+
const version = argv.find((a) => !a.startsWith('--'));
|
|
116
|
+
if (!version) throw new Error('usage: sync-next-version.cjs <version> [--in-place]');
|
|
117
|
+
if (inPlace) {
|
|
118
|
+
applyVersion(version, deps);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
syncViaPr(version, deps);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (require.main === module) {
|
|
125
|
+
try {
|
|
126
|
+
main();
|
|
127
|
+
} catch (e) {
|
|
128
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { isReleaseVersion, versionFromPackageJson, applyVersion, syncViaPr, main };
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* sync-runtime-launcher.cjs
|
|
4
4
|
*
|
|
5
|
-
* Idempotent transform: for every gsd-core/workflows/*.md (and subdirs)
|
|
6
|
-
* rewrite all bash/sh/shell fenced blocks to:
|
|
5
|
+
* Idempotent transform: for every gsd-core/workflows/*.md (and subdirs)
|
|
6
|
+
* AND every agents/*.md, rewrite all bash/sh/shell fenced blocks to:
|
|
7
7
|
* 1. Strip ALL old resolver forms from every bash block (GSD_TOOLS=,
|
|
8
8
|
* GSD_SDK=, the if/elif/else/fi resolver, _GSD_SHIM_NAME=, and any
|
|
9
9
|
* previously-inserted gsd_run preamble).
|
|
@@ -20,6 +20,7 @@ const fs = require('node:fs');
|
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
|
|
22
22
|
const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
|
|
23
|
+
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
|
23
24
|
const SNIPPET_FILE = path.join(WORKFLOWS_DIR, '_runtime-launcher.snippet.sh');
|
|
24
25
|
|
|
25
26
|
// Read canonical preamble (full content of snippet file)
|
|
@@ -376,18 +377,33 @@ function escapeRegExp(str) {
|
|
|
376
377
|
// Main
|
|
377
378
|
function main() {
|
|
378
379
|
const preamble = loadPreamble();
|
|
379
|
-
const files = collectFiles(WORKFLOWS_DIR);
|
|
380
380
|
|
|
381
381
|
let transformedCount = 0;
|
|
382
382
|
let unchangedCount = 0;
|
|
383
383
|
|
|
384
|
-
|
|
384
|
+
// Process workflow files
|
|
385
|
+
const workflowFiles = collectFiles(WORKFLOWS_DIR);
|
|
386
|
+
for (const f of workflowFiles) {
|
|
385
387
|
const content = fs.readFileSync(f, 'utf8');
|
|
386
388
|
const result = transformFile(content, preamble);
|
|
387
389
|
if (result !== null) {
|
|
388
390
|
fs.writeFileSync(f, result, 'utf8');
|
|
389
391
|
transformedCount++;
|
|
390
|
-
console.log(`transformed: ${path.relative(WORKFLOWS_DIR, f)}`);
|
|
392
|
+
console.log(`transformed (workflow): ${path.relative(WORKFLOWS_DIR, f)}`);
|
|
393
|
+
} else {
|
|
394
|
+
unchangedCount++;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Process agent files
|
|
399
|
+
const agentFiles = collectFiles(AGENTS_DIR);
|
|
400
|
+
for (const f of agentFiles) {
|
|
401
|
+
const content = fs.readFileSync(f, 'utf8');
|
|
402
|
+
const result = transformFile(content, preamble);
|
|
403
|
+
if (result !== null) {
|
|
404
|
+
fs.writeFileSync(f, result, 'utf8');
|
|
405
|
+
transformedCount++;
|
|
406
|
+
console.log(`transformed (agent): ${path.relative(AGENTS_DIR, f)}`);
|
|
391
407
|
} else {
|
|
392
408
|
unchangedCount++;
|
|
393
409
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @file update-size-baseline.cjs
|
|
6
|
+
*
|
|
7
|
+
* Regenerates the committed per-file workflow size baseline
|
|
8
|
+
* (`tests/workflow-size-baseline.json`) from the current workflow files.
|
|
9
|
+
*
|
|
10
|
+
* Run via `npm run size:baseline` whenever a workflow file legitimately grows
|
|
11
|
+
* or shrinks. Growth must still be justified in the PR; this script only
|
|
12
|
+
* records the new reality so the CI guard (issue #1074) can diff against it.
|
|
13
|
+
*
|
|
14
|
+
* Idempotent: running it twice with no file changes produces no diff.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { measureMdFiles, WORKFLOWS_DIR } = require('./workflow-size.cjs');
|
|
20
|
+
|
|
21
|
+
const BASELINE_PATH = path.join(__dirname, '..', 'tests', 'workflow-size-baseline.json');
|
|
22
|
+
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
|
23
|
+
const AGENT_BASELINE_PATH = path.join(__dirname, '..', 'tests', 'agent-size-baseline.json');
|
|
24
|
+
const isGsdAgent = (f) => f.startsWith('gsd-');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Serialize a size map to the on-disk baseline format: keys sorted, 2-space
|
|
28
|
+
* indent, trailing newline (so the file is a stable, minimal-diff artifact).
|
|
29
|
+
*
|
|
30
|
+
* @param {Object<string, number>} sizes
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function serializeBaseline(sizes) {
|
|
34
|
+
const sorted = {};
|
|
35
|
+
for (const key of Object.keys(sizes).sort()) sorted[key] = sizes[key];
|
|
36
|
+
return JSON.stringify(sorted, null, 2) + '\n';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Write a baseline file from the measured `.md` sizes in a directory.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} [opts]
|
|
43
|
+
* @param {string} [opts.dir] - Directory to measure (default: workflows).
|
|
44
|
+
* @param {string} [opts.outPath] - Baseline file to write (default: workflow).
|
|
45
|
+
* @param {function(string): boolean} [opts.predicate] - Filename filter.
|
|
46
|
+
* @returns {{ outPath: string, count: number, content: string }}
|
|
47
|
+
*/
|
|
48
|
+
function generateBaseline({ dir = WORKFLOWS_DIR, outPath = BASELINE_PATH, predicate } = {}) {
|
|
49
|
+
const sizes = measureMdFiles(dir, predicate);
|
|
50
|
+
const content = serializeBaseline(sizes);
|
|
51
|
+
fs.writeFileSync(outPath, content);
|
|
52
|
+
return { outPath, count: Object.keys(sizes).length, content };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (require.main === module) {
|
|
56
|
+
const targets = [
|
|
57
|
+
{ label: 'workflow', dir: WORKFLOWS_DIR, outPath: BASELINE_PATH },
|
|
58
|
+
{ label: 'agent', dir: AGENTS_DIR, outPath: AGENT_BASELINE_PATH, predicate: isGsdAgent },
|
|
59
|
+
];
|
|
60
|
+
for (const t of targets) {
|
|
61
|
+
const { outPath, count } = generateBaseline(t);
|
|
62
|
+
process.stdout.write(
|
|
63
|
+
`Wrote ${count} ${t.label} sizes to ${path.relative(process.cwd(), outPath)}\n`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { generateBaseline, serializeBaseline, BASELINE_PATH, AGENT_BASELINE_PATH };
|
|
@@ -36,6 +36,26 @@ function runnerDefault(runner) {
|
|
|
36
36
|
return 'bash'; // ubuntu-* and macos-* both default to bash on GHA
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Matrix expansion helpers
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
// Build the GitHub Actions Cartesian product of the given base-list matrix
|
|
44
|
+
// keys. Returns one context object per realized job, each mapping every key
|
|
45
|
+
// to a stringified value. A single key yields one context per value (identical
|
|
46
|
+
// to the legacy base-list expansion); N keys yield the full cross-product.
|
|
47
|
+
// Values are stringified to match runner-label comparison and matrix-expression
|
|
48
|
+
// resolution, which operate on strings.
|
|
49
|
+
function cartesianProduct(keys, matrix) {
|
|
50
|
+
return keys.reduce(
|
|
51
|
+
(contexts, key) =>
|
|
52
|
+
contexts.flatMap((context) =>
|
|
53
|
+
matrix[key].map((value) => ({ ...context, [key]: String(value) })),
|
|
54
|
+
),
|
|
55
|
+
[{}],
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
// ---------------------------------------------------------------------------
|
|
40
60
|
// Matrix expansion
|
|
41
61
|
// ---------------------------------------------------------------------------
|
|
@@ -88,19 +108,32 @@ function expandRunsOn(runsOnRaw, matrix) {
|
|
|
88
108
|
}
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
111
|
+
// GitHub Actions realizes one job per element of the Cartesian product of all
|
|
112
|
+
// base-list matrix keys (every matrix.<k> that is an array, excluding the
|
|
113
|
+
// include/exclude control keys), and each realized job's context carries a
|
|
114
|
+
// value for EVERY matrix key — so ${{ matrix.<key> }} references (e.g. in a
|
|
115
|
+
// shell: field) resolve against any realization, not only the runs-on key.
|
|
116
|
+
// Single-axis matrices yield exactly one realization per value, identical to
|
|
117
|
+
// the prior behavior; only multi-axis matrices change shape.
|
|
118
|
+
// Each realization is pushed unconditionally — deduplicating by runner label
|
|
119
|
+
// would collapse distinct Cartesian rows (e.g. matrix.os: [macos-latest,
|
|
120
|
+
// macos-latest] paired with different shells) and hide policy violations.
|
|
96
121
|
if (Array.isArray(matrix[key])) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
122
|
+
const baseListKeys = Object.keys(matrix).filter(
|
|
123
|
+
(k) => k !== 'include' && k !== 'exclude' && Array.isArray(matrix[k]),
|
|
124
|
+
);
|
|
125
|
+
for (const context of cartesianProduct(baseListKeys, matrix)) {
|
|
126
|
+
realizations.push({ runner: context[key], resolvable: true, context });
|
|
100
127
|
}
|
|
101
128
|
}
|
|
102
129
|
|
|
103
|
-
// matrix.exclude: remove matches
|
|
130
|
+
// matrix.exclude: remove matches by runner label (first match only).
|
|
131
|
+
// KNOWN LIMITATION (out of scope for #435, tracked as a follow-up): this
|
|
132
|
+
// matches on the runs-on key's runner label rather than the full exclude
|
|
133
|
+
// tuple, so a multi-axis exclude like { os: macos-latest, shell: bash } can
|
|
134
|
+
// remove the wrong cross-product cell. Full GitHub Actions tuple-match
|
|
135
|
+
// (including the include-rows-are-not-excluded rule) is deferred; #435 scopes
|
|
136
|
+
// only the base-list cross-product expansion above.
|
|
104
137
|
if (Array.isArray(matrix.exclude)) {
|
|
105
138
|
for (const excl of matrix.exclude) {
|
|
106
139
|
if (excl && excl[key] != null) {
|