@opengsd/gsd-core 1.13.0 → 1.14.0
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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/bin/install.js +206 -68
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +338 -125
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +79 -67
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -1
- package/gsd-core/bin/lib/check-command-router.cjs +113 -36
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/commands.cjs +650 -72
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +137 -34
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/init.cjs +409 -47
- package/gsd-core/bin/lib/install-engine.cjs +16 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +19 -8
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
- package/gsd-core/bin/lib/phase-id.cjs +161 -22
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +167 -63
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
- package/gsd-core/bin/lib/roadmap.cjs +108 -14
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +142 -28
- package/gsd-core/bin/lib/state.cjs +223 -27
- package/gsd-core/bin/lib/surface.cjs +60 -2
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/uat.cjs +1 -1
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +47 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify.cjs +188 -23
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/tdd.md +5 -2
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/workflows/add-todo.md +3 -2
- package/gsd-core/workflows/autonomous.md +13 -10
- package/gsd-core/workflows/check-todos.md +4 -2
- package/gsd-core/workflows/cleanup.md +3 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
- package/gsd-core/workflows/code-review-fix.md +3 -3
- package/gsd-core/workflows/code-review.md +156 -30
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +39 -262
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +14 -155
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase.md +53 -152
- package/gsd-core/workflows/execute-plan.md +20 -7
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/map-codebase.md +50 -3
- package/gsd-core/workflows/new-milestone.md +54 -12
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +32 -202
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase.md +22 -181
- package/gsd-core/workflows/pr-branch.md +19 -7
- package/gsd-core/workflows/quick.md +8 -1
- package/gsd-core/workflows/reapply-patches.md +77 -3
- package/gsd-core/workflows/settings.md +18 -5
- package/gsd-core/workflows/update.md +7 -5
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +20 -180
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +44 -18
- package/hooks/dist/gsd-statusline.js +11 -7
- package/hooks/dist/gsd-validate-commit.sh +34 -4
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +44 -18
- package/hooks/gsd-statusline.js +11 -7
- package/hooks/gsd-validate-commit.sh +34 -4
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +10 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +7 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
- package/scripts/lint-phase-id-drift.cjs +338 -13
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
- package/scripts/prompt-injection-scan.sh +14 -0
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// ci-next-health.cjs — #4422 base-branch health gate.
|
|
5
|
+
//
|
|
6
|
+
// Risk asymmetry drives this design, same shape as scripts/ci-pr-mergeability.cjs
|
|
7
|
+
// but pointed the other direction: on 2026-09-06 three unrelated PRs merged on
|
|
8
|
+
// top of an already-broken `next` before anyone noticed, because nothing checked
|
|
9
|
+
// the base branch's OWN health before letting a PR land on it. A false positive
|
|
10
|
+
// here (a healthy `next` wrongly reported RED) blocks every PR merge in the repo
|
|
11
|
+
// until a human notices and applies the `fix-next` bypass label — annoying, but
|
|
12
|
+
// loud and immediately actionable. A false negative (a broken `next` reported
|
|
13
|
+
// healthy) reproduces the #4422 incident exactly. So, unlike the mergeability
|
|
14
|
+
// preflight, this gate does NOT fail open on a definite red signal — it fails
|
|
15
|
+
// open only when the signal itself is unavailable or inapplicable (wrong event,
|
|
16
|
+
// no resolvable base ref, an API read that throws). Once GitHub actually answers
|
|
17
|
+
// with a non-success conclusion for the base branch's last push-triggered Tests
|
|
18
|
+
// run, that is treated as authoritative and blocks — with one explicit, visible,
|
|
19
|
+
// human-operated escape hatch (the `fix-next` label) for the PR that is itself
|
|
20
|
+
// the fix-forward.
|
|
21
|
+
//
|
|
22
|
+
// Every non-success conclusion (`failure`, `cancelled`, `timed_out`,
|
|
23
|
+
// `action_required`, ...) is treated as RED, not just `failure` — a cancelled or
|
|
24
|
+
// timed-out run on the base branch is not evidence the branch is healthy, it is
|
|
25
|
+
// evidence nobody knows yet.
|
|
26
|
+
|
|
27
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
28
|
+
|
|
29
|
+
const VERDICT = Object.freeze({
|
|
30
|
+
CLEAN: 'CLEAN',
|
|
31
|
+
RED: 'RED',
|
|
32
|
+
BYPASSED: 'BYPASSED',
|
|
33
|
+
SKIPPED_NOT_APPLICABLE: 'SKIPPED_NOT_APPLICABLE',
|
|
34
|
+
INDETERMINATE: 'INDETERMINATE',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const APPLICABLE_EVENTS = new Set(['pull_request', 'merge_group']);
|
|
38
|
+
const BYPASS_LABEL = 'fix-next';
|
|
39
|
+
const TESTS_WORKFLOW_FILE = 'test.yml';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pure, total classifier: the GitHub "list workflow runs" response payload ->
|
|
43
|
+
* CLEAN | RED | INDETERMINATE. Never throws.
|
|
44
|
+
*
|
|
45
|
+
* - Not an object, or `workflow_runs` isn't an array -> INDETERMINATE: the
|
|
46
|
+
* shape we depend on is not present, so nothing can be concluded.
|
|
47
|
+
* - No completed push runs found yet (e.g. a brand-new `release/**`/`hotfix/**`
|
|
48
|
+
* branch with no Tests history) -> CLEAN: absence of evidence of red is not
|
|
49
|
+
* evidence of red, and a brand-new branch must not be permanently unmergeable.
|
|
50
|
+
* - The most recent run's `conclusion === 'success'` -> CLEAN.
|
|
51
|
+
* - Anything else (failure, cancelled, timed_out, action_required, ...) -> RED.
|
|
52
|
+
*/
|
|
53
|
+
function classifyRunConclusion(payload) {
|
|
54
|
+
if (payload === null || typeof payload !== 'object') return VERDICT.INDETERMINATE;
|
|
55
|
+
if (!Array.isArray(payload.workflow_runs)) return VERDICT.INDETERMINATE;
|
|
56
|
+
if (payload.workflow_runs.length === 0) return VERDICT.CLEAN;
|
|
57
|
+
const [latest] = payload.workflow_runs;
|
|
58
|
+
if (latest && latest.conclusion === 'success') return VERDICT.CLEAN;
|
|
59
|
+
return VERDICT.RED;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Dependency-injected orchestrator. `fetchLatestRun` is an async function
|
|
64
|
+
* taking the resolved base branch name and returning the parsed API payload
|
|
65
|
+
* (or throwing) — injected so tests never touch the network.
|
|
66
|
+
*
|
|
67
|
+
* @returns {Promise<{verdict:string, payload:*, reason:string}>}
|
|
68
|
+
*/
|
|
69
|
+
async function resolveNextHealth({ fetchLatestRun, eventName, baseRef, labels } = {}) {
|
|
70
|
+
if (!APPLICABLE_EVENTS.has(eventName)) {
|
|
71
|
+
return { verdict: VERDICT.SKIPPED_NOT_APPLICABLE, payload: null, reason: 'not-applicable-event' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof baseRef !== 'string' || baseRef.trim() === '') {
|
|
75
|
+
return { verdict: VERDICT.INDETERMINATE, payload: null, reason: 'no-base-ref' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let payload = null;
|
|
79
|
+
try {
|
|
80
|
+
payload = await fetchLatestRun(baseRef);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return { verdict: VERDICT.INDETERMINATE, payload: null, reason: `fetch-failed: ${err.message}` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const classified = classifyRunConclusion(payload);
|
|
86
|
+
if (classified !== VERDICT.RED) {
|
|
87
|
+
return { verdict: classified, payload, reason: 'resolved' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Escape hatch. `labels` is empty/absent for merge_group — that event has no
|
|
91
|
+
// label surface today, which means a queued merge-group commit cannot use
|
|
92
|
+
// this bypass. Known gap, not solved here: a maintainer must land the
|
|
93
|
+
// fix-forward as a direct pull_request merge (where the label IS readable)
|
|
94
|
+
// rather than through the merge queue, until GitHub exposes an equivalent
|
|
95
|
+
// signal for merge_group.
|
|
96
|
+
const labelList = Array.isArray(labels) ? labels : [];
|
|
97
|
+
if (labelList.includes(BYPASS_LABEL)) {
|
|
98
|
+
return { verdict: VERDICT.BYPASSED, payload, reason: 'bypass-label' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { verdict: VERDICT.RED, payload, reason: 'red' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function usage() {
|
|
105
|
+
return [
|
|
106
|
+
'Usage:',
|
|
107
|
+
' node scripts/ci-next-health.cjs',
|
|
108
|
+
'',
|
|
109
|
+
'CI-only gate: checks whether the PR/merge-group\'s base branch\'s own last',
|
|
110
|
+
'push-triggered Tests run is red, and fails the job (exit 1) when it is —',
|
|
111
|
+
'unless the PR carries the "fix-next" bypass label. Every other case',
|
|
112
|
+
'(wrong event, no resolvable base ref, an unreadable API read) fails open',
|
|
113
|
+
'(exit 0).',
|
|
114
|
+
'',
|
|
115
|
+
'Environment variables read:',
|
|
116
|
+
' GITHUB_EVENT_NAME workflow trigger event; only "pull_request" and',
|
|
117
|
+
' "merge_group" are checked',
|
|
118
|
+
' GITHUB_REPOSITORY owner/repo',
|
|
119
|
+
' GITHUB_TOKEN optional bearer token for the API read',
|
|
120
|
+
' GITHUB_BASE_REF PR base branch name (pull_request events; set',
|
|
121
|
+
' automatically by GitHub Actions)',
|
|
122
|
+
' MERGE_GROUP_BASE_REF merge-group base ref (merge_group events; the',
|
|
123
|
+
' caller workflow must populate this from',
|
|
124
|
+
' github.event.merge_group.base_ref — a leading',
|
|
125
|
+
' "refs/heads/" prefix is stripped if present)',
|
|
126
|
+
' GITHUB_API_URL GitHub API base URL (default: https://api.github.com)',
|
|
127
|
+
' PR_LABELS comma-separated PR label names (pull_request events;',
|
|
128
|
+
' the caller workflow must populate this from',
|
|
129
|
+
' github.event.pull_request.labels.*.name); checked',
|
|
130
|
+
' for the literal "fix-next" bypass label',
|
|
131
|
+
' GITHUB_OUTPUT path to append verdict= step output to',
|
|
132
|
+
' GITHUB_STEP_SUMMARY path to append a human-readable summary to',
|
|
133
|
+
].join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseArgs(argv) {
|
|
137
|
+
for (const arg of argv) {
|
|
138
|
+
if (arg === '--help' || arg === '-h') {
|
|
139
|
+
process.stdout.write(`${usage()}\n`);
|
|
140
|
+
throw new ExitError(0);
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function writeOutput(lines) {
|
|
147
|
+
const outputPath = process.env.GITHUB_OUTPUT;
|
|
148
|
+
if (typeof outputPath !== 'string' || outputPath === '') return;
|
|
149
|
+
try {
|
|
150
|
+
const fs = require('node:fs');
|
|
151
|
+
fs.appendFileSync(outputPath, `${lines.join('\n')}\n`);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
// A failure while REPORTING the verdict must never invert the gate.
|
|
154
|
+
process.stderr.write(`::warning::failed to write GITHUB_OUTPUT: ${err.message}\n`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function writeSummary(text) {
|
|
159
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
160
|
+
if (typeof summaryPath !== 'string' || summaryPath === '') return;
|
|
161
|
+
try {
|
|
162
|
+
const fs = require('node:fs');
|
|
163
|
+
fs.appendFileSync(summaryPath, `${text}\n`);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
process.stderr.write(`::warning::failed to write GITHUB_STEP_SUMMARY: ${err.message}\n`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveBaseRef(eventName) {
|
|
170
|
+
if (eventName === 'pull_request') {
|
|
171
|
+
return process.env.GITHUB_BASE_REF || '';
|
|
172
|
+
}
|
|
173
|
+
if (eventName === 'merge_group') {
|
|
174
|
+
const raw = process.env.MERGE_GROUP_BASE_REF || '';
|
|
175
|
+
return raw.startsWith('refs/heads/') ? raw.slice('refs/heads/'.length) : raw;
|
|
176
|
+
}
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseLabels(raw) {
|
|
181
|
+
if (typeof raw !== 'string' || raw.trim() === '') return [];
|
|
182
|
+
return raw.split(',').map((s) => s.trim()).filter((s) => s !== '');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function runUrlOf(payload) {
|
|
186
|
+
const run = payload && Array.isArray(payload.workflow_runs) ? payload.workflow_runs[0] : undefined;
|
|
187
|
+
return run && typeof run.html_url === 'string' ? run.html_url : '(unknown run URL)';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// `argv` defaults to real CLI argv but is a parameter so tests can call
|
|
191
|
+
// main() in-process (e.g. main([])) without inheriting the test runner's own
|
|
192
|
+
// argv, which would otherwise trip parseArgs's "unknown argument" branch.
|
|
193
|
+
async function main(argv = process.argv.slice(2)) {
|
|
194
|
+
parseArgs(argv);
|
|
195
|
+
|
|
196
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
197
|
+
const repo = process.env.GITHUB_REPOSITORY || '';
|
|
198
|
+
const token = process.env.GITHUB_TOKEN || '';
|
|
199
|
+
const apiBase = process.env.GITHUB_API_URL || 'https://api.github.com';
|
|
200
|
+
const baseRef = resolveBaseRef(eventName);
|
|
201
|
+
const labels = parseLabels(process.env.PR_LABELS);
|
|
202
|
+
|
|
203
|
+
const fetchLatestRun = async (branch) => {
|
|
204
|
+
const url = `${apiBase}/repos/${repo}/actions/workflows/${TESTS_WORKFLOW_FILE}/runs`
|
|
205
|
+
+ `?branch=${encodeURIComponent(branch)}&event=push&status=completed&per_page=1`;
|
|
206
|
+
const headers = {
|
|
207
|
+
accept: 'application/vnd.github+json',
|
|
208
|
+
'x-github-api-version': '2022-11-28',
|
|
209
|
+
'user-agent': 'gsd-core-ci-next-health',
|
|
210
|
+
};
|
|
211
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
212
|
+
const response = await fetch(url, { headers, signal: AbortSignal.timeout(10000) });
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
throw new Error(`GitHub API responded ${response.status}`);
|
|
215
|
+
}
|
|
216
|
+
const text = await response.text();
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(text);
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const result = await resolveNextHealth({ fetchLatestRun, eventName, baseRef, labels });
|
|
225
|
+
|
|
226
|
+
writeOutput([`verdict=${result.verdict}`]);
|
|
227
|
+
writeSummary(`Base branch health: ${result.verdict}`);
|
|
228
|
+
|
|
229
|
+
if (result.verdict === VERDICT.RED) {
|
|
230
|
+
const runUrl = runUrlOf(result.payload);
|
|
231
|
+
process.stderr.write(
|
|
232
|
+
`::error::the base branch "${baseRef}"'s own last Tests run is red: ${runUrl} — `
|
|
233
|
+
+ 'wait for a fix-forward merge, or if THIS pull request is the fix, ask a '
|
|
234
|
+
+ `maintainer to apply the "${BYPASS_LABEL}" label to explicitly override this gate.\n`,
|
|
235
|
+
);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (result.verdict === VERDICT.BYPASSED) {
|
|
240
|
+
const runUrl = runUrlOf(result.payload);
|
|
241
|
+
process.stderr.write(
|
|
242
|
+
`::warning::the base branch "${baseRef}"'s own last Tests run is red: ${runUrl} — `
|
|
243
|
+
+ `a human applied the "${BYPASS_LABEL}" label to explicitly override this gate.\n`,
|
|
244
|
+
);
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (result.verdict === VERDICT.INDETERMINATE) {
|
|
249
|
+
process.stderr.write(
|
|
250
|
+
`::warning::could not determine "${baseRef || '(no base ref)'}"'s Tests health (${result.reason}); `
|
|
251
|
+
+ 'proceeding (fail-open).\n',
|
|
252
|
+
);
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
process.stdout.write(`Base branch health: ${result.verdict}\n`);
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (require.main === module) {
|
|
261
|
+
runMain(main);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
VERDICT,
|
|
266
|
+
BYPASS_LABEL,
|
|
267
|
+
TESTS_WORKFLOW_FILE,
|
|
268
|
+
classifyRunConclusion,
|
|
269
|
+
resolveNextHealth,
|
|
270
|
+
main,
|
|
271
|
+
};
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// Shell-agnostic: invoked as `node scripts/ci-prepare-test-scope.cjs` from any shell.
|
|
5
5
|
//
|
|
6
6
|
// Required environment variables (set by the workflow step's `env:` block):
|
|
7
|
-
// TEST_SCOPE — "
|
|
7
|
+
// TEST_SCOPE — "targeted" (the `windows` scope was retired by #4641:
|
|
8
|
+
// test-conformance is now the sole Windows selector)
|
|
8
9
|
// TARGETED_TESTS — space-separated test file list (from ci-test-scope.cjs output)
|
|
9
|
-
// WINDOWS_TESTS — space-separated test file list for the windows lane
|
|
10
10
|
//
|
|
11
11
|
// Writes: .ci-selected-tests.txt (one file per line, no blanks)
|
|
12
12
|
// Exit 0 = success; exit 1 = unknown scope.
|
|
@@ -46,13 +46,14 @@ function isResolvable(entry, root) {
|
|
|
46
46
|
|
|
47
47
|
// Resolve the scoped test selection for the lane. Pure (no I/O beyond the
|
|
48
48
|
// existence probe under `root`) so it can be unit-tested directly.
|
|
49
|
-
function resolveSelection({ scope, targeted,
|
|
49
|
+
function resolveSelection({ scope, targeted, root }) {
|
|
50
50
|
let selected;
|
|
51
|
-
if (scope === '
|
|
52
|
-
selected = windows;
|
|
53
|
-
} else if (scope === 'targeted') {
|
|
51
|
+
if (scope === 'targeted') {
|
|
54
52
|
selected = targeted;
|
|
55
53
|
} else {
|
|
54
|
+
// #4641: the `windows` scope was retired (test-conformance is now the
|
|
55
|
+
// sole Windows selector) — an unknown/retired scope must fail loudly
|
|
56
|
+
// rather than silently selecting nothing.
|
|
56
57
|
throw new ExitError(1, `::error::Unknown test scope: ${scope}`);
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -75,7 +76,6 @@ function main() {
|
|
|
75
76
|
const lines = resolveSelection({
|
|
76
77
|
scope: process.env.TEST_SCOPE || '',
|
|
77
78
|
targeted: process.env.TARGETED_TESTS || '',
|
|
78
|
-
windows: process.env.WINDOWS_TESTS || '',
|
|
79
79
|
root,
|
|
80
80
|
});
|
|
81
81
|
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execFileSync } = require('child_process');
|
|
6
|
-
const { existsSync, readdirSync, appendFileSync } = require('fs');
|
|
6
|
+
const { existsSync, readdirSync, appendFileSync, readFileSync } = require('fs');
|
|
7
7
|
|
|
8
8
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
9
|
+
const { classifyContent, NOISY_FOR_SOURCE_REACHABILITY } = require('./gen-platform-conformance-tier.cjs');
|
|
9
10
|
|
|
10
11
|
// Workflow files that are purely administrative / policy bots. Changes to these
|
|
11
12
|
// files do NOT require the cross-platform test matrix — only a lightweight
|
|
@@ -484,12 +485,68 @@ function addAll(set, values) {
|
|
|
484
485
|
// fullMatrix=true, so the full Windows lane already runs when those paths
|
|
485
486
|
// change. The old six-hint list pulled 102 of ~633 test files into the scoped
|
|
486
487
|
// windows lane, turning it into a ~10-minute job on every PR.
|
|
488
|
+
// #4641: the scoped windows lane itself is gone. These hints now drive
|
|
489
|
+
// full_matrix instead — a matched RULE whose tests[] includes a
|
|
490
|
+
// windows-hint filename AND that hinted file is itself in the conformance
|
|
491
|
+
// tier (per reachesConformanceTierOrSeam) escalates straight to full_matrix
|
|
492
|
+
// (routed to test-conformance, the sole Windows selector) rather than
|
|
493
|
+
// feeding a side lane. Tier-backed on purpose: full_matrix only ever runs
|
|
494
|
+
// CONFORMANCE_TIER_FILES, so a hint on a non-tier file would cost 4 CI jobs
|
|
495
|
+
// with zero Windows coverage of that file. See
|
|
496
|
+
// docs/adr/4641-windows-selector-consolidation.md.
|
|
487
497
|
const WINDOWS_HINTS = ['windows', 'win32', 'shell', 'path'];
|
|
488
498
|
const isWindowsHint = s => WINDOWS_HINTS.some(k => s.toLowerCase().includes(k));
|
|
489
499
|
|
|
490
|
-
|
|
500
|
+
// A change to the classification mechanism itself cannot be presumed safe by
|
|
501
|
+
// the very mechanism being changed (#4592).
|
|
502
|
+
const CLASSIFIER_DEFINITION_FILES = new Set([
|
|
503
|
+
'scripts/gen-platform-conformance-tier.cjs',
|
|
504
|
+
'scripts/lib/platform-conformance-tier.generated.cjs',
|
|
505
|
+
'scripts/lib/suite-detection.cjs',
|
|
506
|
+
]);
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Does `file`'s blast radius reach (a) Phase 2's conformance-tier test-file
|
|
510
|
+
* list or (b) a live platform-conditional signal in src/? Fail-safe: any
|
|
511
|
+
* thrown error (a require failure, a readFileSync failure, a malformed
|
|
512
|
+
* generated module, etc.) is treated as uncertainty and returns true — per
|
|
513
|
+
* #4592's explicit "fail-safe to full_matrix=true on any reachability-
|
|
514
|
+
* computation error or uncertainty" requirement.
|
|
515
|
+
* `loadConformanceTier` is injectable (defaults to the real generated module)
|
|
516
|
+
* solely so tests can simulate a load failure without touching the real,
|
|
517
|
+
* committed generated file.
|
|
518
|
+
* @param {string} file
|
|
519
|
+
* @param {{loadConformanceTier?: () => {CONFORMANCE_TIER_FILES: string[]}}} [deps]
|
|
520
|
+
* @returns {boolean}
|
|
521
|
+
*/
|
|
522
|
+
function reachesConformanceTierOrSeam(file, deps = {}) {
|
|
523
|
+
const loadConformanceTier =
|
|
524
|
+
deps.loadConformanceTier || (() => require('./lib/platform-conformance-tier.generated.cjs'));
|
|
525
|
+
try {
|
|
526
|
+
if (file.startsWith('tests/') && file.endsWith('.test.cjs')) {
|
|
527
|
+
const { CONFORMANCE_TIER_FILES } = loadConformanceTier();
|
|
528
|
+
return CONFORMANCE_TIER_FILES.includes(file);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (file.startsWith('src/')) {
|
|
532
|
+
const content = readFileSync(file, 'utf8');
|
|
533
|
+
const { signals } = classifyContent(content);
|
|
534
|
+
const narrowSignals = signals.filter(signal => !NOISY_FOR_SOURCE_REACHABILITY.has(signal));
|
|
535
|
+
return narrowSignals.length > 0;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return false;
|
|
539
|
+
} catch {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// `reachabilityDeps` is injectable (defaults to {}, which makes
|
|
545
|
+
// reachesConformanceTierOrSeam use the real generated module) solely so
|
|
546
|
+
// tests can simulate a reachability-computation failure without touching
|
|
547
|
+
// the real, committed generated file.
|
|
548
|
+
function classify(files, reachabilityDeps = {}) {
|
|
491
549
|
const targeted = new Set();
|
|
492
|
-
const windows = new Set();
|
|
493
550
|
const reasons = [];
|
|
494
551
|
let productOrPipelineChanged = false; // product/pipeline code (excludes docs)
|
|
495
552
|
let inertCiChanged = false; // inert workflow files
|
|
@@ -526,15 +583,35 @@ function classify(files) {
|
|
|
526
583
|
|
|
527
584
|
if (file.startsWith('tests/') && file.endsWith('.test.cjs')) {
|
|
528
585
|
targeted.add(file);
|
|
529
|
-
// #494
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
586
|
+
// #494 originally narrowed this to skip full_matrix for changed test
|
|
587
|
+
// files, on the theory that ubuntu targeted_tests + the scoped windows
|
|
588
|
+
// lane already covered them. Rescinded per #4421: PR #4384 landed a
|
|
589
|
+
// macOS-only regression on 2026-09-06 that stayed invisible pre-merge
|
|
590
|
+
// precisely because this carve-out suppressed the only macOS signal.
|
|
591
|
+
// #4592: the blanket rule is replaced with a reachability check — only
|
|
592
|
+
// a changed test file that actually reaches Phase 2's conformance-tier
|
|
593
|
+
// list (or is the classification mechanism itself) forces full_matrix.
|
|
594
|
+
if (reachesConformanceTierOrSeam(file, reachabilityDeps)) {
|
|
595
|
+
fullMatrix = true;
|
|
596
|
+
reasons.push(`${file}: conformance-tier reachability`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// #4592: a src/-only diff (no test file touched) must still be able to
|
|
601
|
+
// set full_matrix=true when it carries a live platform-conditional
|
|
602
|
+
// signal — this is independent of the tests/ branch above.
|
|
603
|
+
if (file.startsWith('src/') && reachesConformanceTierOrSeam(file, reachabilityDeps)) {
|
|
604
|
+
fullMatrix = true;
|
|
605
|
+
reasons.push(`${file}: platform seam reachability`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// #4592: a changed file that IS the classification mechanism itself
|
|
609
|
+
// (neither under tests/ nor src/, so neither branch above reaches it)
|
|
610
|
+
// must also force full_matrix — a change to the mechanism cannot be
|
|
611
|
+
// presumed safe by the very mechanism being changed.
|
|
612
|
+
if (CLASSIFIER_DEFINITION_FILES.has(file)) {
|
|
613
|
+
fullMatrix = true;
|
|
614
|
+
reasons.push(`${file}: reachability classifier definition changed`);
|
|
538
615
|
}
|
|
539
616
|
|
|
540
617
|
for (const rule of RULES) {
|
|
@@ -542,6 +619,32 @@ function classify(files) {
|
|
|
542
619
|
addAll(targeted, rule.tests);
|
|
543
620
|
reasons.push(`${file}: ${rule.name}`);
|
|
544
621
|
if (rule.fullMatrix) fullMatrix = true;
|
|
622
|
+
// #4641: a rule that pulls in a test file matching a Windows-sensitive
|
|
623
|
+
// filename hint is the one non-redundant residue of the deleted
|
|
624
|
+
// scoped windows lane — escalate to full_matrix (test-conformance)
|
|
625
|
+
// instead of feeding a side lane. TIER-BACKED (measured): full_matrix
|
|
626
|
+
// routes to test-conformance, which runs ONLY CONFORMANCE_TIER_FILES —
|
|
627
|
+
// escalating on the filename hint alone can fire on a hinted test that
|
|
628
|
+
// isn't in that tier, costing 4 CI jobs while never actually running it
|
|
629
|
+
// on Windows. The predicate below requires BOTH the hint AND tier
|
|
630
|
+
// membership (via reachesConformanceTierOrSeam, the same fail-safe
|
|
631
|
+
// reachability helper used elsewhere in this file, so error/uncertainty
|
|
632
|
+
// behavior stays identical). Measured: over the 16 RULES entries this
|
|
633
|
+
// narrowed form fires on the same rules as the un-narrowed form today
|
|
634
|
+
// (no behavior change now, correct-by-construction going forward). The
|
|
635
|
+
// broader alternative — escalate on ANY tier member a rule pulls in,
|
|
636
|
+
// ignoring the filename hint — was measured and rejected: it fires on
|
|
637
|
+
// 14 of 16 rules, newly escalating most ordinary product-code PRs
|
|
638
|
+
// (src/, agents/, commands/, hooks/, skills/, config paths). Only one
|
|
639
|
+
// rule's escalation is live today: 'portability lint rules (ADR-1703)'.
|
|
640
|
+
// Four others already had fullMatrix: true (no-op here), and 'inert
|
|
641
|
+
// CI''s escalation is overridden downstream by the inert-CI reset.
|
|
642
|
+
// Distinct, greppable reason so the conformance-lane coverage for it
|
|
643
|
+
// is traceable per rule.
|
|
644
|
+
if (rule.tests.some(t => isWindowsHint(t) && reachesConformanceTierOrSeam(t, reachabilityDeps))) {
|
|
645
|
+
fullMatrix = true;
|
|
646
|
+
reasons.push(`${file}: ${rule.name} (windows-hint rule test, #4641)`);
|
|
647
|
+
}
|
|
545
648
|
}
|
|
546
649
|
}
|
|
547
650
|
}
|
|
@@ -555,7 +658,7 @@ function classify(files) {
|
|
|
555
658
|
// covered by .github/workflows/install-smoke.yml
|
|
556
659
|
'tests/release-tarball-smoke.install.test.cjs',
|
|
557
660
|
]);
|
|
558
|
-
for (const f of SCOPED_LANE_EXCLUDE) { targeted.delete(f);
|
|
661
|
+
for (const f of SCOPED_LANE_EXCLUDE) { targeted.delete(f); }
|
|
559
662
|
|
|
560
663
|
// code_changed: true when product/pipeline OR inert CI changed.
|
|
561
664
|
// Docs-only PRs (neither flag set) get code_changed=false → full matrix skip.
|
|
@@ -569,8 +672,6 @@ function classify(files) {
|
|
|
569
672
|
targetedTests.push('unit');
|
|
570
673
|
}
|
|
571
674
|
|
|
572
|
-
const windowsTests = existingTests([...new Set([...windows, ...targetedTests.filter(isWindowsHint)])].sort());
|
|
573
|
-
|
|
574
675
|
// Inert-CI-only: full_matrix must be false (override any RULES that fired).
|
|
575
676
|
if (inertCiChanged && !productOrPipelineChanged) {
|
|
576
677
|
fullMatrix = false;
|
|
@@ -579,12 +680,11 @@ function classify(files) {
|
|
|
579
680
|
// Normalize: when code_changed is false, the output must be self-consistent.
|
|
580
681
|
// A docs file can coincidentally match a coarse content RULE (e.g. docs/installer-migrations.md
|
|
581
682
|
// matches the installer rule via path.includes('install')), leaving full_matrix=true and
|
|
582
|
-
// non-empty targeted_tests
|
|
683
|
+
// non-empty targeted_tests. The workflow skips correctly (gated on code_changed)
|
|
583
684
|
// but the output object would be self-contradictory. Force a clean "nothing to run" result.
|
|
584
685
|
if (!codeChanged) {
|
|
585
686
|
fullMatrix = false;
|
|
586
687
|
targetedTests.length = 0;
|
|
587
|
-
windowsTests.length = 0;
|
|
588
688
|
}
|
|
589
689
|
|
|
590
690
|
return {
|
|
@@ -592,7 +692,6 @@ function classify(files) {
|
|
|
592
692
|
product_changed: productOrPipelineChanged,
|
|
593
693
|
full_matrix: fullMatrix,
|
|
594
694
|
targeted_tests: targetedTests,
|
|
595
|
-
windows_tests: windowsTests,
|
|
596
695
|
reasons: [...new Set(reasons)].sort(),
|
|
597
696
|
};
|
|
598
697
|
}
|
|
@@ -604,7 +703,6 @@ function writeOutputs(result) {
|
|
|
604
703
|
`product_changed=${result.product_changed}`,
|
|
605
704
|
`full_matrix=${result.full_matrix}`,
|
|
606
705
|
`targeted_tests=${result.targeted_tests.join(' ')}`,
|
|
607
|
-
`windows_tests=${result.windows_tests.join(' ')}`,
|
|
608
706
|
];
|
|
609
707
|
appendFileSync(process.env.GITHUB_OUTPUT, `${lines.join('\n')}\n`);
|
|
610
708
|
}
|
|
@@ -630,4 +728,12 @@ if (require.main === module) {
|
|
|
630
728
|
runMain(main);
|
|
631
729
|
}
|
|
632
730
|
|
|
633
|
-
module.exports = {
|
|
731
|
+
module.exports = {
|
|
732
|
+
RULES,
|
|
733
|
+
missingRuleTestFiles,
|
|
734
|
+
PROTECTED_WORKFLOWS,
|
|
735
|
+
INERT_WORKFLOWS,
|
|
736
|
+
missingProtectedWorkflows,
|
|
737
|
+
classify,
|
|
738
|
+
reachesConformanceTierOrSeam,
|
|
739
|
+
};
|
|
@@ -33,8 +33,8 @@ const WORKFLOWS_DIR = path.join(__dirname, '..', '.github', 'workflows');
|
|
|
33
33
|
const JOB_RULES = [
|
|
34
34
|
{ workflowFile: 'test.yml', jobKey: 'test-inert', test: (name) => name === 'test (inert CI)' },
|
|
35
35
|
{ workflowFile: 'test.yml', jobKey: 'test', test: (name) => name.startsWith('test (') && name !== 'test (inert CI)' },
|
|
36
|
-
{ workflowFile: 'test.yml', jobKey: 'test-full', test: (name) => name.startsWith('full test (') },
|
|
37
36
|
{ workflowFile: 'test.yml', jobKey: 'coverage-gate', test: (name) => name === 'Coverage gate (merged shards)' },
|
|
37
|
+
{ workflowFile: 'test.yml', jobKey: 'test-conformance', test: (name) => name.startsWith('conformance test (') },
|
|
38
38
|
{ workflowFile: 'install-smoke.yml', jobKey: 'smoke', test: (name) => name.startsWith('smoke (') },
|
|
39
39
|
];
|
|
40
40
|
|
|
@@ -72,7 +72,7 @@ function isShipped(diffPath, shipPrefixes) {
|
|
|
72
72
|
// forward slashes, but a developer running this locally on a different
|
|
73
73
|
// tool's output shouldn't get a false negative).
|
|
74
74
|
const p = diffPath.replace(/\\/g, '/');
|
|
75
|
-
return shipPrefixes.some((s) => p === s || p.startsWith(s + '/'));
|
|
75
|
+
return shipPrefixes.some((s) => p === s || p.startsWith(s + '/')); // allow-handrolled-containment: shipped-path filtering for a CI check, not a safety decision
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// #2980: commits that touch `.github/workflows/*` cannot be cherry-picked
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
*
|
|
10
10
|
* A prior version of this PR added a `docs guards` RULE to `RULES` in
|
|
11
11
|
* scripts/ci-test-scope.cjs, on the theory that classify()'s `!codeChanged`
|
|
12
|
-
* normalization (which
|
|
13
|
-
* docs-only diffs) made the RULE inert to classify()'s
|
|
12
|
+
* normalization (which at the time zeroed fullMatrix/targeted_tests/
|
|
13
|
+
* windows_tests for docs-only diffs) made the RULE inert to classify()'s
|
|
14
|
+
* scope decision. (#4641 later removed `windows_tests` from classify()'s
|
|
15
|
+
* output entirely; the normalization today only zeroes fullMatrix and
|
|
16
|
+
* targeted_tests. This paragraph is historical narration of a rejected
|
|
17
|
+
* design, not a description of current behaviour.)
|
|
14
18
|
*
|
|
15
19
|
* That is true for docs-ONLY diffs and FALSE for MIXED docs+code diffs:
|
|
16
20
|
* `codeChanged` is true whenever ANY changed file is product/pipeline code,
|
|
@@ -193,6 +197,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
193
197
|
// (commit-files-pathspec.test.cjs:1618) — cannot be resolved to specific
|
|
194
198
|
// files without re-deriving the scan's own file-discovery logic.
|
|
195
199
|
'tests/commit-files-pathspec.test.cjs': ['*'],
|
|
200
|
+
'tests/compact-content-4139.test.cjs': ['docs/CONFIGURATION.md'],
|
|
196
201
|
'tests/config-field-docs.test.cjs': ['docs/CONFIGURATION.md'],
|
|
197
202
|
'tests/config.test.cjs': ['docs/CONFIGURATION.md'],
|
|
198
203
|
'tests/context-index-sync.test.cjs': ['docs/CONTEXT-INDEX.json'],
|
|
@@ -31,6 +31,7 @@ const path = require('node:path');
|
|
|
31
31
|
|
|
32
32
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
33
33
|
const { escapeRegex: escapeRegExp } = require('../gsd-core/bin/lib/pattern.cjs');
|
|
34
|
+
const { isContainedIn } = require('../gsd-core/bin/lib/security.cjs');
|
|
34
35
|
|
|
35
36
|
const ROOT = path.resolve(__dirname, '..');
|
|
36
37
|
const ADR_DIR = path.join(ROOT, 'docs', 'adr');
|
|
@@ -176,10 +177,15 @@ const ADR_FILENAME_RE = /^[0-9]+-[a-z0-9-]+\.md$/;
|
|
|
176
177
|
* segment merely BEGINS with two dots (`..hidden.md`), and a false "escapes
|
|
177
178
|
* the repository" on a valid path is a worse failure than a miss — hence the
|
|
178
179
|
* whole-segment `rel === '..' || rel.startsWith('..' + sep)` form.
|
|
180
|
+
*
|
|
181
|
+
* Delegates to the shared `isContainedIn` (ADR-4650): every call site here
|
|
182
|
+
* already resolved both operands itself (either lexically, before any
|
|
183
|
+
* filesystem call, or via realpathSync after a symlink) and needs only this
|
|
184
|
+
* comparison step — the exact already-resolved-caller case `isContainedIn`
|
|
185
|
+
* documents.
|
|
179
186
|
*/
|
|
180
187
|
function escapesRoot(abs, root) {
|
|
181
|
-
|
|
182
|
-
return rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);
|
|
188
|
+
return !isContainedIn(abs, root);
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
/**
|
|
@@ -91,6 +91,18 @@ const NESTED_FAMILIES = [
|
|
|
91
91
|
subdir: 'steps',
|
|
92
92
|
filter: (f) => f.endsWith('.md'),
|
|
93
93
|
},
|
|
94
|
+
{
|
|
95
|
+
name: 'workflow_detail',
|
|
96
|
+
root: path.join(ROOT, 'gsd-core', 'workflows'),
|
|
97
|
+
subdir: 'detail',
|
|
98
|
+
filter: (f) => f.endsWith('.md'),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'workflow_templates',
|
|
102
|
+
root: path.join(ROOT, 'gsd-core', 'workflows'),
|
|
103
|
+
subdir: 'templates',
|
|
104
|
+
filter: (f) => f.endsWith('.md'),
|
|
105
|
+
},
|
|
94
106
|
];
|
|
95
107
|
|
|
96
108
|
/**
|