@opengsd/gsd-core 1.18.0 → 1.20.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 +2 -1
- package/.opencode/plugins/gsd-core.js +2 -3
- package/agents/gsd-code-fixer.compact.md +7 -7
- package/agents/gsd-code-fixer.md +13 -7
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +179 -30
- package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
- package/gsd-core/bin/lib/args-terminator.cjs +114 -0
- package/gsd-core/bin/lib/broken-windows.cjs +59 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +376 -103
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +32 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +13 -3
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +45 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
- package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
- package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
- package/gsd-core/bin/lib/host-integration.cjs +13 -6
- package/gsd-core/bin/lib/init.cjs +245 -68
- package/gsd-core/bin/lib/install-engine.cjs +246 -32
- package/gsd-core/bin/lib/install-profiles.cjs +9 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
- package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
- package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
- package/gsd-core/bin/lib/milestone.cjs +61 -51
- package/gsd-core/bin/lib/model-catalog.cjs +8 -0
- package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +629 -38
- package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +3066 -653
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
- package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
- package/gsd-core/bin/lib/roadmap.cjs +383 -123
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
- package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
- package/gsd-core/bin/lib/smart-entry.cjs +35 -11
- package/gsd-core/bin/lib/state-contract.cjs +39 -15
- package/gsd-core/bin/lib/state-document.cjs +344 -27
- package/gsd-core/bin/lib/state-transition.cjs +156 -32
- package/gsd-core/bin/lib/state.cjs +527 -186
- package/gsd-core/bin/lib/surface.cjs +9 -0
- package/gsd-core/bin/lib/task-command-router.cjs +5 -25
- package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +4 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/tdd.md +2 -2
- package/gsd-core/templates/DEBUG.md +16 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/audit-fix.md +2 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +64 -7
- package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
- package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
- package/gsd-core/workflows/complete-milestone.md +37 -150
- package/gsd-core/workflows/diagnose-issues.md +29 -5
- package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
- package/gsd-core/workflows/discuss-phase-power.md +19 -2
- package/gsd-core/workflows/discuss-phase.md +6 -10
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
- package/gsd-core/workflows/execute-phase.md +10 -6
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/health.md +2 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +14 -14
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +70 -154
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +15 -4
- package/gsd-core/workflows/spec-phase.md +3 -6
- package/gsd-core/workflows/verify-work.md +4 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +37 -3
- package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
- package/hooks/dist/gsd-workflow-guard.js +3 -2
- package/hooks/dist/gsd-worktree-path-guard.js +29 -2
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/dist/lib/isolation-sentinel.js +44 -0
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +11 -1
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +37 -3
- package/hooks/gsd-windsurf-pre-write.js +29 -2
- package/hooks/gsd-workflow-guard.js +3 -2
- package/hooks/gsd-worktree-path-guard.js +29 -2
- package/hooks/hooks.json +1 -0
- package/hooks/lib/git-probe.js +16 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +15 -8
- package/pi/gsd.cjs +1047 -10
- package/pi/package-agents/gsd-advisor-researcher.md +113 -0
- package/pi/package-agents/gsd-ai-researcher.md +111 -0
- package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
- package/pi/package-agents/gsd-code-fixer.md +774 -0
- package/pi/package-agents/gsd-code-reviewer.md +441 -0
- package/pi/package-agents/gsd-codebase-mapper.md +850 -0
- package/pi/package-agents/gsd-debug-session-manager.md +416 -0
- package/pi/package-agents/gsd-debugger.md +1274 -0
- package/pi/package-agents/gsd-doc-classifier.md +269 -0
- package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
- package/pi/package-agents/gsd-doc-verifier.md +212 -0
- package/pi/package-agents/gsd-doc-writer.md +626 -0
- package/pi/package-agents/gsd-dom-verifier.md +164 -0
- package/pi/package-agents/gsd-domain-researcher.md +144 -0
- package/pi/package-agents/gsd-eval-auditor.md +185 -0
- package/pi/package-agents/gsd-eval-planner.md +149 -0
- package/pi/package-agents/gsd-executor.md +898 -0
- package/pi/package-agents/gsd-framework-selector.md +159 -0
- package/pi/package-agents/gsd-integration-checker.md +476 -0
- package/pi/package-agents/gsd-intel-updater.md +338 -0
- package/pi/package-agents/gsd-mempalace-curator.md +49 -0
- package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
- package/pi/package-agents/gsd-pattern-mapper.md +340 -0
- package/pi/package-agents/gsd-phase-researcher.md +897 -0
- package/pi/package-agents/gsd-plan-checker.md +1077 -0
- package/pi/package-agents/gsd-planner.md +1003 -0
- package/pi/package-agents/gsd-project-researcher.md +611 -0
- package/pi/package-agents/gsd-research-synthesizer.md +259 -0
- package/pi/package-agents/gsd-roadmapper.md +779 -0
- package/pi/package-agents/gsd-security-auditor.md +171 -0
- package/pi/package-agents/gsd-ui-auditor.md +581 -0
- package/pi/package-agents/gsd-ui-checker.md +417 -0
- package/pi/package-agents/gsd-ui-researcher.md +469 -0
- package/pi/package-agents/gsd-user-profiler.md +174 -0
- package/pi/package-agents/gsd-verifier.md +963 -0
- package/pi/package-agents/manifest.json +39 -0
- package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
- package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
- package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
- package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
- package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
- package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
- package/pi/package-skills/gsd-capture/SKILL.md +68 -0
- package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
- package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
- package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
- package/pi/package-skills/gsd-config/SKILL.md +60 -0
- package/pi/package-skills/gsd-debug/SKILL.md +64 -0
- package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
- package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
- package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-explore/SKILL.md +26 -0
- package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
- package/pi/package-skills/gsd-fast/SKILL.md +33 -0
- package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
- package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
- package/pi/package-skills/gsd-health/SKILL.md +36 -0
- package/pi/package-skills/gsd-help/SKILL.md +37 -0
- package/pi/package-skills/gsd-import/SKILL.md +46 -0
- package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
- package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
- package/pi/package-skills/gsd-manager/SKILL.md +46 -0
- package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
- package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
- package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
- package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
- package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
- package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
- package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
- package/pi/package-skills/gsd-next/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
- package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
- package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
- package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
- package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
- package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
- package/pi/package-skills/gsd-phase/SKILL.md +61 -0
- package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
- package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
- package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
- package/pi/package-skills/gsd-progress/SKILL.md +51 -0
- package/pi/package-skills/gsd-quick/SKILL.md +179 -0
- package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
- package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
- package/pi/package-skills/gsd-review/SKILL.md +50 -0
- package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
- package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-settings/SKILL.md +30 -0
- package/pi/package-skills/gsd-ship/SKILL.md +27 -0
- package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
- package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
- package/pi/package-skills/gsd-spike/SKILL.md +53 -0
- package/pi/package-skills/gsd-stats/SKILL.md +22 -0
- package/pi/package-skills/gsd-surface/SKILL.md +177 -0
- package/pi/package-skills/gsd-thread/SKILL.md +29 -0
- package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
- package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
- package/pi/package-skills/gsd-undo/SKILL.md +38 -0
- package/pi/package-skills/gsd-update/SKILL.md +54 -0
- package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
- package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
- package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
- package/scripts/build-hooks.js +4 -0
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +22 -7
- package/scripts/gen-features.cjs +14 -2
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/npm-audit-baseline.cjs +41 -7
- package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
- package/scripts/select-docs-guards.cjs +1 -1
- package/scripts/self-test-report-policy.cjs +350 -0
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
- package/scripts/diagnose-validate-commit-phases.cjs +0 -348
- package/scripts/xtrace-phase-timing.cjs +0 -205
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* "Reviewed or maintainer" policy (#5607).
|
|
6
|
+
*
|
|
7
|
+
* A pull request passes when EITHER
|
|
8
|
+
* 1. its author is listed in .github/MAINTAINERS (a maintainer's own PR needs
|
|
9
|
+
* no second reviewer), OR
|
|
10
|
+
* 2. someone other than the author, holding write/maintain/admin permission,
|
|
11
|
+
* has an APPROVED review on the CURRENT head sha as their latest standing
|
|
12
|
+
* review.
|
|
13
|
+
*
|
|
14
|
+
* Why this is a check and not a ruleset review count: a ruleset's
|
|
15
|
+
* required_approving_review_count is one number for every author, so it cannot
|
|
16
|
+
* say "required unless the author is a maintainer". The sole maintainer cannot
|
|
17
|
+
* approve their own PR, which is what pushed merges onto `--admin` (#4990).
|
|
18
|
+
*
|
|
19
|
+
* Everything that could wrongly COUNT an approval fails closed here: a stale
|
|
20
|
+
* sha, the author's own review, a reviewer without write access, a later
|
|
21
|
+
* dismissal or change request by the same reviewer, a truncated review list, an
|
|
22
|
+
* unreadable maintainer file. Wrongly FAILING only asks a maintainer for one
|
|
23
|
+
* more review.
|
|
24
|
+
*
|
|
25
|
+
* This module is pure. The workflow gathers reviews and roles with `gh api` and
|
|
26
|
+
* hands them in as files; the policy and the maintainer list are read from the
|
|
27
|
+
* BASE branch, never from the PR (.github/workflows/reviewed-or-maintainer.yml).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const { runMain } = require('./lib/cli-exit.cjs');
|
|
32
|
+
|
|
33
|
+
const REASON = Object.freeze({
|
|
34
|
+
OK_MAINTAINER_AUTHOR: 'ok_maintainer_author',
|
|
35
|
+
OK_APPROVED: 'ok_approved',
|
|
36
|
+
FAIL_NO_APPROVAL: 'fail_no_approval',
|
|
37
|
+
FAIL_STALE_APPROVAL_ONLY: 'fail_stale_approval_only',
|
|
38
|
+
FAIL_MAINTAINERS_UNREADABLE: 'fail_maintainers_unreadable',
|
|
39
|
+
FAIL_REVIEWS_INCOMPLETE: 'fail_reviews_incomplete',
|
|
40
|
+
FAIL_INPUT_INVALID: 'fail_input_invalid',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Permission levels allowed to give a qualifying approval. GitHub's
|
|
44
|
+
// `role_name` for collaborators: read, triage, write, maintain, admin.
|
|
45
|
+
const APPROVER_ROLES = new Set(['write', 'maintain', 'admin']);
|
|
46
|
+
|
|
47
|
+
// A GitHub login: alphanumerics and single hyphens, 1-39 chars, optional
|
|
48
|
+
// `[bot]` suffix. Anything else on a MAINTAINERS line is reported, not trusted.
|
|
49
|
+
const LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\[bot\])?$/;
|
|
50
|
+
const SHA_RE = /^[0-9a-f]{40}$/i;
|
|
51
|
+
|
|
52
|
+
// Review states that change a reviewer's standing. COMMENTED and PENDING do
|
|
53
|
+
// not: a later comment does not withdraw an approval on GitHub either.
|
|
54
|
+
const STANDING_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {unknown} text - contents of .github/MAINTAINERS
|
|
58
|
+
* @returns {{ok: boolean, logins: Set<string>, invalid: string[]}}
|
|
59
|
+
*/
|
|
60
|
+
function parseMaintainers(text) {
|
|
61
|
+
const logins = new Set();
|
|
62
|
+
const invalid = [];
|
|
63
|
+
if (typeof text !== 'string') return { ok: false, logins, invalid };
|
|
64
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
65
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
66
|
+
if (line === '') continue;
|
|
67
|
+
if (LOGIN_RE.test(line)) logins.add(line.toLowerCase());
|
|
68
|
+
else invalid.push(line);
|
|
69
|
+
}
|
|
70
|
+
return { ok: logins.size > 0, logins, invalid };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isBot(entry) {
|
|
74
|
+
const user = entry && entry.user;
|
|
75
|
+
return Boolean(user && (user.type === 'Bot' || /\[bot\]$/i.test(String(user.login || ''))));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function lower(value) {
|
|
79
|
+
return typeof value === 'string' ? value.toLowerCase() : '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {object} input
|
|
84
|
+
* @param {string} input.authorLogin
|
|
85
|
+
* @param {Set<string>} input.maintainers - lowercase logins
|
|
86
|
+
* @param {Array<{user: {login: string}, state: string, commit_id: string}>} input.reviews - chronological
|
|
87
|
+
* @param {Record<string, string>} input.roles - lowercase login -> permission role name
|
|
88
|
+
* @param {string} input.headSha - 40 hex
|
|
89
|
+
* @param {boolean} input.reviewsComplete - false when the review list may be truncated
|
|
90
|
+
* @returns {{ok: boolean, reason: string}}
|
|
91
|
+
*/
|
|
92
|
+
function evaluateReviewed({ authorLogin, maintainers, reviews, roles, headSha, reviewsComplete }) {
|
|
93
|
+
if (typeof authorLogin !== 'string' || authorLogin === '') {
|
|
94
|
+
return { ok: false, reason: REASON.FAIL_INPUT_INVALID };
|
|
95
|
+
}
|
|
96
|
+
if (typeof headSha !== 'string' || !SHA_RE.test(headSha)) {
|
|
97
|
+
return { ok: false, reason: REASON.FAIL_INPUT_INVALID };
|
|
98
|
+
}
|
|
99
|
+
if (!(maintainers instanceof Set) || maintainers.size === 0) {
|
|
100
|
+
return { ok: false, reason: REASON.FAIL_MAINTAINERS_UNREADABLE };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const author = lower(authorLogin);
|
|
104
|
+
if (maintainers.has(author)) return { ok: true, reason: REASON.OK_MAINTAINER_AUTHOR };
|
|
105
|
+
|
|
106
|
+
if (!Array.isArray(reviews)) return { ok: false, reason: REASON.FAIL_INPUT_INVALID };
|
|
107
|
+
if (reviewsComplete !== true) return { ok: false, reason: REASON.FAIL_REVIEWS_INCOMPLETE };
|
|
108
|
+
|
|
109
|
+
// Latest standing review per reviewer (input is chronological).
|
|
110
|
+
const standing = new Map();
|
|
111
|
+
for (const entry of reviews) {
|
|
112
|
+
const login = lower(entry && entry.user && entry.user.login);
|
|
113
|
+
if (login === '' || !STANDING_STATES.has(entry.state)) continue;
|
|
114
|
+
standing.set(login, entry);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let sawStaleApproval = false;
|
|
118
|
+
for (const [login, entry] of standing) {
|
|
119
|
+
if (login === author) continue;
|
|
120
|
+
if (entry.state !== 'APPROVED') continue;
|
|
121
|
+
// An app or bot (for example github-actions[bot] when Actions may approve
|
|
122
|
+
// PRs) is not a human review, whatever permission it holds.
|
|
123
|
+
if (isBot(entry)) continue;
|
|
124
|
+
const role = roles && typeof roles === 'object' ? roles[login] : undefined;
|
|
125
|
+
if (!APPROVER_ROLES.has(role)) continue;
|
|
126
|
+
if (lower(entry.commit_id) === lower(headSha)) return { ok: true, reason: REASON.OK_APPROVED };
|
|
127
|
+
sawStaleApproval = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: sawStaleApproval ? REASON.FAIL_STALE_APPROVAL_ONLY : REASON.FAIL_NO_APPROVAL,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readJson(file) {
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
139
|
+
} catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function main() {
|
|
145
|
+
const maintainersText = (() => {
|
|
146
|
+
try {
|
|
147
|
+
return fs.readFileSync(process.env.MAINTAINERS_FILE || '.github/MAINTAINERS', 'utf8');
|
|
148
|
+
} catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
const parsed = parseMaintainers(maintainersText);
|
|
153
|
+
for (const line of parsed.invalid) {
|
|
154
|
+
process.stderr.write(`::warning::MAINTAINERS: ignored malformed line: ${JSON.stringify(line)}\n`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const reviews = process.env.REVIEWS_FILE ? readJson(process.env.REVIEWS_FILE) : undefined;
|
|
158
|
+
const roles = process.env.ROLES_FILE ? readJson(process.env.ROLES_FILE) : undefined;
|
|
159
|
+
|
|
160
|
+
const result = evaluateReviewed({
|
|
161
|
+
authorLogin: process.env.PR_AUTHOR_LOGIN || '',
|
|
162
|
+
maintainers: parsed.ok ? parsed.logins : null,
|
|
163
|
+
reviews,
|
|
164
|
+
roles: roles && typeof roles === 'object' ? roles : {},
|
|
165
|
+
headSha: process.env.HEAD_SHA || '',
|
|
166
|
+
// The workflow's `gh api --paginate` either returns every page or exits
|
|
167
|
+
// non-zero before this runs; it states completeness explicitly.
|
|
168
|
+
reviewsComplete: process.env.REVIEWS_COMPLETE === 'true',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
172
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
173
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `ok=${result.ok ? 'true' : 'false'}\n`);
|
|
174
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `reason=${result.reason}\n`);
|
|
175
|
+
}
|
|
176
|
+
return result.ok ? 0 : 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (require.main === module) runMain(main);
|
|
180
|
+
|
|
181
|
+
module.exports = { REASON, APPROVER_ROLES, parseMaintainers, evaluateReviewed };
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* @param {string} changedPath - a single changed docs/ path (e.g. 'docs/
|
|
25
|
+
* @param {string} changedPath - a single changed docs/ path (e.g. 'docs/reference/agents.md').
|
|
26
26
|
* @param {string} pattern - one entry from a registry value array.
|
|
27
27
|
* @returns {boolean}
|
|
28
28
|
*/
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Self-test report policy (#5612).
|
|
6
|
+
*
|
|
7
|
+
* A fix PR from someone who is not a listed maintainer must carry a
|
|
8
|
+
* `## Self-test report` section holding a fenced `json` block that records what
|
|
9
|
+
* the submitter ran: a failing-first test (fails on the base, passes on the
|
|
10
|
+
* head), the commands, the environment, and a passing full suite, bound to the
|
|
11
|
+
* PR's CURRENT head commit.
|
|
12
|
+
*
|
|
13
|
+
* TRUST MODEL. This validates shape and sha binding, not truth. A local run
|
|
14
|
+
* cannot be attested; CI on the merge-group commit stays authoritative. The
|
|
15
|
+
* report is a head start for the reviewer and a nudge on effort, nothing more.
|
|
16
|
+
* Every result here is advisory until a maintainer chooses to require it.
|
|
17
|
+
*
|
|
18
|
+
* Pure module: the workflow hands in the PR body, head ref, title, author, head
|
|
19
|
+
* sha and the maintainer list (read from the BASE sha, never the PR).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const { runMain } = require('./lib/cli-exit.cjs');
|
|
24
|
+
const { parseMaintainers } = require('./reviewed-or-maintainer-policy.cjs');
|
|
25
|
+
|
|
26
|
+
const REASON = Object.freeze({
|
|
27
|
+
OK_NOT_A_FIX: 'ok_not_a_fix',
|
|
28
|
+
OK_MAINTAINER_AUTHOR: 'ok_maintainer_author',
|
|
29
|
+
OK_BOT_AUTHOR: 'ok_bot_author',
|
|
30
|
+
OK_REPORT: 'ok_report',
|
|
31
|
+
FAIL_NO_REPORT: 'fail_no_report',
|
|
32
|
+
FAIL_MALFORMED_JSON: 'fail_malformed_json',
|
|
33
|
+
FAIL_SCHEMA: 'fail_schema',
|
|
34
|
+
FAIL_STALE_SHA: 'fail_stale_sha',
|
|
35
|
+
FAIL_NOT_FAILING_FIRST: 'fail_not_failing_first',
|
|
36
|
+
FAIL_SUITE_NOT_PASSED: 'fail_suite_not_passed',
|
|
37
|
+
FAIL_MAINTAINERS_UNREADABLE: 'fail_maintainers_unreadable',
|
|
38
|
+
FAIL_INPUT_INVALID: 'fail_input_invalid',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const SCHEMA_VERSION = 1;
|
|
42
|
+
const TOOLS = new Set(['gsd-test', 'npm-test', 'manual']);
|
|
43
|
+
const SUITE_RESULTS = new Set(['passed', 'failed', 'skipped']);
|
|
44
|
+
const SHA_RE = /^[0-9a-f]{40}$/i;
|
|
45
|
+
const PLACEHOLDER_RE = /^<[^<>]*>$/;
|
|
46
|
+
const EXCERPT_MAX = 2000;
|
|
47
|
+
const SECTION_TITLE = 'self-test report';
|
|
48
|
+
|
|
49
|
+
/** A PR is a "fix PR" when its head ref is fix/… or its title is a fix. */
|
|
50
|
+
function isFixPr({ headRef, title }) {
|
|
51
|
+
if (typeof headRef === 'string' && headRef.startsWith('fix/')) return true;
|
|
52
|
+
return typeof title === 'string' && /^fix(\([^)]*\))?!?:/i.test(title.trim());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isBotLogin(login) {
|
|
56
|
+
return typeof login === 'string' && /\[bot\]$/i.test(login);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Find the first fenced ```json block inside the `## Self-test report` section.
|
|
61
|
+
*
|
|
62
|
+
* - Fence-aware: a `#` line inside a fence is not a heading. A fence may carry
|
|
63
|
+
* trailing info after the language (```json title="x"); only its first word
|
|
64
|
+
* names the language. A closing fence has no info and is at least as long as
|
|
65
|
+
* the opener, with the same character.
|
|
66
|
+
* - HTML comments are skipped outside fences, so a block hidden in a comment
|
|
67
|
+
* (invisible in the rendered PR) is not the report.
|
|
68
|
+
* - The section ends at the next heading of the same or a higher level.
|
|
69
|
+
* - The first json block wins.
|
|
70
|
+
*
|
|
71
|
+
* @returns {{found: boolean, json: string|null}}
|
|
72
|
+
*/
|
|
73
|
+
// Lines longer than this are not fence or heading syntax. Capping what the
|
|
74
|
+
// regexes see keeps a crafted line of whitespace from costing more than a bounded
|
|
75
|
+
// amount of work (the body is attacker-controlled); captured JSON keeps its full text.
|
|
76
|
+
const SYNTAX_LINE_MAX = 1000;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* ATX heading → {level, text}, parsed without a backtracking regex: a body line
|
|
80
|
+
* is attacker input and the obvious `^(#+)\s+(.*?)\s*#*\s*$` is cubic on a long
|
|
81
|
+
* run of spaces. A trailing run of `#` is a closing sequence only when preceded
|
|
82
|
+
* by whitespace (or the text is only hashes).
|
|
83
|
+
*/
|
|
84
|
+
function parseHeading(line) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
let level = 0;
|
|
87
|
+
while (level < trimmed.length && trimmed[level] === '#') level += 1;
|
|
88
|
+
if (level < 1 || level > 6) return null;
|
|
89
|
+
if (level >= trimmed.length || !/\s/.test(trimmed[level])) return null;
|
|
90
|
+
let text = trimmed.slice(level).trim();
|
|
91
|
+
let end = text.length;
|
|
92
|
+
while (end > 0 && text[end - 1] === '#') end -= 1;
|
|
93
|
+
if (end < text.length && (end === 0 || /\s/.test(text[end - 1]))) text = text.slice(0, end).trimEnd();
|
|
94
|
+
return { level, text: text.replace(/\*\*/g, '').trim().toLowerCase() };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractReport(body) {
|
|
98
|
+
const lines = String(body || '').split(/\r?\n/);
|
|
99
|
+
let inFence = false;
|
|
100
|
+
let fenceChar = '';
|
|
101
|
+
let fenceLen = 0;
|
|
102
|
+
let inComment = false;
|
|
103
|
+
let sectionLevel = 0;
|
|
104
|
+
let capturing = false;
|
|
105
|
+
let captured = [];
|
|
106
|
+
|
|
107
|
+
for (const full of lines) {
|
|
108
|
+
const line = full.length > SYNTAX_LINE_MAX ? full.slice(0, SYNTAX_LINE_MAX) : full;
|
|
109
|
+
if (!inFence) {
|
|
110
|
+
if (inComment) {
|
|
111
|
+
if (line.includes('-->')) inComment = false;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const commentAt = line.indexOf('<!--');
|
|
115
|
+
if (commentAt !== -1 && line.indexOf('-->', commentAt + 4) === -1) {
|
|
116
|
+
inComment = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!inFence) {
|
|
122
|
+
const open = /^\s*(`{3,}|~{3,})\s*([^\s`]*)(?:\s.*)?$/.exec(line);
|
|
123
|
+
if (open) {
|
|
124
|
+
if (sectionLevel > 0 && !capturing && open[2].toLowerCase() === 'json') {
|
|
125
|
+
capturing = true;
|
|
126
|
+
captured = [];
|
|
127
|
+
}
|
|
128
|
+
inFence = true;
|
|
129
|
+
fenceChar = open[1][0];
|
|
130
|
+
fenceLen = open[1].length;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
const close = /^\s*(`{3,}|~{3,})\s*$/.exec(line);
|
|
135
|
+
if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
|
|
136
|
+
inFence = false;
|
|
137
|
+
if (capturing) return { found: true, json: captured.join('\n') };
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (capturing) captured.push(full);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const heading = parseHeading(line);
|
|
145
|
+
if (heading) {
|
|
146
|
+
if (sectionLevel === 0) {
|
|
147
|
+
if (heading.text === SECTION_TITLE) sectionLevel = heading.level;
|
|
148
|
+
} else if (heading.level <= sectionLevel) {
|
|
149
|
+
return { found: false, json: null };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { found: false, json: null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isNonEmptyString(value) {
|
|
157
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isCount(value) {
|
|
161
|
+
return Number.isInteger(value) && value >= 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Names of report fields whose string value is still an unfilled `<…>` placeholder. */
|
|
165
|
+
function placeholderFields(report) {
|
|
166
|
+
const found = [];
|
|
167
|
+
const check = (name, value) => {
|
|
168
|
+
if (typeof value === 'string' && PLACEHOLDER_RE.test(value.trim())) found.push(name);
|
|
169
|
+
};
|
|
170
|
+
check('head_sha', report.head_sha);
|
|
171
|
+
check('output_excerpt', report.output_excerpt);
|
|
172
|
+
if (report.failing_first && typeof report.failing_first === 'object') {
|
|
173
|
+
check('failing_first.test', report.failing_first.test);
|
|
174
|
+
check('failing_first.not_applicable_reason', report.failing_first.not_applicable_reason);
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(report.commands)) report.commands.forEach((c) => check('commands', c));
|
|
177
|
+
if (report.full_suite && typeof report.full_suite === 'object') check('full_suite.command', report.full_suite.command);
|
|
178
|
+
if (report.environment && typeof report.environment === 'object') {
|
|
179
|
+
check('environment.os', report.environment.os);
|
|
180
|
+
check('environment.node', report.environment.node);
|
|
181
|
+
}
|
|
182
|
+
return [...new Set(found)];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {unknown} report - parsed JSON
|
|
187
|
+
* @param {string} headSha
|
|
188
|
+
* @returns {{ok: boolean, reason: string, problems: string[]}}
|
|
189
|
+
*/
|
|
190
|
+
function validateReport(report, headSha) {
|
|
191
|
+
const problems = [];
|
|
192
|
+
const fail = (reason) => ({ ok: false, reason, problems });
|
|
193
|
+
|
|
194
|
+
if (!report || typeof report !== 'object' || Array.isArray(report)) {
|
|
195
|
+
problems.push('the report must be a JSON object');
|
|
196
|
+
return fail(REASON.FAIL_SCHEMA);
|
|
197
|
+
}
|
|
198
|
+
if (report.schema !== SCHEMA_VERSION) problems.push(`schema must be ${SCHEMA_VERSION}`);
|
|
199
|
+
if (typeof report.head_sha !== 'string' || !SHA_RE.test(report.head_sha)) {
|
|
200
|
+
problems.push('head_sha must be the 40-hex sha of your final commit');
|
|
201
|
+
}
|
|
202
|
+
if (!TOOLS.has(report.tool)) problems.push(`tool must be one of ${[...TOOLS].join(', ')}`);
|
|
203
|
+
|
|
204
|
+
const ff = report.failing_first;
|
|
205
|
+
if (!ff || typeof ff !== 'object' || Array.isArray(ff)) {
|
|
206
|
+
problems.push('failing_first must be an object');
|
|
207
|
+
} else if (ff.not_applicable_reason !== undefined) {
|
|
208
|
+
if (!isNonEmptyString(ff.not_applicable_reason)) {
|
|
209
|
+
problems.push('failing_first.not_applicable_reason must be a non-empty explanation');
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
if (!isNonEmptyString(ff.test)) problems.push('failing_first.test must name the test file');
|
|
213
|
+
if (typeof ff.fails_on_base !== 'boolean') problems.push('failing_first.fails_on_base must be true or false');
|
|
214
|
+
if (typeof ff.passes_on_head !== 'boolean') problems.push('failing_first.passes_on_head must be true or false');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!Array.isArray(report.commands) || report.commands.length === 0 || !report.commands.every(isNonEmptyString)) {
|
|
218
|
+
problems.push('commands must be a non-empty list of the commands you ran');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const suite = report.full_suite;
|
|
222
|
+
if (!suite || typeof suite !== 'object' || Array.isArray(suite)) {
|
|
223
|
+
problems.push('full_suite must be an object');
|
|
224
|
+
} else {
|
|
225
|
+
if (!isNonEmptyString(suite.command)) problems.push('full_suite.command must name the command');
|
|
226
|
+
if (!SUITE_RESULTS.has(suite.result)) problems.push(`full_suite.result must be one of ${[...SUITE_RESULTS].join(', ')}`);
|
|
227
|
+
if (!isCount(suite.passed)) problems.push('full_suite.passed must be a non-negative integer');
|
|
228
|
+
if (!isCount(suite.failed)) problems.push('full_suite.failed must be a non-negative integer');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const env = report.environment;
|
|
232
|
+
if (!env || typeof env !== 'object' || Array.isArray(env) || !isNonEmptyString(env.os) || !isNonEmptyString(env.node)) {
|
|
233
|
+
problems.push('environment must give os and node');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!isNonEmptyString(report.output_excerpt) || report.output_excerpt.length > EXCERPT_MAX) {
|
|
237
|
+
problems.push(`output_excerpt must be a non-empty excerpt of at most ${EXCERPT_MAX} characters`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// A value still in `<…>` is the template, not something the submitter filled in.
|
|
241
|
+
for (const name of placeholderFields(report)) problems.push(`${name} is still the template placeholder; fill it in`);
|
|
242
|
+
|
|
243
|
+
if (problems.length > 0) return fail(REASON.FAIL_SCHEMA);
|
|
244
|
+
|
|
245
|
+
// Structurally sound. Now the substantive checks, most informative first.
|
|
246
|
+
if (report.head_sha.toLowerCase() !== String(headSha).toLowerCase()) {
|
|
247
|
+
problems.push(`head_sha ${report.head_sha} is not the PR's current head ${headSha}; update the report after your last push`);
|
|
248
|
+
return fail(REASON.FAIL_STALE_SHA);
|
|
249
|
+
}
|
|
250
|
+
if (ff.not_applicable_reason === undefined && !(ff.fails_on_base === true && ff.passes_on_head === true)) {
|
|
251
|
+
problems.push('the failing-first test must fail on the base and pass on your head');
|
|
252
|
+
return fail(REASON.FAIL_NOT_FAILING_FIRST);
|
|
253
|
+
}
|
|
254
|
+
if (suite.result !== 'passed' || suite.failed !== 0 || suite.passed < 1) {
|
|
255
|
+
problems.push('the full suite must have passed, with at least one passing test and no failures');
|
|
256
|
+
return fail(REASON.FAIL_SUITE_NOT_PASSED);
|
|
257
|
+
}
|
|
258
|
+
return { ok: true, reason: REASON.OK_REPORT, problems };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {object} input
|
|
263
|
+
* @param {string} input.body
|
|
264
|
+
* @param {string} input.headRef
|
|
265
|
+
* @param {string} input.title
|
|
266
|
+
* @param {string} input.authorLogin
|
|
267
|
+
* @param {string} input.headSha - 40 hex, read live
|
|
268
|
+
* @param {Set<string>|null} input.maintainers - lowercase logins
|
|
269
|
+
* @returns {{ok: boolean, reason: string, problems: string[]}}
|
|
270
|
+
*/
|
|
271
|
+
function evaluateSelfTestReport({ body, headRef, title, authorLogin, headSha, maintainers }) {
|
|
272
|
+
const none = [];
|
|
273
|
+
if (typeof authorLogin !== 'string' || authorLogin === '' || typeof headSha !== 'string' || !SHA_RE.test(headSha)) {
|
|
274
|
+
return { ok: false, reason: REASON.FAIL_INPUT_INVALID, problems: none };
|
|
275
|
+
}
|
|
276
|
+
if (!isFixPr({ headRef, title })) return { ok: true, reason: REASON.OK_NOT_A_FIX, problems: none };
|
|
277
|
+
if (isBotLogin(authorLogin)) return { ok: true, reason: REASON.OK_BOT_AUTHOR, problems: none };
|
|
278
|
+
if (!(maintainers instanceof Set) || maintainers.size === 0) {
|
|
279
|
+
return { ok: false, reason: REASON.FAIL_MAINTAINERS_UNREADABLE, problems: none };
|
|
280
|
+
}
|
|
281
|
+
if (maintainers.has(authorLogin.toLowerCase())) {
|
|
282
|
+
return { ok: true, reason: REASON.OK_MAINTAINER_AUTHOR, problems: none };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const extracted = extractReport(body);
|
|
286
|
+
if (!extracted.found) {
|
|
287
|
+
return { ok: false, reason: REASON.FAIL_NO_REPORT, problems: ['no ```json block under a "## Self-test report" heading'] };
|
|
288
|
+
}
|
|
289
|
+
let parsed;
|
|
290
|
+
try {
|
|
291
|
+
parsed = JSON.parse(extracted.json);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return { ok: false, reason: REASON.FAIL_MALFORMED_JSON, problems: [`the report is not valid JSON: ${error.message}`] };
|
|
294
|
+
}
|
|
295
|
+
return validateReport(parsed, headSha);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** A value from the environment, or from the file named by `<NAME>_FILE` (live reads are too big for env). */
|
|
299
|
+
function fromEnvOrFile(name) {
|
|
300
|
+
const file = process.env[`${name}_FILE`];
|
|
301
|
+
if (file) {
|
|
302
|
+
try {
|
|
303
|
+
return fs.readFileSync(file, 'utf8');
|
|
304
|
+
} catch {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return process.env[name] || '';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function main() {
|
|
312
|
+
let maintainersText;
|
|
313
|
+
try {
|
|
314
|
+
maintainersText = fs.readFileSync(process.env.MAINTAINERS_FILE || '.github/MAINTAINERS', 'utf8');
|
|
315
|
+
} catch {
|
|
316
|
+
maintainersText = undefined;
|
|
317
|
+
}
|
|
318
|
+
const parsed = parseMaintainers(maintainersText);
|
|
319
|
+
|
|
320
|
+
const body = fromEnvOrFile('PR_BODY');
|
|
321
|
+
const title = fromEnvOrFile('PR_TITLE');
|
|
322
|
+
|
|
323
|
+
const result = body === undefined || title === undefined
|
|
324
|
+
? { ok: false, reason: REASON.FAIL_INPUT_INVALID, problems: [] }
|
|
325
|
+
: evaluateSelfTestReport({
|
|
326
|
+
body,
|
|
327
|
+
headRef: process.env.HEAD_REF || '',
|
|
328
|
+
title,
|
|
329
|
+
authorLogin: process.env.PR_AUTHOR_LOGIN || '',
|
|
330
|
+
headSha: process.env.HEAD_SHA || '',
|
|
331
|
+
maintainers: parsed.ok ? parsed.logins : null,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
335
|
+
for (const problem of result.problems) {
|
|
336
|
+
// The annotation text is the validator's own wording plus, for a JSON syntax
|
|
337
|
+
// error, the parser's message (which can echo attacker text): strip newlines
|
|
338
|
+
// so no extra workflow command can start a line.
|
|
339
|
+
process.stderr.write(`::error::Self-test report: ${problem.replace(/[\r\n]+/g, ' ').slice(0, 300)}\n`);
|
|
340
|
+
}
|
|
341
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
342
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `ok=${result.ok ? 'true' : 'false'}\n`);
|
|
343
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `reason=${result.reason}\n`);
|
|
344
|
+
}
|
|
345
|
+
return result.ok ? 0 : 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (require.main === module) runMain(main);
|
|
349
|
+
|
|
350
|
+
module.exports = { REASON, SCHEMA_VERSION, EXCERPT_MAX, isFixPr, extractReport, validateReport, evaluateSelfTestReport };
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* release type (rc / hotfix / final). See issue #1104.
|
|
10
10
|
*
|
|
11
11
|
* Modes:
|
|
12
|
-
* default — open + admin-merge a `chore: sync next version` PR
|
|
12
|
+
* default — open + admin-merge a `chore: sync next version` PR, or enqueue it
|
|
13
|
+
* when the base requires the merge queue (#5746) (used by
|
|
13
14
|
* release.yml's rc job, which has no next-targeting PR of its
|
|
14
15
|
* own). Idempotent: a no-op when `next` is already at the target.
|
|
15
16
|
* --in-place — set the version (+ manifests, via the npm `version` hook) in
|
|
@@ -52,7 +53,19 @@ function applyVersion(version, { run = defaultRun } = {}) {
|
|
|
52
53
|
run('npm', ['version', version, '--no-git-tag-version', '--allow-same-version'], { timeout: 60000 });
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
// What `gh pr merge --admin` says when the base branch is behind a merge-queue ruleset.
|
|
57
|
+
const MERGE_QUEUE_REJECTION_RE = /merge queue/i;
|
|
58
|
+
|
|
59
|
+
// stdin ignored, stdout and stderr captured so a failed `gh` call carries its own text.
|
|
60
|
+
const PIPED_STDERR = ['ignore', 'pipe', 'pipe'];
|
|
61
|
+
|
|
62
|
+
/** Piped stderr is not visible on its own: write it out, then rethrow. */
|
|
63
|
+
function rethrowWithStderr(err) {
|
|
64
|
+
if (err && err.stderr) process.stderr.write(String(err.stderr));
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Full PR-based sync to the `next` branch. Returns 'noop' | 'synced' | 'queued' (enqueued, not yet merged). */
|
|
56
69
|
function syncViaPr(version, { run = defaultRun } = {}) {
|
|
57
70
|
if (!isReleaseVersion(version)) {
|
|
58
71
|
throw new Error(`refusing to sync next to invalid/non-release version '${version}'`);
|
|
@@ -100,11 +113,30 @@ function syncViaPr(version, { run = defaultRun } = {}) {
|
|
|
100
113
|
/* labels are best-effort; admin-merge below still lands the PR */
|
|
101
114
|
}
|
|
102
115
|
try {
|
|
103
|
-
|
|
116
|
+
// stderr is piped, not inherited: with the default 'inherit' the GitHub text goes
|
|
117
|
+
// to the terminal and err.stderr is null, so the classification below would only
|
|
118
|
+
// ever see "Command failed: gh pr merge ..." (#5746).
|
|
119
|
+
run('gh', ['pr', 'merge', '--admin', '--merge', prNum], { timeout: 60000, stdio: PIPED_STDERR });
|
|
104
120
|
} catch (err) {
|
|
105
|
-
// tolerate "already merged"; rethrow anything else
|
|
106
121
|
const msg = String((err && (err.stderr || err.message)) || '');
|
|
107
|
-
if (
|
|
122
|
+
if (/already merged/i.test(msg)) {
|
|
123
|
+
// a PR that is already merged must never be enqueued
|
|
124
|
+
} else if (MERGE_QUEUE_REJECTION_RE.test(msg)) {
|
|
125
|
+
// #5746: `next` is governed by a merge-queue ruleset, which --admin cannot bypass.
|
|
126
|
+
// Enqueue instead; the queue chooses the merge strategy, so no --merge here. A
|
|
127
|
+
// failed enqueue is a real failure: its text is surfaced and it propagates.
|
|
128
|
+
try {
|
|
129
|
+
run('gh', ['pr', 'merge', '--auto', prNum], { timeout: 60000, stdio: PIPED_STDERR });
|
|
130
|
+
} catch (enqueueErr) {
|
|
131
|
+
rethrowWithStderr(enqueueErr);
|
|
132
|
+
}
|
|
133
|
+
process.stdout.write(`queued sync of next -> ${version} via PR #${prNum}\n`);
|
|
134
|
+
return 'queued';
|
|
135
|
+
} else if (!/not mergeable|Merged/i.test(msg)) {
|
|
136
|
+
// the older tolerance, kept after the queue rule so a queue rejection that happens
|
|
137
|
+
// to mention "mergeable" is not mistaken for success
|
|
138
|
+
rethrowWithStderr(err);
|
|
139
|
+
}
|
|
108
140
|
}
|
|
109
141
|
process.stdout.write(`synced next -> ${version} via PR #${prNum}\n`);
|
|
110
142
|
return 'synced';
|
|
@@ -130,4 +162,4 @@ if (require.main === module) {
|
|
|
130
162
|
}
|
|
131
163
|
}
|
|
132
164
|
|
|
133
|
-
module.exports = { isReleaseVersion, versionFromPackageJson, applyVersion, syncViaPr, main };
|
|
165
|
+
module.exports = { isReleaseVersion, versionFromPackageJson, applyVersion, syncViaPr, main, defaultRun };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gsd-update
|
|
3
3
|
description: "Update GSD to latest version with changelog display"
|
|
4
|
-
argument-hint: "[--sync | --reapply | --next | --rc]"
|
|
4
|
+
argument-hint: "[--sync | --reapply | --next | --rc] [--interactive=exceptions|all]"
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Read
|
|
7
7
|
- Write
|
|
@@ -36,14 +36,17 @@ Routes to the update workflow which handles:
|
|
|
36
36
|
<flags>
|
|
37
37
|
- **--sync**: Sync managed GSD skills across runtime roots so multi-runtime users stay aligned after an update. Runs the sync-skills workflow (--from, --to, --dry-run, --apply flags supported).
|
|
38
38
|
- **--reapply**: Reapply local modifications after a GSD update. Uses three-way comparison (pristine baseline, user-modified backup, newly installed version) to merge user customizations back. Runs the reapply-patches workflow.
|
|
39
|
+
- **--interactive[=exceptions|all]** (with `--reapply` only): show each user edit next to the stock text with a recommended disposition and wait for a keep / retire / rewrite decision per edit. `all` shows every edit; `exceptions` (bare `--interactive`) shows only edits upstream adopted, partly adopted, reworded around, or orphaned, and keeps the rest. Without it, `--reapply` behaves exactly as before.
|
|
39
40
|
- **--next** (alias **--rc**): Target the `@next` RC dist-tag instead of `@latest` so you can install or refresh a release candidate (e.g. `1.4.0-rc.1`) through the normal update flow — scope/runtime detection, changelog preview, custom-file backup, and cache clearing all still apply. Omitting it keeps targeting `@latest` (no change). See ADR #660 for the RC channel.
|
|
40
41
|
- **(no flag)**: Standard update — check for new version, show changelog, install.
|
|
41
42
|
</flags>
|
|
42
43
|
|
|
43
44
|
<process>
|
|
45
|
+
If the arguments contain `--interactive` (bare or with a value) but not `--reapply`, STOP with: `--interactive applies only to --reapply (for example: /gsd-update --reapply --interactive=all)`. Do nothing else.
|
|
46
|
+
|
|
44
47
|
Parse the first token of the `<arguments>` block:
|
|
45
48
|
- If it is `--sync`: strip the flag, execute the sync-skills workflow (passing remaining args for --from/--to/--dry-run/--apply).
|
|
46
|
-
- If it is `--reapply`: strip the flag, execute the reapply-patches workflow.
|
|
49
|
+
- If it is `--reapply`: strip the flag, execute the reapply-patches workflow, passing the remaining arguments (for example `--interactive=all`) to it.
|
|
47
50
|
- Otherwise (including `--next` / `--rc`): execute the update workflow end-to-end, passing the `<arguments>` block through so the workflow's parse_update_channel step can select the release channel.
|
|
48
51
|
|
|
49
52
|
</process>
|
package/vscode/package.json
CHANGED