@oneie/claude 0.8.0 → 0.10.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/plugin.json +1 -1
- package/agents/abm-strategist.md +67 -1
- package/agents/ads-meta.md +67 -1
- package/agents/analyst.md +67 -1
- package/agents/animator.md +108 -0
- package/agents/architect.md +269 -20
- package/agents/brand-guardian.md +67 -1
- package/agents/brand-strategist.md +67 -1
- package/agents/campaign-content.md +67 -1
- package/agents/campaign-email.md +67 -1
- package/agents/campaign-sms.md +67 -1
- package/agents/campaign-social.md +67 -1
- package/agents/cco.md +83 -2
- package/agents/ceo.md +108 -11
- package/agents/chairman.md +197 -0
- package/agents/cmo.md +82 -2
- package/agents/community-greeter.md +67 -1
- package/agents/community-moderator.md +67 -1
- package/agents/compliance.md +67 -1
- package/agents/copywriter.md +67 -1
- package/agents/creative-strategist.md +67 -1
- package/agents/cro.md +81 -1
- package/agents/cto.md +266 -28
- package/agents/customer-interviewer.md +67 -1
- package/agents/customer-researcher.md +67 -1
- package/agents/customer-success-manager.md +67 -1
- package/agents/customer-trainer.md +67 -1
- package/agents/cxo.md +82 -1
- package/agents/demand-creator.md +67 -1
- package/agents/demo-mover.md +67 -1
- package/agents/demo-specialist.md +67 -1
- package/agents/demo-thai-family-law.md +67 -1
- package/agents/designer.md +67 -1
- package/agents/discovery-caller.md +67 -1
- package/agents/doctor.md +269 -0
- package/agents/educate-coach.md +67 -1
- package/agents/elevate-tutor.md +67 -1
- package/agents/email-lifecycle-marketer.md +67 -1
- package/agents/engage-specialist.md +67 -1
- package/agents/events-coordinator.md +67 -1
- package/agents/foundation-builder.md +67 -1
- package/agents/funnel-architect.md +67 -1
- package/agents/gift-creator.md +67 -1
- package/agents/google-ads.md +67 -1
- package/agents/guide.md +67 -1
- package/agents/helpdesk-dispatcher.md +67 -1
- package/agents/hook-specialist.md +67 -1
- package/agents/identify-optimizer.md +67 -1
- package/agents/implementer.md +313 -45
- package/agents/incident-commander.md +67 -1
- package/agents/insights-lead.md +87 -1
- package/agents/journey-runner.md +67 -1
- package/agents/linkedin-ads.md +67 -1
- package/agents/live-sales-chat.md +67 -1
- package/agents/market-researcher.md +67 -1
- package/agents/media-buyer.md +67 -1
- package/agents/memory-keeper.md +195 -0
- package/agents/movers-customer-researcher.md +67 -1
- package/agents/movers-foundation-builder.md +67 -1
- package/agents/movers-market-researcher.md +67 -1
- package/agents/movers-pricing-strategist.md +67 -1
- package/agents/nurture-architect.md +67 -1
- package/agents/offer-architect.md +67 -1
- package/agents/onboarder.md +67 -1
- package/agents/onboarding-specialist.md +67 -1
- package/agents/operations-dashboard.md +87 -1
- package/agents/perf-engineer.md +333 -37
- package/agents/playbook-writer.md +67 -1
- package/agents/plg-strategist.md +67 -1
- package/agents/positioning-architect.md +67 -1
- package/agents/press-officer.md +67 -1
- package/agents/pricing-strategist.md +67 -1
- package/agents/privacy-officer.md +67 -1
- package/agents/referral-manager.md +67 -1
- package/agents/refine-analyst.md +67 -1
- package/agents/release-manager.md +446 -39
- package/agents/renewals-upsell-rep.md +67 -1
- package/agents/review-engineer.md +319 -45
- package/agents/rewards-steward.md +67 -1
- package/agents/sales-call-coach.md +67 -1
- package/agents/sales-closer.md +67 -1
- package/agents/security-auditor.md +343 -48
- package/agents/sell-closer.md +67 -1
- package/agents/share-amplifier.md +67 -1
- package/agents/social-media-manager.md +67 -1
- package/agents/storyteller.md +301 -0
- package/agents/strategist.md +67 -1
- package/agents/strategy-aligner.md +67 -1
- package/agents/support-agent.md +67 -1
- package/agents/tagger.md +327 -0
- package/agents/tech-writer.md +195 -22
- package/agents/test-engineer.md +398 -29
- package/agents/tiktok-ads.md +67 -1
- package/agents/tracking-engineer.md +67 -1
- package/agents/trailkeeper.md +181 -0
- package/agents/upsell-strategist.md +67 -1
- package/agents/voice.md +67 -1
- package/agents/w1-recon.md +1 -1
- package/agents/w2-decide.md +1 -1
- package/agents/w3-edit.md +8 -2
- package/agents/w4-verify.md +13 -0
- package/agents/workflow-optimiser.md +81 -1
- package/commands/close.md +916 -160
- package/commands/deploy.md +102 -724
- package/commands/do.md +58 -2
- package/commands/sweep.md +159 -0
- package/commands/tasks.md +222 -0
- package/hooks/scripts/dev-only.sh +135 -0
- package/hooks/scripts/git-add-guard.sh +37 -2
- package/hooks/scripts/session-start.sh +32 -4
- package/package.json +1 -1
- package/rules/scripts.md +85 -0
- package/scripts/CLAUDE.md +315 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agent-actor-parity.sh +129 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/chrome.mjs +18 -0
- package/scripts/close-metrics.sh +587 -0
- package/scripts/close-owner.sh +326 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-emit.sh +311 -0
- package/scripts/deploy-gate-check.sh +155 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy-schema-check.sh +58 -0
- package/scripts/deploy.sh +393 -243
- package/scripts/do-auto.sh +127 -26
- package/scripts/do-board.sh +429 -0
- package/scripts/do-close.sh +1184 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-fleet.sh +8 -2
- package/scripts/do-plan-json.mjs +110 -12
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +86 -10
- package/scripts/do-rank.py +200 -3
- package/scripts/do-reconcile.sh +73 -12
- package/scripts/do-signal.sh +101 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-w4-gates.sh +11 -1
- package/scripts/do-world-check.sh +153 -0
- package/scripts/download-stats.sh +172 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +18 -1
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +71 -0
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/flywheel-outcome.sh +63 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +9 -0
- package/scripts/gate-watchdog.sh +619 -0
- package/scripts/gc-content-check.sh +142 -0
- package/scripts/gh-traffic-capture.sh +153 -0
- package/scripts/govern-order-check.sh +202 -0
- package/scripts/governor-doors-check.sh +86 -5
- package/scripts/health.sh +448 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/incident.sh +212 -0
- package/scripts/land.sh +755 -45
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/livekit-ratchet.sh +18 -1
- package/scripts/machine-check.sh +1 -1
- package/scripts/memory-index-budget.sh +79 -0
- package/scripts/npm-downloads.sh +109 -0
- package/scripts/one-agents.mjs +204 -8
- package/scripts/one-resume.sh +31 -3
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/redirect-lint.sh +169 -0
- package/scripts/release.sh +40 -6
- package/scripts/resume-lost-sessions.sh +68 -0
- package/scripts/shoot-pages.mjs +140 -0
- package/scripts/signal-meta-backfill.ts +451 -0
- package/scripts/signal-watch.sh +63 -6
- package/scripts/speed-cache-check.sh +12 -2
- package/scripts/sweep.sh +426 -0
- package/scripts/task-titles-dump.ts +101 -0
- package/scripts/test-cached.sh +47 -10
- package/scripts/test-lanes.sh +14 -0
- package/scripts/thread-name-backfill.ts +215 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +155 -8
- package/scripts/typedb-flake-check.sh +3 -1
- package/scripts/urls-lint.sh +8 -0
- package/scripts/verify-board-doors.sh +80 -0
- package/scripts/verify-fast.sh +159 -6
- package/scripts/worktree-up.sh +21 -3
- package/skills/astro/SKILL.md +9 -3
- package/skills/astro/optimize-performance.md +3 -2
- package/skills/cloudflare/SKILL.md +3 -2
- package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
- package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
- package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
- package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/skills/cloudflare-security-audit/HUNTING.md +251 -0
- package/skills/cloudflare-security-audit/LICENSE +21 -0
- package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
- package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
- package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/skills/cloudflare-security-audit/SKILL.md +192 -0
- package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/skills/cloudflare-security-audit/report-schema.json +461 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
- package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
- package/skills/deploy/REFERENCE.md +713 -0
- package/skills/deploy/SKILL.md +140 -0
- package/skills/fleet-audit/SKILL.md +58 -0
- package/skills/meeting/SKILL.md +220 -0
- package/skills/planning/SKILL.md +256 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/typedb/SKILL.md +7 -0
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/templates/template-agent.md +7 -1
- package/templates/template-feature.md +9 -0
- package/templates/template-todo.md +29 -0
package/scripts/deploy.sh
CHANGED
|
@@ -38,18 +38,14 @@
|
|
|
38
38
|
# · CLOUDFLARE_API_TOKEN is unset; auth is GLOBAL_API_KEY + EMAIL
|
|
39
39
|
# · health-check custom domains only (*.workers.dev is blocked on this net)
|
|
40
40
|
# · sync is cron-only: a clean `wrangler deploy` IS its health signal
|
|
41
|
+
# · a fast pass is NEVER reported as a full pass, and `dev` never touches prod
|
|
42
|
+
# · dev.one.ie shares PRODUCTION's D1 and KV — ship dev freely, treat its DATA
|
|
43
|
+
# as production incident:deploy-two-tiers
|
|
41
44
|
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# authority for the dev procedure.
|
|
47
|
-
# prod → https://one.ie and the other four services. FULL gate always
|
|
48
|
-
# (FULL_VERIFY=1), approval prompt unless --yes.
|
|
49
|
-
# A fast pass is NEVER reported as a full pass, and `dev` never touches prod.
|
|
50
|
-
# dev.one.ie shares PRODUCTION's D1 and KV byte-for-byte — only the cron strip
|
|
51
|
-
# differs. So a dev deploy is NOT a sandbox for data: it reads and writes the
|
|
52
|
-
# same rows one.ie does. Ship dev freely; treat its DATA as production.
|
|
45
|
+
# ACCOUNTS — why each gate is shaped the way it is (dates, shas, what it cost):
|
|
46
|
+
# bash .claude/scripts/incident.sh --for .claude/scripts/deploy.sh
|
|
47
|
+
# What a gate MEANS: .claude/commands/deploy.md · its traps:
|
|
48
|
+
# .claude/skills/deploy/REFERENCE.md
|
|
53
49
|
|
|
54
50
|
set -uo pipefail
|
|
55
51
|
|
|
@@ -69,10 +65,8 @@ cd "$ROOT" || exit 1
|
|
|
69
65
|
MODE="full"
|
|
70
66
|
SKIP_TESTS=0 SKIP_TYPECHECK=0 SKIP_BUILD=0 SKIP_MIGRATIONS=0 SKIP_HEALTH=0
|
|
71
67
|
ASSUME_YES="${DEPLOY_YES:-0}" DRY=0 ALLOW_DIRTY=0 ONLY_CHANGED=0 GATES_ONLY=0 CHECK_CREDS=0
|
|
72
|
-
# A
|
|
73
|
-
#
|
|
74
|
-
# filename, by typedb-flake-check.sh, which refuses the tasks:claim not_found
|
|
75
|
-
# privilege boundary outright. --no-typedb-flake-waiver restores the hard stop.
|
|
68
|
+
# A red suite that is only the shared TypeDB cluster refusing to answer is
|
|
69
|
+
# waivable — classified by SIGNATURE, never by filename. incident:waived-pass-is-a-fact
|
|
76
70
|
TYPEDB_FLAKE_WAIVER="${DEPLOY_ALLOW_TYPEDB_FLAKE:-1}" TESTS_WAIVED=0
|
|
77
71
|
|
|
78
72
|
for a in "$@"; do
|
|
@@ -98,14 +92,11 @@ done
|
|
|
98
92
|
[[ "$MODE" == channels ]] && MODE=agents
|
|
99
93
|
|
|
100
94
|
# ── dev tier ─────────────────────────────────────────────────────────────────
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# deploy-dev.sh owns the dev procedure; this is only the door to it.
|
|
95
|
+
# A different destination with a different gate is a different procedure.
|
|
96
|
+
# deploy-dev.sh owns it; this is only the door. incident:deploy-two-tiers
|
|
104
97
|
if [[ "$MODE" == dev ]]; then
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
# the latter expands on the literal string "0" and would silently skip the
|
|
108
|
-
# gate on every dev deploy.
|
|
98
|
+
# `(( SKIP_TESTS ))`, never `${SKIP_TESTS:+...}` — the latter expands on the
|
|
99
|
+
# literal "0" and skips the gate every time. incident:deploy-two-tiers
|
|
109
100
|
dev_gate=0; (( SKIP_TESTS )) && dev_gate=1
|
|
110
101
|
if (( DRY )); then
|
|
111
102
|
echo "[dry] DEV_SKIP_GATE=$dev_gate bash .claude/scripts/deploy-dev.sh"
|
|
@@ -148,10 +139,8 @@ case "$MODE" in
|
|
|
148
139
|
pay) want_pay=1 ;;
|
|
149
140
|
esac
|
|
150
141
|
|
|
151
|
-
# --changed
|
|
152
|
-
#
|
|
153
|
-
# deploy, so an interrupted run never marks anything shipped. Secrets/bindings
|
|
154
|
-
# change outside git — use --all (the default) when that's what you changed.
|
|
142
|
+
# --changed skips a service with no new commits since its last CLEAN deploy.
|
|
143
|
+
# Secrets and bindings change outside git — use --all. incident:changed-marker
|
|
155
144
|
svc_paths() { case "$1" in
|
|
156
145
|
astro) echo "one.ie/web packages" ;;
|
|
157
146
|
gateway) echo "api" ;;
|
|
@@ -203,40 +192,11 @@ say "deploy · mode=$MODE · $(date '+%Y-%m-%d %H:%M:%S')"
|
|
|
203
192
|
say "log: $LOG"
|
|
204
193
|
|
|
205
194
|
# ── Cloudflare credential resolution ────────────────────────────────────────
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
# cwd happened to surface:
|
|
212
|
-
#
|
|
213
|
-
# 1. a STALE 37-char global key in the ambient env (injected by
|
|
214
|
-
# ~/.claude/settings.json `env`, so it exists in Claude Code sessions and
|
|
215
|
-
# NOT in a plain terminal — which is why it was invisible for so long).
|
|
216
|
-
# /user → 403. Dead.
|
|
217
|
-
# 2. the GOOD 52-char key in one.ie/web/.env + .env.local. /user → 200.
|
|
218
|
-
# wrangler 4.x auto-loads `.env` from the CWD, so this one is only
|
|
219
|
-
# reachable from one.ie/web — the single directory of the five that has it.
|
|
220
|
-
# 3. the `wrangler login` OAuth session, used when neither of the above is in
|
|
221
|
-
# scope.
|
|
222
|
-
#
|
|
223
|
-
# Process env BEATS a per-directory `.env`. So the stale key shadowed the good
|
|
224
|
-
# one everywhere, Step 4's whoami probe failed, its `unset` cleared the ambient
|
|
225
|
-
# key, and then: Step 6.5 in one.ie/web picked up (2) from `.env` and passed,
|
|
226
|
-
# while Step 6.6 in channels — which has no `.env` — fell through to (3) and
|
|
227
|
-
# died `code: 7403`. A deploy must never be able to use two different
|
|
228
|
-
# credentials in two consecutive steps.
|
|
229
|
-
#
|
|
230
|
-
# The rules that follow from that:
|
|
231
|
-
# · RESOLVE one credential explicitly, from an ordered ladder of sources.
|
|
232
|
-
# · Test it with curl against the API — ground truth — never `wrangler
|
|
233
|
-
# whoami` in one directory, which answers about a credential the other four
|
|
234
|
-
# services will not use.
|
|
235
|
-
# · EXPORT the winner so all five run() subshells share it; that also
|
|
236
|
-
# neutralises the per-dir `.env` divergence without editing any `.env`.
|
|
237
|
-
# · NAME the source in the log (sha8 + length, never bytes).
|
|
238
|
-
# · PROVE all five services agree on the account id before shipping.
|
|
239
|
-
# · Run all of this BEFORE the 20-minute gates, not after them.
|
|
195
|
+
# A LADDER, not a lookup. Resolve ONE credential from an ordered list, prove it
|
|
196
|
+
# with curl against the API (never `wrangler whoami`, which answers about one
|
|
197
|
+
# directory), EXPORT the winner so all five subshells share it, name the source
|
|
198
|
+
# as sha8+length (never bytes), and prove all five agree on the account id —
|
|
199
|
+
# all of it BEFORE the 20-minute gates. incident:cf-credential-ladder
|
|
240
200
|
CF_AUTH="" CF_AUTH_SOURCE="" CF_ACCOUNT=""
|
|
241
201
|
|
|
242
202
|
# _cf_probe <email> <key> — 200 from /user means these exact bytes authenticate.
|
|
@@ -300,10 +260,8 @@ _cf_resolve() {
|
|
|
300
260
|
return 1
|
|
301
261
|
}
|
|
302
262
|
|
|
303
|
-
# _cf_agree — every service must
|
|
304
|
-
#
|
|
305
|
-
# answering about a different credential than channels IS the bug, and it is
|
|
306
|
-
# only visible by asking each directory separately.
|
|
263
|
+
# _cf_agree — every service must deploy to the SAME account, and that is only
|
|
264
|
+
# visible by asking each directory separately. incident:cf-credential-ladder
|
|
307
265
|
_cf_agree() {
|
|
308
266
|
(( DRY )) && return 0
|
|
309
267
|
local d id first="" bad=0
|
|
@@ -323,10 +281,8 @@ _cf_agree() {
|
|
|
323
281
|
return 0
|
|
324
282
|
}
|
|
325
283
|
|
|
326
|
-
# --check-creds — the
|
|
327
|
-
#
|
|
328
|
-
# that cannot authenticate and asserts the ladder REJECTS it and falls through,
|
|
329
|
-
# then asserts the five services agree. Ships nothing.
|
|
284
|
+
# --check-creds — the RED half: a gate never seen to fail is not known to work.
|
|
285
|
+
# Plants a key that cannot authenticate. incident:check-creds-red-half
|
|
330
286
|
if (( CHECK_CREDS )); then
|
|
331
287
|
step "credential ladder — self-test"
|
|
332
288
|
fails=0
|
|
@@ -366,6 +322,59 @@ if (( CHECK_CREDS )); then
|
|
|
366
322
|
say "log: $LOG"; exit 0
|
|
367
323
|
fi
|
|
368
324
|
|
|
325
|
+
# ── Step 0.1 — the box (BEFORE everything, because everything below is a gate) ──
|
|
326
|
+
# Same reasoning as Step 0.4 one step further back: the cheapest check runs
|
|
327
|
+
# first. health.sh --box costs ~0.2s and owns every number in it (this reads,
|
|
328
|
+
# never restates). The verdict vocabulary is HEALTHY | DEGRADED | UNHEALTHY, and
|
|
329
|
+
# its own --self-test proves each threshold bites.
|
|
330
|
+
#
|
|
331
|
+
# WHY A PAGING BOX IS A DEPLOY CONCERN. A deploy runs five heavy gates. On a box
|
|
332
|
+
# under swap pressure each one runs long, and a slow gate is indistinguishable
|
|
333
|
+
# from a red one at the wall clock — measured 2026-09-05, an 8-fork vitest run
|
|
334
|
+
# showed a >20-minute wall clock that was governor QUEUEING, not runtime. A good
|
|
335
|
+
# tree then gets diagnosed as broken code, which is the most expensive wrong
|
|
336
|
+
# answer this script can give.
|
|
337
|
+
#
|
|
338
|
+
# DEGRADED never refuses: it means headroom of one gate, or orphans that survived
|
|
339
|
+
# a reap, and one gate is what a deploy needs. Only UNHEALTHY stops, and it names
|
|
340
|
+
# the agent that can fix it rather than telling you to wait.
|
|
341
|
+
step "Step 0.1 — box"
|
|
342
|
+
if [ "${DEPLOY_SKIP_DOCTOR:-0}" = "1" ]; then
|
|
343
|
+
say " skipped (DEPLOY_SKIP_DOCTOR=1)"
|
|
344
|
+
elif [ ! -x "$ROOT/.claude/scripts/health.sh" ] && [ ! -f "$ROOT/.claude/scripts/health.sh" ]; then
|
|
345
|
+
say " health.sh absent — box unmeasured (never read this as healthy)"
|
|
346
|
+
else
|
|
347
|
+
_box_json="$( bash "$ROOT/.claude/scripts/health.sh" --box --json 2>/dev/null )"
|
|
348
|
+
_box_v="$(printf '%s' "$_box_json" | sed -n 's/.*"verdict":"\([A-Z]*\)".*/\1/p')"
|
|
349
|
+
_box_why="$(printf '%s' "$_box_json" | sed -n 's/.*"why":"\([^"]*\)".*/\1/p')"
|
|
350
|
+
_box_orph="$(printf '%s' "$_box_json" | sed -n 's/.*"orphans":\([0-9]*\).*/\1/p')"
|
|
351
|
+
case "${_box_v:-unknown}" in
|
|
352
|
+
HEALTHY) ok "box HEALTHY" ;;
|
|
353
|
+
DEGRADED) say " box DEGRADED — ${_box_why:-?} (proceeding: a deploy needs one gate)" ;;
|
|
354
|
+
UNHEALTHY)
|
|
355
|
+
# Reap first, but only when there is something to reap: with orphans=0 the
|
|
356
|
+
# verdict is about memory or headroom and no reaper can move it. Running one
|
|
357
|
+
# anyway is the ritual that makes a refusal look like it tried something.
|
|
358
|
+
if [ "${_box_orph:-0}" != "0" ]; then
|
|
359
|
+
bash "$ROOT/.claude/scripts/gate-reaper.sh" --once 2>/dev/null | sed 's/^/ /'
|
|
360
|
+
_box_json="$( bash "$ROOT/.claude/scripts/health.sh" --box --json 2>/dev/null )"
|
|
361
|
+
_box_v="$(printf '%s' "$_box_json" | sed -n 's/.*"verdict":"\([A-Z]*\)".*/\1/p')"
|
|
362
|
+
_box_why="$(printf '%s' "$_box_json" | sed -n 's/.*"why":"\([^"]*\)".*/\1/p')"
|
|
363
|
+
say " box after reap: ${_box_v:-unknown}"
|
|
364
|
+
fi
|
|
365
|
+
if [ "${_box_v:-unknown}" = "UNHEALTHY" ] && (( ! DRY )); then
|
|
366
|
+
say " ${_box_why:-the box is saturated}"
|
|
367
|
+
say " Hand the box to the doctor, then deploy:"
|
|
368
|
+
say " Agent({ subagent_type: \"doctor\", model: \"opus\","
|
|
369
|
+
say " prompt: \"health.sh says: ${_box_why:-saturated}. Reclaim what nothing is coming back for, then report.\" })"
|
|
370
|
+
say " Override (the gates will be slow, not wrong): DEPLOY_SKIP_DOCTOR=1 ./deploy"
|
|
371
|
+
die "box is UNHEALTHY — refusing to start five gates on it"
|
|
372
|
+
fi
|
|
373
|
+
say " box UNHEALTHY — ${_box_why:-?} (dry run: reporting, refusing nothing)" ;;
|
|
374
|
+
*) say " box verdict unreadable — unmeasured, which is never a pass" ;;
|
|
375
|
+
esac
|
|
376
|
+
fi
|
|
377
|
+
|
|
369
378
|
# ── Step 0.4 — credentials (BEFORE the slow gates) ──────────────────────────
|
|
370
379
|
# It was after them: on 2026-08-18 a bad credential was discovered only at Step
|
|
371
380
|
# 6.5, having already spent the full test suite and a 2m14s production build.
|
|
@@ -378,14 +387,8 @@ say " source: $CF_AUTH_SOURCE"
|
|
|
378
387
|
|
|
379
388
|
# ── Step 0 — tree state (before the slow gates: a dirty tree should fail fast)
|
|
380
389
|
step "Step 0 — tree"
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
# answer it pays for every typecheck, the whole suite and the astro build and
|
|
384
|
-
# only then dies at `aborted at approval`. Measured 2026-09-04: one nohup'd
|
|
385
|
-
# deploy burned a 189s green cycle exactly that way, and the box was contended
|
|
386
|
-
# enough at the time that the preceding cycle had cost 5508s.
|
|
387
|
-
#
|
|
388
|
-
# stdin not a TTY means nobody can type "yes". Refuse here, where it is free.
|
|
390
|
+
# stdin not a TTY means nobody can type "yes" at Step 6. Refuse here, where it
|
|
391
|
+
# is free, not after the whole gate battery. incident:approval-needs-a-tty
|
|
389
392
|
if (( ! ASSUME_YES )) && (( ! DRY )) && [[ "$(git rev-parse --abbrev-ref HEAD)" == "main" ]] && [[ ! -t 0 ]]; then
|
|
390
393
|
die "non-interactive (stdin is not a TTY) and the Step 6 approval prompt cannot be answered. Pass --yes / -y, or set DEPLOY_YES=1."
|
|
391
394
|
fi
|
|
@@ -404,23 +407,52 @@ else
|
|
|
404
407
|
ok "clean tree"
|
|
405
408
|
fi
|
|
406
409
|
|
|
407
|
-
# ──
|
|
408
|
-
#
|
|
409
|
-
#
|
|
410
|
-
# makes W0+build cost max() instead of sum(). Nothing here has a side effect
|
|
411
|
-
# outside the working tree, so a red gate still stops the deploy before
|
|
412
|
-
# anything ships.
|
|
410
|
+
# ── Step 0.5 — generated artifacts (cheap, before the slow gates) ─────────
|
|
411
|
+
# One cheap deterministic check, run where failing is free. It exits non-zero on
|
|
412
|
+
# drift AND on failing to run, and both stop the deploy.
|
|
413
413
|
#
|
|
414
|
-
#
|
|
415
|
-
#
|
|
416
|
-
#
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
#
|
|
421
|
-
#
|
|
422
|
-
#
|
|
423
|
-
#
|
|
414
|
+
# THE DRY BRANCH IS EXPLICIT: `run` returns 0 under --dry-run, so
|
|
415
|
+
# `if run ...; then ok` would print GREEN for a check that never executed — an
|
|
416
|
+
# unrun gate reported as a pass. incident:blocks-manifest-unrun
|
|
417
|
+
# ── Step 0.5a — deps present (0.05s, and it buys ~290s) ─────────────────────
|
|
418
|
+
# Every service this run will typecheck must have a node_modules. A missing one
|
|
419
|
+
# does not read as missing: tsc reports `TS2688: Cannot find type definition file
|
|
420
|
+
# for '@cloudflare/workers-types'` — a TypeScript error naming TypeScript — and
|
|
421
|
+
# it surfaces at the END of the gate fan-out, ~290s in, on code that is fine.
|
|
422
|
+
# Measured 2026-09-17 at the ship gate: api and sync red in all four worktrees at
|
|
423
|
+
# once, because worktree-up.sh's LINKS list carried neither. That list is fixed;
|
|
424
|
+
# this check is the half that cannot rot, because it holds for a tree materialised
|
|
425
|
+
# any other way.
|
|
426
|
+
step "Step 0.5a — deps"
|
|
427
|
+
_deps_missing=()
|
|
428
|
+
for _svc in "${SERVICES_TYPECHECK[@]}"; do
|
|
429
|
+
[ -e "$ROOT/$_svc/node_modules" ] || _deps_missing+=("$_svc")
|
|
430
|
+
done
|
|
431
|
+
if (( ${#_deps_missing[@]} )); then
|
|
432
|
+
for _svc in "${_deps_missing[@]}"; do say " ✗ $_svc/node_modules missing"; done
|
|
433
|
+
say " A worktree is not \`git worktree add\` — that command omits every gitignored path."
|
|
434
|
+
say " Link them: bash .claude/scripts/worktree-up.sh <name> --no-dev"
|
|
435
|
+
say " Or by hand: ln -sfn <main-tree>/$'{'svc'}'/node_modules \$svc/node_modules"
|
|
436
|
+
die "${#_deps_missing[@]} service(s) cannot typecheck — refusing at t≈7s rather than t≈290s"
|
|
437
|
+
fi
|
|
438
|
+
ok "${#SERVICES_TYPECHECK[@]}/${#SERVICES_TYPECHECK[@]} services have deps"
|
|
439
|
+
|
|
440
|
+
step "Step 0.5 — generated artifacts"
|
|
441
|
+
if (( DRY )); then
|
|
442
|
+
say " [dry] node .claude/scripts/blocks-manifest.mjs --check"
|
|
443
|
+
elif run "blocks-manifest --check" . node "$ROOT/.claude/scripts/blocks-manifest.mjs" --check; then
|
|
444
|
+
ok "block manifest: no drift"
|
|
445
|
+
else
|
|
446
|
+
tail -5 "$LOG" | sed 's/^/ /'
|
|
447
|
+
die "block manifest DRIFT (or --check could not run) — regenerate with 'node .claude/scripts/blocks-manifest.mjs' and commit all three artifacts"
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
# ── Steps 1+3 — the slow gates, all at once ─────────────────────────────────
|
|
451
|
+
# They share no STATE (only the SDK's dist/, built first), so concurrency makes
|
|
452
|
+
# W0+build cost max() instead of sum(). They are NOT independent in COST:
|
|
453
|
+
# TEST BUDGETS HERE MUST BE SIZED FOR A STARVED RUN, not a quiet one, and a gate
|
|
454
|
+
# that needs the box to itself leaves this block rather than widening every
|
|
455
|
+
# timeout downstream. incident:gates-not-independent-in-cost
|
|
424
456
|
|
|
425
457
|
# web + channels resolve @oneie/sdk types out of packages/sdk/dist — an unbuilt
|
|
426
458
|
# dist fakes a wall of TS2307, so this one is a real dependency, done serially.
|
|
@@ -431,33 +463,15 @@ if ! (( SKIP_TYPECHECK )) && [[ ! -d packages/sdk/dist ]]; then
|
|
|
431
463
|
fi
|
|
432
464
|
|
|
433
465
|
# ── heavy-gate concurrency, priced in MEMORY not cores ──────────────────────
|
|
434
|
-
#
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
#
|
|
438
|
-
#
|
|
439
|
-
# Cores were never the binding constraint, so price the gates in memory. What
|
|
440
|
-
# this DOES buy: two gates that want ~14GB no longer overlap on a 24GB box.
|
|
441
|
-
#
|
|
442
|
-
# What it does NOT buy, and an earlier version of this comment wrongly claimed:
|
|
443
|
-
# it is not the fix for the vitest gate HANGING. That was reproduced with the
|
|
444
|
-
# gates serialised, the box at 41% free, no swap thrash and no sockets held —
|
|
445
|
-
# see .claude/commands/deploy.md "The vitest gate hangs". Serialising addresses
|
|
446
|
-
# a slow gate; the bound below is what addresses a parked one.
|
|
447
|
-
#
|
|
448
|
-
# Same reasoning as lib/govern.sh gate_headroom, but priced for THESE two gates
|
|
449
|
-
# rather than a generic ~2GB cycle. A probe that cannot read memory returns a
|
|
450
|
-
# large number — a broken sensor must never silently serialise the pipeline.
|
|
466
|
+
# Cores were never the binding constraint. Serialising addresses a SLOW gate; it
|
|
467
|
+
# is NOT the fix for the vitest gate hanging — the bound below is. A probe that
|
|
468
|
+
# cannot read memory returns a large number: a broken sensor must never silently
|
|
469
|
+
# serialise the pipeline. incident:heavy-gates-priced-in-memory
|
|
451
470
|
HEAVY_NEED_GB="${DEPLOY_HEAVY_NEED_GB:-14}" # build 8 + vitest ~6
|
|
452
471
|
# shellcheck source=lib/govern.sh
|
|
453
472
|
. "$ROOT/.claude/scripts/lib/govern.sh"
|
|
454
|
-
#
|
|
455
|
-
#
|
|
456
|
-
# cache, purgeable and compressed pages as free. Measured 2026-09-03 it read 74%
|
|
457
|
-
# on a box with 1.3% actually free and 9.4 GB in swap — so this gate overlapped
|
|
458
|
-
# two ~7 GB gates on a machine that had ~3 GB. gate_mem_avail_mb is the honest
|
|
459
|
-
# quantity. Unreadable probe still returns 999: a broken sensor must never
|
|
460
|
-
# silently serialise the pipeline.
|
|
473
|
+
# gate_mem_avail_mb, never memory_pressure's "free percentage" — that counts
|
|
474
|
+
# cache and compressed pages as free. incident:heavy-gates-priced-in-memory
|
|
461
475
|
heavy_free_gb() {
|
|
462
476
|
local avail_mb
|
|
463
477
|
avail_mb="$(gate_mem_avail_mb 2>/dev/null || echo "")"
|
|
@@ -477,19 +491,63 @@ else
|
|
|
477
491
|
say " (overlapping them pages; measured 1145s vs 176s — see deploy.sh)"
|
|
478
492
|
fi
|
|
479
493
|
|
|
494
|
+
# ── the run, streamed into the world ────────────────────────────────────────
|
|
495
|
+
# THREE RULES, all about not making the telemetry load-bearing: it can never FAIL
|
|
496
|
+
# the deploy (backgrounded, `|| true`) · it can never SLOW the deploy (`&`) ·
|
|
497
|
+
# --dry-run emits NOTHING. A dropped emit degrades to a GAP in the trace, never
|
|
498
|
+
# to a lie. incident:deploy-emit-never-load-bearing
|
|
499
|
+
# An `if`, not `$( case ... esac )`: bash 3.2 closes `$(` at the FIRST `)`, and a
|
|
500
|
+
# case pattern ends in one — it wrote a literal into deploy-runs.json, which is
|
|
501
|
+
# the one degradation the rules above forbid. incident:deploy-emit-never-load-bearing
|
|
502
|
+
if [ "$MODE" = dev ]; then EMIT_TARGET="dev.one.ie"; else EMIT_TARGET="one.ie"; fi
|
|
503
|
+
EMIT_ENV="$( [[ "$MODE" == dev ]] && echo dev || echo prod )"
|
|
504
|
+
|
|
505
|
+
# Anything unmapped emits NOTHING rather than inventing a stage — better absent
|
|
506
|
+
# than mislabelled. incident:deploy-emit-never-load-bearing
|
|
507
|
+
_spine_stage() {
|
|
508
|
+
case "$1" in
|
|
509
|
+
tsc-*) echo typecheck ;;
|
|
510
|
+
vitest) echo tests ;;
|
|
511
|
+
build) echo build ;;
|
|
512
|
+
*) echo "" ;;
|
|
513
|
+
esac
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
# The verdict CLOSES the run, and is a field of its own — --gates-only is a
|
|
517
|
+
# complete run that never reaches ship. incident:deploy-verdict-closes-the-run
|
|
518
|
+
_emit_verdict() {
|
|
519
|
+
local v="$1"
|
|
520
|
+
_emit --verdict "$v" \
|
|
521
|
+
--detail "shipped=${TARGETS[*]:-none}" \
|
|
522
|
+
--detail "wallSec=$(( SECONDS ))" \
|
|
523
|
+
--detail "mode=${MODE}" \
|
|
524
|
+
--detail "heavyParallel=$( (( ${HEAVY_PARALLEL:-0} )) && echo true || echo false )" \
|
|
525
|
+
--detail "memFreeGb=${HEAVY_FREE_GB:-0}"
|
|
526
|
+
# Give the backgrounded emits a moment to land. Bounded and ignored: the run
|
|
527
|
+
# is closing either way, and a deploy must not wait on its own telemetry.
|
|
528
|
+
wait -n 2>/dev/null || true
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
# --slug is load-bearing: without it prod answers `forbidden` to every frame.
|
|
532
|
+
# incident:ask-slug-load-bearing
|
|
533
|
+
_emit() { # ...deploy-emit.sh args
|
|
534
|
+
[[ -x "$ROOT/.claude/scripts/deploy-emit.sh" ]] || return 0
|
|
535
|
+
(( DRY )) && return 0
|
|
536
|
+
( bash "$ROOT/.claude/scripts/deploy-emit.sh" \
|
|
537
|
+
--run "$STAMP" --target "$EMIT_TARGET" --env "$EMIT_ENV" \
|
|
538
|
+
--slug "${DEPLOY_SLUG:-one}" \
|
|
539
|
+
--door "deploy${MODE:+ $MODE}" \
|
|
540
|
+
--sha "$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo '')" \
|
|
541
|
+
"$@" >/dev/null 2>&1 || true ) &
|
|
542
|
+
return 0
|
|
543
|
+
}
|
|
544
|
+
|
|
480
545
|
GATE_NAMES=() GATE_PIDS=() GATE_LOGS=() GATE_RC=() GATE_T=() GATE_DUR=()
|
|
481
546
|
GATE_BOUND="${DEPLOY_GATE_TIMEOUT:-900}" # healthy vitest 176s, build 174s
|
|
482
|
-
# Typechecks
|
|
483
|
-
#
|
|
484
|
-
#
|
|
485
|
-
#
|
|
486
|
-
# below never covered, because it prices only vitest+build. On a cold worktree
|
|
487
|
-
# that is the whole melt (load 52, swap 20GB).
|
|
488
|
-
#
|
|
489
|
-
# So price them the same way: if the box cannot hold the fan-out, route them
|
|
490
|
-
# through the semaphore. deploy.sh holds no slot itself, so the nested calls
|
|
491
|
-
# really do queue (gate-run.sh is re-entrant — this would be cosmetic if an
|
|
492
|
-
# outer slot were held). A warm, healthy box still runs them bare and parallel.
|
|
547
|
+
# Typechecks are NOT "cached ~1s each" — no tsconfig sets `incremental`, so five
|
|
548
|
+
# full checks fan out to ~7.5GB that HEAVY_NEED_GB never priced. If the box
|
|
549
|
+
# cannot hold the fan-out, route them through the semaphore; a warm box runs them
|
|
550
|
+
# bare and parallel. incident:tsc-not-cached-on-ship
|
|
493
551
|
TSC_EACH_GB="${DEPLOY_TSC_EACH_GB:-2}"
|
|
494
552
|
TSC_NEED_GB=$(( TSC_EACH_GB * ${#SERVICES_TYPECHECK[@]} ))
|
|
495
553
|
if (( HEAVY_FREE_GB >= TSC_NEED_GB )); then TSC_GOVERNED=0; else TSC_GOVERNED=1; fi
|
|
@@ -502,6 +560,10 @@ gate_start() { # <name> <dir> <cmd...>
|
|
|
502
560
|
( cd "$ROOT/$dir" && unset CLOUDFLARE_API_TOKEN && "$@" ) >"$out" 2>&1 &
|
|
503
561
|
GATE_NAMES+=("$name"); GATE_PIDS+=("$!"); GATE_LOGS+=("$out"); GATE_RC+=(-1)
|
|
504
562
|
GATE_T+=("$SECONDS"); GATE_DUR+=(-1)
|
|
563
|
+
# The `start` frame is what makes a run render as IN PROGRESS rather than
|
|
564
|
+
# appearing whole at the end. Emitted per LAUNCH, so a serialised gate's
|
|
565
|
+
# start is the moment it really started, not the moment the batch did.
|
|
566
|
+
{ st="$(_spine_stage "$name")"; [[ -n "$st" ]] && _emit --stage "$st" --status start; } || true
|
|
505
567
|
say " → $name (pid $!)"
|
|
506
568
|
}
|
|
507
569
|
# Heavy gates run under gate-run.sh for its BOUND and its process-group REAP:
|
|
@@ -516,12 +578,15 @@ gate_start_governed() { # <name> <dir> <cmd...>
|
|
|
516
578
|
exec "$ROOT/.claude/scripts/gate-run.sh" "deploy-$name" -- "$@" ) >"$out" 2>&1 &
|
|
517
579
|
GATE_NAMES+=("$name"); GATE_PIDS+=("$!"); GATE_LOGS+=("$out"); GATE_RC+=(-1)
|
|
518
580
|
GATE_T+=("$SECONDS"); GATE_DUR+=(-1)
|
|
581
|
+
# The `start` frame is what makes a run render as IN PROGRESS rather than
|
|
582
|
+
# appearing whole at the end. Emitted per LAUNCH, so a serialised gate's
|
|
583
|
+
# start is the moment it really started, not the moment the batch did.
|
|
584
|
+
{ st="$(_spine_stage "$name")"; [[ -n "$st" ]] && _emit --stage "$st" --status start; } || true
|
|
519
585
|
say " → $name (pid $!, bound ${GATE_BOUND}s)"
|
|
520
586
|
}
|
|
521
|
-
# gate_start_heavy —
|
|
522
|
-
#
|
|
523
|
-
#
|
|
524
|
-
# (a second `wait` on the same child returns 127 and would read as a failure).
|
|
587
|
+
# gate_start_heavy — blocks until the previous heavy gate finished when memory
|
|
588
|
+
# says they cannot overlap, and records its status in GATE_RC so the collection
|
|
589
|
+
# loop never waits on a reaped pid. incident:gate-start-heavy-wait-127
|
|
525
590
|
HEAVY_IDX=()
|
|
526
591
|
gate_start_heavy() {
|
|
527
592
|
if (( ! DRY )) && (( ! HEAVY_PARALLEL )) && (( ${#HEAVY_IDX[@]} )); then
|
|
@@ -541,15 +606,20 @@ if ! (( SKIP_TYPECHECK )); then
|
|
|
541
606
|
else
|
|
542
607
|
say " memory: ${HEAVY_FREE_GB}GB free >= ${TSC_NEED_GB}GB needed — typechecks bare (parallel)"
|
|
543
608
|
fi
|
|
544
|
-
# NOT routed through tsc-cached.sh, and that is a measurement
|
|
545
|
-
#
|
|
546
|
-
#
|
|
547
|
-
#
|
|
548
|
-
#
|
|
549
|
-
#
|
|
550
|
-
#
|
|
551
|
-
#
|
|
552
|
-
|
|
609
|
+
# NOT routed through tsc-cached.sh, and that is a measurement: it would buy
|
|
610
|
+
# zero seconds and could turn a ship gate red on LOCK CONTENTION rather than on
|
|
611
|
+
# code. incident:tsc-not-cached-on-ship
|
|
612
|
+
# CONTENT TYPES FIRST, and worth its four seconds. Raw `tsc --noEmit` does not
|
|
613
|
+
# sync, `DataEntryMap` is generated into the gitignored .astro/, and a TS2344
|
|
614
|
+
# on a collection key is an ASTRO fault wearing a TypeScript error. Must run
|
|
615
|
+
# BEFORE the fan-out, or a sync races the tscs already reading those types.
|
|
616
|
+
# incident:astro-content-sync-before-tsc
|
|
617
|
+
if (( want_astro )) && [[ -d one.ie/web ]]; then
|
|
618
|
+
( cd one.ie/web && NODE_OPTIONS=--max-old-space-size=4096 \
|
|
619
|
+
./node_modules/.bin/astro sync >/dev/null 2>&1 ) \
|
|
620
|
+
&& ok "content types synced" \
|
|
621
|
+
|| warn "astro sync failed — a new content collection will read as a tsc error"
|
|
622
|
+
fi
|
|
553
623
|
for svc in "${SERVICES_TYPECHECK[@]}"; do
|
|
554
624
|
if (( TSC_GOVERNED )); then
|
|
555
625
|
gate_start_governed "tsc-$(echo "$svc" | tr / -)" "$svc" bunx tsc --noEmit
|
|
@@ -559,25 +629,75 @@ if ! (( SKIP_TYPECHECK )); then
|
|
|
559
629
|
done
|
|
560
630
|
fi
|
|
561
631
|
# the suite lives in one.ie/web and gates that worker
|
|
562
|
-
# The vitest gate hangs
|
|
563
|
-
#
|
|
564
|
-
#
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
#
|
|
569
|
-
# a full suite already proven green at this exact commit is not run twice. Only
|
|
570
|
-
# passes are memoised, so a red suite re-runs every time until it is green.
|
|
632
|
+
# The vitest gate hangs on a non-TTY stdout; `script -qeF` + `--reporter=dot` is
|
|
633
|
+
# the unstick. incident:vitest-pty-and-memo-probe
|
|
634
|
+
# TEST_CACHE_PTY=1 is not optional (this gate's stdout is a log file), the argv
|
|
635
|
+
# lives in test-full.sh so /close and deploy share one stamp, and the memo is
|
|
636
|
+
# PROBED BEFORE A SLOT IS TAKEN — a hit needs no memory, no forks and no slot.
|
|
637
|
+
# incident:vitest-pty-and-memo-probe
|
|
638
|
+
# incident:memo-hit-is-not-all-pass
|
|
571
639
|
#
|
|
572
|
-
#
|
|
573
|
-
#
|
|
574
|
-
#
|
|
575
|
-
#
|
|
576
|
-
#
|
|
577
|
-
#
|
|
578
|
-
#
|
|
579
|
-
|
|
580
|
-
|
|
640
|
+
# >>> vitest-memo-probe (extracted VERBATIM by deploy-gate-check.sh — keep self-contained)
|
|
641
|
+
# _memo_probe — "is EVERY lane already a green stamp for this exact tree?" Runs
|
|
642
|
+
# nothing, computes nothing, writes nothing.
|
|
643
|
+
# 0 = every lane HIT (stdout: one `already PASSED <ts>` line per lane)
|
|
644
|
+
# 1 = at least one lane MISSES · 2 = cannot answer
|
|
645
|
+
# Anything but 0 takes the slot and runs the gate: an unrun check is not a pass.
|
|
646
|
+
# It asks per LANE, never `TEST_CACHE_PROBE=1 test-full.sh` — that would skip the
|
|
647
|
+
# whole gate on a pool-only hit. incident:vitest-pty-and-memo-probe
|
|
648
|
+
_memo_probe() {
|
|
649
|
+
local cache="${TEST_CACHE_DIR:-${TMPDIR:-/tmp}/one-test-cache}"
|
|
650
|
+
[[ "${TEST_CACHE_DISABLE:-0}" == "1" ]] && return 2
|
|
651
|
+
local keys
|
|
652
|
+
keys="$( TEST_CACHE_KEY_ONLY=1 bash "$ROOT/.claude/scripts/test-full.sh" 2>/dev/null \
|
|
653
|
+
| grep -Eo '^[0-9a-f]{64}$' )"
|
|
654
|
+
[[ -n "$keys" ]] || return 2 # no key ⇒ no answer ⇒ run the gate
|
|
655
|
+
local k f hit n=0
|
|
656
|
+
while IFS= read -r k; do
|
|
657
|
+
[[ -n "$k" ]] || continue
|
|
658
|
+
n=$((n+1)); hit=""
|
|
659
|
+
for f in "$cache"/*"$k".pass; do
|
|
660
|
+
if [[ -f "$f" ]]; then hit="$f"; break; fi
|
|
661
|
+
done
|
|
662
|
+
[[ -n "$hit" ]] || return 1 # one miss is a miss: the gate runs
|
|
663
|
+
printf ' lane %s — identical inputs already PASSED %s\n' \
|
|
664
|
+
"${k:0:12}" "$(cat "$hit" 2>/dev/null)"
|
|
665
|
+
done <<< "$keys"
|
|
666
|
+
(( n )) || return 2
|
|
667
|
+
return 0
|
|
668
|
+
}
|
|
669
|
+
if (( SKIP_TESTS || ! want_astro )); then
|
|
670
|
+
: # --skip-tests keeps its own meaning
|
|
671
|
+
else
|
|
672
|
+
_mp_t0=$SECONDS
|
|
673
|
+
_mp_out="$(_memo_probe)"; _mp_rc=$?
|
|
674
|
+
_mp_dur=$(( SECONDS - _mp_t0 ))
|
|
675
|
+
if (( _mp_rc == 0 )) && (( DRY )); then
|
|
676
|
+
say " [dry] vitest gate would be REUSED from the memo (no slot taken)"
|
|
677
|
+
elif (( _mp_rc == 0 )); then
|
|
678
|
+
# NOT via gate_start_heavy and not in HEAVY_IDX: a gate with no process is
|
|
679
|
+
# not a heavy gate, and `wait 0` on a non-child exits 127.
|
|
680
|
+
# incident:gate-start-heavy-wait-127
|
|
681
|
+
_mp_log="$LOG_DIR/vitest-$STAMP.log"
|
|
682
|
+
{ echo "[deploy] vitest gate SKIPPED by the memo probe — every lane of the full suite"
|
|
683
|
+
echo "[deploy] is already a green stamp for this exact tree. No governor slot taken."
|
|
684
|
+
printf '%s\n' "$_mp_out"
|
|
685
|
+
echo "[deploy] probe cost ${_mp_dur}s"
|
|
686
|
+
} > "$_mp_log"
|
|
687
|
+
GATE_NAMES+=(vitest); GATE_PIDS+=(0); GATE_LOGS+=("$_mp_log"); GATE_RC+=(0)
|
|
688
|
+
GATE_T+=("$SECONDS"); GATE_DUR+=("$_mp_dur")
|
|
689
|
+
{ st="$(_spine_stage vitest)"; [[ -n "$st" ]] && _emit --stage "$st" --status start; } || true
|
|
690
|
+
say " → vitest (memo HIT in ${_mp_dur}s — no slot taken)"
|
|
691
|
+
else
|
|
692
|
+
case "$_mp_rc" in
|
|
693
|
+
1) say " memo probe: MISS — no green stamp for at least one lane of this tree" ;;
|
|
694
|
+
*) say " memo probe: could not answer — running the gate (an unrun check is not a pass)" ;;
|
|
695
|
+
esac
|
|
696
|
+
gate_start_heavy vitest one.ie/web \
|
|
697
|
+
env TYPEDB_LANE_NONBLOCKING=1 TEST_CACHE_PTY=1 bash "$ROOT/.claude/scripts/test-full.sh"
|
|
698
|
+
fi
|
|
699
|
+
fi
|
|
700
|
+
# <<< vitest-memo-probe
|
|
581
701
|
if (( want_astro )); then
|
|
582
702
|
if (( SKIP_BUILD )); then
|
|
583
703
|
[[ -d one.ie/web/dist/server ]] || die "--skip-build but one.ie/web/dist/server/ does not exist"
|
|
@@ -595,6 +715,11 @@ for i in $(seq 0 $(( ${#GATE_NAMES[@]} - 1 )) ); do
|
|
|
595
715
|
elif wait "${GATE_PIDS[$i]}"; then rc=0; else rc=1; fi
|
|
596
716
|
GATE_RC[$i]=$rc # record it: the deploy receipt below reads results, not just timings
|
|
597
717
|
(( ${GATE_DUR[$i]} < 0 )) && GATE_DUR[$i]=$(( SECONDS - GATE_T[$i] ))
|
|
718
|
+
# …and onto the stream. ONE hook for the three gates that are ~95% of the wall
|
|
719
|
+
# clock. Deferred past the `case` for `tests`, whose reused/waived facts are not
|
|
720
|
+
# known at this line. incident:deploy-emit-never-load-bearing
|
|
721
|
+
_emit_stage="$(_spine_stage "$name")"
|
|
722
|
+
_emit_dur=$(( GATE_DUR[i] < 0 ? 0 : GATE_DUR[i] ))
|
|
598
723
|
cat "$glog" >>"$LOG"
|
|
599
724
|
case "$name" in
|
|
600
725
|
tsc-*)
|
|
@@ -602,10 +727,8 @@ for i in $(seq 0 $(( ${#GATE_NAMES[@]} - 1 )) ); do
|
|
|
602
727
|
if (( rc )); then bad "${name#tsc-} typecheck FAILED"; gate_fail+=("$name")
|
|
603
728
|
else tc_pass=$((tc_pass+1)); fi ;;
|
|
604
729
|
vitest)
|
|
605
|
-
#
|
|
606
|
-
#
|
|
607
|
-
# A deploy that ran 10940 tests said "143 passed", which understates the
|
|
608
|
-
# gate by two orders of magnitude and reads like a partial run. Sum them.
|
|
730
|
+
# TWO lanes means TWO "Tests N passed" lines — SUM them; `tail -1` reported
|
|
731
|
+
# 143 for a 10940-test run. incident:two-lanes-tail-1-understates
|
|
609
732
|
TESTS_REPORT="$(awk '
|
|
610
733
|
/Tests +[0-9]+ (passed|failed)/ {
|
|
611
734
|
for (i = 1; i <= NF; i++) {
|
|
@@ -614,10 +737,8 @@ for i in $(seq 0 $(( ${#GATE_NAMES[@]} - 1 )) ); do
|
|
|
614
737
|
}
|
|
615
738
|
n++
|
|
616
739
|
}
|
|
617
|
-
#
|
|
618
|
-
#
|
|
619
|
-
# only the Tests line printed "11327 passed" for a red run and the flake
|
|
620
|
-
# checker called it GREEN (measured 2026-09-05). Read the Errors line too.
|
|
740
|
+
# Read the Errors line too: vitest exits non-zero on UNHANDLED errors
|
|
741
|
+
# with ZERO failed tests. incident:two-lanes-tail-1-understates
|
|
621
742
|
/Errors +[0-9]+ errors?/ { for (i = 1; i <= NF; i++) if ($i ~ /^errors?$/) e += $(i-1) + 0 }
|
|
622
743
|
END {
|
|
623
744
|
if (n == 0) exit
|
|
@@ -648,12 +769,14 @@ for i in $(seq 0 $(( ${#GATE_NAMES[@]} - 1 )) ); do
|
|
|
648
769
|
else
|
|
649
770
|
gate_fail+=(vitest)
|
|
650
771
|
fi
|
|
772
|
+
elif grep -q 'SKIPPED by the memo probe' "$glog"; then
|
|
773
|
+
# A reused pass is never reported as "all pass", and the stamp's own
|
|
774
|
+
# timestamp rides along. incident:memo-hit-is-not-all-pass
|
|
775
|
+
TESTS_REPORT="full suite REUSED — identical inputs already passed (memo probe, no slot taken: $(grep -o 'already PASSED.*' "$glog" | tail -1))"
|
|
776
|
+
ok "$TESTS_REPORT"
|
|
651
777
|
elif grep -q 'cache HIT' "$glog"; then
|
|
652
|
-
# Say WHICH it was
|
|
653
|
-
#
|
|
654
|
-
# reporting a fast lane as a full one -- the operator cannot tell whether
|
|
655
|
-
# 10,780 tests just executed or whether an earlier identical tree's
|
|
656
|
-
# result was replayed. Both are sound; only one of them took 20 minutes.
|
|
778
|
+
# Say WHICH it was — both are sound, only one took 20 minutes.
|
|
779
|
+
# incident:memo-hit-is-not-all-pass
|
|
657
780
|
TESTS_REPORT="full suite REUSED — identical inputs already passed ($(grep -o 'already PASSED.*' "$glog" | tail -1))"
|
|
658
781
|
ok "$TESTS_REPORT"
|
|
659
782
|
else
|
|
@@ -663,6 +786,27 @@ for i in $(seq 0 $(( ${#GATE_NAMES[@]} - 1 )) ); do
|
|
|
663
786
|
if (( rc )); then bad "astro build FAILED"; gate_fail+=(build)
|
|
664
787
|
else BUILD_REPORT="ok"; ok "astro build clean"; fi ;;
|
|
665
788
|
esac
|
|
789
|
+
# AFTER the case: `tests` only learns reused/waived inside it, and those two
|
|
790
|
+
# booleans are the whole reason this stream exists. A waived pass is a FACT on
|
|
791
|
+
# the run. incident:waived-pass-is-a-fact
|
|
792
|
+
if [[ -n "$_emit_stage" ]]; then
|
|
793
|
+
_emit_args=(--stage "$_emit_stage" --wall-ms "$(( _emit_dur * 1000 ))")
|
|
794
|
+
if (( rc )) && ! { [[ "$_emit_stage" == tests ]] && (( TESTS_WAIVED )); }; then
|
|
795
|
+
_emit_args+=(--status fail --reason "${name}")
|
|
796
|
+
else
|
|
797
|
+
_emit_args+=(--status ok)
|
|
798
|
+
fi
|
|
799
|
+
case "$_emit_stage" in
|
|
800
|
+
tests)
|
|
801
|
+
_emit_args+=(--detail "reused=$( [[ "$TESTS_REPORT" == *REUSED* ]] && echo true || echo false )")
|
|
802
|
+
_emit_args+=(--detail "waived=$( (( TESTS_WAIVED )) && echo true || echo false )") ;;
|
|
803
|
+
typecheck)
|
|
804
|
+
# Which of the five, not just how many — a reader chasing a red gate
|
|
805
|
+
# should not have to open the log to learn the service.
|
|
806
|
+
_emit_args+=(--detail "service=${name#tsc-}") ;;
|
|
807
|
+
esac
|
|
808
|
+
_emit "${_emit_args[@]}"
|
|
809
|
+
fi
|
|
666
810
|
done
|
|
667
811
|
(( tc_total )) && {
|
|
668
812
|
TYPECHECK_REPORT="$tc_pass/$tc_total services clean"
|
|
@@ -670,34 +814,29 @@ done
|
|
|
670
814
|
}
|
|
671
815
|
if (( ${#GATE_NAMES[@]} )); then
|
|
672
816
|
GATE_WALL="$((SECONDS - GATE_T0))s"
|
|
673
|
-
# Per-gate, not just the aggregate
|
|
674
|
-
#
|
|
675
|
-
#
|
|
676
|
-
# and "make the deploy faster" turns into caching whatever finishes inside
|
|
677
|
-
# vitest's shadow for zero seconds.
|
|
817
|
+
# Per-gate, not just the aggregate — without the breakdown "make the deploy
|
|
818
|
+
# faster" caches whatever finishes inside vitest's shadow for zero seconds.
|
|
819
|
+
# incident:deploy-verdict-closes-the-run
|
|
678
820
|
for i in $(seq 0 $(( ${#GATE_NAMES[@]} - 1 )) ); do
|
|
679
821
|
say " ${GATE_NAMES[$i]}: ${GATE_DUR[$i]}s"
|
|
680
822
|
done
|
|
681
823
|
say " gates wall-clock: $GATE_WALL"
|
|
824
|
+
# Repeat the branch HERE, next to the durations it explains. Without it a
|
|
825
|
+
# reader comparing two runs cannot tell a slow gate from a serialised one.
|
|
826
|
+
if (( HEAVY_PARALLEL )); then
|
|
827
|
+
say " heavy gates: OVERLAPPED (${HEAVY_FREE_GB}GB free >= ${HEAVY_NEED_GB}GB)"
|
|
828
|
+
else
|
|
829
|
+
say " heavy gates: SERIALISED (${HEAVY_FREE_GB}GB free < ${HEAVY_NEED_GB}GB) — the 6.5x branch"
|
|
830
|
+
fi
|
|
682
831
|
[[ "$BUILD_REPORT" == ok ]] && BUILD_REPORT="$GATE_WALL (overlapped)"
|
|
683
832
|
fi
|
|
684
833
|
(( ${#gate_fail[@]} )) && die "gates red: ${gate_fail[*]} — nothing deployed"
|
|
685
834
|
|
|
686
835
|
# ── deferred-pin debt ───────────────────────────────────────────────────────
|
|
687
|
-
# verify-fast
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
691
|
-
# branch in verify-fast.sh that settles the ledger. A production deploy could
|
|
692
|
-
# therefore ship over pins that no run had ever executed.
|
|
693
|
-
#
|
|
694
|
-
# Two rules, both one-directional:
|
|
695
|
-
# · a GREEN full suite PAYS the debt. The suite is a superset of every pin, so
|
|
696
|
-
# reaching here green is the payment -- same reasoning verify-fast uses.
|
|
697
|
-
# · anything else REFUSES. A skipped suite does not pay a debt; neither does a
|
|
698
|
-
# red one (we never get here) nor a `--gates-only` run that skipped tests.
|
|
699
|
-
# An unpaid debt with no run to settle it is an unrun gate, and an unrun gate
|
|
700
|
-
# is not a passing gate.
|
|
836
|
+
# deploy is the LAST place verify-fast's deferred pins can come due. Two rules,
|
|
837
|
+
# both one-directional: a GREEN full suite PAYS the debt (the suite is a superset
|
|
838
|
+
# of every pin) · anything else REFUSES. An unpaid debt with no run to settle it
|
|
839
|
+
# is an unrun gate. incident:deferred-pin-debt
|
|
701
840
|
DEBT_FILE="${VERIFY_FAST_DEBT_FILE:-${TMPDIR:-/tmp}/one-verify-fast-debt}"
|
|
702
841
|
if [[ -s "$DEBT_FILE" ]]; then
|
|
703
842
|
step "Deferred-pin debt"
|
|
@@ -718,6 +857,10 @@ fi
|
|
|
718
857
|
if (( GATES_ONLY )); then
|
|
719
858
|
say ""; ok "gates green in ${GATE_WALL:-0s} — nothing shipped (--gates-only)"
|
|
720
859
|
say " Typecheck: $TYPECHECK_REPORT"; say " Tests: $TESTS_REPORT"; say " Log: $LOG"
|
|
860
|
+
# CLOSE THE RUN — the --gates-only exit, which every release.sh promote takes.
|
|
861
|
+
# Without this line the run sits `open` for ever.
|
|
862
|
+
# incident:deploy-verdict-closes-the-run
|
|
863
|
+
_emit_verdict green
|
|
721
864
|
exit 0
|
|
722
865
|
fi
|
|
723
866
|
|
|
@@ -782,15 +925,9 @@ if (( want_astro )) && ! (( SKIP_MIGRATIONS )); then
|
|
|
782
925
|
fi
|
|
783
926
|
|
|
784
927
|
# ── Step 6.6 — D1 migrations (channels) ─────────────────────────────────────
|
|
785
|
-
# A SECOND database
|
|
786
|
-
#
|
|
787
|
-
#
|
|
788
|
-
# enters it, and channels' own `bun run migrate` was pinned to 0001_init.sql.
|
|
789
|
-
# So 0010_action_bindings.sql shipped with the C6 worker code and its table was
|
|
790
|
-
# never created — and because bindAction's INSERT was fire-and-forget, every
|
|
791
|
-
# button binding failed silently, leaving taps that resolve to nothing. The
|
|
792
|
-
# promise's accept (`ls channels/migrations/*action*.sql`) proved the FILE
|
|
793
|
-
# existed, never the table. Same rule as above: schema before code.
|
|
928
|
+
# A SECOND database: channels binds `DB` to `claw` and keeps its own migrations,
|
|
929
|
+
# and nothing applied that directory. Same rule as 6.5: schema before code.
|
|
930
|
+
# incident:channels-second-d1
|
|
794
931
|
MIG_CHAN_REPORT="skipped"
|
|
795
932
|
if (( want_agents )) && ! (( SKIP_MIGRATIONS )); then
|
|
796
933
|
step "Step 6.6 — D1 migrations (channels DB, remote)"
|
|
@@ -892,20 +1029,11 @@ else
|
|
|
892
1029
|
(( want_sync )) && HEALTH_REPORT="$HEALTH_REPORT + sync deploy-confirmed"
|
|
893
1030
|
|
|
894
1031
|
# ── deploy receipt ────────────────────────────────────────────────────────
|
|
895
|
-
#
|
|
896
|
-
#
|
|
897
|
-
#
|
|
898
|
-
#
|
|
899
|
-
#
|
|
900
|
-
# long before Step 8, so a red gate never reaches a receipt. The page says
|
|
901
|
-
# that out loud rather than letting "all pass" imply a full audit.
|
|
902
|
-
# * `healthy` is null for sync. It is never probed (cron-only — the line two
|
|
903
|
-
# above), and a worker nobody asked must not render healthy OR unhealthy.
|
|
904
|
-
# * `deployed` comes from BG_NAMES — what actually uploaded — never from
|
|
905
|
-
# TARGETS, which is only the intent.
|
|
906
|
-
#
|
|
907
|
-
# Wrapped in `{ … } || true` so a failed write can never fail a deploy that
|
|
908
|
-
# has already shipped.
|
|
1032
|
+
# Three honesty rules the data itself forces: gates here are only ever GREEN (a
|
|
1033
|
+
# red one died long before Step 8) · `healthy` is NULL for sync, which is never
|
|
1034
|
+
# probed · `deployed` is BG_NAMES (what uploaded), never TARGETS (the intent).
|
|
1035
|
+
# `|| true` so a failed write cannot fail a shipped deploy.
|
|
1036
|
+
# incident:deploy-receipt-honesty
|
|
909
1037
|
_receipt="$ROOT/one.ie/web/src/lib/generated/deploy-receipt.json"
|
|
910
1038
|
{
|
|
911
1039
|
mkdir -p "$(dirname "$_receipt")"
|
|
@@ -930,10 +1058,8 @@ else
|
|
|
930
1058
|
(( ${#BG_NAMES[@]} )) || break
|
|
931
1059
|
_n="${BG_NAMES[$i]}"
|
|
932
1060
|
if [[ "$_n" == sync ]]; then _h=null # never probed
|
|
933
|
-
# `${hfail[*]-}` not `${hfail[*]}
|
|
934
|
-
# array as unbound
|
|
935
|
-
# so a perfect deploy died here (line 933, 2026-09-05), truncated its own
|
|
936
|
-
# receipt mid-JSON and reported exit 1. Same trap test-lanes.sh already paid.
|
|
1061
|
+
# `${hfail[*]-}` not `${hfail[*]}` — bash 3.2 under `set -u` treats an
|
|
1062
|
+
# EMPTY array as unbound. incident:empty-array-unbound-bash32
|
|
937
1063
|
elif [[ " ${hfail[*]-} " == *" $_n "* ]]; then _h=false
|
|
938
1064
|
else _h=true; fi
|
|
939
1065
|
printf '%s\n {"name": "%s", "deployed": true, "healthy": %s}' "$_sep" "$_n" "$_h"
|
|
@@ -947,17 +1073,9 @@ fi
|
|
|
947
1073
|
|
|
948
1074
|
# ── Report ──────────────────────────────────────────────────────────────────
|
|
949
1075
|
step "Step 9 — speed (advisory)"
|
|
950
|
-
# Post-deploy Lighthouse
|
|
951
|
-
#
|
|
952
|
-
#
|
|
953
|
-
# because live-site numbers swing ~30 points on network jitter and cannot gate.
|
|
954
|
-
#
|
|
955
|
-
# Placed here, serial and after the parallel deploys, deliberately: Lighthouse
|
|
956
|
-
# drives a full Chrome and would fight the Step 1+3 gate fan-out and the machine
|
|
957
|
-
# governor's slot cap if it ran alongside them.
|
|
958
|
-
#
|
|
959
|
-
# Exit 2 = INCONCLUSIVE (could not measure). It is reported as such and NEVER
|
|
960
|
-
# as a pass — an unrun check is not a green check.
|
|
1076
|
+
# Post-deploy Lighthouse on the LIVE site. ADVISORY — the ENFORCED ratchet is
|
|
1077
|
+
# `bun run speed` pre-deploy against the local build. Exit 2 = INCONCLUSIVE and
|
|
1078
|
+
# is never a pass. incident:speed-step-detached
|
|
961
1079
|
if (( DRY )); then
|
|
962
1080
|
echo " (dry run — speed check skipped)" | tee -a "$LOG"
|
|
963
1081
|
elif [[ "${SKIP_SPEED:-0}" == "1" ]]; then
|
|
@@ -965,13 +1083,9 @@ elif [[ "${SKIP_SPEED:-0}" == "1" ]]; then
|
|
|
965
1083
|
else
|
|
966
1084
|
sp_out="$LOG_DIR/speed-$STAMP.log"
|
|
967
1085
|
sp_cmd=(node "$(dirname "${BASH_SOURCE[0]}")/speed-check.mjs" --prod --runs 3)
|
|
968
|
-
#
|
|
969
|
-
#
|
|
970
|
-
#
|
|
971
|
-
# correct here — Step 9 runs AFTER the deploy has shipped, so failing the
|
|
972
|
-
# pipeline could not un-ship it. This step reports; it does not block. The
|
|
973
|
-
# blocking gate is `bun run speed` pre-deploy against the local build.
|
|
974
|
-
# warn() is used so a red x is never mistaken for a blocked deploy.
|
|
1086
|
+
# One verdict function for both the inline and the detached path. bad() only
|
|
1087
|
+
# PRINTS here: Step 9 runs after the ship, so it reports and does not block.
|
|
1088
|
+
# incident:speed-step-detached
|
|
975
1089
|
speed_verdict() {
|
|
976
1090
|
grep -E 'perf=|faster|SLOWER|performance:|advisory only' "$sp_out" | sed 's/^/ /' | tee -a "$LOG" || true
|
|
977
1091
|
case "$1" in
|
|
@@ -993,14 +1107,9 @@ else
|
|
|
993
1107
|
run_bounded "${SPEED_TIMEOUT:-600}" "${sp_cmd[@]}" >"$sp_out" 2>&1
|
|
994
1108
|
speed_verdict $?
|
|
995
1109
|
else
|
|
996
|
-
# DETACHED
|
|
997
|
-
#
|
|
998
|
-
#
|
|
999
|
-
# and already passed health at +61s. The number is advisory by design, so
|
|
1000
|
-
# nothing downstream waits on it; the only thing that waited was the
|
|
1001
|
-
# operator. The verdict is appended to $LOG when it lands, in the same
|
|
1002
|
-
# words the inline path uses, and the process is bounded so a hung Chrome
|
|
1003
|
-
# cannot outlive the deploy by more than SPEED_TIMEOUT.
|
|
1110
|
+
# DETACHED by default — it was 166s of a 227s pipeline, after the site had
|
|
1111
|
+
# already shipped and passed health. Bounded so a hung Chrome cannot outlive
|
|
1112
|
+
# the deploy. incident:speed-step-detached
|
|
1004
1113
|
(
|
|
1005
1114
|
run_bounded "${SPEED_TIMEOUT:-600}" "${sp_cmd[@]}" >"$sp_out" 2>&1
|
|
1006
1115
|
rc=$?
|
|
@@ -1027,11 +1136,52 @@ say " Health: $HEALTH_REPORT"
|
|
|
1027
1136
|
[[ -n "${GZIP_HINT:-}" ]] && say " Bundle: $GZIP_HINT"
|
|
1028
1137
|
say " Log: $LOG"
|
|
1029
1138
|
|
|
1139
|
+
# The record the /deploy page renders, on BOTH exits — a record holding only
|
|
1140
|
+
# green runs cannot be read for a trend. Guarded: a recorder fault must never
|
|
1141
|
+
# turn a good deploy into a bad exit. incident:deploy-record-trunk-leg
|
|
1142
|
+
_record_run() {
|
|
1143
|
+
local verdict="$1"
|
|
1144
|
+
[[ -x "$ROOT/.claude/scripts/deploy-record.sh" ]] || return 0
|
|
1145
|
+
(( DRY )) && return 0
|
|
1146
|
+
local -a args=(--door "deploy${MODE:+ $MODE}" --target "${TARGETS[*]:-—}"
|
|
1147
|
+
--branch "$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
|
|
1148
|
+
--sha "$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo '')"
|
|
1149
|
+
--verdict "$verdict" --wall "$(( SECONDS ))"
|
|
1150
|
+
--note "heavy=$( (( ${HEAVY_PARALLEL:-0} )) && echo parallel || echo serial ) memFreeGb=${HEAVY_FREE_GB:-?} needGb=${HEAVY_NEED_GB:-?}")
|
|
1151
|
+
local i st
|
|
1152
|
+
for i in "${!GATE_NAMES[@]}"; do
|
|
1153
|
+
# -1 means the gate never reported. It is recorded as `unrun`, never as a
|
|
1154
|
+
# pass and never as a zero — the page draws it as an outline for that reason.
|
|
1155
|
+
if [[ "${GATE_RC[$i]}" == 0 ]]; then st=pass
|
|
1156
|
+
elif [[ "${GATE_RC[$i]}" == -1 ]]; then st=unrun
|
|
1157
|
+
else st=fail; fi
|
|
1158
|
+
args+=(--phase "${GATE_NAMES[$i]}:$(( GATE_T[i] - GATE_T0 )):$(( GATE_DUR[i] < 0 ? 0 : GATE_DUR[i] )):gate:$st")
|
|
1159
|
+
done
|
|
1160
|
+
bash "$ROOT/.claude/scripts/deploy-record.sh" "${args[@]}" >/dev/null 2>&1 || true
|
|
1161
|
+
# THE TRUNK LEG. The row lands in the PRIMARY worktree and NOTHING commits it,
|
|
1162
|
+
# so /deploy never renders it until someone does. Deliberately manual, and
|
|
1163
|
+
# printed at the moment it becomes true. incident:deploy-record-trunk-leg
|
|
1164
|
+
local pend; pend="$(bash "$ROOT/.claude/scripts/deploy-record.sh" --pending 2>/dev/null)" || true
|
|
1165
|
+
[[ -n "$pend" ]] && say "$pend"
|
|
1166
|
+
return 0
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1030
1169
|
if (( ${#hfail[@]} )); then
|
|
1170
|
+
_emit --stage health --status fail --reason "unhealthy: ${hfail[*]}"
|
|
1171
|
+
_emit_verdict red
|
|
1172
|
+
_record_run degraded
|
|
1173
|
+
# A degraded ship is an outcome. Close it as one.
|
|
1174
|
+
# --only dims,feedback: `deploy` is a LABEL, not a plan slug — the task leg
|
|
1175
|
+
# would close a plan named deploy. incident:deploy-record-trunk-leg
|
|
1176
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" deploy --status failed --only dims,feedback || true
|
|
1031
1177
|
say ""
|
|
1032
1178
|
bad "degraded — unhealthy: ${hfail[*]}"
|
|
1033
1179
|
say " rollback: cd one.ie/web && bunx wrangler rollback --name one-prod"
|
|
1034
1180
|
exit 1
|
|
1035
1181
|
fi
|
|
1182
|
+
_emit --stage health --status ok --detail "probes=${#TARGETS[@]}"
|
|
1183
|
+
_emit_verdict green
|
|
1184
|
+
_record_run green
|
|
1185
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" deploy --only dims,feedback || true
|
|
1036
1186
|
say ""
|
|
1037
1187
|
ok "deploy:success"
|