elanous 0.2.17 → 0.2.18
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-aac7332576c4d6fc.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +8 -1
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +8 -1
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +145 -40
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +73 -1
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +10 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/helper-scan.ts +10 -3
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +133 -15
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +38 -0
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +6 -4
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-supervisor.ts +14 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +59 -12
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +39 -6
- package/src/task-agent/judge.ts +31 -5
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +63 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -54,6 +54,8 @@ export interface GateResult {
|
|
|
54
54
|
knownEnvCleared: string[];
|
|
55
55
|
stalledEnv: Array<{ file: string; reason: 'no-output' | 'isolated-timeout'; local: 'passed' | 'failed' | 'timeout' }>;
|
|
56
56
|
baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
|
|
57
|
+
/** GATE-BASELINE-CACHE — isolated baseline files answered from the ledger (hits) or run now (misses). */
|
|
58
|
+
baselineCache?: { hits: number; misses: number };
|
|
57
59
|
durationMs: number;
|
|
58
60
|
error?: string;
|
|
59
61
|
stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
|
|
@@ -583,6 +585,61 @@ function fileOf(id: string): string {
|
|
|
583
585
|
return file;
|
|
584
586
|
}
|
|
585
587
|
|
|
588
|
+
/**
|
|
589
|
+
* GATE-BASELINE-CACHE (0.2.18) — the isolated `bun run test:deterministic <file>` result of one test file at one commit,
|
|
590
|
+
* kept in the machine ledger so a later gate does not run it again (0.2.17: the failing files were re-run on the previous
|
|
591
|
+
* release commit although that release had already measured them). Cache format harvested from draft #24500.
|
|
592
|
+
* `host` is where the result was measured (`local` or the ssh host) — a result from another host is a miss.
|
|
593
|
+
* Timeouts are never stored: an environment stall is measured again, not remembered.
|
|
594
|
+
*/
|
|
595
|
+
export interface BaselineFileResult { commit: string; file: string; host: string; failures: string[]; errors: string[]; missing: boolean }
|
|
596
|
+
export const baselineCachePath = (ledger: string, commit: string) => join(ledger, 'gate-baseline-cache', `${commit}.jsonl`);
|
|
597
|
+
function validFileResult(value: unknown, commit: string): value is BaselineFileResult {
|
|
598
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
599
|
+
const row = value as Partial<BaselineFileResult>;
|
|
600
|
+
if (row.commit !== commit || typeof row.file !== 'string' || typeof row.host !== 'string' || !row.host.trim()
|
|
601
|
+
|| typeof row.missing !== 'boolean' || !Array.isArray(row.failures) || !Array.isArray(row.errors)
|
|
602
|
+
|| (row.missing && (row.failures.length || row.errors.length))) return false;
|
|
603
|
+
try {
|
|
604
|
+
fileOf(`${row.file} > case`);
|
|
605
|
+
return [...row.failures, ...row.errors].every((id) => typeof id === 'string' && fileOf(id) === row.file);
|
|
606
|
+
} catch { return false; }
|
|
607
|
+
}
|
|
608
|
+
/** A missing cache file is empty; any unreadable or invalid row discards the whole file — it is never read as a pass. */
|
|
609
|
+
export function readBaselineFileCache(ledger: string, commit: string): Map<string, BaselineFileResult> {
|
|
610
|
+
const entries = new Map<string, BaselineFileResult>();
|
|
611
|
+
if (!sha.test(commit)) return entries;
|
|
612
|
+
const path = baselineCachePath(ledger, commit);
|
|
613
|
+
if (!existsSync(path)) return entries;
|
|
614
|
+
try {
|
|
615
|
+
// The writer always ends with one newline and never writes an empty line — anything else is a damaged file.
|
|
616
|
+
const text = readFileSync(path, 'utf8');
|
|
617
|
+
if (!text.endsWith('\n')) throw new Error('missing final newline');
|
|
618
|
+
for (const line of text.slice(0, -1).split('\n')) {
|
|
619
|
+
if (!line.trim()) throw new Error('empty line');
|
|
620
|
+
const row: unknown = JSON.parse(line);
|
|
621
|
+
if (!validFileResult(row, commit)) throw new Error('invalid row');
|
|
622
|
+
entries.set(`${row.file}\0${row.host}`, row);
|
|
623
|
+
}
|
|
624
|
+
} catch (error) {
|
|
625
|
+
debug.log('release.gate', 'baseline-cache-corrupt', { path, error: String(error) });
|
|
626
|
+
entries.clear();
|
|
627
|
+
}
|
|
628
|
+
return entries;
|
|
629
|
+
}
|
|
630
|
+
export function saveBaselineFileCache(ledger: string, commit: string, rows: BaselineFileResult[]): void {
|
|
631
|
+
if (!rows.length || !sha.test(commit)) return;
|
|
632
|
+
const entries = readBaselineFileCache(ledger, commit);
|
|
633
|
+
for (const row of rows) entries.set(`${row.file}\0${row.host}`, row);
|
|
634
|
+
const path = baselineCachePath(ledger, commit);
|
|
635
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
636
|
+
const temp = join(dirname(path), `.gate-baseline-${process.pid}-${randomUUID()}.tmp`);
|
|
637
|
+
try {
|
|
638
|
+
writeFileSync(temp, [...entries.values()].map((row) => JSON.stringify(row)).join('\n') + '\n', { mode: 0o600 });
|
|
639
|
+
renameSync(temp, path);
|
|
640
|
+
} finally { if (existsSync(temp)) rmSync(temp); }
|
|
641
|
+
}
|
|
642
|
+
|
|
586
643
|
export async function judgeGate(opts: GateOptions, runner: GateRunner = createGateRunner(opts.repo ?? process.cwd(), opts.remote)): Promise<GateResult> {
|
|
587
644
|
const start = Date.now();
|
|
588
645
|
const explicitConfig = getElanousConfigDirOverride();
|
|
@@ -743,7 +800,23 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
743
800
|
const diff = diffFailures(countedCut.counted, countedBaseline.counted);
|
|
744
801
|
result.fixed = diff.fixed.length;
|
|
745
802
|
result.preexisting = diff.common.length;
|
|
746
|
-
|
|
803
|
+
// GATE-BASELINE-CACHE — only the files whose new failures reproduce in isolation reach the baseline, and each
|
|
804
|
+
// (commit, file, host) is run at most once across gates: this gate's cut results also seed the next gate's baseline.
|
|
805
|
+
const cacheHost = opts.remote ?? 'local';
|
|
806
|
+
const newFailureFiles = new Set(diff.newFailures.map(fileOf));
|
|
807
|
+
const baseCache = newFailureFiles.size ? readBaselineFileCache(ledger, baseSha) : new Map<string, BaselineFileResult>();
|
|
808
|
+
const cacheStats = { hits: 0, misses: 0 };
|
|
809
|
+
const remember = (row: BaselineFileResult) => {
|
|
810
|
+
// Only a row the reader would accept is stored; an unstorable result skips the cache and leaves the verdict alone.
|
|
811
|
+
const { commit, file } = row;
|
|
812
|
+
if (!validFileResult(row, commit)) {
|
|
813
|
+
debug.log('release.gate', 'baseline-cache-skip', { commit, file });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
try { saveBaselineFileCache(ledger, row.commit, [row]); }
|
|
817
|
+
catch (error) { debug.log('release.gate', 'baseline-cache-write-failed', { commit: row.commit, file: row.file, error: String(error) }); }
|
|
818
|
+
};
|
|
819
|
+
for (const file of newFailureFiles) {
|
|
747
820
|
// 0.2.15: the cut re-check of a new-failure file (InsidePage.test.tsx · headless Chrome) hung for good — limit it like the host re-run.
|
|
748
821
|
const cutCheck = cutHostFailed.has(file) ? undefined : await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree, GATE_ISOLATED_TIMEOUT_MS);
|
|
749
822
|
if (cutCheck?.timedOut) {
|
|
@@ -754,30 +827,42 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
754
827
|
const isolatedCut = cutCheck === undefined
|
|
755
828
|
? { failures: cut.failures.filter((id) => fileOf(id) === file), errors: cut.errors.filter((id) => fileOf(id) === file) }
|
|
756
829
|
: failuresOf(cutCheck, `cut isolated ${file}`);
|
|
830
|
+
if (cutCheck !== undefined) remember({ commit: opts.commit, file, host: cacheHost, failures: isolatedCut.failures, errors: isolatedCut.errors, missing: false });
|
|
757
831
|
const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
|
|
758
832
|
const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
|
|
759
833
|
if (candidates.length === 0) continue;
|
|
760
|
-
const baselineTree = await getBaseTree();
|
|
761
|
-
const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree, GATE_ISOLATED_TIMEOUT_MS);
|
|
762
|
-
if (previous.timedOut) {
|
|
763
|
-
debug.log('release-loop.gate', 'isolated-timeout', { file, label: 'baseline isolated', limitMs: GATE_ISOLATED_TIMEOUT_MS });
|
|
764
|
-
result.stalledEnv.push({ file, reason: 'isolated-timeout', local: 'timeout' });
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
834
|
let oldFailures: Set<string>;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
oldFailures = new Set();
|
|
835
|
+
const hit = baseCache.get(`${file}\0${cacheHost}`);
|
|
836
|
+
if (hit) {
|
|
837
|
+
cacheStats.hits++;
|
|
838
|
+
oldFailures = new Set([...hit.failures, ...hit.errors]);
|
|
772
839
|
} else {
|
|
773
|
-
|
|
774
|
-
|
|
840
|
+
cacheStats.misses++;
|
|
841
|
+
const baselineTree = await getBaseTree();
|
|
842
|
+
const previous = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], baselineTree, GATE_ISOLATED_TIMEOUT_MS);
|
|
843
|
+
if (previous.timedOut) {
|
|
844
|
+
debug.log('release-loop.gate', 'isolated-timeout', { file, label: 'baseline isolated', limitMs: GATE_ISOLATED_TIMEOUT_MS });
|
|
845
|
+
result.stalledEnv.push({ file, reason: 'isolated-timeout', local: 'timeout' });
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (/No tests found|had no matches/i.test(previous.output) && previous.rc === 1) {
|
|
849
|
+
const lookup = await runner.command('git', ['ls-tree', '--name-only', baseSha, '--', file], opts.remote ? baselineTree : repo);
|
|
850
|
+
if (lookup.rc !== 0 || lookup.output.trim()) throw new Error(`baseline isolated run incomplete: ${file}`);
|
|
851
|
+
oldFailures = new Set();
|
|
852
|
+
remember({ commit: baseSha, file, host: cacheHost, failures: [], errors: [], missing: true });
|
|
853
|
+
} else {
|
|
854
|
+
const prior = failuresOf(previous, `baseline isolated ${file}`);
|
|
855
|
+
oldFailures = new Set([...prior.failures, ...prior.errors]);
|
|
856
|
+
remember({ commit: baseSha, file, host: cacheHost, failures: prior.failures, errors: prior.errors, missing: false });
|
|
857
|
+
}
|
|
775
858
|
}
|
|
776
859
|
for (const id of candidates) {
|
|
777
860
|
if (oldFailures.has(id)) result.preexisting++;
|
|
778
861
|
else result.introduced.push(id);
|
|
779
862
|
}
|
|
780
863
|
}
|
|
864
|
+
result.baselineCache = cacheStats;
|
|
865
|
+
debug.log('release.gate', 'baseline-cache', { hits: cacheStats.hits, misses: cacheStats.misses, commit: baseSha });
|
|
781
866
|
result.outcome = result.introduced.length ? 'regression' : 'ok';
|
|
782
867
|
} catch (error) {
|
|
783
868
|
result.error = error instanceof Error ? error.message : String(error);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { isReleaseVersion } from './release-version.js';
|
|
3
4
|
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { debug } from '../../src/debug/log.js';
|
|
5
6
|
|
|
@@ -17,7 +18,7 @@ export function readGraphContext(env: NodeJS.ProcessEnv = process.env): GraphCon
|
|
|
17
18
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid graph context');
|
|
18
19
|
const context = value as Record<string, unknown>;
|
|
19
20
|
const input = context.input as Record<string, unknown> | undefined;
|
|
20
|
-
if (!input || typeof input.version !== 'string' ||
|
|
21
|
+
if (!input || typeof input.version !== 'string' || !isReleaseVersion(input.version)
|
|
21
22
|
|| typeof input.previousVersion !== 'string' || !/^\d+\.\d+\.\d+$/.test(input.previousVersion)) throw new Error('version and previousVersion required');
|
|
22
23
|
return { input: input as GraphContext['input'], outputs: context.outputs && typeof context.outputs === 'object' ? context.outputs as GraphContext['outputs'] : {} };
|
|
23
24
|
}
|
|
@@ -7,6 +7,7 @@ import { debug } from '../../src/debug/log.js';
|
|
|
7
7
|
import { sendOutbound } from '../../src/domains/outbound-alert.js';
|
|
8
8
|
import { effectiveInstanceRoot, prodInstanceRoot } from '../../src/instance/resolve.js';
|
|
9
9
|
import { emitNodeResult, readGraphContext, type GraphContext } from './node-verdict.js';
|
|
10
|
+
import { prereleaseKind } from './release-version.js';
|
|
10
11
|
|
|
11
12
|
type Result = { outcome: 'ok' | 'fail'; verdict: 'pass' | 'fail'; summary: string; npm?: 'published' | 'already-published' | 'staged-not-visible'; humanAction?: 'npm-support' };
|
|
12
13
|
type Exec = (command: string, args: string[], env?: NodeJS.ProcessEnv) => { status: number | null; stdout: string; stderr: string };
|
|
@@ -47,8 +48,11 @@ function metadata(value: unknown): { name?: string; version?: string } {
|
|
|
47
48
|
export async function runNpmPublish(context: GraphContext = readGraphContext(), deps: NpmPublishDeps = {}): Promise<Result> {
|
|
48
49
|
const version = context.input.version;
|
|
49
50
|
let token = '';
|
|
51
|
+
// RELEASE-REHEARSAL-RC: a prerelease goes to `next` so `npm i elanous` never resolves to a rehearsal build.
|
|
52
|
+
let distTag = 'latest';
|
|
50
53
|
try {
|
|
51
54
|
if (context.outputs.publish?.outcome !== 'ok') throw new Error('publish must succeed before npm publish');
|
|
55
|
+
distTag = prereleaseKind(version) === null ? 'latest' : 'next';
|
|
52
56
|
const stateDir = deps.stateDir ?? effectiveInstanceRoot();
|
|
53
57
|
// REL3 — the archive prepare actually built (its output) wins over this process's universe: a resumed run can
|
|
54
58
|
// resolve another universe (10-01 0.2.7: prepared in the pilot test universe, resumed in production).
|
|
@@ -67,14 +71,14 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
|
|
|
67
71
|
const [manifest, tags] = await Promise.all([readRegistry(`${PACKAGE_URL}/${version}`), readRegistry(PACKAGE_URL)]);
|
|
68
72
|
if (manifest.status !== 200 && manifest.status !== 404) throw new Error(`npm registry version lookup failed (${manifest.status})`);
|
|
69
73
|
if (tags.status !== 200) throw new Error(`npm registry tags lookup failed (${tags.status})`);
|
|
70
|
-
const
|
|
71
|
-
? (tags.body as { 'dist-tags'?:
|
|
74
|
+
const tagged = tags.body && typeof tags.body === 'object' && 'dist-tags' in tags.body
|
|
75
|
+
? (tags.body as { 'dist-tags'?: Record<string, unknown> })['dist-tags']?.[distTag] : undefined;
|
|
72
76
|
if (manifest.status === 200 && (metadata(manifest.body).name !== 'elanous' || metadata(manifest.body).version !== version))
|
|
73
77
|
throw new Error('npm registry version metadata mismatch');
|
|
74
|
-
return { visible: manifest.status === 200, tagged: manifest.status === 200 &&
|
|
78
|
+
return { visible: manifest.status === 200, tagged: manifest.status === 200 && tagged === version };
|
|
75
79
|
};
|
|
76
80
|
const before = await inspect();
|
|
77
|
-
if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with
|
|
81
|
+
if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with ${distTag} tag` };
|
|
78
82
|
if (!before.visible) {
|
|
79
83
|
// The npm token is a machine secret (like the GitHub App key): input → this universe → the production root.
|
|
80
84
|
const tokenFile = context.input.npmTokenFile ?? [join(stateDir, 'secrets', 'npm-token'), join(deps.productionRoot ?? prodInstanceRoot(), 'secrets', 'npm-token')].find((p) => existsSync(p)) ?? join(stateDir, 'secrets', 'npm-token');
|
|
@@ -86,7 +90,7 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
|
|
|
86
90
|
try {
|
|
87
91
|
const npmrc = join(dir, 'npmrc');
|
|
88
92
|
writeFileSync(npmrc, `//registry.npmjs.org/:_authToken=${token}\n`, { mode: 0o600, flag: 'wx' });
|
|
89
|
-
published = run('npm', ['publish', candidate, '--ignore-scripts', '--tag',
|
|
93
|
+
published = run('npm', ['publish', candidate, '--ignore-scripts', '--tag', distTag, '--access', 'public'], { ...process.env, NPM_CONFIG_USERCONFIG: npmrc });
|
|
90
94
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
91
95
|
const output = `${published.stdout}\n${published.stderr}`;
|
|
92
96
|
const staged = published.status !== 0 && /\bE409\b/i.test(output) && /previously staged/i.test(output);
|
|
@@ -96,10 +100,10 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
|
|
|
96
100
|
let last = before;
|
|
97
101
|
for (let i = 0; i <= polls; i++) {
|
|
98
102
|
last = await inspect();
|
|
99
|
-
if (last.tagged) return { outcome: 'ok', verdict: 'pass', npm: before.visible ? 'already-published' : 'published', summary: `npm ${version} visible with
|
|
103
|
+
if (last.tagged) return { outcome: 'ok', verdict: 'pass', npm: before.visible ? 'already-published' : 'published', summary: `npm ${version} visible with ${distTag} tag` };
|
|
100
104
|
if (i < polls) await wait(Math.min(30_000, waitMinutes * 60_000 - i * 30_000));
|
|
101
105
|
}
|
|
102
|
-
if (last.visible) throw new Error(`npm ${version} visible but dist-tags
|
|
106
|
+
if (last.visible) throw new Error(`npm ${version} visible but dist-tags.${distTag} did not reach ${version} within ${waitMinutes} minutes`);
|
|
103
107
|
try { (deps.log ?? ((v, minutes) => debug.log('release-loop.npm', 'staged-not-visible', { version: v, waitedMinutes: minutes })))(version, waitMinutes); } catch { /* observation must not mask npm's staged state */ }
|
|
104
108
|
try { (deps.alert ?? sendOutbound)(`npm ${version} 가 스테이징에 멈춤 — 계정 소유자 확인 필요`, 'alert'); } catch { /* alert failure must not block docs */ }
|
|
105
109
|
return { outcome: 'ok', verdict: 'pass', npm: 'staged-not-visible', humanAction: 'npm-support', summary: 'npm 스테이징 — 레지스트리에 아직 없음' };
|
|
@@ -4,6 +4,7 @@ import { homedir, tmpdir } from 'node:os';
|
|
|
4
4
|
import { join, isAbsolute } from 'node:path';
|
|
5
5
|
import { debug } from '../../src/debug/log.js';
|
|
6
6
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandResult, type CommandRunner } from './node-verdict.js';
|
|
7
|
+
import { prereleaseKind } from './release-version.js';
|
|
7
8
|
|
|
8
9
|
interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string; hooks?: string; restart?: 'no-service' }
|
|
9
10
|
/** Where the internal feed is packed from: a checkout of the release tag, never the run's working tree. */
|
|
@@ -199,6 +200,12 @@ export function runOpsUpgrade(run: CommandRunner = runCommand, deps: OpsUpgradeD
|
|
|
199
200
|
|
|
200
201
|
if (import.meta.main) {
|
|
201
202
|
let version = '';
|
|
202
|
-
try {
|
|
203
|
+
try {
|
|
204
|
+
version = readGraphContext().input.version;
|
|
205
|
+
// RELEASE-REHEARSAL-RC: a prerelease never lands docs on main or upgrades the operating hosts.
|
|
206
|
+
process.exitCode = finishNode('ops-upgrade', version, prereleaseKind(version) !== null
|
|
207
|
+
? { outcome: 'ok', verdict: 'pass', summary: `ops-upgrade skipped — ${version} is a prerelease`, skipped: 'prerelease' }
|
|
208
|
+
: runOpsUpgrade());
|
|
209
|
+
}
|
|
203
210
|
catch (error) { process.exitCode = finishNode('ops-upgrade', version, errorResult(error)); }
|
|
204
211
|
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// `elanous release preflight` — 컷 30분 전 사전 점검(RELEASE-PREFLIGHT · 10-06).
|
|
2
|
+
//
|
|
3
|
+
// 🩸 계기(10-06 실측): 0.2.17 릴리스 런이 첫 노드(checklist-gate)에서 멎었다 — «발행 뒤» 칸(POSTPUB-VERIFY)이 그 판에
|
|
4
|
+
// 남아 있었고 게이트가 그것을 미완으로 셌다. 컷이 올 때까지 아무도 몰랐다. 새 규칙 = 착지 마감 = 컷 − 30분, 그 30분에 이 점검.
|
|
5
|
+
// ⛔ 읽기 전용 — 칸을 옮기지도, 동결을 바꾸지도, push 하지도 않는다. 옮길 것은 «명령»으로 제안만 한다.
|
|
6
|
+
// ⛔ 게이트 규칙을 다시 쓰지 않는다 — checklist-gate 노드 · release run 진입 검사 · 예약 시작 준비 검사(releaseReadiness, #24557)가
|
|
7
|
+
// 함께 쓰는 단 하나의 판정 cutChecklistGate 를 그대로 부르고, 예약 시작 경로는 releaseReadiness 를 CLI 와 같은 인자로 부른다.
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { cutChecklistGate, listChecklist, type ChecklistItem } from '../../src/release-loop/checklist.js';
|
|
11
|
+
import { getSchedule, formatKst, type ReleaseSchedule } from '../../src/release-loop/release-schedule.js';
|
|
12
|
+
import { readLandingFreeze, landingFreezeMessage, type LandingFreeze } from '../../src/release-loop/landing-freeze.js';
|
|
13
|
+
import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
14
|
+
import { debug } from '../../src/debug/log.js';
|
|
15
|
+
import { releaseReadiness } from './release-readiness.js';
|
|
16
|
+
import { latestPublishedPreviousVersion, runUnattendedRelease, type UnattendedReleaseDeps } from './unattended-release.js';
|
|
17
|
+
|
|
18
|
+
export type PreflightLevel = 'ok' | 'warn' | 'block';
|
|
19
|
+
export type PreflightCheck = 'checklist' | 'postpub' | 'run-input' | 'freeze' | 'schedule';
|
|
20
|
+
export interface PreflightFinding { level: PreflightLevel; check: PreflightCheck; id?: string; message: string; suggestion?: string }
|
|
21
|
+
export interface GateMeasure { version: string; runId: string; gateSeconds: number; toPublishSeconds?: number }
|
|
22
|
+
const compareStable = (a: string, b: string) => { const x = a.split('.').map(Number), y = b.split('.').map(Number); return x[0]! - y[0]! || x[1]! - y[1]! || x[2]! - y[2]!; };
|
|
23
|
+
export interface PreflightResult {
|
|
24
|
+
version: string;
|
|
25
|
+
next: string;
|
|
26
|
+
verdict: PreflightLevel;
|
|
27
|
+
blockers: number;
|
|
28
|
+
warnings: number;
|
|
29
|
+
findings: PreflightFinding[];
|
|
30
|
+
schedule: { cutAt: string; landBy: string | null; publishAt: string | null; publishAtSource: 'option' | 'cadence' | null } | null;
|
|
31
|
+
/** The immediately previous release (by version order) whose gate is compared. */
|
|
32
|
+
previousVersion: string | null;
|
|
33
|
+
gateMeasure: GateMeasure | null;
|
|
34
|
+
/** When the previous release has no record: the newest measured older release, labelled with its version. */
|
|
35
|
+
fallbackGateMeasure: GateMeasure | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PreflightDeps {
|
|
39
|
+
now?: Date;
|
|
40
|
+
/** Planned publish time (ISO). Without it, the October cadence (07:00 · 18:00 KST) after the cut is assumed. */
|
|
41
|
+
publishAt?: string;
|
|
42
|
+
runInput?: (version: string) => Promise<{ input: unknown }>;
|
|
43
|
+
freeze?: () => LandingFreeze | null;
|
|
44
|
+
schedule?: (version: string) => ReleaseSchedule | null;
|
|
45
|
+
graphRunsRoot?: string;
|
|
46
|
+
releaseRunDeps?: UnattendedReleaseDeps;
|
|
47
|
+
/** The immediately previous release; default = the run input's previousVersion (release ledger). */
|
|
48
|
+
previousVersion?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const POSTPUB_ID = /^POSTPUB-/i;
|
|
52
|
+
// Only a title that «starts» with the marker: a pre-publish cell often mentions «발행 뒤» in its body (a slot, a plan).
|
|
53
|
+
const POSTPUB_TITLE = /^[«"'(]?\s*(발행\s*(뒤|후)|post-?publish)/i;
|
|
54
|
+
const STABLE = /^\d+\.\d+\.\d+$/;
|
|
55
|
+
|
|
56
|
+
export function isPostPublishCell(item: Pick<ChecklistItem, 'id' | 'title'> & { kind?: string }): boolean {
|
|
57
|
+
return POSTPUB_ID.test(item.id) || POSTPUB_TITLE.test(item.title) || (item as { kind?: string }).kind === 'postpub';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function nextPatch(version: string): string {
|
|
61
|
+
const [major, minor, patch] = version.split('.').map(Number);
|
|
62
|
+
return `${major}.${minor}.${patch! + 1}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const kstHour = (ms: number) => Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Seoul', hour: '2-digit', hourCycle: 'h23' }).format(new Date(ms)));
|
|
66
|
+
|
|
67
|
+
/** October cadence: the first 07:00 or 18:00 KST strictly after the cut (a cut at 07:00 publishes at 18:00). */
|
|
68
|
+
export function cadencePublishAt(cutAt: string): string {
|
|
69
|
+
const cut = Date.parse(cutAt);
|
|
70
|
+
const startOfHour = Math.floor(cut / 3_600_000) * 3_600_000 + 3_600_000;
|
|
71
|
+
for (let t = startOfHour; t < cut + 48 * 3_600_000; t += 3_600_000) {
|
|
72
|
+
const h = kstHour(t);
|
|
73
|
+
if (h === 7 || h === 18) return new Date(t).toISOString();
|
|
74
|
+
}
|
|
75
|
+
return new Date(cut).toISOString();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A release's measured gate: the newest finished release-loop run of «that» version (`{ version }`) or, with
|
|
80
|
+
* `{ below }`, the most recently measured run of any version under it. Node timings are not stored in the run state, so the gate time is the start of the gate node's context file to
|
|
81
|
+
* the start of the next node's (contexts/<n>.json is written when node n starts). Null when nothing was measured.
|
|
82
|
+
*/
|
|
83
|
+
export function releaseGateMeasure(which: { version: string } | { below: string }, root = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop')): GateMeasure | null {
|
|
84
|
+
if (!existsSync(root)) return null;
|
|
85
|
+
const runs: Array<{ runId: string; version: string; startedAt: string; path: string[]; file: string }> = [];
|
|
86
|
+
for (const file of readdirSync(root)) {
|
|
87
|
+
if (!file.endsWith('.json') || file.includes('.decision')) continue;
|
|
88
|
+
try {
|
|
89
|
+
const state = JSON.parse(readFileSync(join(root, file), 'utf8')) as { runId?: string; status?: string; startedAt?: string; path?: string[]; input?: { version?: string } };
|
|
90
|
+
const v = state.input?.version;
|
|
91
|
+
if (state.status !== 'done' || typeof v !== 'string' || !STABLE.test(v) || !Array.isArray(state.path) || typeof state.startedAt !== 'string') continue;
|
|
92
|
+
if ('version' in which ? v !== which.version : compareStable(v, which.below) >= 0) continue;
|
|
93
|
+
runs.push({ runId: state.runId ?? file.slice(0, -5), version: v, startedAt: state.startedAt, path: state.path, file });
|
|
94
|
+
} catch { /* an unreadable run is skipped — the measure is best-effort */ }
|
|
95
|
+
}
|
|
96
|
+
// `{ version }`: the newest run of that release. `{ below }` (only a labelled reference when the previous release has
|
|
97
|
+
// no record): the most recently measured run, not the highest version.
|
|
98
|
+
runs.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
|
|
99
|
+
for (const run of runs) {
|
|
100
|
+
const contexts = join(root, `${run.file}.contexts`);
|
|
101
|
+
const index = run.path.indexOf('gate');
|
|
102
|
+
if (index < 0 || !existsSync(contexts)) continue;
|
|
103
|
+
const at = (i: number): number | null => {
|
|
104
|
+
const path = join(contexts, `${i + 1}.json`);
|
|
105
|
+
try {
|
|
106
|
+
const ctx = JSON.parse(readFileSync(path, 'utf8')) as { nodeId?: string };
|
|
107
|
+
return ctx.nodeId === run.path[i] ? statSync(path).mtimeMs : null;
|
|
108
|
+
} catch { return null; }
|
|
109
|
+
};
|
|
110
|
+
const gateStart = at(index), gateEnd = at(index + 1);
|
|
111
|
+
if (gateStart === null || gateEnd === null || gateEnd < gateStart) continue;
|
|
112
|
+
const publishIndex = run.path.indexOf('publish');
|
|
113
|
+
const publishStart = publishIndex >= 0 ? at(publishIndex) : null;
|
|
114
|
+
return {
|
|
115
|
+
version: run.version, runId: run.runId, gateSeconds: Math.round((gateEnd - gateStart) / 1000),
|
|
116
|
+
...(publishStart !== null ? { toPublishSeconds: Math.round((publishStart - Date.parse(run.startedAt)) / 1000) } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const minutes = (seconds: number) => `${Math.round(seconds / 60)}분`;
|
|
123
|
+
const short = (title: string) => (title.length > 60 ? `${title.slice(0, 60)}…` : title);
|
|
124
|
+
|
|
125
|
+
export async function releasePreflight(version: string, deps: PreflightDeps = {}): Promise<PreflightResult> {
|
|
126
|
+
if (!STABLE.test(version)) throw new Error(`release preflight takes a stable version x.y.z: ${version}`);
|
|
127
|
+
const now = deps.now ?? new Date();
|
|
128
|
+
const next = nextPatch(version);
|
|
129
|
+
const findings: PreflightFinding[] = [];
|
|
130
|
+
const moveHint = (id: string) => `elanous release checklist move ${id} --from ${version} --to ${next} --reason "발행 뒤 칸 — 다음 판에서 확인"`;
|
|
131
|
+
|
|
132
|
+
// 1·2. Checklist gate simulation «at the cut» (the node and the run entry both judge after land-by) ⊕ post-publish cells.
|
|
133
|
+
const schedule = (deps.schedule ?? ((v: string) => getSchedule(v, deps.releaseRunDeps?.ledgerRoot)))(version);
|
|
134
|
+
// Always the stored cut (before or after it): the preflight simulates «the cut», not the moment it is run.
|
|
135
|
+
const judgeAt = schedule ? new Date(Date.parse(schedule.cutAt)) : now;
|
|
136
|
+
const data = listChecklist(version);
|
|
137
|
+
const { autoMoved, ...gate } = cutChecklistGate(version, schedule?.landBy, judgeAt);
|
|
138
|
+
const p0 = data.items.filter((item) => item.status === 'yellow' && item.priority === 'P0').map((item) => item.id);
|
|
139
|
+
const byId = new Map(data.items.map((item) => [item.id, item]));
|
|
140
|
+
const blockReason = new Map<string, string>();
|
|
141
|
+
for (const id of gate.red) blockReason.set(id, '빨강');
|
|
142
|
+
for (const id of gate.undecided) blockReason.set(id, '노랑 · 처분 없음(판정 없음)');
|
|
143
|
+
for (const id of gate.blocked) blockReason.set(id, p0.includes(id) ? 'P0 노랑' : '노랑 · 처분 block');
|
|
144
|
+
// The scheduled start (`release run --if-ready` · `auto-start`) first asks releaseReadiness — the same call the CLI
|
|
145
|
+
// makes, judged at the cut. It shares cutChecklistGate (#24557); an injected deps.checklist may still differ, so any
|
|
146
|
+
// cell it refuses that the cut judgement did not is reported too (report, don't re-rule).
|
|
147
|
+
try {
|
|
148
|
+
const readiness = releaseReadiness(version, { ledgerRoot: deps.releaseRunDeps?.ledgerRoot, checklist: deps.releaseRunDeps?.checklist, now: () => judgeAt });
|
|
149
|
+
if (readiness.reason === 'checklist-blocked') {
|
|
150
|
+
for (const id of [...readiness.details.red, ...readiness.details.undecided, ...readiness.details.blocked]) {
|
|
151
|
+
if (!blockReason.has(id)) blockReason.set(id, '예약 시작(release run --if-ready · auto-start)의 준비 검사(releaseReadiness)가 런을 거부한다');
|
|
152
|
+
}
|
|
153
|
+
} else if (readiness.reason === 'already-published' || readiness.reason === 'already-running') {
|
|
154
|
+
// Both refuse the scheduled start — a running release (live run.lock) or a published one.
|
|
155
|
+
findings.push({ level: 'block', check: 'run-input', message: `준비 검사(releaseReadiness)가 예약 시작을 거부한다: ${readiness.reason} (${readiness.details})` });
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
findings.push({ level: 'block', check: 'run-input', message: `준비 검사(releaseReadiness) 오류 — ${error instanceof Error ? error.message : String(error)}` });
|
|
159
|
+
}
|
|
160
|
+
for (const [id, reason] of blockReason) {
|
|
161
|
+
const item = byId.get(id);
|
|
162
|
+
const postpub = item ? isPostPublishCell(item) : false;
|
|
163
|
+
findings.push({
|
|
164
|
+
level: 'block', check: postpub ? 'postpub' : 'checklist', id,
|
|
165
|
+
message: `${id} — ${reason}${postpub ? ' · 발행 뒤 칸이 이 판에 남아 있다' : ''}${item ? ` · «${short(item.title)}»` : ''}`,
|
|
166
|
+
...(postpub ? { suggestion: moveHint(id) } : {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
for (const item of data.items) {
|
|
170
|
+
if (blockReason.has(item.id) || !isPostPublishCell(item)) continue;
|
|
171
|
+
findings.push({ level: 'warn', check: 'postpub', id: item.id, message: `${item.id} — 발행 뒤 칸이 이 판에 남아 있다(지금은 막지 않음 · 상태 ${item.status} · 처분 ${item.disposition ?? '-'})`, suggestion: moveHint(item.id) });
|
|
172
|
+
}
|
|
173
|
+
for (const id of autoMoved) {
|
|
174
|
+
if (!blockReason.has(id)) findings.push({ level: 'warn', check: 'checklist', id, message: `${id} — 처분 없는 노랑 · 컷(착지 마감 뒤)에서 노드가 ${next} 로 자동 이월한다` });
|
|
175
|
+
}
|
|
176
|
+
for (const p of gate.parity ?? []) findings.push({ level: 'warn', check: 'checklist', id: p.id, message: `${p.id} — 화면 짝 경고: ${p.why}` });
|
|
177
|
+
if (!blockReason.size) findings.push({ level: 'ok', check: 'checklist', message: `체크리스트 게이트 모의 통과 — ${data.items.length}칸 · 이월 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length}` });
|
|
178
|
+
|
|
179
|
+
// 3. The same input check `release run --dry-run` makes.
|
|
180
|
+
let runPrevious: string | undefined;
|
|
181
|
+
try {
|
|
182
|
+
const result = await (deps.runInput ?? ((v: string) => runUnattendedRelease({ version: v, dryRun: true }, deps.releaseRunDeps ?? {})))(version);
|
|
183
|
+
const input = result.input as { previousVersion?: string; gatePodPool?: string };
|
|
184
|
+
runPrevious = input.previousVersion;
|
|
185
|
+
findings.push({ level: 'ok', check: 'run-input', message: `release run --dry-run 입력 OK — 직전 판 ${input.previousVersion ?? '-'} · 게이트 풀 ${input.gatePodPool ?? '-'}` });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
findings.push({ level: 'block', check: 'run-input', message: `release run --dry-run 입력 오류 — ${error instanceof Error ? error.message : String(error)}` });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 4. Landing freeze (emergency switch since RELEASE-BRANCH): when on, `release run` refuses.
|
|
191
|
+
try {
|
|
192
|
+
const frozen = (deps.freeze ?? (() => readLandingFreeze(deps.releaseRunDeps?.freezeRoot)))();
|
|
193
|
+
if (frozen) findings.push({ level: 'block', check: 'freeze', message: `${landingFreezeMessage(frozen)} — release run 이 거부한다`, suggestion: 'elanous freeze off (또는 의도한 강행이면 release run --force-freeze)' });
|
|
194
|
+
else findings.push({ level: 'ok', check: 'freeze', message: '착지 동결 꺼짐' });
|
|
195
|
+
} catch (error) {
|
|
196
|
+
findings.push({ level: 'block', check: 'freeze', message: `동결 상태를 못 읽었다 — ${error instanceof Error ? error.message : String(error)}` });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 5. Schedule sanity: land-by = cut − 30 min · cut→publish window vs the previous measured gate.
|
|
200
|
+
let scheduleOut: PreflightResult['schedule'] = null;
|
|
201
|
+
let gateMeasure: GateMeasure | null = null;
|
|
202
|
+
let fallbackGateMeasure: GateMeasure | null = null;
|
|
203
|
+
let previousVersion: string | null = deps.previousVersion ?? runPrevious ?? null;
|
|
204
|
+
if (!previousVersion) { try { previousVersion = latestPublishedPreviousVersion(version, deps.releaseRunDeps?.ledgerRoot); } catch { previousVersion = null; } }
|
|
205
|
+
if (!schedule) {
|
|
206
|
+
findings.push({ level: 'warn', check: 'schedule', message: `판 일정 없음 — elanous release schedule set --version ${version} --cut-at <iso> --land-by <iso>` });
|
|
207
|
+
} else {
|
|
208
|
+
const cut = Date.parse(schedule.cutAt);
|
|
209
|
+
const publishAt = deps.publishAt ? new Date(deps.publishAt).toISOString() : cadencePublishAt(schedule.cutAt);
|
|
210
|
+
scheduleOut = { cutAt: schedule.cutAt, landBy: schedule.landBy, publishAt, publishAtSource: deps.publishAt ? 'option' : 'cadence' };
|
|
211
|
+
if (!schedule.landBy) findings.push({ level: 'warn', check: 'schedule', message: `착지 마감 없음 — 컷 ${formatKst(schedule.cutAt)} · 규칙은 컷 − 30분` });
|
|
212
|
+
else if (cut - Date.parse(schedule.landBy) < 30 * 60_000) findings.push({ level: 'warn', check: 'schedule', message: `착지 마감 ${formatKst(schedule.landBy)} 이 컷 − 30분보다 늦다(컷 ${formatKst(schedule.cutAt)})` });
|
|
213
|
+
else findings.push({ level: 'ok', check: 'schedule', message: `일정 — 착지 마감 ${formatKst(schedule.landBy)} · 컷 ${formatKst(schedule.cutAt)}` });
|
|
214
|
+
gateMeasure = previousVersion ? releaseGateMeasure({ version: previousVersion }, deps.graphRunsRoot) : null;
|
|
215
|
+
if (!gateMeasure) fallbackGateMeasure = releaseGateMeasure({ below: version }, deps.graphRunsRoot);
|
|
216
|
+
const windowSeconds = Math.round((Date.parse(publishAt) - cut) / 1000);
|
|
217
|
+
const publishLabel = `발행 ${formatKst(publishAt)}${deps.publishAt ? '' : '(기본: 판 주기 07·18시 · --publish-at 로 바꾼다)'}`;
|
|
218
|
+
if (!gateMeasure) {
|
|
219
|
+
const fallback = fallbackGateMeasure ? ` · 참고: 가장 최근 실측은 ${fallbackGateMeasure.version} 게이트 ${minutes(fallbackGateMeasure.gateSeconds)}(직전 판 아님)` : ' · 게이트 실측 없음';
|
|
220
|
+
findings.push({ level: 'warn', check: 'schedule', message: `직전 판${previousVersion ? `(${previousVersion})` : ''} 실측 없음 — 컷→${publishLabel} 창 ${minutes(windowSeconds)}${fallback}` });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const measured = `직전 실측 ${gateMeasure.version} 게이트 ${minutes(gateMeasure.gateSeconds)}${gateMeasure.toPublishSeconds !== undefined ? ` · 런 시작→발행 노드 ${minutes(gateMeasure.toPublishSeconds)}` : ''}`;
|
|
224
|
+
const need = Math.max(gateMeasure.gateSeconds, gateMeasure.toPublishSeconds ?? 0);
|
|
225
|
+
findings.push(windowSeconds < need
|
|
226
|
+
? { level: 'warn', check: 'schedule', message: `컷→${publishLabel} 창 ${minutes(windowSeconds)} < ${measured} — 발행이 늦어진다` }
|
|
227
|
+
: { level: 'ok', check: 'schedule', message: `컷→${publishLabel} 창 ${minutes(windowSeconds)} ≥ ${measured}` });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const blockers = findings.filter((f) => f.level === 'block').length;
|
|
232
|
+
const warnings = findings.filter((f) => f.level === 'warn').length;
|
|
233
|
+
debug.log('release.preflight', 'checked', { version, blockers, warnings });
|
|
234
|
+
return { version, next, verdict: blockers ? 'block' : warnings ? 'warn' : 'ok', blockers, warnings, findings, schedule: scheduleOut, previousVersion, gateMeasure, fallbackGateMeasure };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ICON: Record<PreflightLevel, string> = { ok: '✅', warn: '⚠️', block: '⛔' };
|
|
238
|
+
|
|
239
|
+
export function formatPreflight(result: PreflightResult): string[] {
|
|
240
|
+
const order: PreflightLevel[] = ['block', 'warn', 'ok'];
|
|
241
|
+
const lines = [...result.findings].sort((a, b) => order.indexOf(a.level) - order.indexOf(b.level))
|
|
242
|
+
.flatMap((f) => [`${ICON[f.level]} ${f.message}`, ...(f.suggestion ? [` → ${f.suggestion}`] : [])]);
|
|
243
|
+
const verdict = result.verdict === 'block' ? `⛔ 판정: 막힘 — ⛔${result.blockers} ⚠️${result.warnings} · 컷 전에 고친다`
|
|
244
|
+
: result.verdict === 'warn' ? `⚠️ 판정: 통과(경고 ${result.warnings})` : '✅ 판정: 통과';
|
|
245
|
+
return [`release preflight ${result.version} (다음 판 ${result.next})`, ...lines, verdict];
|
|
246
|
+
}
|
|
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { WARNING_ONLY } from './auto-approve-node.js';
|
|
6
6
|
import { beginLandingMerge, landingFreezeMessage } from '../../src/release-loop/landing-freeze.js';
|
|
7
7
|
import { debug } from '../../src/debug/log.js';
|
|
8
|
+
import { prereleaseKind } from './release-version.js';
|
|
8
9
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandRunner } from './node-verdict.js';
|
|
9
10
|
|
|
10
11
|
export function publicNotes(markdown: string, pages: { pages: Array<{ id: string; slug?: string; source?: string }> }): string {
|
|
@@ -20,6 +21,12 @@ export function publicNotes(markdown: string, pages: { pages: Array<{ id: string
|
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/** RELEASE-REHEARSAL-RC: prerelease notes open with a label, so nobody mistakes a rehearsal for the stable release. */
|
|
25
|
+
export function labelPrerelease(version: string, body: string): string {
|
|
26
|
+
if (prereleaseKind(version) === null) return body;
|
|
27
|
+
return `> **Pre-release** — ${version} is a pre-release, not the latest version. Install it with \`npm i -g elanous@next\`; \`latest\` stays on the current stable release.\n\n${body}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
/** Polls each release asset until it answers 200 (following redirects). Returns the names still missing. */
|
|
24
31
|
export function waitForAssets(run: CommandRunner, repo: string, tag: string, names: readonly string[], waitSeconds: unknown = 600,
|
|
25
32
|
sleep: (ms: number) => void = (ms) => Bun.sleepSync(ms)): string[] {
|
|
@@ -61,7 +68,7 @@ export function runPublish(run: CommandRunner = runCommand, prodFreezeRoot?: str
|
|
|
61
68
|
const registry = run('git', ['show', `${branch}:website/pages.json`]);
|
|
62
69
|
if (doc.status !== 0 || registry.status !== 0) throw new Error(`docs branch missing notes or pages: ${branch}`);
|
|
63
70
|
if (!doc.stdout.startsWith(`# ${version}\n`)) throw new Error('release notes version mismatch');
|
|
64
|
-
const body = publicNotes(doc.stdout, JSON.parse(registry.stdout));
|
|
71
|
+
const body = labelPrerelease(version, publicNotes(doc.stdout, JSON.parse(registry.stdout)));
|
|
65
72
|
const dir = mkdtempSync(join(tmpdir(), 'release-publish-notes-'));
|
|
66
73
|
try {
|
|
67
74
|
const file = join(dir, 'notes.md');
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
4
|
-
import {
|
|
4
|
+
import { cutChecklistGate, type ChecklistGate } from '../../src/release-loop/checklist.js';
|
|
5
|
+
|
|
6
|
+
/** Only the verdict fields are read — so a `release run` deps checklist (`Pick<…>`) fits as well. */
|
|
7
|
+
export type ReadinessChecklist = Pick<ChecklistGate, 'ok' | 'red' | 'undecided' | 'blocked'>;
|
|
5
8
|
import { getSchedule, formatKst } from '../../src/release-loop/release-schedule.js';
|
|
6
9
|
import { debug } from '../../src/debug/log.js';
|
|
7
10
|
|
|
@@ -9,12 +12,12 @@ export type ReleaseReadiness =
|
|
|
9
12
|
| { ready: false; reason: 'already-published'; details: string }
|
|
10
13
|
| { ready: false; reason: 'already-running'; details: string }
|
|
11
14
|
| { ready: false; reason: 'before-cut'; details: string }
|
|
12
|
-
| { ready: false; reason: 'checklist-blocked'; details:
|
|
13
|
-
| { ready: true; reason: 'ready'; details:
|
|
15
|
+
| { ready: false; reason: 'checklist-blocked'; details: ReadinessChecklist }
|
|
16
|
+
| { ready: true; reason: 'ready'; details: ReadinessChecklist };
|
|
14
17
|
|
|
15
18
|
export interface ReleaseReadinessDeps {
|
|
16
19
|
ledgerRoot?: string;
|
|
17
|
-
checklist?:
|
|
20
|
+
checklist?: (version: string) => ReadinessChecklist;
|
|
18
21
|
isPidAlive?: (pid: number) => boolean;
|
|
19
22
|
now?: () => Date;
|
|
20
23
|
}
|
|
@@ -57,11 +60,13 @@ export function releaseReadiness(version: string, deps: ReleaseReadinessDeps = {
|
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
const schedule = getSchedule(version, deps.ledgerRoot);
|
|
60
|
-
|
|
63
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
64
|
+
if (schedule && now.getTime() < Date.parse(schedule.cutAt)) {
|
|
61
65
|
debug.log('release.schedule', 'before-cut', { version, cutAt: schedule.cutAt, landBy: schedule.landBy });
|
|
62
66
|
return { ready: false, reason: 'before-cut', details: `before-cut (${formatKst(schedule.cutAt)})` };
|
|
63
67
|
}
|
|
64
|
-
|
|
68
|
+
// GATE-ENTRY-ALIGN: the same cut judgement as `release run`'s entry check and the checklist-gate node.
|
|
69
|
+
const gate = (deps.checklist ?? ((v: string) => cutChecklistGate(v, schedule?.landBy, now)))(version);
|
|
65
70
|
if (!gate.ok) return { ready: false, reason: 'checklist-blocked', details: gate };
|
|
66
71
|
return { ready: true, reason: 'ready', details: gate };
|
|
67
72
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Release version shapes for the release loop: a stable x.y.z or a prerelease x.y.z-(rc|alpha|beta).N.
|
|
2
|
+
// RELEASE-BRANCH ⊕ RELEASE-REHEARSAL-RC (10-06): prerelease runs publish to npm `next` and skip the
|
|
3
|
+
// post-publish nodes that touch main or the operating hosts.
|
|
4
|
+
|
|
5
|
+
export const PRERELEASE_KINDS = ['rc', 'alpha', 'beta'] as const;
|
|
6
|
+
export type PrereleaseKind = (typeof PRERELEASE_KINDS)[number];
|
|
7
|
+
|
|
8
|
+
const RELEASE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(rc|alpha|beta)\.(0|[1-9]\d*))?$/;
|
|
9
|
+
const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
10
|
+
|
|
11
|
+
export function isStableVersion(version: string): boolean { return STABLE_VERSION.test(version); }
|
|
12
|
+
|
|
13
|
+
export function isReleaseVersion(version: string): boolean { return RELEASE_VERSION.test(version); }
|
|
14
|
+
|
|
15
|
+
/** `0.2.18-rc.0` → `0.2.18`; a stable version is returned as is. Throws on anything else. */
|
|
16
|
+
export function baseVersion(version: string): string {
|
|
17
|
+
const match = RELEASE_VERSION.exec(version);
|
|
18
|
+
if (!match) throw new Error(`invalid release version: ${version}`);
|
|
19
|
+
return `${match[1]}.${match[2]}.${match[3]}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The prerelease kind of `0.2.18-rc.0` (`rc`), or null for a stable version. */
|
|
23
|
+
export function prereleaseKind(version: string): PrereleaseKind | null {
|
|
24
|
+
const match = RELEASE_VERSION.exec(version);
|
|
25
|
+
if (!match) throw new Error(`invalid release version: ${version}`);
|
|
26
|
+
return (match[4] as PrereleaseKind | undefined) ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isPrereleaseKind(value: unknown): value is PrereleaseKind {
|
|
30
|
+
return typeof value === 'string' && (PRERELEASE_KINDS as readonly string[]).includes(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The next free prerelease number for `base` and `kind`, given the names already taken
|
|
35
|
+
* (release branches `release/<v>` and tags `v<v>`, with or without those prefixes).
|
|
36
|
+
*/
|
|
37
|
+
export function nextPrereleaseVersion(base: string, kind: PrereleaseKind, taken: readonly string[]): string {
|
|
38
|
+
if (!STABLE_VERSION.test(base)) throw new Error(`prerelease base must be x.y.z: ${base}`);
|
|
39
|
+
const prefix = `${base}-${kind}.`;
|
|
40
|
+
let next = 0;
|
|
41
|
+
for (const raw of taken) {
|
|
42
|
+
const name = raw.replace(/^refs\/heads\//, '').replace(/^refs\/tags\//, '').replace(/^release\//, '').replace(/^v(?=\d)/, '').replace(/\^\{\}$/, '');
|
|
43
|
+
if (!name.startsWith(prefix)) continue;
|
|
44
|
+
const n = name.slice(prefix.length);
|
|
45
|
+
if (/^(0|[1-9]\d*)$/.test(n)) next = Math.max(next, Number(n) + 1);
|
|
46
|
+
}
|
|
47
|
+
return `${prefix}${next}`;
|
|
48
|
+
}
|