session-orchestrator 5.12.0 → 5.13.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 +2 -2
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +162 -4
- package/README.md +126 -127
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +1 -1
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +90 -17
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +49 -15
- package/docs/session-config-template.md +30 -22
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +139 -49
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -2
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +39 -27
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +2 -2
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +4 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +72 -9
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +49 -5
- package/scripts/lib/so-hud/runtime.mjs +15 -4
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +14 -5
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +19 -7
- package/scripts/lib/vault-mirror/process.mjs +56 -27
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +79 -0
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +10 -17
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +29 -6
- package/scripts/state-md-idle-reset.mjs +2 -1
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/skills/_shared/fleet-protocol.md +142 -1
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +3 -2
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +94 -11
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +4 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +9 -3
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
package/scripts/release.mjs
CHANGED
|
@@ -21,8 +21,12 @@
|
|
|
21
21
|
// they are enforced by --check instead.
|
|
22
22
|
// --check Preflight: surface parity, CHANGELOG entry present
|
|
23
23
|
// + Unreleased folded, drift sweep, tag collision
|
|
24
|
-
// (local, origin, github), github/main mirror parity
|
|
25
|
-
//
|
|
24
|
+
// (local, origin, github), github/main mirror parity
|
|
25
|
+
// (under `mirror: curated`: github/main's Source-Commit
|
|
26
|
+
// trailer == HEAD instead of the sha, plus a green
|
|
27
|
+
// snapshot-probe receipt for HEAD's export tree),
|
|
28
|
+
// npm registry collision, npm token liveness and
|
|
29
|
+
// publish capability (Bypass-2FA), CI green
|
|
26
30
|
// on HEAD, leakage gate over retained archive filenames and contents.
|
|
27
31
|
// --publish Runs --check first, then token publish via temp
|
|
28
32
|
// userconfig (NPM_TOKEN from .env.local). A
|
|
@@ -30,7 +34,10 @@
|
|
|
30
34
|
// boundary: before it, failure aborts normally; after it,
|
|
31
35
|
// never rerun --publish. The tail tags AFTER receipt
|
|
32
36
|
// (never before — eliminates "tagged but unpublished"),
|
|
33
|
-
// pushes main + tag to origin AND github
|
|
37
|
+
// pushes main + tag to origin AND github (under
|
|
38
|
+
// `mirror: curated`: the verified export snapshot to
|
|
39
|
+
// github main and the tag on THAT snapshot — never HEAD
|
|
40
|
+
// or main, #1645), handles the
|
|
34
41
|
// GitHub release (`--verify-tag`), reconciles registry
|
|
35
42
|
// propagation, and polls the live site. A tag/push
|
|
36
43
|
// failure skips the tag-dependent GitHub-release and
|
|
@@ -102,6 +109,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
102
109
|
import { resolveRepoSpec } from './lib/vcs-repo-spec.mjs';
|
|
103
110
|
import { enumerateRepoFiles } from './lib/validate/enumerate-repo-files.mjs';
|
|
104
111
|
import { writeJsonAtomicSync } from './lib/io.mjs';
|
|
112
|
+
import {
|
|
113
|
+
buildGithubExport,
|
|
114
|
+
createGitRunner,
|
|
115
|
+
parseSourceCommitTrailer,
|
|
116
|
+
plainPushGuard,
|
|
117
|
+
pushUrlProblem,
|
|
118
|
+
readMirrorSetting,
|
|
119
|
+
verifyExportCommit,
|
|
120
|
+
} from './lib/github-export.mjs';
|
|
121
|
+
import { computeSnapshotTree, readSnapshotProbeReceipt } from './lib/snapshot-probe.mjs';
|
|
105
122
|
|
|
106
123
|
const PACKAGE_NAME = 'session-orchestrator';
|
|
107
124
|
const SPAWN_OPTS = { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 };
|
|
@@ -958,6 +975,172 @@ export function evaluateRemoteHeadParity(remote, ls, head, branch = 'main') {
|
|
|
958
975
|
: { ok: false, detail: `${remote}/${branch} at ${sha.slice(0, 8)}, HEAD at ${head.slice(0, 8)} — the mirror is behind` };
|
|
959
976
|
}
|
|
960
977
|
|
|
978
|
+
/**
|
|
979
|
+
* `head-pushed-github` under `mirror: curated` (#1645). The public main is a
|
|
980
|
+
* snapshot commit, never HEAD, so sha parity can never hold; what must hold is
|
|
981
|
+
* that github/main IS the snapshot of the commit being released — its
|
|
982
|
+
* `Source-Commit` trailer equals HEAD. Fail-closed on every gathering failure:
|
|
983
|
+
* an unreadable ls-remote, no sha, or a commit object that is not local.
|
|
984
|
+
*
|
|
985
|
+
* @param {{status: number, stdout?: string, stderr?: string}} ls — `git ls-remote github refs/heads/main`
|
|
986
|
+
* @param {string} head — the local HEAD sha
|
|
987
|
+
* @param {string|null} commitBody — `git cat-file commit <github main>` output, null when unreadable
|
|
988
|
+
* @returns {{ok: boolean, detail: string, sha: string|null}}
|
|
989
|
+
*/
|
|
990
|
+
export function evaluateCuratedMirrorParity(ls, head, commitBody) {
|
|
991
|
+
if (ls.status !== 0) {
|
|
992
|
+
return { ok: false, sha: null, detail: `ls-remote github failed (exit ${ls.status}): ${(ls.stderr || '').trim().slice(0, 200)}` };
|
|
993
|
+
}
|
|
994
|
+
const sha = (ls.stdout || '').trim().split(/\s+/)[0] || '';
|
|
995
|
+
if (!/^[0-9a-f]{40}$/i.test(sha)) {
|
|
996
|
+
return { ok: false, sha: null, detail: 'ls-remote github returned no sha for refs/heads/main — cannot compare' };
|
|
997
|
+
}
|
|
998
|
+
if (commitBody === null) {
|
|
999
|
+
return { ok: false, sha, detail: `github/main ${sha.slice(0, 8)} is not available locally (git fetch github failed) — snapshot source unknown` };
|
|
1000
|
+
}
|
|
1001
|
+
const source = parseSourceCommitTrailer(commitBody);
|
|
1002
|
+
if (!source) {
|
|
1003
|
+
return { ok: false, sha, detail: `github/main ${sha.slice(0, 8)} carries no Source-Commit trailer — publish the curated snapshot first (/close, mirror: curated)` };
|
|
1004
|
+
}
|
|
1005
|
+
return source === head
|
|
1006
|
+
? { ok: true, sha, detail: `curated: github/main ${sha.slice(0, 8)} is the snapshot of HEAD ${head.slice(0, 8)}` }
|
|
1007
|
+
: { ok: false, sha, detail: `curated: github/main ${sha.slice(0, 8)} is the snapshot of ${source.slice(0, 8)}, HEAD at ${head.slice(0, 8)} — the mirror is behind (/close publishes it)` };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* The two GitHub-mirror preflight rows plus the sha GitHub's CI must be asked
|
|
1012
|
+
* about. Reads `mirror` exactly as session-end § 4.4 does (`readMirrorSetting`):
|
|
1013
|
+
* - `curated`: `head-pushed-github` is the trailer parity above, and the CI sha
|
|
1014
|
+
* is the snapshot (GitHub never sees HEAD itself).
|
|
1015
|
+
* - anything else: the plain sha parity, unchanged — but the plain push is only
|
|
1016
|
+
* allowed when `plainPushGuard` agrees (no export policy in HEAD or on
|
|
1017
|
+
* origin's main), so a checkout whose Session Config predates
|
|
1018
|
+
* `mirror: curated` fails `github-mirror-mode` instead of later pushing the
|
|
1019
|
+
* private history.
|
|
1020
|
+
* - `none`: GitHub is not a release target at all — `skip` is true and the
|
|
1021
|
+
* github rows are reported as skipped (release.mjs used to push anyway).
|
|
1022
|
+
*
|
|
1023
|
+
* - every mode but `none`: `pushUrl` is the `github-push-url` row (#1693). The
|
|
1024
|
+
* publish tail refuses a push URL that differs from the fetch URL
|
|
1025
|
+
* (`pushUrlProblem`), but only AFTER the irreversible npm publish; the
|
|
1026
|
+
* preflight must see the same refusal first. A checkout with no `github`
|
|
1027
|
+
* remote has no push target, so the row is `skipped`. The detail is
|
|
1028
|
+
* `pushUrlProblem`'s own message, which names the remote but never a URL
|
|
1029
|
+
* (a URL may carry credentials).
|
|
1030
|
+
*
|
|
1031
|
+
* @param {string} repoRoot
|
|
1032
|
+
* @param {string} head
|
|
1033
|
+
* @returns {{ skip: boolean, curated?: boolean, mode: {ok: boolean, detail: string}, parity: {ok: boolean, detail: string}, pushUrl: {ok: boolean, detail: string}, ciSha: string }}
|
|
1034
|
+
*/
|
|
1035
|
+
export function evaluateGithubMirrorPreflight(repoRoot, head) {
|
|
1036
|
+
const setting = readMirrorSetting(repoRoot);
|
|
1037
|
+
if (setting.ok && setting.mirror === 'none') {
|
|
1038
|
+
const skipped = { ok: true, detail: "skipped — 'mirror: none'" };
|
|
1039
|
+
return { skip: true, mode: skipped, parity: skipped, pushUrl: skipped, ciSha: head };
|
|
1040
|
+
}
|
|
1041
|
+
const pushUrl = evaluateGithubPushUrl(repoRoot);
|
|
1042
|
+
const ls = run('git', ['ls-remote', 'github', 'refs/heads/main'], { cwd: repoRoot });
|
|
1043
|
+
if (setting.ok && setting.mirror === 'curated') {
|
|
1044
|
+
const sha = ls.status === 0 ? (ls.stdout || '').trim().split(/\s+/)[0] : '';
|
|
1045
|
+
let body = null;
|
|
1046
|
+
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
1047
|
+
const readBody = () => run('git', ['cat-file', 'commit', sha], { cwd: repoRoot });
|
|
1048
|
+
let cat = readBody();
|
|
1049
|
+
if (cat.status !== 0) {
|
|
1050
|
+
run('git', ['fetch', '--quiet', '--no-tags', 'github', 'refs/heads/main'], { cwd: repoRoot });
|
|
1051
|
+
cat = readBody();
|
|
1052
|
+
}
|
|
1053
|
+
body = cat.status === 0 ? cat.stdout : null;
|
|
1054
|
+
}
|
|
1055
|
+
const parity = evaluateCuratedMirrorParity(ls, head, body);
|
|
1056
|
+
return {
|
|
1057
|
+
skip: false,
|
|
1058
|
+
curated: true,
|
|
1059
|
+
mode: { ok: true, detail: 'curated — GitHub receives the export snapshot, never HEAD' },
|
|
1060
|
+
parity: { ok: parity.ok, detail: parity.detail },
|
|
1061
|
+
pushUrl,
|
|
1062
|
+
ciSha: parity.ok && parity.sha ? parity.sha : head,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
const parity = evaluateRemoteHeadParity('github', ls, head);
|
|
1066
|
+
if (!setting.ok) return { skip: false, mode: { ok: false, detail: setting.reason }, parity, pushUrl, ciSha: head };
|
|
1067
|
+
const verdict = plainPushGuard({ git: createGitRunner(repoRoot), explicitGithub: setting.mirror === 'github' });
|
|
1068
|
+
return {
|
|
1069
|
+
skip: false,
|
|
1070
|
+
mode: verdict.ok
|
|
1071
|
+
? { ok: true, detail: `plain (mirror: ${setting.mirror ?? 'unset'})` }
|
|
1072
|
+
: { ok: false, detail: `plain push refused — ${verdict.reason}` },
|
|
1073
|
+
parity,
|
|
1074
|
+
pushUrl,
|
|
1075
|
+
ciSha: head,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** Steps that run the test suite; a receipt without one proves only validate-plugin. */
|
|
1080
|
+
const SNAPSHOT_SUITE_STEPS = ['test', 'remote-gate'];
|
|
1081
|
+
const SNAPSHOT_PROBE_REMEDY = 'run: node scripts/snapshot-probe.mjs --host <alias> (or --local)';
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Verdict of the `snapshot-probe` preflight row (#1712). Release 5.13.0 found
|
|
1085
|
+
* three blockers only after the first curated GitHub push, because nobody had
|
|
1086
|
+
* ever run the suite on the exported tree. Green only for a receipt of THIS
|
|
1087
|
+
* snapshot tree with `ok: true`, every step `pass`, and at least one suite step
|
|
1088
|
+
* (`test` or `remote-gate`) — a validate-plugin-only receipt never ran a test.
|
|
1089
|
+
* Runs nothing itself. Every red detail ends with the remediation.
|
|
1090
|
+
*
|
|
1091
|
+
* @param {{ tree: {ok: boolean, snapshotTree?: string, reason?: string}, receiptResult?: {ok: boolean, receipt: any, reason?: string}|null }} input
|
|
1092
|
+
* @returns {{ ok: boolean, detail: string }}
|
|
1093
|
+
*/
|
|
1094
|
+
export function evaluateSnapshotProbeRow({ tree, receiptResult }) {
|
|
1095
|
+
const red = (why) => ({ ok: false, detail: `${why} — ${SNAPSHOT_PROBE_REMEDY}` });
|
|
1096
|
+
if (!tree?.ok) return red(`snapshot tree not computable (${tree?.reason ?? 'unknown'})`);
|
|
1097
|
+
const short = String(tree.snapshotTree).slice(0, 8);
|
|
1098
|
+
if (!receiptResult?.ok) {
|
|
1099
|
+
const reason = receiptResult?.reason ?? 'absent';
|
|
1100
|
+
return red(reason === 'absent' ? `no probe receipt for snapshot tree ${short}` : `probe receipt for tree ${short} ${reason}`);
|
|
1101
|
+
}
|
|
1102
|
+
const receipt = receiptResult.receipt;
|
|
1103
|
+
const steps = Array.isArray(receipt?.steps) ? receipt.steps : [];
|
|
1104
|
+
const failed = steps.filter((s) => s?.status !== 'pass').map((s) => `${s?.name ?? '?'}=${s?.status ?? '?'}`);
|
|
1105
|
+
if (failed.length > 0) return red(`probe of tree ${short} red: ${failed.join(', ')}`);
|
|
1106
|
+
if (receipt.ok !== true) return red(`probe receipt for tree ${short} is not ok`);
|
|
1107
|
+
const names = steps.map((s) => s.name);
|
|
1108
|
+
if (!names.some((n) => SNAPSHOT_SUITE_STEPS.includes(n))) {
|
|
1109
|
+
return red(`probe of tree ${short} ran no suite step (${names.join(', ') || 'no steps'})`);
|
|
1110
|
+
}
|
|
1111
|
+
return { ok: true, detail: `tree ${short} probed green (${names.join(', ')}; route ${receipt.route ?? 'unknown'})` };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Wiring of the `snapshot-probe` row: only under `mirror: curated` (read off the
|
|
1116
|
+
* {@link evaluateGithubMirrorPreflight} verdict). Under any other mode GitHub
|
|
1117
|
+
* never receives an exported tree, so the row is skipped.
|
|
1118
|
+
* @param {string} repoRoot @param {string} head @param {{ curated?: boolean }} mirror
|
|
1119
|
+
* @returns {{ ok: boolean, detail: string }}
|
|
1120
|
+
*/
|
|
1121
|
+
export function evaluateSnapshotProbePreflight(repoRoot, head, mirror) {
|
|
1122
|
+
if (!mirror?.curated) return { ok: true, detail: 'skipped — not mirror: curated' };
|
|
1123
|
+
const tree = computeSnapshotTree({ repoRoot, source: head });
|
|
1124
|
+
const receiptResult = tree.ok ? readSnapshotProbeReceipt({ repoRoot, snapshotTree: tree.snapshotTree }) : null;
|
|
1125
|
+
return evaluateSnapshotProbeRow({ tree, receiptResult });
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* The `github-push-url` preflight row (#1693): red when a push to `github`
|
|
1130
|
+
* would reach a different URL than the one every github read goes through.
|
|
1131
|
+
* @param {string} repoRoot
|
|
1132
|
+
* @returns {{ok: boolean, detail: string}}
|
|
1133
|
+
*/
|
|
1134
|
+
function evaluateGithubPushUrl(repoRoot) {
|
|
1135
|
+
const remotes = run('git', ['remote'], { cwd: repoRoot });
|
|
1136
|
+
if (remotes.status !== 0) return { ok: false, detail: `git remote failed (exit ${remotes.status}) — push URL unknown` };
|
|
1137
|
+
if (!(remotes.stdout || '').split('\n').map((r) => r.trim()).includes('github')) {
|
|
1138
|
+
return { ok: true, detail: "skipped — no 'github' remote" };
|
|
1139
|
+
}
|
|
1140
|
+
const problem = pushUrlProblem(createGitRunner(repoRoot), 'github');
|
|
1141
|
+
return problem ? { ok: false, detail: problem } : { ok: true, detail: 'push URL equals fetch URL' };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
961
1144
|
/**
|
|
962
1145
|
* npm-auth verdict over `npm whoami --userconfig <tmp>`.
|
|
963
1146
|
*
|
|
@@ -981,6 +1164,78 @@ export function evaluateNpmAuth(whoami) {
|
|
|
981
1164
|
: { ok: false, detail: 'npm whoami exited 0 with an empty identity — the token could not be confirmed' };
|
|
982
1165
|
}
|
|
983
1166
|
|
|
1167
|
+
/**
|
|
1168
|
+
* Does `abbreviated` (an `npm token list --json` `token` field) denote
|
|
1169
|
+
* `fullToken`? npm 11 prints the first 8 characters, a literal `...`, and the
|
|
1170
|
+
* last 4 (measured 2026-10-21, npm 11.19.1). Any other shape is no match —
|
|
1171
|
+
* guessing a looser form would let a different token answer for this one.
|
|
1172
|
+
*
|
|
1173
|
+
* @param {string} fullToken
|
|
1174
|
+
* @param {unknown} abbreviated
|
|
1175
|
+
* @returns {boolean}
|
|
1176
|
+
*/
|
|
1177
|
+
export function matchAbbreviatedNpmToken(fullToken, abbreviated) {
|
|
1178
|
+
if (typeof fullToken !== 'string' || typeof abbreviated !== 'string') return false;
|
|
1179
|
+
const m = abbreviated.match(/^(.{8})\.\.\.(.{4})$/);
|
|
1180
|
+
if (!m) return false;
|
|
1181
|
+
return fullToken.length > 12 && fullToken.startsWith(m[1]) && fullToken.endsWith(m[2]);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Judge whether the token in use can publish NON-INTERACTIVELY (#1712).
|
|
1186
|
+
*
|
|
1187
|
+
* `npm-token-live` (`npm whoami`) proves only that the token authenticates.
|
|
1188
|
+
* Release 5.13.0 passed it and then failed `npm publish` with EOTP: the
|
|
1189
|
+
* granular token lacked "Bypass 2FA". Without bypass, publish fails with EOTP
|
|
1190
|
+
* (account with 2FA) or E403 (account without) — either way the release
|
|
1191
|
+
* cannot finish, so `bypass_2fa !== true` is red, never a warning.
|
|
1192
|
+
*
|
|
1193
|
+
* The caller matches the token and strips `token`/`key`/`id` before calling;
|
|
1194
|
+
* this function never sees a token value, and its details name only the
|
|
1195
|
+
* token's NAME. Fail-closed: anything it cannot confirm is red.
|
|
1196
|
+
*
|
|
1197
|
+
* @param {{matches?: number, entry?: {name?: string, bypass_2fa?: unknown,
|
|
1198
|
+
* permissions?: Array<{name?: string, action?: string}>,
|
|
1199
|
+
* scopes?: Array<{name?: string|null, type?: string}>, revoked?: unknown,
|
|
1200
|
+
* expiry?: string|null} | null, now?: Date, packageName?: string, error?: string}} input
|
|
1201
|
+
* @returns {{ok: boolean, detail: string}}
|
|
1202
|
+
*/
|
|
1203
|
+
export function evaluateNpmPublishCapability({ matches, entry, now = new Date(), packageName = PACKAGE_NAME, error } = {}) {
|
|
1204
|
+
if (error) return { ok: false, detail: `npm token list failed: ${String(error).slice(0, 200)}` };
|
|
1205
|
+
if (matches !== 1 || !entry) {
|
|
1206
|
+
return { ok: false, detail: `cannot identify the token in use in npm token list (matches: ${matches})` };
|
|
1207
|
+
}
|
|
1208
|
+
const name = String(entry.name ?? '(unnamed)').slice(0, 80);
|
|
1209
|
+
if (entry.revoked !== null && entry.revoked !== undefined) {
|
|
1210
|
+
return { ok: false, detail: `token '${name}' is revoked` };
|
|
1211
|
+
}
|
|
1212
|
+
let expiry = null;
|
|
1213
|
+
if (entry.expiry !== null && entry.expiry !== undefined) {
|
|
1214
|
+
expiry = new Date(entry.expiry);
|
|
1215
|
+
if (Number.isNaN(expiry.getTime())) return { ok: false, detail: `token '${name}' has an unreadable expiry` };
|
|
1216
|
+
if (expiry.getTime() <= now.getTime()) {
|
|
1217
|
+
return { ok: false, detail: `token '${name}' expired ${expiry.toISOString().slice(0, 10)}` };
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
const perms = Array.isArray(entry.permissions) ? entry.permissions : [];
|
|
1221
|
+
if (!perms.some((p) => p?.name === 'package' && p?.action === 'write')) {
|
|
1222
|
+
return { ok: false, detail: `token '${name}' has no package write permission` };
|
|
1223
|
+
}
|
|
1224
|
+
// A package scope with `name: null` covers all packages (measured shape).
|
|
1225
|
+
const scopes = Array.isArray(entry.scopes) ? entry.scopes : [];
|
|
1226
|
+
if (!scopes.some((sc) => sc?.type === 'package' && (sc.name === null || sc.name === packageName))) {
|
|
1227
|
+
return { ok: false, detail: `token '${name}' is not scoped to ${packageName}` };
|
|
1228
|
+
}
|
|
1229
|
+
if (entry.bypass_2fa !== true) {
|
|
1230
|
+
return {
|
|
1231
|
+
ok: false,
|
|
1232
|
+
detail: `token '${name}' lacks Bypass-2FA — npm publish will fail with EOTP/E403; create a granular token per skills/npm-publish § Token requirements`,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
const expires = expiry ? `expires ${expiry.toISOString().slice(0, 10)}` : 'no expiry';
|
|
1236
|
+
return { ok: true, detail: `token '${name}': bypass-2FA, write, ${expires}` };
|
|
1237
|
+
}
|
|
1238
|
+
|
|
984
1239
|
/**
|
|
985
1240
|
* Turn a `checkCiStatus` result into the `ci-green-on-head` preflight row.
|
|
986
1241
|
*
|
|
@@ -1006,8 +1261,8 @@ export function evaluateNpmAuth(whoami) {
|
|
|
1006
1261
|
* those two. With neither present the text is byte-identical to
|
|
1007
1262
|
* before, so a plain green or a named failing job reads exactly as it did.
|
|
1008
1263
|
*
|
|
1009
|
-
* @param {null | {status?: string, failingJobName?: string,
|
|
1010
|
-
* detail?: string, details?: {reason?: string}}} ci
|
|
1264
|
+
* @param {null | {status?: string, failingJobName?: string, failingJobNames?: string[],
|
|
1265
|
+
* degraded?: string, detail?: string, details?: {reason?: string}}} ci
|
|
1011
1266
|
* @returns {{ok: boolean, detail: string}}
|
|
1012
1267
|
*/
|
|
1013
1268
|
export function evaluateCiRow(ci) {
|
|
@@ -1018,7 +1273,11 @@ export function evaluateCiRow(ci) {
|
|
|
1018
1273
|
}
|
|
1019
1274
|
// A failing job NAMES the failure, so it wins over the generic reason;
|
|
1020
1275
|
// `reason` is what fills the gap when there is no job to name (`unknown`).
|
|
1021
|
-
|
|
1276
|
+
// Several red jobs are ALL named (#1712): 5.13.0 showed only the first, and
|
|
1277
|
+
// a red informational Windows run sat beside the real cause. The names come
|
|
1278
|
+
// from `failingJobNames`, sanitised at generation like `failingJobName`.
|
|
1279
|
+
const names = Array.isArray(ci.failingJobNames) ? ci.failingJobNames : [];
|
|
1280
|
+
const reason = (names.length > 1 ? names.join(', ') : ci.failingJobName) || ci.details?.reason || '';
|
|
1022
1281
|
const why = reason ? ` (${reason})` : '';
|
|
1023
1282
|
return { ok: ci.status === 'green', detail: `status: ${ci.status}${why}` };
|
|
1024
1283
|
}
|
|
@@ -1149,6 +1408,8 @@ function readPackageVersion(repoRoot) {
|
|
|
1149
1408
|
* local HEAD `preflight()` already computed for the remote-parity rows).
|
|
1150
1409
|
* @param {{
|
|
1151
1410
|
* skipCi?: boolean,
|
|
1411
|
+
* githubSha?: string,
|
|
1412
|
+
* skipGithub?: boolean,
|
|
1152
1413
|
* checkCiStatus?: Function,
|
|
1153
1414
|
* resolveRepoSpec?: (opts: { repoRoot: string, vcs: 'github' }) => string|undefined,
|
|
1154
1415
|
* }} [deps]
|
|
@@ -1156,6 +1417,11 @@ function readPackageVersion(repoRoot) {
|
|
|
1156
1417
|
*/
|
|
1157
1418
|
export async function evaluateCiPreflightRows(repoRoot, head, {
|
|
1158
1419
|
skipCi = false,
|
|
1420
|
+
// Under `mirror: curated` GitHub never sees `head`; its CI ran on the snapshot
|
|
1421
|
+
// whose Source-Commit is `head` (#1645). Plain mirrors keep `head`.
|
|
1422
|
+
githubSha = head,
|
|
1423
|
+
// `mirror: none`: GitHub is no release target, so its CI is not consulted.
|
|
1424
|
+
skipGithub = false,
|
|
1159
1425
|
checkCiStatus: checkCiStatusDep,
|
|
1160
1426
|
resolveRepoSpec: resolveRepoSpecDep = resolveRepoSpec,
|
|
1161
1427
|
} = {}) {
|
|
@@ -1168,10 +1434,11 @@ export async function evaluateCiPreflightRows(repoRoot, head, {
|
|
|
1168
1434
|
const ci = await checkCiStatusImpl({ repoRoot, timeoutMs: 15000, sha: head });
|
|
1169
1435
|
const gitlab = evaluateCiRow(ci);
|
|
1170
1436
|
|
|
1437
|
+
if (skipGithub) return { gitlab, github: { ok: true, detail: "skipped — 'mirror: none'" } };
|
|
1171
1438
|
const githubSpec = resolveRepoSpecDep({ repoRoot, vcs: 'github' });
|
|
1172
1439
|
let githubCi = null;
|
|
1173
1440
|
if (githubSpec) {
|
|
1174
|
-
githubCi = await checkCiStatusImpl({ repoRoot, vcs: 'github', timeoutMs: 15000, sha:
|
|
1441
|
+
githubCi = await checkCiStatusImpl({ repoRoot, vcs: 'github', timeoutMs: 15000, sha: githubSha });
|
|
1175
1442
|
}
|
|
1176
1443
|
const github = evaluateGithubCiRow(githubSpec, githubCi);
|
|
1177
1444
|
|
|
@@ -1219,11 +1486,18 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
|
|
|
1219
1486
|
: '',
|
|
1220
1487
|
);
|
|
1221
1488
|
const head = run('git', ['rev-parse', 'HEAD'], { cwd: repoRoot }).stdout.trim();
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1489
|
+
const originLs = run('git', ['ls-remote', 'origin', 'refs/heads/main'], { cwd: repoRoot });
|
|
1490
|
+
const originParity = evaluateRemoteHeadParity('origin', originLs, head);
|
|
1491
|
+
add('head-pushed-origin', originParity.ok, originParity.detail);
|
|
1492
|
+
// github: sha parity for a plain mirror, Source-Commit parity under
|
|
1493
|
+
// `mirror: curated` (#1645) — see evaluateGithubMirrorPreflight.
|
|
1494
|
+
const mirror = evaluateGithubMirrorPreflight(repoRoot, head);
|
|
1495
|
+
add('github-mirror-mode', mirror.mode.ok, mirror.mode.detail);
|
|
1496
|
+
add('head-pushed-github', mirror.parity.ok, mirror.parity.detail);
|
|
1497
|
+
add('github-push-url', mirror.pushUrl.ok, mirror.pushUrl.detail);
|
|
1498
|
+
// Curated only: the exported tree must have been probed (#1712).
|
|
1499
|
+
const probe = evaluateSnapshotProbePreflight(repoRoot, head, mirror);
|
|
1500
|
+
add('snapshot-probe', probe.ok, probe.detail);
|
|
1227
1501
|
|
|
1228
1502
|
// 2. Surface parity.
|
|
1229
1503
|
const surfaceRows = scanSurfaces(repoRoot, target);
|
|
@@ -1277,7 +1551,7 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
|
|
|
1277
1551
|
? `git tag -l failed (exit ${localTagRes.status}) — local tag collision unknown`
|
|
1278
1552
|
: localTag && `${tag} already exists locally`,
|
|
1279
1553
|
);
|
|
1280
|
-
for (const remote of ['origin', 'github']) {
|
|
1554
|
+
for (const remote of mirror.skip ? ['origin'] : ['origin', 'github']) {
|
|
1281
1555
|
const ls = run('git', ['ls-remote', '--tags', remote, `refs/tags/${tag}`], { cwd: repoRoot });
|
|
1282
1556
|
const collision = ls.status === 0 && ls.stdout.trim() !== '';
|
|
1283
1557
|
add(`tag-free-${remote}`, ls.status === 0 && !collision, collision ? `${tag} already on ${remote}` : ls.status !== 0 ? `ls-remote ${remote} failed` : '');
|
|
@@ -1307,6 +1581,35 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
|
|
|
1307
1581
|
add('npm-token-live', false, err.message);
|
|
1308
1582
|
}
|
|
1309
1583
|
|
|
1584
|
+
// 5c. npm token PUBLISH capability (#1712). `whoami` was green for 5.13.0,
|
|
1585
|
+
// yet publish failed with EOTP: the token lacked Bypass-2FA. `npm token list`
|
|
1586
|
+
// (works with a granular token; `npm profile get` does not) carries that
|
|
1587
|
+
// flag. The token is matched INSIDE this block and only the stripped entry
|
|
1588
|
+
// leaves it — no token value, prefix or key reaches a detail string.
|
|
1589
|
+
try {
|
|
1590
|
+
const token = loadNpmToken(repoRoot);
|
|
1591
|
+
const listed = withTempUserconfig(token, (rc) => {
|
|
1592
|
+
const res = run('npm', ['token', 'list', '--json', '--userconfig', rc], { cwd: repoRoot });
|
|
1593
|
+
if (res.status !== 0) return { error: `exit ${res.status}: ${(res.stderr || '').trim().slice(0, 200)}` };
|
|
1594
|
+
let list;
|
|
1595
|
+
try {
|
|
1596
|
+
list = JSON.parse(res.stdout || '');
|
|
1597
|
+
} catch {
|
|
1598
|
+
return { error: 'output is not JSON' };
|
|
1599
|
+
}
|
|
1600
|
+
if (!Array.isArray(list)) return { error: 'output is not a JSON array' };
|
|
1601
|
+
const hits = list.filter((e) => matchAbbreviatedNpmToken(token, e?.token));
|
|
1602
|
+
if (hits.length !== 1) return { matches: hits.length, entry: null };
|
|
1603
|
+
// Destructured only to strip them: they never leave this closure.
|
|
1604
|
+
const { token: _abbr, key: _key, id: _id, ...entry } = hits[0];
|
|
1605
|
+
return { matches: 1, entry };
|
|
1606
|
+
});
|
|
1607
|
+
const verdict = evaluateNpmPublishCapability(listed);
|
|
1608
|
+
add('npm-token-publish', verdict.ok, verdict.detail);
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
add('npm-token-publish', false, err.message);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1310
1613
|
// 6/6b. CI green on HEAD, both platforms — judged for the commit actually
|
|
1311
1614
|
// being released, never whichever HEAD the queried platform reports (see
|
|
1312
1615
|
// evaluateCiPreflightRows's docblock for the `sha`-wiring history, #1332).
|
|
@@ -1321,11 +1624,13 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
|
|
|
1321
1624
|
// red, so `evaluateGithubCiRow` reports `skipped` rather than red — see its
|
|
1322
1625
|
// own docblock. --skip-ci is refused under --publish upstream in
|
|
1323
1626
|
// validateFlags(); it can only reach this branch from --check.
|
|
1324
|
-
|
|
1627
|
+
if (mirror.skip) add('tag-free-github', true, "skipped — 'mirror: none'");
|
|
1628
|
+
const ciRows = await evaluateCiPreflightRows(repoRoot, head, { skipCi, githubSha: mirror.ciSha, skipGithub: mirror.skip });
|
|
1325
1629
|
add('ci-green-on-head', ciRows.gitlab.ok, ciRows.gitlab.detail);
|
|
1326
1630
|
add('ci-green-on-head-github', ciRows.github.ok, ciRows.github.detail);
|
|
1327
1631
|
|
|
1328
|
-
|
|
1632
|
+
// `head` is handed to the publish tail, which refuses to tag anything else.
|
|
1633
|
+
return { checks, head };
|
|
1329
1634
|
}
|
|
1330
1635
|
|
|
1331
1636
|
function changelogExcerpt(repoRoot, target) {
|
|
@@ -1401,7 +1706,11 @@ export function evaluatePublishReceipt(result, target) {
|
|
|
1401
1706
|
target,
|
|
1402
1707
|
detail: confirmed
|
|
1403
1708
|
? `${PACKAGE_NAME}@${target} receipt confirmed`
|
|
1404
|
-
:
|
|
1709
|
+
: /\bEOTP\b/.test(output)
|
|
1710
|
+
// #1712: npm asks for a one-time password BEFORE uploading, so nothing
|
|
1711
|
+
// reached the registry and a re-run is safe.
|
|
1712
|
+
? `npm publish failed with EOTP (exit ${result.status}): the token lacks Bypass-2FA — nothing was published; re-running --publish with a Bypass-2FA granular token is safe (skills/npm-publish § Token requirements)`
|
|
1713
|
+
: `npm publish did not emit a target-confirmed receipt for ${PACKAGE_NAME}@${target} (exit ${result.status})`,
|
|
1405
1714
|
};
|
|
1406
1715
|
}
|
|
1407
1716
|
|
|
@@ -1835,10 +2144,100 @@ export function settlePublishReceipt(repoRoot, target, res, deps = {}) {
|
|
|
1835
2144
|
return { receipt, propagation, proof };
|
|
1836
2145
|
}
|
|
1837
2146
|
|
|
1838
|
-
|
|
2147
|
+
/**
|
|
2148
|
+
* Put the curated snapshot of `source` on github main and return its sha
|
|
2149
|
+
* (`mirror: curated`, #1645). Mirrors session-end § 4.4: the export is built on
|
|
2150
|
+
* the ls-remote-verified github main, re-verified BY SHA right before the push,
|
|
2151
|
+
* and that sha — never a ref name, never HEAD — is pushed. When github main
|
|
2152
|
+
* already is the snapshot of `source` (the normal case: `/close` published it
|
|
2153
|
+
* and the preflight proved the trailer), nothing is pushed and that sha is
|
|
2154
|
+
* returned; an unchanged tree whose trailer names another commit refuses,
|
|
2155
|
+
* because the tag must point at the snapshot OF the release commit.
|
|
2156
|
+
*
|
|
2157
|
+
* @param {string} repoRoot
|
|
2158
|
+
* @param {string} source — full sha of the release commit
|
|
2159
|
+
* @param {{ snapshotCheck?: Function }} [deps]
|
|
2160
|
+
* @returns {string}
|
|
2161
|
+
*/
|
|
2162
|
+
function publishCuratedSnapshot(repoRoot, source, deps = {}) {
|
|
2163
|
+
// The export requires refs/remotes/github/main == the remote's main.
|
|
2164
|
+
mustRun('git', ['fetch', '--quiet', '--no-tags', 'github', '+refs/heads/main:refs/remotes/github/main'], { cwd: repoRoot });
|
|
2165
|
+
const git = createGitRunner(repoRoot);
|
|
2166
|
+
const built = buildGithubExport({ git, source, ...(deps.snapshotCheck ? { snapshotCheck: deps.snapshotCheck } : {}) });
|
|
2167
|
+
if (built.status === 'created') {
|
|
2168
|
+
const verdict = verifyExportCommit({ git, sha: built.sha, source });
|
|
2169
|
+
if (!verdict.ok) throw new Error(`curated snapshot ${built.sha} failed verification — ${verdict.reason}`);
|
|
2170
|
+
mustRun('git', ['push', 'github', `${built.sha}:refs/heads/main`], { cwd: repoRoot });
|
|
2171
|
+
return built.sha;
|
|
2172
|
+
}
|
|
2173
|
+
if (built.status === 'unchanged') {
|
|
2174
|
+
const body = git(['cat-file', 'commit', built.sha]);
|
|
2175
|
+
if (body.status !== 0 || parseSourceCommitTrailer(body.stdout) !== source) {
|
|
2176
|
+
throw new Error(`github main ${built.sha} has the release tree but is not the snapshot of ${source} — refusing to tag it`);
|
|
2177
|
+
}
|
|
2178
|
+
const pushProblem = pushUrlProblem(git, 'github');
|
|
2179
|
+
if (pushProblem) throw new Error(pushProblem);
|
|
2180
|
+
return built.sha;
|
|
2181
|
+
}
|
|
2182
|
+
throw new Error(`curated export refused — ${built.reason}`);
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
/**
|
|
2186
|
+
* Why the publish tail may still tag HEAD, or null when it may (#1645). The
|
|
2187
|
+
* preflight checked one commit; minutes later (npm publish in between) HEAD can
|
|
2188
|
+
* have moved — a parallel session switching branch is enough — and whatever
|
|
2189
|
+
* HEAD is then gets tagged and, under `mirror: curated`, snapshotted onto
|
|
2190
|
+
* GitHub main. So, right before `git tag`: HEAD must still be the preflight
|
|
2191
|
+
* HEAD, on branch main, and equal origin's main read live (the same source
|
|
2192
|
+
* anchor session-end § 4.4 uses for curated pushes).
|
|
2193
|
+
*
|
|
2194
|
+
* @param {string} repoRoot
|
|
2195
|
+
* @param {string|undefined} expectedHead — the HEAD the preflight checked
|
|
2196
|
+
* @returns {string|null}
|
|
2197
|
+
*/
|
|
2198
|
+
export function releaseAnchorProblem(repoRoot, expectedHead) {
|
|
2199
|
+
if (typeof expectedHead !== 'string' || !/^[0-9a-f]{40}$/.test(expectedHead)) {
|
|
2200
|
+
return 'no preflight HEAD handed in — the release commit is not anchored';
|
|
2201
|
+
}
|
|
2202
|
+
const head = run('git', ['rev-parse', 'HEAD'], { cwd: repoRoot });
|
|
2203
|
+
const now = head.status === 0 ? head.stdout.trim() : '';
|
|
2204
|
+
if (now !== expectedHead) return `HEAD moved since preflight (${expectedHead.slice(0, 8)} → ${now.slice(0, 8) || 'unreadable'})`;
|
|
2205
|
+
const branch = run('git', ['branch', '--show-current'], { cwd: repoRoot });
|
|
2206
|
+
const name = branch.status === 0 ? branch.stdout.trim() : '';
|
|
2207
|
+
if (name !== 'main') return `HEAD is on '${name || 'detached'}', not main`;
|
|
2208
|
+
const origin = evaluateRemoteHeadParity('origin', run('git', ['ls-remote', 'origin', 'refs/heads/main'], { cwd: repoRoot }), expectedHead);
|
|
2209
|
+
return origin.ok ? null : `HEAD is not origin's main — ${origin.detail}`;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
/**
|
|
2213
|
+
* Tag the release commit and push main + tag to origin and the GitHub mirror.
|
|
2214
|
+
*
|
|
2215
|
+
* Under `mirror: curated` (read like session-end § 4.4) github never receives
|
|
2216
|
+
* main or the release commit: it gets the export snapshot as main, and the tag
|
|
2217
|
+
* `v<target>` is pushed onto THAT snapshot (#1645, decided: same tag name,
|
|
2218
|
+
* different target sha — the GitLab tag is the annotated tag on the release
|
|
2219
|
+
* commit, the GitHub tag a lightweight tag on its public snapshot). A plain
|
|
2220
|
+
* mirror is unchanged, except that `plainPushGuard` must allow it. Under
|
|
2221
|
+
* `mirror: none` GitHub is skipped entirely (`githubSkipped: true`). Nothing is
|
|
2222
|
+
* tagged unless {@link releaseAnchorProblem} accepts HEAD.
|
|
2223
|
+
*
|
|
2224
|
+
* @param {string} repoRoot
|
|
2225
|
+
* @param {string} target
|
|
2226
|
+
* @param {{ expectedHead?: string, snapshotCheck?: Function }} [deps] — `expectedHead`: the
|
|
2227
|
+
* preflight HEAD (required); `snapshotCheck`: test seam (the default leakage scan needs a
|
|
2228
|
+
* host owner policy)
|
|
2229
|
+
* @returns {{ tag: string, pushed: string[], githubSnapshot?: string, githubSkipped?: boolean }}
|
|
2230
|
+
*/
|
|
2231
|
+
export function tagAndPush(repoRoot, target, deps = {}) {
|
|
1839
2232
|
const tag = `v${target}`;
|
|
1840
2233
|
const progress = { tag, localTagCreated: false, pushed: [], remotes: [] };
|
|
1841
2234
|
try {
|
|
2235
|
+
const setting = readMirrorSetting(repoRoot);
|
|
2236
|
+
if (!setting.ok) throw new Error(`GitHub mirror mode unknown — ${setting.reason}; nothing tagged or pushed`);
|
|
2237
|
+
const curated = setting.mirror === 'curated';
|
|
2238
|
+
const githubSkipped = setting.mirror === 'none';
|
|
2239
|
+
const anchor = releaseAnchorProblem(repoRoot, deps.expectedHead);
|
|
2240
|
+
if (anchor) throw new Error(`release anchor lost — ${anchor}; nothing tagged or pushed`);
|
|
1842
2241
|
const excerpt = changelogExcerpt(repoRoot, target);
|
|
1843
2242
|
const msgDir = mkdtempSync(join(tmpdir(), 'release-tagmsg-'));
|
|
1844
2243
|
const msgFile = join(msgDir, 'msg');
|
|
@@ -1849,16 +2248,36 @@ function tagAndPush(repoRoot, target) {
|
|
|
1849
2248
|
} finally {
|
|
1850
2249
|
rmSync(msgDir, { recursive: true, force: true });
|
|
1851
2250
|
}
|
|
1852
|
-
for (const remote of ['origin', 'github']) {
|
|
2251
|
+
for (const remote of githubSkipped ? ['origin'] : ['origin', 'github']) {
|
|
1853
2252
|
const remoteProgress = { remote, mainPushed: false, tagPushed: false };
|
|
1854
2253
|
progress.remotes.push(remoteProgress);
|
|
2254
|
+
if (remote === 'github' && curated) {
|
|
2255
|
+
const released = mustRun('git', ['rev-parse', `${tag}^{commit}`], { cwd: repoRoot }).stdout.trim();
|
|
2256
|
+
if (released !== deps.expectedHead) throw new Error(`${tag} points at ${released}, not the preflight HEAD — refusing to publish it to github`);
|
|
2257
|
+
const snapshot = publishCuratedSnapshot(repoRoot, released, deps);
|
|
2258
|
+
remoteProgress.mainPushed = true;
|
|
2259
|
+
progress.githubSnapshot = snapshot;
|
|
2260
|
+
mustRun('git', ['push', 'github', `${snapshot}:refs/tags/${tag}`], { cwd: repoRoot });
|
|
2261
|
+
remoteProgress.tagPushed = true;
|
|
2262
|
+
progress.pushed.push(remote);
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
if (remote === 'github') {
|
|
2266
|
+
const verdict = plainPushGuard({ git: createGitRunner(repoRoot), explicitGithub: setting.mirror === 'github' });
|
|
2267
|
+
if (!verdict.ok) throw new Error(`plain push to github refused — ${verdict.reason}`);
|
|
2268
|
+
}
|
|
1855
2269
|
mustRun('git', ['push', remote, 'main'], { cwd: repoRoot });
|
|
1856
2270
|
remoteProgress.mainPushed = true;
|
|
1857
2271
|
mustRun('git', ['push', remote, tag], { cwd: repoRoot });
|
|
1858
2272
|
remoteProgress.tagPushed = true;
|
|
1859
2273
|
progress.pushed.push(remote);
|
|
1860
2274
|
}
|
|
1861
|
-
return {
|
|
2275
|
+
return {
|
|
2276
|
+
tag,
|
|
2277
|
+
pushed: progress.pushed,
|
|
2278
|
+
...(progress.githubSnapshot ? { githubSnapshot: progress.githubSnapshot } : {}),
|
|
2279
|
+
...(githubSkipped ? { githubSkipped: true } : {}),
|
|
2280
|
+
};
|
|
1862
2281
|
} catch (err) {
|
|
1863
2282
|
const failure = err instanceof Error ? err : new Error(String(err));
|
|
1864
2283
|
failure.releaseProgress = progress;
|
|
@@ -1873,7 +2292,8 @@ function tagAndPush(repoRoot, target) {
|
|
|
1873
2292
|
*
|
|
1874
2293
|
* @param {string} repoRoot
|
|
1875
2294
|
* @param {string} target
|
|
1876
|
-
* @param {{publishImpl?: Function, tagAndPushImpl?: Function, ensureGithubReleaseImpl?: Function, verifyLiveSiteImpl?: Function}} [deps]
|
|
2295
|
+
* @param {{publishImpl?: Function, tagAndPushImpl?: Function, ensureGithubReleaseImpl?: Function, verifyLiveSiteImpl?: Function, expectedHead?: string}} [deps]
|
|
2296
|
+
* `expectedHead` — the HEAD the preflight checked; the tail refuses to tag any other commit.
|
|
1877
2297
|
* @returns {Promise<{status: 'complete'|'post-publish-reconciliation', receipt: object, tag: string|null, pushed: string[], tagProgress?: object, release: object, live: object, propagation: object, reconciliation: Array<{phase: string, kind?: string, detail: string}>}>}
|
|
1878
2298
|
*/
|
|
1879
2299
|
export async function runPublishRelease(repoRoot, target, deps = {}) {
|
|
@@ -1898,7 +2318,7 @@ export async function runPublishRelease(repoRoot, target, deps = {}) {
|
|
|
1898
2318
|
const propagation = publication.propagation;
|
|
1899
2319
|
let tagAndPushResult;
|
|
1900
2320
|
try {
|
|
1901
|
-
tagAndPushResult = tagAndPushImpl(repoRoot, target);
|
|
2321
|
+
tagAndPushResult = tagAndPushImpl(repoRoot, target, { expectedHead: deps.expectedHead });
|
|
1902
2322
|
} catch (err) {
|
|
1903
2323
|
const rawProgress = err?.releaseProgress;
|
|
1904
2324
|
const remotes = Array.isArray(rawProgress?.remotes)
|
|
@@ -1935,8 +2355,10 @@ export async function runPublishRelease(repoRoot, target, deps = {}) {
|
|
|
1935
2355
|
};
|
|
1936
2356
|
}
|
|
1937
2357
|
|
|
1938
|
-
const { tag, pushed } = tagAndPushResult;
|
|
1939
|
-
const release =
|
|
2358
|
+
const { tag, pushed, githubSnapshot, githubSkipped } = tagAndPushResult;
|
|
2359
|
+
const release = githubSkipped
|
|
2360
|
+
? { ok: true, skipped: true, state: 'skipped-mirror-none', detail: "GitHub release skipped — 'mirror: none'" }
|
|
2361
|
+
: ensureGithubReleaseImpl(repoRoot, target);
|
|
1940
2362
|
const live = await verifyLiveSiteImpl(target);
|
|
1941
2363
|
const reconciliation = [];
|
|
1942
2364
|
if (!propagation?.ok) {
|
|
@@ -1954,6 +2376,7 @@ export async function runPublishRelease(repoRoot, target, deps = {}) {
|
|
|
1954
2376
|
receipt: publication.receipt,
|
|
1955
2377
|
tag,
|
|
1956
2378
|
pushed,
|
|
2379
|
+
...(githubSnapshot ? { githubSnapshot } : {}),
|
|
1957
2380
|
release,
|
|
1958
2381
|
live,
|
|
1959
2382
|
propagation,
|
|
@@ -2030,6 +2453,9 @@ export function printPublishOutcome(outcome, target, io = {}) {
|
|
|
2030
2453
|
}
|
|
2031
2454
|
if (!tagAndPushFailed) {
|
|
2032
2455
|
log(` tagged ${outcome.tag} (AFTER publish) and pushed main+tag to: ${outcome.pushed.join(', ')}.`);
|
|
2456
|
+
if (outcome.githubSnapshot) {
|
|
2457
|
+
log(` github (mirror: curated): main and ${outcome.tag} are the public snapshot ${outcome.githubSnapshot.slice(0, 12)}, not the release commit.`);
|
|
2458
|
+
}
|
|
2033
2459
|
} else {
|
|
2034
2460
|
// The npm receipt is already irreversible at this point, so the ONLY thing
|
|
2035
2461
|
// that helps the operator is the exact partial state tag-and-push reached.
|
|
@@ -2329,7 +2755,7 @@ async function main() {
|
|
|
2329
2755
|
|
|
2330
2756
|
if (values.check || values.publish) {
|
|
2331
2757
|
const target = readPackageVersion(repoRoot);
|
|
2332
|
-
const checks = await preflight(repoRoot, target, { skipCi: values['skip-ci'] });
|
|
2758
|
+
const { checks, head } = await preflight(repoRoot, target, { skipCi: values['skip-ci'] });
|
|
2333
2759
|
const checked = await withCheckedPackage(repoRoot, async (artifact) => {
|
|
2334
2760
|
checks.push({ name: 'leakage-gate', ok: true, detail: 'packed filenames and required owner-policy content scan verified' });
|
|
2335
2761
|
const ok = printChecks(checks, values.json && !values.publish, target);
|
|
@@ -2339,6 +2765,7 @@ async function main() {
|
|
|
2339
2765
|
console.log(' Publish and post-receipt reconciliation may take several minutes; do not kill it mid-run.');
|
|
2340
2766
|
const outcome = await runPublishRelease(repoRoot, target, {
|
|
2341
2767
|
publishImpl: (root, version) => publish(root, version, { artifact }),
|
|
2768
|
+
expectedHead: head,
|
|
2342
2769
|
});
|
|
2343
2770
|
return printPublishOutcome(outcome, target);
|
|
2344
2771
|
});
|