@khoavu882/doflow 2.0.0-beta.5 → 2.0.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/README.md +12 -11
- package/bin/doflow.js +21 -11
- package/core/.antigravity-plugin/plugin.json +1 -2
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/settings/settings.json +18 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
- package/core/harnesses/codex/agents/core-implementer.toml +1 -0
- package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
- package/core/harnesses/codex/agents/research-writer.toml +1 -1
- package/core/harnesses/codex/agents/reviewer.toml +10 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
- package/core/harnesses/codex/agents/system-architect.toml +1 -1
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
- package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow.json +57 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -1
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +1 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +1 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +1 -0
- package/core/harnesses/kiro/hooks/session-start.sh +1 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
- package/core/harnesses/pi/extensions/doflow-guard.js +8 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +0 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +102 -9
- package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +10 -2
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +48 -15
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +2 -11
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +3 -2
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +213 -12
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +114 -38
- package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
- package/core/harnesses/shared/hooks/policies/stop-check.sh +20 -9
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +5 -2
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
- package/core/harnesses/shared/hooks/stream-hook-runner.js +47 -8
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +55 -82
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +58 -31
- package/core/registry/contribution-points.json +32 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1442 -363
- package/core/registry/lifecycle.json +485 -304
- package/core/registry/mcp.json +5 -1
- package/core/registry/models.json +7 -1
- package/core/registry/package-templates/README.md +19 -0
- package/core/registry/package-templates/agent/files/agent.md +9 -0
- package/core/registry/package-templates/agent/template.json +4 -0
- package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
- package/core/registry/package-templates/capability-provider/template.json +11 -0
- package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
- package/core/registry/package-templates/debate-preset/template.json +4 -0
- package/core/registry/package-templates/review-preset/files/preset.json +30 -0
- package/core/registry/package-templates/review-preset/template.json +4 -0
- package/core/registry/package-templates/skill/files/SKILL.md +8 -0
- package/core/registry/package-templates/skill/template.json +4 -0
- package/core/registry/package-templates/worker/files/worker.json +92 -0
- package/core/registry/package-templates/worker/template.json +11 -0
- package/core/registry/packages/doflow/doflow-package.json +15 -0
- package/core/registry/packages/doflow/presets/debate-3.json +26 -0
- package/core/registry/protocols.json +61 -0
- package/core/registry/routes.json +17 -0
- package/core/registry/rules.json +431 -0
- package/core/registry/settings.json +276 -0
- package/core/registry/storage.json +679 -31
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -1
- package/core/registry/workflows.json +55 -0
- package/core/shared/agent-specs/core-implementer.md +3 -0
- package/core/shared/agent-specs/delivery-lead.md +68 -0
- package/core/shared/agent-specs/quality-guardian.md +5 -1
- package/core/shared/agent-specs/research-writer.md +10 -3
- package/core/shared/agent-specs/reviewer.md +53 -0
- package/core/shared/agent-specs/spec-analyst.md +5 -6
- package/core/shared/agent-specs/system-architect.md +5 -1
- package/core/shared/guidance/DOFLOW_CORE.md +1 -5
- package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
- package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
- package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
- package/core/shared/guidance/pointers/codex.md +1 -4
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
- package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
- package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
- package/core/shared/guidance/rules/RULES.md +45 -0
- package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
- package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
- package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
- package/core/shared/scripts/doflow/bin/doflow-run +149 -34
- package/core/shared/skills/do/SKILL.md +12 -10
- package/core/shared/skills/do/references/advice_calls.md +26 -0
- package/core/shared/skills/do/references/pm_routing.md +1 -2
- package/core/shared/skills/do/references/program_mode.md +31 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
- package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
- package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
- package/core/shared/skills/do-code-review/SKILL.md +41 -144
- package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
- package/core/shared/skills/do-code-review/references/tools.md +90 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
- package/core/shared/skills/do-design/SKILL.md +18 -17
- package/core/shared/skills/do-diagnose/SKILL.md +14 -4
- package/core/shared/skills/do-document/SKILL.md +11 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +18 -6
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +15 -58
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
- package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
- package/core/shared/skills/do-flow/SKILL.md +8 -9
- package/core/shared/skills/do-flow/references/task_classes.md +2 -2
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +4 -7
- package/core/shared/skills/do-plan/SKILL.md +26 -15
- package/core/shared/skills/do-test/SKILL.md +7 -4
- package/core/shared/templates/doflow/constitution-template.md +9 -3
- package/core/shared/templates/doflow/data-model-template.md +1 -1
- package/core/shared/templates/doflow/design-template.md +113 -16
- package/core/shared/templates/doflow/plan-template.md +11 -10
- package/core/shared/templates/doflow/question-log-template.md +14 -2
- package/core/shared/templates/doflow/requirement-template.md +1 -1
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +143 -66
- package/src/adapters/claude/index.js +24 -4
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/index.js +48 -13
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +290 -44
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +23 -12
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +2 -1
- package/src/adapters/pi/index.js +14 -7
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +21 -2
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +2 -1
- package/src/cli/index.js +322 -29
- package/src/cli/runtime-commands.js +209 -57
- package/src/cli/shared.js +0 -2
- package/src/helper/git.js +1 -1
- package/src/helper/guardrails.js +1 -1
- package/src/helper/marker-merge.js +31 -13
- package/src/helper/package-ids.js +37 -0
- package/src/helper/semver.js +94 -0
- package/src/install/backup.js +105 -15
- package/src/install/context.js +1 -1
- package/src/install/targets.js +31 -16
- package/src/lifecycle/coverage.js +20 -3
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +412 -78
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +551 -26
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +492 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +20 -11
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +13 -7
- package/src/runtime/cli.js +196 -19
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/delivery-store.js +4 -6
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/primary-command.js +117 -0
- package/src/runtime/context/primary.js +344 -0
- package/src/runtime/context/summary-store.js +655 -0
- package/src/runtime/context-pack.js +154 -78
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-ledger.js +34 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +33 -3
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +565 -17
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +277 -22
- package/src/runtime/inventory/coverage.js +1 -1
- package/src/runtime/inventory/index.js +1 -1
- package/src/runtime/inventory/precedence.js +2 -2
- package/src/runtime/inventory/siblings.js +6 -4
- package/src/runtime/knowledge/code-corpus.js +277 -0
- package/src/runtime/knowledge/code-lexical.js +130 -0
- package/src/runtime/knowledge/code-retrieval.js +307 -0
- package/src/runtime/knowledge/code-validation.js +106 -0
- package/src/runtime/knowledge/gp3-gate.js +74 -0
- package/src/runtime/knowledge/graph-adapter.js +288 -0
- package/src/runtime/knowledge/graph-fallback.js +394 -0
- package/src/runtime/knowledge/hybrid.js +136 -8
- package/src/runtime/knowledge/retrieval.js +34 -1
- package/src/runtime/knowledge/rtk-compress.js +77 -0
- package/src/runtime/knowledge/semble-bridge.py +106 -0
- package/src/runtime/knowledge/semble-lifecycle.js +114 -0
- package/src/runtime/knowledge/semble-provider.js +365 -0
- package/src/runtime/lifecycle/cli.js +16 -6
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +6 -1
- package/src/runtime/lifecycle/overview.js +24 -1
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +2 -2
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +477 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +46 -5
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +10 -30
- package/src/runtime/retrieval-record.js +75 -5
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +270 -0
- package/src/runtime/roadmap/loop.js +1636 -0
- package/src/runtime/roadmap/merge.js +381 -0
- package/src/runtime/roadmap/rows.js +1113 -0
- package/src/runtime/roadmap/seed.js +951 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +60 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +1 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +47 -11
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +176 -55
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +374 -64
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -9
- package/src/runtime/verification/impact.js +617 -0
- package/src/runtime/verification/inflight.js +205 -0
- package/src/runtime/verification/registry.js +176 -0
- package/src/runtime/verification/reuse.js +184 -0
- package/src/runtime/verification/telemetry-emit.js +254 -0
- package/src/runtime/verification/test-selection.js +67 -7
- package/src/runtime/worker/active.js +90 -0
- package/src/runtime/worker/cancel.js +222 -0
- package/src/runtime/worker/catalogue.js +122 -0
- package/src/runtime/worker/command.js +677 -0
- package/src/runtime/worker/evidence.js +288 -0
- package/src/runtime/worker/launch.js +288 -0
- package/src/runtime/worker/models.js +137 -0
- package/src/runtime/worker/parsers.js +218 -0
- package/src/runtime/worker/probes/index.js +706 -0
- package/src/runtime/worker/process-tree.js +415 -0
- package/src/runtime/worker/quota.js +43 -0
- package/src/runtime/worker/readiness.js +713 -0
- package/src/runtime/worker/run-store.js +166 -0
- package/src/runtime/worker/supervisor.js +541 -0
- package/src/runtime/worker/worktree-check.js +252 -0
- package/src/runtime/workflow-engine.js +5 -1
- package/src/runtime/workflow-orchestrator.js +578 -63
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -7
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -251
- package/src/adapters/gemini/index.js +0 -429
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Finding ids are a stable interface: IC-004 keeps R1-R12 and adds R13-R14.
|
|
4
|
+
// R3/R4 cover write-all, flow-map, and alias forms; R9 checks every publish invocation.
|
|
5
|
+
const RELEASE_WORKFLOW_RULE_IDS = Object.freeze([
|
|
6
|
+
'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10', 'R11', 'R12', 'R13', 'R14',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
function indentation(line ) {
|
|
23
|
+
return (line.match(/^ */) || [''])[0].length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stripInlineComment(value ) {
|
|
27
|
+
let quote = '';
|
|
28
|
+
for (let index = 0; index < value.length; index++) {
|
|
29
|
+
const character = value[index];
|
|
30
|
+
if (quote) {
|
|
31
|
+
if (quote === "'" && character === "'" && value[index + 1] === "'") {
|
|
32
|
+
index++;
|
|
33
|
+
} else if (character === quote && !isEscaped(value, index)) {
|
|
34
|
+
quote = '';
|
|
35
|
+
}
|
|
36
|
+
} else if (character === '"' || character === "'") {
|
|
37
|
+
quote = character;
|
|
38
|
+
} else if (character === '#' && (index === 0 || /\s/.test(value[index - 1]))) {
|
|
39
|
+
return value.slice(0, index).trimEnd();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return value.trimEnd();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isEscaped(value , index ) {
|
|
46
|
+
let backslashes = 0;
|
|
47
|
+
for (let cursor = index - 1; cursor >= 0 && value[cursor] === '\\'; cursor--) backslashes++;
|
|
48
|
+
return backslashes % 2 === 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stripYamlComments(text ) {
|
|
52
|
+
const source = String(text);
|
|
53
|
+
let quote = '';
|
|
54
|
+
let result = '';
|
|
55
|
+
for (let index = 0; index < source.length; index++) {
|
|
56
|
+
const character = source[index];
|
|
57
|
+
if (quote) {
|
|
58
|
+
if (quote === "'" && character === "'" && source[index + 1] === "'") {
|
|
59
|
+
result += "''";
|
|
60
|
+
index++;
|
|
61
|
+
} else {
|
|
62
|
+
if (character === quote && !isEscaped(source, index)) quote = '';
|
|
63
|
+
result += character;
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (character === '"' || character === "'") {
|
|
68
|
+
quote = character;
|
|
69
|
+
result += character;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (character === '#' && (index === 0 || /\s/.test(source[index - 1]))) {
|
|
73
|
+
while (index < source.length && source[index] !== '\n') index++;
|
|
74
|
+
if (index < source.length) result += '\n';
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
result += character;
|
|
78
|
+
}
|
|
79
|
+
return result.split(/\r?\n/).map((line) => line.trimEnd()).join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function blockValue(lines , lineIndex , keyIndent , inline ) {
|
|
83
|
+
const value = stripInlineComment(inline).trim();
|
|
84
|
+
if (value) return value;
|
|
85
|
+
|
|
86
|
+
const contents = [];
|
|
87
|
+
for (let index = lineIndex + 1; index < lines.length; index++) {
|
|
88
|
+
const line = lines[index];
|
|
89
|
+
if (!line.trim() || line.trimStart().startsWith('#')) continue;
|
|
90
|
+
if (indentation(line) <= keyIndent) break;
|
|
91
|
+
contents.push(line.trim());
|
|
92
|
+
}
|
|
93
|
+
return contents.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function unquote(value ) {
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
99
|
+
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
100
|
+
return trimmed.slice(1, -1);
|
|
101
|
+
}
|
|
102
|
+
return trimmed;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseNeeds(value ) {
|
|
106
|
+
const clean = stripInlineComment(value).trim();
|
|
107
|
+
if (!clean) return [];
|
|
108
|
+
if (clean.startsWith('[') && clean.endsWith(']')) {
|
|
109
|
+
return clean.slice(1, -1).split(',').map(unquote).filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
const blockItems = clean.split(/\r?\n/).map((line) => line.match(/^\s*-\s*(.*?)\s*$/));
|
|
112
|
+
if (blockItems.every(Boolean)) {
|
|
113
|
+
return blockItems.map((match) => unquote((match )[1])).filter(Boolean);
|
|
114
|
+
}
|
|
115
|
+
return [unquote(clean)];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function topLevelBlock(lines , key ) {
|
|
119
|
+
const header = new RegExp('^' + key + ':\\s*(?:#.*)?$');
|
|
120
|
+
const start = lines.findIndex((line) => indentation(line) === 0 && header.test(line));
|
|
121
|
+
if (start < 0) return null;
|
|
122
|
+
let end = lines.length;
|
|
123
|
+
for (let index = start + 1; index < lines.length; index++) {
|
|
124
|
+
if (!lines[index].trim() || lines[index].trimStart().startsWith('#')) continue;
|
|
125
|
+
if (indentation(lines[index]) === 0) {
|
|
126
|
+
end = index;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { start, end };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseWorkflow(text ) {
|
|
134
|
+
const lines = stripYamlComments(text).split(/\r?\n/);
|
|
135
|
+
let topPermissions = null;
|
|
136
|
+
for (let index = 0; index < lines.length; index++) {
|
|
137
|
+
const match = lines[index].match(/^permissions:\s*(.*)$/);
|
|
138
|
+
if (match) {
|
|
139
|
+
topPermissions = blockValue(lines, index, 0, match[1]);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const jobsBlock = topLevelBlock(lines, 'jobs');
|
|
145
|
+
const jobs = new Map ();
|
|
146
|
+
if (!jobsBlock) return { topPermissions, jobs };
|
|
147
|
+
|
|
148
|
+
const jobHeaders = [];
|
|
149
|
+
for (let index = jobsBlock.start + 1; index < jobsBlock.end; index++) {
|
|
150
|
+
const match = lines[index].match(/^ {2}("[A-Za-z0-9_-]+"|'[A-Za-z0-9_-]+'|[A-Za-z0-9_-]+):\s*$/);
|
|
151
|
+
if (match) jobHeaders.push({ id: unquote(match[1]), start: index });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (let index = 0; index < jobHeaders.length; index++) {
|
|
155
|
+
const header = jobHeaders[index];
|
|
156
|
+
const end = index + 1 < jobHeaders.length ? jobHeaders[index + 1].start : jobsBlock.end;
|
|
157
|
+
const jobLines = lines.slice(header.start, end);
|
|
158
|
+
let needs = [];
|
|
159
|
+
let permissions = null;
|
|
160
|
+
const uses = [];
|
|
161
|
+
|
|
162
|
+
for (let offset = 1; offset < jobLines.length; offset++) {
|
|
163
|
+
const line = jobLines[offset];
|
|
164
|
+
if (!line.trim() || line.trimStart().startsWith('#')) continue;
|
|
165
|
+
const fieldIndent = indentation(line);
|
|
166
|
+
if (fieldIndent === 4) {
|
|
167
|
+
const needsMatch = line.match(/^ {4}needs:\s*(.*)$/);
|
|
168
|
+
if (needsMatch) needs = parseNeeds(blockValue(jobLines, offset, 4, needsMatch[1]));
|
|
169
|
+
const permissionMatch = line.match(/^ {4}permissions:\s*(.*)$/);
|
|
170
|
+
if (permissionMatch) {
|
|
171
|
+
permissions = blockValue(jobLines, offset, 4, permissionMatch[1]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const usesMatch = line.match(/^\s*(?:-\s*)?uses:\s*(.*?)\s*$/);
|
|
175
|
+
if (usesMatch) uses.push(stripInlineComment(usesMatch[1]).trim());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
jobs.set(header.id, {
|
|
179
|
+
needs,
|
|
180
|
+
permissions,
|
|
181
|
+
uses,
|
|
182
|
+
text: jobLines.join('\n'),
|
|
183
|
+
line: header.start + 1,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { topPermissions, jobs };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hasPermission(permissions , name , access ) {
|
|
191
|
+
if (permissions === null) return false;
|
|
192
|
+
if (access === 'write' && unquote(permissions) === 'write-all') return true;
|
|
193
|
+
const blockPattern = new RegExp('(?:^|\\n)\\s*' + name + '\\s*:\\s*' + access + '\\b');
|
|
194
|
+
const flowMapPattern = new RegExp('(?:^|[,{])\\s*["\']?' + name
|
|
195
|
+
+ '["\']?\\s*:\\s*["\']?' + access + '["\']?(?=\\s*[,}])');
|
|
196
|
+
return blockPattern.test(permissions) || flowMapPattern.test(permissions);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function hasYamlAliasOrAnchor(text ) {
|
|
200
|
+
const source = String(text);
|
|
201
|
+
let quote = '';
|
|
202
|
+
for (let index = 0; index < source.length; index++) {
|
|
203
|
+
const character = source[index];
|
|
204
|
+
if (quote) {
|
|
205
|
+
if (quote === "'" && character === "'" && source[index + 1] === "'") {
|
|
206
|
+
index++;
|
|
207
|
+
} else if (character === quote && !isEscaped(source, index)) {
|
|
208
|
+
quote = '';
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (character === '"' || character === "'") {
|
|
213
|
+
quote = character;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const previous = source[index - 1];
|
|
217
|
+
const tokenBoundary = previous === undefined || /\s/.test(previous)
|
|
218
|
+
|| [':', ',', '{', '['].includes(previous);
|
|
219
|
+
if (source.startsWith('<<:', index) && tokenBoundary) return true;
|
|
220
|
+
if ((character === '&' || character === '*') && tokenBoundary
|
|
221
|
+
&& /[A-Za-z0-9_]/.test(source[index + 1] || '')) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function jobHasCommand(job , command ) {
|
|
229
|
+
return Boolean(job && command.test(job.text));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function shellCommands(text ) {
|
|
233
|
+
const source = String(text).replace(/\\\r?\n/g, ' ');
|
|
234
|
+
const commands = [];
|
|
235
|
+
let current = '';
|
|
236
|
+
let quote = '';
|
|
237
|
+
const finish = () => {
|
|
238
|
+
const command = current.trim();
|
|
239
|
+
if (command) commands.push(command);
|
|
240
|
+
current = '';
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
for (let index = 0; index < source.length; index++) {
|
|
244
|
+
const character = source[index];
|
|
245
|
+
if (quote) {
|
|
246
|
+
current += character;
|
|
247
|
+
if (character === quote && !isEscaped(source, index)) quote = '';
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (character === '"' || character === "'") {
|
|
251
|
+
quote = character;
|
|
252
|
+
current += character;
|
|
253
|
+
} else if (character === '\n' || character === ';' || character === '&' || character === '|') {
|
|
254
|
+
finish();
|
|
255
|
+
if ((character === '&' || character === '|') && source[index + 1] === character) index++;
|
|
256
|
+
} else {
|
|
257
|
+
current += character;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
finish();
|
|
261
|
+
return commands;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function npmPublishInvocations(text ) {
|
|
265
|
+
const invocations = [];
|
|
266
|
+
for (const command of shellCommands(text)) {
|
|
267
|
+
const pattern = /npm\s+publish\b/g;
|
|
268
|
+
for (const match of command.matchAll(pattern)) {
|
|
269
|
+
invocations.push(command.slice(match.index));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return invocations;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function jobFieldValue(job , field ) {
|
|
276
|
+
const pattern = new RegExp('^ {4}' + field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ':\\s*(.*)$');
|
|
277
|
+
const lines = job.text.split(/\r?\n/);
|
|
278
|
+
for (let index = 0; index < lines.length; index++) {
|
|
279
|
+
const match = lines[index].match(pattern);
|
|
280
|
+
if (match) return blockValue(lines, index, 4, match[1]);
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function caseMapsHyphenatedTagsToBeta(text ) {
|
|
286
|
+
const caseBlock = text.match(/case\s+["']?\$GITHUB_REF_NAME["']?\s+in([\s\S]*?)\besac\b/);
|
|
287
|
+
if (!caseBlock) return false;
|
|
288
|
+
const prereleaseBranch = caseBlock[1].match(/(?:^|\n)\s*\*-\*\)\s*([\s\S]*?)\s*;;/);
|
|
289
|
+
if (!prereleaseBranch) return false;
|
|
290
|
+
return /(?:^|\n)\s*channel\s*=\s*["']?beta["']?\s*(?:$|\n)/m.test(prereleaseBranch[1]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function hasOnlyVersionTagPush(text ) {
|
|
294
|
+
const lines = String(text).split(/\r?\n/);
|
|
295
|
+
const onBlock = topLevelBlock(lines, 'on');
|
|
296
|
+
if (!onBlock || stripInlineComment(lines[onBlock.start].slice(3)).trim()) return false;
|
|
297
|
+
|
|
298
|
+
const eventLines = lines.slice(onBlock.start + 1, onBlock.end);
|
|
299
|
+
const events = eventLines
|
|
300
|
+
.filter((line) => line.trim() && !line.trimStart().startsWith('#') && indentation(line) === 2)
|
|
301
|
+
.map((line) => line.match(/^ {2}([A-Za-z0-9_-]+):/))
|
|
302
|
+
.filter(Boolean) ;
|
|
303
|
+
if (events.length !== 1 || events[0][1] !== 'push') return false;
|
|
304
|
+
|
|
305
|
+
const pushIndex = onBlock.start + 1 + eventLines.findIndex((line) => /^ {2}push:/.test(line));
|
|
306
|
+
if (pushIndex <= onBlock.start) return false;
|
|
307
|
+
let pushEnd = onBlock.end;
|
|
308
|
+
for (let index = pushIndex + 1; index < onBlock.end; index++) {
|
|
309
|
+
if (lines[index].trim() && !lines[index].trimStart().startsWith('#') && indentation(lines[index]) <= 2) {
|
|
310
|
+
pushEnd = index;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const pushLines = lines.slice(pushIndex + 1, pushEnd);
|
|
315
|
+
const pushKeys = pushLines
|
|
316
|
+
.filter((line) => line.trim() && !line.trimStart().startsWith('#') && indentation(line) === 4)
|
|
317
|
+
.map((line) => line.match(/^ {4}([A-Za-z0-9_-]+):/))
|
|
318
|
+
.filter(Boolean) ;
|
|
319
|
+
if (pushKeys.length !== 1 || pushKeys[0][1] !== 'tags') return false;
|
|
320
|
+
|
|
321
|
+
const tagsLine = pushLines.find((line) => /^ {4}tags:/.test(line));
|
|
322
|
+
if (!tagsLine) return false;
|
|
323
|
+
const tagsMatch = tagsLine.match(/^ {4}tags:\s*(.*)$/);
|
|
324
|
+
if (!tagsMatch) return false;
|
|
325
|
+
const inline = stripInlineComment(tagsMatch[1]).trim();
|
|
326
|
+
if (inline) return /^\[\s*["']?v\*["']?\s*\]$/.test(inline);
|
|
327
|
+
|
|
328
|
+
const tagValues = pushLines.filter((line) => line.trim() && !line.trimStart().startsWith('#')
|
|
329
|
+
&& indentation(line) > 4);
|
|
330
|
+
return tagValues.length === 1
|
|
331
|
+
&& /^ {6}-\s*["']?v\*["']?\s*(?:#.*)?$/.test(tagValues[0]);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const fs = require('node:fs');
|
|
335
|
+
const path = require('node:path');
|
|
336
|
+
|
|
337
|
+
/** The first x.y.z in this checkout's package.json engines.node: the floor every Node pin must equal. */
|
|
338
|
+
function enginesNodeFloor() {
|
|
339
|
+
try {
|
|
340
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8'));
|
|
341
|
+
return String(pkg.engines.node).match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Every literal Node version a workflow pins: a `node-version:` or matrix `node:` key at the start of
|
|
349
|
+
* a line, with a scalar, quoted or `[a, b]` list value. Expressions (`${{ matrix.node }}`) pin
|
|
350
|
+
* nothing and are skipped; comments are ignored.
|
|
351
|
+
*/
|
|
352
|
+
function workflowNodePins(text ) {
|
|
353
|
+
const pins = [];
|
|
354
|
+
stripYamlComments(text).split('\n').forEach((line, index) => {
|
|
355
|
+
const match = /^\s*(?:-\s*)?(?:node-version|node):\s*(.+)$/.exec(line);
|
|
356
|
+
if (!match || match[1].includes('${{')) return;
|
|
357
|
+
for (const item of match[1].trim().replace(/^\[|\]$/g, '').split(',')) {
|
|
358
|
+
const value = item.trim().replace(/^["']|["']$/g, '');
|
|
359
|
+
if (value) pins.push({ line: index + 1, value });
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
return pins;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// A mapping key as YAML may spell it: plain, double-quoted (escapes allowed) or single-quoted.
|
|
366
|
+
const YAML_KEY = '("(?:[^"\\\\]|\\\\.)*"|\'(?:[^\']|\'\')*\'|[^\\s"\'#:,\\[\\]{}](?:[^:#,\\[\\]{}]*[^\\s:#,\\[\\]{}])?)';
|
|
367
|
+
|
|
368
|
+
function decodeYamlKey(raw ) {
|
|
369
|
+
if (raw.startsWith('"')) {
|
|
370
|
+
const named = { n: '\n', t: '\t', r: '\r', '0': '\0', ' ': ' ' };
|
|
371
|
+
return raw.slice(1, -1).replace(/\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}|.)/g,
|
|
372
|
+
(_match, escape ) => (escape.length > 1
|
|
373
|
+
? String.fromCodePoint(parseInt(escape.slice(1), 16)) : named[escape] ?? escape));
|
|
374
|
+
}
|
|
375
|
+
if (raw.startsWith("'")) return raw.slice(1, -1).replace(/''/g, "'");
|
|
376
|
+
return raw.trim();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Every mapping key in a comment-stripped YAML fragment, however it is spelled: block keys
|
|
381
|
+
* (plain, quoted, escaped, explicit `? key`, after `- `) with the column they sit at, and keys of
|
|
382
|
+
* flow mappings (`{ a: b }`, column -1). Block scalar bodies (`run: |`) are text, not keys, and
|
|
383
|
+
* `${{ }}` expressions are not flow mappings.
|
|
384
|
+
*/
|
|
385
|
+
function yamlKeys(text ) {
|
|
386
|
+
const keys = [];
|
|
387
|
+
const blockKey = new RegExp('^( *)((?:-[ \\t]+)*)(?:\\?[ \\t]+)?' + YAML_KEY + '[ \\t]*(?::(?:[ \\t]+(.*))?)?$');
|
|
388
|
+
const flowText = [];
|
|
389
|
+
const lines = String(text).split(/\r?\n/);
|
|
390
|
+
for (let index = 0; index < lines.length; index++) {
|
|
391
|
+
const line = lines[index];
|
|
392
|
+
const match = blockKey.exec(line);
|
|
393
|
+
const explicit = /^ *(?:-[ \t]+)*\?[ \t]/.test(line);
|
|
394
|
+
if (match && (explicit || /:(?:[ \t]|$)/.test(line.slice(match[1].length + match[2].length + match[3].length)))) {
|
|
395
|
+
const column = match[1].length + match[2].length;
|
|
396
|
+
keys.push({ key: decodeYamlKey(match[3]), column, value: blockValue(lines, index, column, match[4] || '') });
|
|
397
|
+
if (/^[|>][-+0-9]*$/.test((match[4] || '').trim())) {
|
|
398
|
+
while (index + 1 < lines.length && (!lines[index + 1].trim() || indentation(lines[index + 1]) > column)) index++;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
flowText.push(line);
|
|
403
|
+
}
|
|
404
|
+
for (const key of flowMappingKeys(flowText.join('\n'))) keys.push({ key, column: -1, value: '' });
|
|
405
|
+
return keys;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function flowMappingKeys(text ) {
|
|
409
|
+
const keys = [];
|
|
410
|
+
const entryKey = new RegExp('^\\s*(?:\\?\\s+)?' + YAML_KEY + '\\s*:');
|
|
411
|
+
const stack = [];
|
|
412
|
+
const finish = (frame ) => {
|
|
413
|
+
const match = entryKey.exec(frame.segment);
|
|
414
|
+
if (match) keys.push(decodeYamlKey(match[1]));
|
|
415
|
+
frame.segment = '';
|
|
416
|
+
};
|
|
417
|
+
let quote = '';
|
|
418
|
+
for (let index = 0; index < text.length; index++) {
|
|
419
|
+
const character = text[index];
|
|
420
|
+
const top = stack[stack.length - 1];
|
|
421
|
+
if (quote) {
|
|
422
|
+
if (top) top.segment += character;
|
|
423
|
+
if (quote === "'" && character === "'" && text[index + 1] === "'") {
|
|
424
|
+
if (top) top.segment += "'";
|
|
425
|
+
index++;
|
|
426
|
+
} else if (character === quote && !(quote === '"' && isEscaped(text, index))) {
|
|
427
|
+
quote = '';
|
|
428
|
+
}
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (text.startsWith('${{', index)) {
|
|
432
|
+
const end = text.indexOf('}}', index + 3);
|
|
433
|
+
const stop = end < 0 ? text.length : end + 2;
|
|
434
|
+
if (top) top.segment += text.slice(index, stop);
|
|
435
|
+
index = stop - 1;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if ((character === '"' || character === "'") && /^$|[\s{\[,:]/.test(text[index - 1] || '')) {
|
|
439
|
+
quote = character;
|
|
440
|
+
if (top) top.segment += character;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (character === '{') {
|
|
444
|
+
stack.push({ segment: '', brackets: 0 });
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (!top) continue;
|
|
448
|
+
if (character === '}') {
|
|
449
|
+
finish(top);
|
|
450
|
+
stack.pop();
|
|
451
|
+
const parent = stack[stack.length - 1];
|
|
452
|
+
if (parent) parent.segment += '{}';
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (character === '[') top.brackets++;
|
|
456
|
+
if (character === ']') top.brackets--;
|
|
457
|
+
if (character === ',' && top.brackets <= 0) {
|
|
458
|
+
finish(top);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
top.segment += character;
|
|
462
|
+
}
|
|
463
|
+
return keys;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The values of a job-level key (column 4), however the key is spelled; [] when it is absent. */
|
|
467
|
+
function jobLevelValues(job , key ) {
|
|
468
|
+
return yamlKeys(job.text).filter((entry) => entry.column === 4 && entry.key === key)
|
|
469
|
+
.map((entry) => entry.value);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** The GitHub environment whose required reviewers hold the preflight job of a stable tag. */
|
|
473
|
+
const STABLE_ENVIRONMENT = 'release-stable';
|
|
474
|
+
/** The environment a prerelease preflight runs in: it needs no setup and must stay unprotected. */
|
|
475
|
+
const PRERELEASE_ENVIRONMENT = 'release-prerelease';
|
|
476
|
+
// The one accepted binding: stable tags (no hyphen, the shell's `*)` branch) run in the stable
|
|
477
|
+
// environment and prereleases in the prerelease one. Any other form is refused, not interpreted.
|
|
478
|
+
const STABLE_ENVIRONMENT_BINDING = new RegExp('^\\$\\{\\{\\s*!\\s*contains\\(\\s*github\\.ref_name\\s*,\\s*\'-\'\\s*\\)'
|
|
479
|
+
+ '\\s*&&\\s*\'' + STABLE_ENVIRONMENT + '\'\\s*\\|\\|\\s*\'' + PRERELEASE_ENVIRONMENT + '\'\\s*\\}\\}$');
|
|
480
|
+
// The one accepted flag selector: prereleases leave allow_stable empty, every other tag sets it.
|
|
481
|
+
const STABLE_FLAG_CASE = /^\s*\*-\*\)\s*;;\s*\*\)\s*allow_stable=(?:"--allow-stable"|'--allow-stable'|--allow-stable)\s*;;\s*$/;
|
|
482
|
+
|
|
483
|
+
function countMatches(text , pattern ) {
|
|
484
|
+
return (String(text).match(pattern) || []).length;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* R14: the stable path opens only from the preflight job, only for a stable tag, and only while
|
|
489
|
+
* GitHub holds that job for the stable environment (preflight then refuses an environment that
|
|
490
|
+
* has no required reviewer). A workflow that never names the flag keeps the stable path shut.
|
|
491
|
+
*/
|
|
492
|
+
function stableGateViolations(cleanText , jobs ) {
|
|
493
|
+
const violations = [];
|
|
494
|
+
const flag = /allow-stable/g;
|
|
495
|
+
const preflight = jobs.get('preflight');
|
|
496
|
+
if (preflight) {
|
|
497
|
+
const keys = yamlKeys(preflight.text).map((entry) => entry.key);
|
|
498
|
+
if (keys.includes('continue-on-error')) violations.push('preflight must not declare continue-on-error');
|
|
499
|
+
if (keys.includes('if')) violations.push('preflight must not declare an if condition');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const total = countMatches(cleanText, flag);
|
|
503
|
+
if (total === 0) return violations;
|
|
504
|
+
let inJobs = 0;
|
|
505
|
+
for (const [id, job] of jobs) {
|
|
506
|
+
const count = countMatches(job.text, flag);
|
|
507
|
+
inJobs += count;
|
|
508
|
+
if (count && id !== 'preflight') violations.push(id + ' must not carry --allow-stable');
|
|
509
|
+
}
|
|
510
|
+
if (inJobs !== total) violations.push('--allow-stable must not appear outside the preflight job');
|
|
511
|
+
const inPreflight = preflight ? countMatches(preflight.text, flag) : 0;
|
|
512
|
+
if (!preflight || inPreflight === 0) return violations;
|
|
513
|
+
if (inPreflight !== 1) violations.push('preflight must name --allow-stable exactly once');
|
|
514
|
+
|
|
515
|
+
const binding = jobFieldValue(preflight, 'environment');
|
|
516
|
+
if (binding === null || !STABLE_ENVIRONMENT_BINDING.test(unquote(binding))) {
|
|
517
|
+
violations.push('preflight must declare environment: ${{ !contains(github.ref_name, \'-\') && \''
|
|
518
|
+
+ STABLE_ENVIRONMENT + '\' || \'' + PRERELEASE_ENVIRONMENT + '\' }}');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const cases = [...preflight.text.matchAll(/case\s+["']?\$GITHUB_REF_NAME["']?\s+in([\s\S]*?)\besac\b/g)]
|
|
522
|
+
.filter((match) => /allow-stable/.test(match[1]));
|
|
523
|
+
const selector = cases.length === 1 && STABLE_FLAG_CASE.test(cases[0][1]) ? cases[0] : null;
|
|
524
|
+
const assignments = countMatches(preflight.text, /\ballow_stable\s*=/g);
|
|
525
|
+
const reset = /(?:^|\n)\s*allow_stable=(?:""|'')?[ \t]*(?:\n|$)/.exec(preflight.text);
|
|
526
|
+
const invocations = shellCommands(preflight.text).filter((command) =>
|
|
527
|
+
/npm\s+run\s+release:preflight(?:\s|$)/.test(command));
|
|
528
|
+
const passed = invocations.length === 1
|
|
529
|
+
&& /\s\$(?:allow_stable|\{allow_stable\})\s*$/.test(invocations[0]);
|
|
530
|
+
// Needs the empty reset first, then the selector, then the one invocation.
|
|
531
|
+
const ordered = Boolean(selector && reset && reset.index < (selector.index )
|
|
532
|
+
&& (selector.index ) < preflight.text.indexOf(invocations[0] || '\0'));
|
|
533
|
+
if (!selector || assignments !== 2 || !passed || !ordered) {
|
|
534
|
+
violations.push('preflight must reset allow_stable="", set it to --allow-stable only in the *) branch'
|
|
535
|
+
+ ' after an empty *-*) branch of a case on $GITHUB_REF_NAME, and end its one release:preflight'
|
|
536
|
+
+ ' command with unquoted $allow_stable');
|
|
537
|
+
}
|
|
538
|
+
return violations;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* `nodeFloor` is the engines floor the Node pins must equal: undefined reads this checkout's
|
|
543
|
+
* package.json, null means the caller could not derive one (R11 then reports it).
|
|
544
|
+
*/
|
|
545
|
+
function releaseWorkflowFindings(text , { nodeFloor } = {}) {
|
|
546
|
+
const floor = nodeFloor === undefined ? enginesNodeFloor() : nodeFloor;
|
|
547
|
+
const cleanText = stripYamlComments(text);
|
|
548
|
+
const { topPermissions, jobs } = parseWorkflow(cleanText);
|
|
549
|
+
const findings = [];
|
|
550
|
+
const report = (rule , owner , message ) => {
|
|
551
|
+
findings.push(rule + ' ' + owner + ': ' + message);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
if (topPermissions !== '{}') {
|
|
555
|
+
report('R1', 'workflow', 'top-level permissions must be {}');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const missingPermissions = [...jobs].filter(([, job]) => job.permissions === null).map(([id]) => id);
|
|
559
|
+
if (missingPermissions.length) {
|
|
560
|
+
report('R2', missingPermissions.length === 1 ? missingPermissions[0] : 'workflow',
|
|
561
|
+
'every job must declare permissions: ' + missingPermissions.join(', '));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const idTokenViolations = [
|
|
565
|
+
...(hasPermission(topPermissions, 'id-token', 'write') ? ['workflow'] : []),
|
|
566
|
+
...[...jobs].filter(([id, job]) => id !== 'publish'
|
|
567
|
+
&& hasPermission(job.permissions, 'id-token', 'write')).map(([id]) => id),
|
|
568
|
+
];
|
|
569
|
+
if (idTokenViolations.length) {
|
|
570
|
+
report('R3', idTokenViolations.length === 1 ? idTokenViolations[0] : 'workflow',
|
|
571
|
+
'id-token: write is only allowed on publish; found on ' + idTokenViolations.join(', '));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const contentsViolations = [
|
|
575
|
+
...(hasPermission(topPermissions, 'contents', 'write') ? ['workflow'] : []),
|
|
576
|
+
...[...jobs].filter(([id, job]) => !['draft-release', 'publish-release'].includes(id)
|
|
577
|
+
&& hasPermission(job.permissions, 'contents', 'write')).map(([id]) => id),
|
|
578
|
+
];
|
|
579
|
+
if (contentsViolations.length) {
|
|
580
|
+
report('R4', contentsViolations.length === 1 ? contentsViolations[0] : 'workflow',
|
|
581
|
+
'contents: write is only allowed on draft-release and publish-release; found on '
|
|
582
|
+
+ contentsViolations.join(', '));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (hasYamlAliasOrAnchor(cleanText)) {
|
|
586
|
+
report('R3', 'workflow', 'aliases are not allowed in release.yml');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const unpinnedJobs = [...jobs].filter(([, job]) => job.uses.some((value) => !value.startsWith('./')
|
|
590
|
+
&& !/^[\w.-]+\/[\w./-]+@[0-9a-f]{40}$/.test(value))).map(([id]) => id);
|
|
591
|
+
if (unpinnedJobs.length) {
|
|
592
|
+
report('R5', unpinnedJobs.length === 1 ? unpinnedJobs[0] : 'workflow',
|
|
593
|
+
'every external uses value must be pinned to a 40-character SHA; found on ' + unpinnedJobs.join(', '));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const preflight = jobs.get('preflight');
|
|
597
|
+
if (!preflight || preflight.needs.length > 0
|
|
598
|
+
|| !jobHasCommand(preflight, /npm\s+run\s+release:preflight(?:\s|$)/)
|
|
599
|
+
|| !jobHasCommand(preflight, /--tag-pushed(?:\s|$)/)) {
|
|
600
|
+
report('R6', preflight ? 'preflight' : 'workflow',
|
|
601
|
+
'preflight must have no needs and run release:preflight with --tag-pushed');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const requiredNeeds = [
|
|
605
|
+
['verify-artifact', ['preflight', 'test']],
|
|
606
|
+
['draft-release', ['verify-artifact']],
|
|
607
|
+
['publish', ['draft-release']],
|
|
608
|
+
['verify-registry', ['publish']],
|
|
609
|
+
['promote', ['verify-registry']],
|
|
610
|
+
['publish-release', ['promote']],
|
|
611
|
+
];
|
|
612
|
+
const brokenEdges = [];
|
|
613
|
+
for (const [id, required] of requiredNeeds) {
|
|
614
|
+
const job = jobs.get(id);
|
|
615
|
+
if (!job || required.some((dependency) => !job.needs.includes(dependency))) {
|
|
616
|
+
brokenEdges.push(id);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (brokenEdges.length) {
|
|
620
|
+
report('R7', brokenEdges.length === 1 ? brokenEdges[0] : 'workflow',
|
|
621
|
+
'needs edges are incomplete for ' + brokenEdges.join(', '));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const draftRelease = jobs.get('draft-release');
|
|
625
|
+
const publishRelease = jobs.get('publish-release');
|
|
626
|
+
const draftCreateValid = jobHasCommand(draftRelease, /gh\s+release\s+create(?:\s|$)/)
|
|
627
|
+
&& jobHasCommand(draftRelease, /--draft(?!\s*=\s*false)\b/);
|
|
628
|
+
const draftFalseOwner = [...jobs].filter(([, job]) => /--draft=false\b/.test(job.text))
|
|
629
|
+
.every(([id]) => id === 'publish-release');
|
|
630
|
+
if (!draftCreateValid || !draftFalseOwner
|
|
631
|
+
|| !jobHasCommand(publishRelease, /gh\s+release\s+edit\b/)
|
|
632
|
+
|| !jobHasCommand(publishRelease, /--draft=false\b/)) {
|
|
633
|
+
report('R8', 'workflow',
|
|
634
|
+
'draft-release must create a draft and only publish-release may edit with --draft=false');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const publish = jobs.get('publish');
|
|
638
|
+
const candidateTag = /--tag\s+(?:"\$\{channel\}-candidate"|"\$channel-candidate"|\$\{channel\}-candidate|\$channel-candidate)/;
|
|
639
|
+
const literalChannelTag = /--tag\s+["']?(?:beta|latest)["']?(?:\s|$)/;
|
|
640
|
+
const publishCommands = publish ? npmPublishInvocations(publish.text) : [];
|
|
641
|
+
if (!publish || !publishCommands.length || publishCommands.some((command) => !candidateTag.test(command))
|
|
642
|
+
|| publishCommands.some((command) => literalChannelTag.test(command))
|
|
643
|
+
|| /npm\s+dist-tag\b/.test(publish.text)) {
|
|
644
|
+
report('R9', 'publish',
|
|
645
|
+
'publish must use the channel-candidate tag and must not run npm dist-tag');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const promote = jobs.get('promote');
|
|
649
|
+
const promoteAdd = promote?.text.match(/npm\s+dist-tag\s+add\b[^\r\n]*/);
|
|
650
|
+
const promoteRemove = promote?.text.match(/npm\s+dist-tag\s+rm\b[^\r\n]*/);
|
|
651
|
+
const literalLatestAdd = [...jobs.values()].some((job) =>
|
|
652
|
+
/npm\s+dist-tag\s+add\b[^\r\n]*\blatest\b/.test(job.text));
|
|
653
|
+
const r10Violations = [];
|
|
654
|
+
if (!publish || !caseMapsHyphenatedTagsToBeta(publish.text)) r10Violations.push('publish channel case');
|
|
655
|
+
const promotionValid = Boolean(promote && caseMapsHyphenatedTagsToBeta(promote.text)
|
|
656
|
+
&& promoteAdd && promoteRemove
|
|
657
|
+
&& promote.text.indexOf(promoteAdd[0]) < promote.text.indexOf(promoteRemove[0])
|
|
658
|
+
&& /\s+["']?\$channel["']?\s*$/.test(promoteAdd[0])
|
|
659
|
+
&& /\s+(?:"\$\{channel\}-candidate"|"\$channel-candidate"|\$\{channel\}-candidate|\$channel-candidate)\s*$/.test(promoteRemove[0]));
|
|
660
|
+
if (!promotionValid) r10Violations.push('promote channel case or promotion commands');
|
|
661
|
+
if (literalLatestAdd) r10Violations.push('literal latest dist-tag add');
|
|
662
|
+
if (r10Violations.length) {
|
|
663
|
+
const owner = r10Violations.length === 1 && r10Violations[0] === 'publish channel case'
|
|
664
|
+
? 'publish'
|
|
665
|
+
: r10Violations.length === 1 && r10Violations[0].startsWith('promote')
|
|
666
|
+
? 'promote' : 'workflow';
|
|
667
|
+
report('R10', owner, 'channel and dist-tag rules are broken: ' + r10Violations.join('; '));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const verifyRegistry = jobs.get('verify-registry');
|
|
671
|
+
const matrix = verifyRegistry?.text || '';
|
|
672
|
+
const floorPattern = floor ? floor.replace(/\./g, '\\.') : null;
|
|
673
|
+
const matrixValid = floorPattern !== null && /matrix\s*:/.test(matrix)
|
|
674
|
+
&& new RegExp('-\\s*os:\\s*["\']?ubuntu-latest["\']?\\s*\\r?\\n\\s*node:\\s*["\']?' + floorPattern + '["\']?(?:\\s|$)').test(matrix)
|
|
675
|
+
&& new RegExp('-\\s*os:\\s*["\']?macos-latest["\']?\\s*\\r?\\n\\s*node:\\s*["\']?' + floorPattern + '["\']?(?:\\s|$)').test(matrix);
|
|
676
|
+
const unpinnedNode = workflowNodePins(cleanText).filter((pin) => pin.value !== floor);
|
|
677
|
+
const installValid = /\bVER\s*=\s*["']?\$\{GITHUB_REF_NAME#v\}["']?/.test(matrix)
|
|
678
|
+
&& /npm\s+install\b[^\r\n]*@khoavu882\/doflow@\$VER\b/.test(matrix);
|
|
679
|
+
const noPackageCheckout = verifyRegistry
|
|
680
|
+
&& !verifyRegistry.uses.some((value) => /(?:^|\/)checkout@/i.test(value))
|
|
681
|
+
&& !/\.pack\b/.test(matrix);
|
|
682
|
+
if (!verifyRegistry || !matrixValid || !installValid || !noPackageCheckout) {
|
|
683
|
+
report('R11', verifyRegistry ? 'verify-registry' : 'workflow',
|
|
684
|
+
'registry verification must install the tag version on the engines floor Node (' + (floor || 'unreadable')
|
|
685
|
+
+ ') on the required OS matrix without checkout or .pack');
|
|
686
|
+
} else if (unpinnedNode.length) {
|
|
687
|
+
report('R11', 'workflow', 'every Node pin must equal the engines floor ' + floor + ': '
|
|
688
|
+
+ unpinnedNode.map((pin) => 'line ' + pin.line + ' pins ' + pin.value).join(', '));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (!hasOnlyVersionTagPush(cleanText)) {
|
|
692
|
+
report('R12', 'workflow', 'trigger must be push of tags v* only');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const failureBypassJobs = [];
|
|
696
|
+
for (const id of ['verify-registry', 'promote', 'publish-release', 'publish']) {
|
|
697
|
+
const job = jobs.get(id);
|
|
698
|
+
if (job && jobLevelValues(job, 'continue-on-error').length) {
|
|
699
|
+
failureBypassJobs.push(id + ' declares continue-on-error');
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// The publishing chain runs only on success: no job-level if at all on the jobs up to publish,
|
|
703
|
+
// and none that can run after a failure or cancellation on promote and publish-release.
|
|
704
|
+
for (const id of ['verify-artifact', 'draft-release', 'publish']) {
|
|
705
|
+
const job = jobs.get(id);
|
|
706
|
+
if (job && jobLevelValues(job, 'if').length) failureBypassJobs.push(id + ' declares a job-level if condition');
|
|
707
|
+
}
|
|
708
|
+
for (const id of ['promote', 'publish-release']) {
|
|
709
|
+
const job = jobs.get(id);
|
|
710
|
+
const conditions = job ? jobLevelValues(job, 'if') : [];
|
|
711
|
+
if (conditions.some((condition) => /(?:always|cancelled|failure)\s*\(\s*\)/i.test(condition))) {
|
|
712
|
+
failureBypassJobs.push(id + ' has a failure-bypassing if condition');
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (failureBypassJobs.length) {
|
|
716
|
+
const owners = [...new Set(failureBypassJobs.map((violation) => violation.split(' ')[0]))];
|
|
717
|
+
report('R13', owners.length === 1 ? owners[0] : 'workflow',
|
|
718
|
+
'release chain jobs must not continue after failure: ' + failureBypassJobs.join('; '));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const stableGate = stableGateViolations(cleanText, jobs);
|
|
722
|
+
if (stableGate.length) {
|
|
723
|
+
report('R14', 'workflow', 'the stable path must open only behind the preflight reviewer gate: '
|
|
724
|
+
+ stableGate.join('; '));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return findings;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
module.exports = {
|
|
731
|
+
RELEASE_WORKFLOW_RULE_IDS, STABLE_ENVIRONMENT, PRERELEASE_ENVIRONMENT, parseWorkflow, releaseWorkflowFindings,
|
|
732
|
+
workflowNodePins, yamlKeys,
|
|
733
|
+
};
|