@oneie/claude 0.7.0 → 0.9.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/agents/abm-strategist.md +89 -0
- package/agents/ads-meta.md +97 -0
- package/agents/analyst.md +173 -0
- package/agents/architect.md +280 -0
- package/agents/brand-guardian.md +88 -0
- package/agents/brand-strategist.md +92 -0
- package/agents/campaign-content.md +90 -0
- package/agents/campaign-email.md +88 -0
- package/agents/campaign-sms.md +87 -0
- package/agents/campaign-social.md +87 -0
- package/agents/cco.md +99 -0
- package/agents/ceo.md +106 -0
- package/agents/chairman.md +120 -0
- package/agents/cmo.md +101 -0
- package/agents/community-greeter.md +89 -0
- package/agents/community-moderator.md +92 -0
- package/agents/compliance.md +92 -0
- package/agents/copywriter.md +100 -0
- package/agents/creative-strategist.md +95 -0
- package/agents/cro.md +98 -0
- package/agents/cto.md +267 -0
- package/agents/customer-interviewer.md +93 -0
- package/agents/customer-researcher.md +89 -0
- package/agents/customer-success-manager.md +90 -0
- package/agents/customer-trainer.md +90 -0
- package/agents/cxo.md +100 -0
- package/agents/demand-creator.md +89 -0
- package/agents/demo-mover.md +83 -0
- package/agents/demo-specialist.md +89 -0
- package/agents/demo-thai-family-law.md +87 -0
- package/agents/designer.md +112 -0
- package/agents/discovery-caller.md +98 -0
- package/agents/doctor.md +139 -0
- package/agents/educate-coach.md +100 -0
- package/agents/elevate-tutor.md +83 -0
- package/agents/email-lifecycle-marketer.md +91 -0
- package/agents/engage-specialist.md +99 -0
- package/agents/events-coordinator.md +90 -0
- package/agents/foundation-builder.md +94 -0
- package/agents/funnel-architect.md +89 -0
- package/agents/gift-creator.md +100 -0
- package/agents/google-ads.md +103 -0
- package/agents/guide.md +292 -0
- package/agents/helpdesk-dispatcher.md +88 -0
- package/agents/hook-specialist.md +91 -0
- package/agents/identify-optimizer.md +101 -0
- package/agents/implementer.md +305 -0
- package/agents/incident-commander.md +120 -0
- package/agents/insights-lead.md +93 -0
- package/agents/journey-runner.md +113 -0
- package/agents/linkedin-ads.md +123 -0
- package/agents/live-sales-chat.md +90 -0
- package/agents/market-researcher.md +88 -0
- package/agents/media-buyer.md +96 -0
- package/agents/movers-customer-researcher.md +95 -0
- package/agents/movers-foundation-builder.md +96 -0
- package/agents/movers-market-researcher.md +97 -0
- package/agents/movers-pricing-strategist.md +171 -0
- package/agents/nurture-architect.md +99 -0
- package/agents/offer-architect.md +92 -0
- package/agents/onboarder.md +222 -0
- package/agents/onboarding-specialist.md +92 -0
- package/agents/operations-dashboard.md +98 -0
- package/agents/perf-engineer.md +348 -0
- package/agents/playbook-writer.md +71 -0
- package/agents/plg-strategist.md +91 -0
- package/agents/positioning-architect.md +88 -0
- package/agents/press-officer.md +89 -0
- package/agents/pricing-strategist.md +94 -0
- package/agents/privacy-officer.md +92 -0
- package/agents/referral-manager.md +91 -0
- package/agents/refine-analyst.md +102 -0
- package/agents/release-manager.md +261 -0
- package/agents/renewals-upsell-rep.md +90 -0
- package/agents/review-engineer.md +327 -0
- package/agents/rewards-steward.md +82 -0
- package/agents/sales-call-coach.md +94 -0
- package/agents/sales-closer.md +98 -0
- package/agents/security-auditor.md +313 -0
- package/agents/sell-closer.md +101 -0
- package/agents/share-amplifier.md +99 -0
- package/agents/social-media-manager.md +92 -0
- package/agents/storyteller.md +250 -0
- package/agents/strategist.md +83 -0
- package/agents/strategy-aligner.md +94 -0
- package/agents/support-agent.md +90 -0
- package/agents/tagger.md +245 -0
- package/agents/tech-writer.md +216 -0
- package/agents/test-engineer.md +243 -0
- package/agents/tiktok-ads.md +105 -0
- package/agents/tracking-engineer.md +92 -0
- package/agents/upsell-strategist.md +101 -0
- package/agents/voice.md +85 -0
- package/agents/w4-tools.md +153 -0
- package/agents/w4-verify.md +20 -138
- package/agents/workflow-optimiser.md +84 -0
- package/commands/close.md +814 -160
- package/commands/create.md +2 -2
- package/commands/deploy.md +554 -175
- package/commands/do.md +119 -109
- package/commands/fast.md +67 -0
- package/commands/improve.md +2 -2
- package/commands/one.md +418 -0
- package/commands/see.md +1 -1
- package/commands/sync.md +1 -1
- package/commands/tasks.md +222 -0
- package/commands/vespio.md +110 -0
- package/commands/vespio.remote.md +227 -0
- package/hooks/hooks.json +25 -79
- package/hooks/lib/governor-escape-match.sh +88 -0
- package/hooks/lib/hook.sh +4 -13
- package/hooks/lib/signal.sh +9 -2
- package/hooks/scripts/branch-pin.sh +6 -0
- package/hooks/scripts/config-protect.sh +6 -0
- package/hooks/scripts/do-outcome-gate.sh +19 -2
- package/hooks/scripts/git-add-guard.sh +43 -2
- package/hooks/scripts/governor-escape.sh +96 -0
- package/hooks/scripts/load-guard.sh +127 -0
- package/hooks/scripts/post-edit-check.sh +36 -29
- package/hooks/scripts/session-start.sh +34 -80
- package/hooks/scripts/task-complete-verify.sh +46 -40
- package/package.json +1 -1
- package/rules/documentation.md +9 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agentverse-audit.sh +256 -0
- package/scripts/asi-walk.sh +435 -0
- package/scripts/astro-build-cached.sh +143 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/blocks-manifest.mjs +497 -0
- package/scripts/blocks-ratchet.sh +116 -0
- package/scripts/blocks-render-probe.mjs +529 -0
- package/scripts/blocks-usage.mjs +334 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/close-metrics.sh +558 -0
- package/scripts/close-owner.sh +268 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-dev.sh +35 -0
- package/scripts/deploy-emit.sh +298 -0
- package/scripts/deploy-gate-check.sh +128 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy.sh +1273 -0
- package/scripts/do-auto.sh +598 -48
- package/scripts/do-brief.sh +113 -0
- package/scripts/do-close.sh +1137 -0
- package/scripts/do-commit.sh +75 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-cycle-shape-check.sh +160 -0
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-derive-check.sh +436 -0
- package/scripts/do-fleet.sh +106 -28
- package/scripts/do-folder.sh +10 -1
- package/scripts/do-next.sh +106 -0
- package/scripts/do-orchestrate.sh +17 -5
- package/scripts/do-plan-json.mjs +201 -0
- package/scripts/do-plan-json.sh +8 -0
- package/scripts/do-preflight.sh +117 -0
- package/scripts/do-project.sh +157 -0
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +295 -23
- package/scripts/do-rank.py +31 -1
- package/scripts/do-recon-cache.sh +7 -1
- package/scripts/do-recon-pack.sh +196 -0
- package/scripts/do-reconcile.sh +121 -4
- package/scripts/do-signal.sh +280 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-test-gate.sh +80 -0
- package/scripts/do-tick.sh +102 -0
- package/scripts/do-tier.sh +6 -0
- package/scripts/do-triage.sh +182 -0
- package/scripts/do-ui-gate.sh +1 -1
- package/scripts/do-w4-gates.sh +451 -0
- package/scripts/do-walk.sh +12 -1
- package/scripts/env-sync.sh +173 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +68 -14
- 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 +118 -3
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-ship.sh +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/factory-turn.sh +326 -0
- package/scripts/factory-walk.sh +396 -0
- package/scripts/factory-width.sh +57 -0
- package/scripts/fade-toxic.sh +4 -3
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/fixtures/triage-dupe.md +5 -0
- package/scripts/fleet-manifest.mjs +108 -0
- package/scripts/fleet-status.sh +110 -0
- package/scripts/full-suite-paths-check.sh +144 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +125 -0
- package/scripts/gate-run.sh +73 -0
- package/scripts/gc-content-check.sh +140 -0
- package/scripts/gen-dev-config.py +20 -0
- package/scripts/govern-bound-check.sh +60 -0
- package/scripts/govern-claims-check.sh +233 -0
- package/scripts/govern-mem-check.sh +290 -0
- package/scripts/governor-doors-check.sh +362 -0
- package/scripts/governor-escape-check.sh +171 -0
- package/scripts/health.sh +413 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/land.sh +551 -0
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/lib/govern.sh +361 -0
- package/scripts/lib/govern.ts +756 -0
- package/scripts/lighthouse-run.sh +187 -0
- package/scripts/livekit-live-check.sh +61 -0
- package/scripts/livekit-ratchet.sh +46 -0
- package/scripts/load-guard-check.sh +49 -0
- package/scripts/machine-check.sh +102 -0
- package/scripts/machine-watch.sh +177 -0
- package/scripts/one-agents.mjs +415 -0
- package/scripts/one-resume.sh +78 -0
- package/scripts/orphan-baseline.json +182 -0
- package/scripts/orphan-modules.mjs +179 -0
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/promise-manifest.mjs +24 -1
- package/scripts/release.sh +322 -0
- package/scripts/roles-check.sh +946 -0
- package/scripts/sdk-build-cached.sh +64 -0
- package/scripts/signal-watch.sh +241 -0
- package/scripts/skills-publish.sh +94 -0
- package/scripts/speed-cache-check.sh +173 -0
- package/scripts/speed-check.mjs +907 -0
- package/scripts/speed-parity-check.sh +648 -0
- package/scripts/speed-waterfall-check.sh +355 -0
- package/scripts/substrate-env-parity.mjs +156 -0
- package/scripts/tasks-claim-race.mjs +108 -0
- package/scripts/tasks-loop.sh +185 -0
- package/scripts/test-cached.sh +255 -0
- package/scripts/test-full.sh +87 -0
- package/scripts/test-honesty.mjs +137 -0
- package/scripts/test-lanes.sh +166 -0
- package/scripts/test-speed.sh +94 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +179 -0
- package/scripts/typedb-flake-check.sh +197 -0
- package/scripts/urls-lint.sh +15 -0
- package/scripts/verify-fast.sh +445 -0
- package/scripts/vespio-sync.sh +149 -0
- package/scripts/wf-check.mjs +104 -1
- package/scripts/worktree-preview.sh +879 -0
- package/scripts/worktree-up.sh +208 -0
- package/skills/livekit-agents/SKILL.md +285 -0
- package/skills/livekit-agents/references/freshness-rules.md +168 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/signal/SKILL.md +0 -1
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/hooks/scripts/compact-hint.sh +0 -35
- package/hooks/scripts/gate-guard.sh +0 -83
- package/hooks/scripts/read-tracker.sh +0 -26
- package/hooks/scripts/session-end-verify.sh +0 -51
- package/hooks/scripts/stop-reflect.sh +0 -140
- package/hooks/scripts/sync-priority-todo.sh +0 -57
- package/hooks/scripts/sync-todo-docs.sh +0 -46
- package/hooks/scripts/tool-signal.sh +0 -48
- package/scripts/do-tasks-bridge.py +0 -366
|
@@ -0,0 +1,1273 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Deterministic /deploy — the whole pipeline with no LLM in the loop.
|
|
3
|
+
#
|
|
4
|
+
# Ships the five services to Cloudflare: gateway (api) · sync · channels ·
|
|
5
|
+
# pay/backend · astro (one-prod). Every step reports a number; the final
|
|
6
|
+
# report is the same shape /deploy prints by hand.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# bash .claude/scripts/deploy.sh dev # one.ie/web → one-dev (dev.one.ie)
|
|
10
|
+
# bash .claude/scripts/deploy.sh # full pipeline (PRODUCTION)
|
|
11
|
+
# bash .claude/scripts/deploy.sh astro # one.ie/web → one-prod only
|
|
12
|
+
# bash .claude/scripts/deploy.sh workers # api + sync + channels
|
|
13
|
+
# bash .claude/scripts/deploy.sh gateway|sync|agents|pay
|
|
14
|
+
#
|
|
15
|
+
# Flags:
|
|
16
|
+
# --skip-tests skip vitest (typecheck still runs)
|
|
17
|
+
# --no-typedb-flake-waiver
|
|
18
|
+
# hard-stop on a red suite even when every failure is the
|
|
19
|
+
# shared TypeDB Cloud cluster being unreachable. The waiver
|
|
20
|
+
# is ON by default (DEPLOY_ALLOW_TYPEDB_FLAKE=0 also disables)
|
|
21
|
+
# and is classified by signature — a real assertion break, or
|
|
22
|
+
# a tasks:claim `not_found`, still blocks.
|
|
23
|
+
# --skip-typecheck skip per-service tsc
|
|
24
|
+
# --skip-build reuse an existing one.ie/web/dist
|
|
25
|
+
# --skip-migrations skip `d1 migrations apply DB --remote`
|
|
26
|
+
# --skip-health deploy without the post-deploy probes
|
|
27
|
+
# --changed skip services with no commits since their last deploy
|
|
28
|
+
# --gates-only run the gates, ship nothing (a local pre-flight)
|
|
29
|
+
# --check-creds self-test the credential ladder, ship nothing
|
|
30
|
+
# --yes / -y no approval prompt (also: DEPLOY_YES=1)
|
|
31
|
+
# --dry-run print every command, deploy nothing
|
|
32
|
+
#
|
|
33
|
+
# NOT done here (it needs judgment): commit / PR / merge. Commit before you
|
|
34
|
+
# run this. The script refuses a dirty tree unless --allow-dirty.
|
|
35
|
+
#
|
|
36
|
+
# HARD RULES baked in (see .claude/commands/deploy.md):
|
|
37
|
+
# · never `--env production` — it ships to the `one-prod-production` decoy
|
|
38
|
+
# · CLOUDFLARE_API_TOKEN is unset; auth is GLOBAL_API_KEY + EMAIL
|
|
39
|
+
# · health-check custom domains only (*.workers.dev is blocked on this net)
|
|
40
|
+
# · sync is cron-only: a clean `wrangler deploy` IS its health signal
|
|
41
|
+
#
|
|
42
|
+
# THE TWO TIERS — one command, two destinations:
|
|
43
|
+
# dev → https://dev.one.ie (worker `one-dev`). Agents finish loops here.
|
|
44
|
+
# MINIMUM gate: the FAST lane. No approval prompt, no migrations, no
|
|
45
|
+
# five-service fan-out. Delegates to deploy-dev.sh, which is the
|
|
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.
|
|
53
|
+
|
|
54
|
+
set -uo pipefail
|
|
55
|
+
|
|
56
|
+
# The deploy gate is the FULL gate, always. Build cycles run the dev lane
|
|
57
|
+
# (verify:fast — tsc + related tests + pinned suites); this is the one place
|
|
58
|
+
# every test must run, so any script that honours FULL_VERIFY runs everything.
|
|
59
|
+
export FULL_VERIFY=1
|
|
60
|
+
|
|
61
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
62
|
+
cd "$ROOT" || exit 1
|
|
63
|
+
|
|
64
|
+
[[ -d one.ie/web && -d api && -d sync && -d channels && -d pay/backend ]] || {
|
|
65
|
+
echo "deploy.sh: not the one-ie monorepo (missing a service dir): $ROOT" >&2
|
|
66
|
+
exit 2
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
MODE="full"
|
|
70
|
+
SKIP_TESTS=0 SKIP_TYPECHECK=0 SKIP_BUILD=0 SKIP_MIGRATIONS=0 SKIP_HEALTH=0
|
|
71
|
+
ASSUME_YES="${DEPLOY_YES:-0}" DRY=0 ALLOW_DIRTY=0 ONLY_CHANGED=0 GATES_ONLY=0 CHECK_CREDS=0
|
|
72
|
+
# A RED suite whose every failure is the shared TypeDB Cloud cluster refusing to
|
|
73
|
+
# answer is waivable — nothing was disproven. Classified by signature, never by
|
|
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.
|
|
76
|
+
TYPEDB_FLAKE_WAIVER="${DEPLOY_ALLOW_TYPEDB_FLAKE:-1}" TESTS_WAIVED=0
|
|
77
|
+
|
|
78
|
+
for a in "$@"; do
|
|
79
|
+
case "$a" in
|
|
80
|
+
dev|full|astro|workers|gateway|sync|agents|channels|pay) MODE="$a" ;;
|
|
81
|
+
--skip-tests) SKIP_TESTS=1 ;;
|
|
82
|
+
--skip-typecheck) SKIP_TYPECHECK=1 ;;
|
|
83
|
+
--skip-build) SKIP_BUILD=1 ;;
|
|
84
|
+
--skip-migrations) SKIP_MIGRATIONS=1 ;;
|
|
85
|
+
--skip-health) SKIP_HEALTH=1 ;;
|
|
86
|
+
--allow-dirty) ALLOW_DIRTY=1 ;;
|
|
87
|
+
--no-typedb-flake-waiver) TYPEDB_FLAKE_WAIVER=0 ;;
|
|
88
|
+
--changed) ONLY_CHANGED=1 ;;
|
|
89
|
+
--gates-only) GATES_ONLY=1 ;;
|
|
90
|
+
--check-creds) CHECK_CREDS=1 ;;
|
|
91
|
+
--all) ONLY_CHANGED=0 ;;
|
|
92
|
+
-y|--yes) ASSUME_YES=1 ;;
|
|
93
|
+
-n|--dry-run) DRY=1 ;;
|
|
94
|
+
-h|--help) sed -n '2,46p' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
95
|
+
*) echo "deploy.sh: unknown argument '$a' (try --help)" >&2; exit 2 ;;
|
|
96
|
+
esac
|
|
97
|
+
done
|
|
98
|
+
[[ "$MODE" == channels ]] && MODE=agents
|
|
99
|
+
|
|
100
|
+
# ── dev tier ─────────────────────────────────────────────────────────────────
|
|
101
|
+
# `./deploy dev` is a different destination with a different gate, so it exits
|
|
102
|
+
# here rather than threading a flag through 700 lines of production pipeline.
|
|
103
|
+
# deploy-dev.sh owns the dev procedure; this is only the door to it.
|
|
104
|
+
if [[ "$MODE" == dev ]]; then
|
|
105
|
+
# --skip-tests is the only production flag that carries over: it is the dev
|
|
106
|
+
# script's DEV_SKIP_GATE. `(( SKIP_TESTS ))` and not `${SKIP_TESTS:+...}` —
|
|
107
|
+
# the latter expands on the literal string "0" and would silently skip the
|
|
108
|
+
# gate on every dev deploy.
|
|
109
|
+
dev_gate=0; (( SKIP_TESTS )) && dev_gate=1
|
|
110
|
+
if (( DRY )); then
|
|
111
|
+
echo "[dry] DEV_SKIP_GATE=$dev_gate bash .claude/scripts/deploy-dev.sh"
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
exec env "DEV_SKIP_GATE=$dev_gate" bash "$ROOT/.claude/scripts/deploy-dev.sh"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
LOG_DIR="$ROOT/.deploy-logs"
|
|
118
|
+
mkdir -p "$LOG_DIR"
|
|
119
|
+
STAMP="$(date +%Y%m%d-%H%M%S)"
|
|
120
|
+
LOG="$LOG_DIR/deploy-$STAMP.log"
|
|
121
|
+
|
|
122
|
+
RED=$'\033[31m'; GRN=$'\033[32m'; YEL=$'\033[33m'; DIM=$'\033[2m'; OFF=$'\033[0m'
|
|
123
|
+
[[ -t 1 ]] || { RED=""; GRN=""; YEL=""; DIM=""; OFF=""; }
|
|
124
|
+
|
|
125
|
+
say() { printf '%s\n' "$*" | tee -a "$LOG"; }
|
|
126
|
+
step() { printf '\n%s══ %s%s\n' "$DIM" "$*" "$OFF" | tee -a "$LOG"; }
|
|
127
|
+
ok() { printf '%s ✓%s %s\n' "$GRN" "$OFF" "$*" | tee -a "$LOG"; }
|
|
128
|
+
bad() { printf '%s ✗%s %s\n' "$RED" "$OFF" "$*" | tee -a "$LOG"; }
|
|
129
|
+
warn() { printf '%s !%s %s\n' "$YEL" "$OFF" "$*" | tee -a "$LOG"; }
|
|
130
|
+
die() { bad "$*"; say ""; say "log: $LOG"; exit 1; }
|
|
131
|
+
|
|
132
|
+
# run <label> <dir> <cmd...> — logs, honours --dry-run, returns the exit code
|
|
133
|
+
run() {
|
|
134
|
+
local label="$1" dir="$2"; shift 2
|
|
135
|
+
if (( DRY )); then say " [dry] (cd $dir && $*)"; return 0; fi
|
|
136
|
+
say " → $label"
|
|
137
|
+
( cd "$ROOT/$dir" && unset CLOUDFLARE_API_TOKEN && "$@" ) >>"$LOG" 2>&1
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
want_astro=0 want_gateway=0 want_sync=0 want_agents=0 want_pay=0
|
|
141
|
+
case "$MODE" in
|
|
142
|
+
full) want_astro=1 want_gateway=1 want_sync=1 want_agents=1 want_pay=1 ;;
|
|
143
|
+
astro) want_astro=1 ;;
|
|
144
|
+
workers) want_gateway=1 want_sync=1 want_agents=1 ;;
|
|
145
|
+
gateway) want_gateway=1 ;;
|
|
146
|
+
sync) want_sync=1 ;;
|
|
147
|
+
agents) want_agents=1 ;;
|
|
148
|
+
pay) want_pay=1 ;;
|
|
149
|
+
esac
|
|
150
|
+
|
|
151
|
+
# --changed: a service whose tree has no new commits since its last successful
|
|
152
|
+
# deploy has nothing to ship. The marker is written per service AFTER a clean
|
|
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.
|
|
155
|
+
svc_paths() { case "$1" in
|
|
156
|
+
astro) echo "one.ie/web packages" ;;
|
|
157
|
+
gateway) echo "api" ;;
|
|
158
|
+
sync) echo "sync" ;;
|
|
159
|
+
channels) echo "channels packages" ;;
|
|
160
|
+
pay) echo "pay/backend" ;;
|
|
161
|
+
esac }
|
|
162
|
+
mark_file() { echo "$LOG_DIR/last-$1.sha"; }
|
|
163
|
+
svc_changed() { # 0 = has new commits (or no marker yet)
|
|
164
|
+
local last; last="$(cat "$(mark_file "$1")" 2>/dev/null)"
|
|
165
|
+
[[ -n "$last" ]] || return 0
|
|
166
|
+
git merge-base --is-ancestor "$last" HEAD 2>/dev/null || return 0
|
|
167
|
+
[[ -n "$(git log --oneline "$last..HEAD" -- $(svc_paths "$1") 2>/dev/null)" ]]
|
|
168
|
+
}
|
|
169
|
+
SKIPPED_UNCHANGED=()
|
|
170
|
+
if (( ONLY_CHANGED )); then
|
|
171
|
+
for svc in astro gateway sync channels pay; do
|
|
172
|
+
case "$svc" in
|
|
173
|
+
astro) (( want_astro )) || continue ;;
|
|
174
|
+
gateway) (( want_gateway )) || continue ;;
|
|
175
|
+
sync) (( want_sync )) || continue ;;
|
|
176
|
+
channels) (( want_agents )) || continue ;;
|
|
177
|
+
pay) (( want_pay )) || continue ;;
|
|
178
|
+
esac
|
|
179
|
+
svc_changed "$svc" && continue
|
|
180
|
+
SKIPPED_UNCHANGED+=("$svc")
|
|
181
|
+
case "$svc" in
|
|
182
|
+
astro) want_astro=0 ;; gateway) want_gateway=0 ;; sync) want_sync=0 ;;
|
|
183
|
+
channels) want_agents=0 ;; pay) want_pay=0 ;;
|
|
184
|
+
esac
|
|
185
|
+
done
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
# Typecheck only what is being shipped — `./deploy gateway` has no business
|
|
189
|
+
# spending 30s on one.ie/web. Full mode still covers all five.
|
|
190
|
+
SERVICES_TYPECHECK=()
|
|
191
|
+
(( want_astro )) && SERVICES_TYPECHECK+=(one.ie/web)
|
|
192
|
+
(( want_gateway )) && SERVICES_TYPECHECK+=(api)
|
|
193
|
+
(( want_sync )) && SERVICES_TYPECHECK+=(sync)
|
|
194
|
+
(( want_agents )) && SERVICES_TYPECHECK+=(channels)
|
|
195
|
+
(( want_pay )) && SERVICES_TYPECHECK+=(pay/backend)
|
|
196
|
+
|
|
197
|
+
if (( ONLY_CHANGED )) && ! (( want_astro + want_gateway + want_sync + want_agents + want_pay )); then
|
|
198
|
+
echo "deploy: nothing changed since the last deploy of ${SKIPPED_UNCHANGED[*]} — use --all to ship anyway"
|
|
199
|
+
exit 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
say "deploy · mode=$MODE · $(date '+%Y-%m-%d %H:%M:%S')"
|
|
203
|
+
say "log: $LOG"
|
|
204
|
+
|
|
205
|
+
# ── Cloudflare credential resolution ────────────────────────────────────────
|
|
206
|
+
# WHY THIS IS A LADDER AND NOT A LOOKUP (measured 2026-08-19, cost one red
|
|
207
|
+
# deploy and one wrong diagnosis):
|
|
208
|
+
#
|
|
209
|
+
# There were THREE credentials reachable from one `./deploy`, and nothing chose
|
|
210
|
+
# between them — each of the five services silently got whichever one its own
|
|
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.
|
|
240
|
+
CF_AUTH="" CF_AUTH_SOURCE="" CF_ACCOUNT=""
|
|
241
|
+
|
|
242
|
+
# _cf_probe <email> <key> — 200 from /user means these exact bytes authenticate.
|
|
243
|
+
# Ground truth, and direction-agnostic: it is equally able to prove a key ALIVE
|
|
244
|
+
# (2026-08-19) as dead (2026-08-18). Never conclude either from wrangler alone.
|
|
245
|
+
_cf_probe() {
|
|
246
|
+
local e="$1" k="$2" code
|
|
247
|
+
[[ -n "$e" && -n "$k" ]] || return 1
|
|
248
|
+
code="$(curl -s -m 15 -o /dev/null -w '%{http_code}' \
|
|
249
|
+
-H "X-Auth-Email: $e" -H "X-Auth-Key: $k" \
|
|
250
|
+
https://api.cloudflare.com/client/v4/user 2>/dev/null)"
|
|
251
|
+
[[ "$code" == "200" ]]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# _cf_fingerprint <secret> — length + sha8. The whole point of logging the
|
|
255
|
+
# credential's identity is to make "which key was that?" answerable next time,
|
|
256
|
+
# so it must be logged in a form that can never leak the key itself.
|
|
257
|
+
_cf_fingerprint() { printf 'len=%s sha=%s' "${#1}" "$(printf %s "$1" | shasum | cut -c1-8)"; }
|
|
258
|
+
|
|
259
|
+
# _cf_read <file> <var> — one value out of a .env-style file, quotes stripped.
|
|
260
|
+
_cf_read() {
|
|
261
|
+
[[ -f "$1" ]] || return 1
|
|
262
|
+
local v; v="$(grep -m1 "^$2=" "$1" 2>/dev/null | cut -d= -f2- | tr -d '"'"'"'\r' | xargs)"
|
|
263
|
+
[[ -n "$v" ]] && printf '%s' "$v"
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
_cf_resolve() {
|
|
267
|
+
local e k src
|
|
268
|
+
# Rung 1-3: a global key from ambient env, then repo root, then one.ie/web.
|
|
269
|
+
# Ordered so an operator's explicit export still wins, but a STALE export
|
|
270
|
+
# cannot block a good key on disk — it just fails its probe and falls through.
|
|
271
|
+
for src in "ambient env" "$ROOT/.env.local" "$ROOT/one.ie/web/.env"; do
|
|
272
|
+
case "$src" in
|
|
273
|
+
"ambient env")
|
|
274
|
+
e="${CLOUDFLARE_EMAIL:-}"
|
|
275
|
+
k="${CLOUDFLARE_API_KEY:-${CLOUDFLARE_GLOBAL_API_KEY:-}}" ;;
|
|
276
|
+
*)
|
|
277
|
+
e="$(_cf_read "$src" CLOUDFLARE_EMAIL || true)"
|
|
278
|
+
k="$(_cf_read "$src" CLOUDFLARE_API_KEY || _cf_read "$src" CLOUDFLARE_GLOBAL_API_KEY || true)"
|
|
279
|
+
[[ -n "${e:-}" ]] || e="${CLOUDFLARE_EMAIL:-}" ;;
|
|
280
|
+
esac
|
|
281
|
+
if _cf_probe "${e:-}" "${k:-}"; then
|
|
282
|
+
# wrangler reads CLOUDFLARE_API_KEY; older copies of this script exported
|
|
283
|
+
# only CLOUDFLARE_GLOBAL_API_KEY, a name wrangler ignores (the 2026-08-18
|
|
284
|
+
# bug). Export BOTH so neither spelling can diverge again.
|
|
285
|
+
export CLOUDFLARE_EMAIL="$e" CLOUDFLARE_API_KEY="$k" CLOUDFLARE_GLOBAL_API_KEY="$k"
|
|
286
|
+
CF_AUTH="global-api-key"
|
|
287
|
+
CF_AUTH_SOURCE="$src ($(_cf_fingerprint "$k"))"
|
|
288
|
+
return 0
|
|
289
|
+
fi
|
|
290
|
+
[[ -n "${k:-}" ]] && say " ${DIM}rejected: $src ($(_cf_fingerprint "$k")) — /user did not answer 200${OFF}"
|
|
291
|
+
done
|
|
292
|
+
# Rung 4: OAuth. Last, because it expires (~1h) and because a shadowing key
|
|
293
|
+
# must never let the deploy land here by accident. A DEAD ambient key would
|
|
294
|
+
# otherwise sit in the env poisoning every service, so strip all three now.
|
|
295
|
+
unset CLOUDFLARE_EMAIL CLOUDFLARE_API_KEY CLOUDFLARE_GLOBAL_API_KEY
|
|
296
|
+
if ( cd "$ROOT/one.ie/web" && ./node_modules/.bin/wrangler whoami >/dev/null 2>&1 ); then
|
|
297
|
+
CF_AUTH="oauth"; CF_AUTH_SOURCE="wrangler login session (expires ~1h — re-run 'wrangler login' if a step 403s)"
|
|
298
|
+
return 0
|
|
299
|
+
fi
|
|
300
|
+
return 1
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# _cf_agree — every service must be deploying to the SAME account. This is the
|
|
304
|
+
# check that would have caught 6.5-vs-6.6 before either ran: one.ie/web
|
|
305
|
+
# answering about a different credential than channels IS the bug, and it is
|
|
306
|
+
# only visible by asking each directory separately.
|
|
307
|
+
_cf_agree() {
|
|
308
|
+
(( DRY )) && return 0
|
|
309
|
+
local d id first="" bad=0
|
|
310
|
+
for d in one.ie/web api sync channels pay/backend; do
|
|
311
|
+
id="$( cd "$ROOT/$d" 2>/dev/null && bunx wrangler whoami 2>/dev/null \
|
|
312
|
+
| grep -oE '[0-9a-f]{32}' | head -1 )"
|
|
313
|
+
if [[ -z "$id" ]]; then warn "$d — no account id from wrangler whoami"; bad=1; continue; fi
|
|
314
|
+
if [[ -z "$first" ]]; then first="$id"; CF_ACCOUNT="$id"
|
|
315
|
+
elif [[ "$id" != "$first" ]]; then
|
|
316
|
+
bad "$d resolves account $id, but one.ie/web resolves $first"
|
|
317
|
+
say " a per-directory .env is overriding the resolved credential in $d"
|
|
318
|
+
bad=1
|
|
319
|
+
fi
|
|
320
|
+
done
|
|
321
|
+
(( bad )) && return 1
|
|
322
|
+
ok "5/5 services agree on account ${first:0:8}…"
|
|
323
|
+
return 0
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# --check-creds — the red half. A gate that has never been seen to FAIL is not
|
|
327
|
+
# known to work (repo canon: presence greps are not proof). This plants a key
|
|
328
|
+
# that cannot authenticate and asserts the ladder REJECTS it and falls through,
|
|
329
|
+
# then asserts the five services agree. Ships nothing.
|
|
330
|
+
if (( CHECK_CREDS )); then
|
|
331
|
+
step "credential ladder — self-test"
|
|
332
|
+
fails=0
|
|
333
|
+
unset CLOUDFLARE_API_TOKEN
|
|
334
|
+
# Resolve for real first — tests 3 and 4 assert properties OF a resolution,
|
|
335
|
+
# so running them against an unresolved shell tests nothing.
|
|
336
|
+
_cf_resolve || die "self-test cannot run: no working credential on any rung"
|
|
337
|
+
ok "resolved: $CF_AUTH"
|
|
338
|
+
|
|
339
|
+
say " 1/4 · a dead key must be rejected, not exported"
|
|
340
|
+
if _cf_probe "nobody@example.invalid" "0000000000000000000000000000000000000"; then
|
|
341
|
+
bad "_cf_probe returned 0 for a key that cannot authenticate — the probe is fail-open"; fails=1
|
|
342
|
+
else ok "dead key rejected by /user probe"; fi
|
|
343
|
+
|
|
344
|
+
say " 2/4 · a live credential must still resolve with a dead key shadowing it"
|
|
345
|
+
( export CLOUDFLARE_API_KEY="0000000000000000000000000000000000000" \
|
|
346
|
+
CLOUDFLARE_GLOBAL_API_KEY="0000000000000000000000000000000000000" \
|
|
347
|
+
CLOUDFLARE_EMAIL="nobody@example.invalid"
|
|
348
|
+
_cf_resolve >/dev/null 2>&1 && [[ -n "$CF_AUTH" ]] ) \
|
|
349
|
+
&& ok "ladder fell through the shadowing key to a working rung" \
|
|
350
|
+
|| { bad "a dead ambient key BLOCKS resolution — this is the 2026-08-19 bug"; fails=1; }
|
|
351
|
+
|
|
352
|
+
say " 3/4 · the resolved source must be named, and must not be the key itself"
|
|
353
|
+
if [[ "$CF_AUTH_SOURCE" == *"sha="* || "$CF_AUTH" == "oauth" ]]; then
|
|
354
|
+
ok "source reported: $CF_AUTH_SOURCE"
|
|
355
|
+
else bad "credential source is unnamed — the next confusion is unavoidable"; fails=1; fi
|
|
356
|
+
if [[ -n "${CLOUDFLARE_API_KEY:-}" && "$CF_AUTH_SOURCE" == *"${CLOUDFLARE_API_KEY}"* ]]; then
|
|
357
|
+
bad "the log line contains the key BYTES"; fails=1
|
|
358
|
+
else ok "no key bytes in the log line"; fi
|
|
359
|
+
|
|
360
|
+
say " 4/4 · all five services must agree on the account"
|
|
361
|
+
_cf_agree || fails=1
|
|
362
|
+
|
|
363
|
+
say ""
|
|
364
|
+
(( fails )) && die "credential self-test FAILED"
|
|
365
|
+
ok "credential self-test passed"
|
|
366
|
+
say "log: $LOG"; exit 0
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
# ── Step 0.4 — credentials (BEFORE the slow gates) ──────────────────────────
|
|
370
|
+
# It was after them: on 2026-08-18 a bad credential was discovered only at Step
|
|
371
|
+
# 6.5, having already spent the full test suite and a 2m14s production build.
|
|
372
|
+
# One curl costs nothing. Fail here.
|
|
373
|
+
step "Step 0.4 — credentials"
|
|
374
|
+
unset CLOUDFLARE_API_TOKEN
|
|
375
|
+
_cf_resolve || die "no working Cloudflare credential — run 'wrangler login', or put a VALID CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL in .env.local"
|
|
376
|
+
ok "resolved: $CF_AUTH"
|
|
377
|
+
say " source: $CF_AUTH_SOURCE"
|
|
378
|
+
|
|
379
|
+
# ── Step 0 — tree state (before the slow gates: a dirty tree should fail fast)
|
|
380
|
+
step "Step 0 — tree"
|
|
381
|
+
# Same reasoning as the dirty-tree check, applied to the APPROVAL gate. That
|
|
382
|
+
# prompt lives at Step 6, AFTER the gate battery, so a launch that can never
|
|
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.
|
|
389
|
+
if (( ! ASSUME_YES )) && (( ! DRY )) && [[ "$(git rev-parse --abbrev-ref HEAD)" == "main" ]] && [[ ! -t 0 ]]; then
|
|
390
|
+
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
|
+
fi
|
|
392
|
+
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
393
|
+
HEAD_SHA="$(git rev-parse --short HEAD)"
|
|
394
|
+
say " branch=$BRANCH head=$HEAD_SHA"
|
|
395
|
+
DIRTY="$(git status --porcelain | wc -l | tr -d ' ')"
|
|
396
|
+
if [[ "$DIRTY" != "0" ]]; then
|
|
397
|
+
git status --porcelain | sed 's/^/ /' | tee -a "$LOG"
|
|
398
|
+
if (( ALLOW_DIRTY )); then
|
|
399
|
+
warn "$DIRTY uncommitted path(s) — shipping anyway (--allow-dirty)"
|
|
400
|
+
else
|
|
401
|
+
die "$DIRTY uncommitted path(s). Commit them (deploy.sh does not write git history), or pass --allow-dirty."
|
|
402
|
+
fi
|
|
403
|
+
else
|
|
404
|
+
ok "clean tree"
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
# ── Step 0.5 — generated artifacts (cheap, before the slow gates) ─────────
|
|
408
|
+
# Same reasoning as Step 0.4: one cheap deterministic check, run where failing is
|
|
409
|
+
# free, rather than after five typechecks, the whole suite and an astro build.
|
|
410
|
+
#
|
|
411
|
+
# blocks-manifest.mjs builds THREE COMMITTED artifacts from one block registry --
|
|
412
|
+
# one.ie/web/src/lib/puck/block-manifest.json (what tooling reads),
|
|
413
|
+
# channels/src/generated/block-enum.ts (what the model is told) and
|
|
414
|
+
# one.ie/web/src/lib/puck/block-schema.generated.ts (what the browser normalises
|
|
415
|
+
# against) -- and until 2026-09-09 nothing ran it: not this script, not
|
|
416
|
+
# verify-fast.sh, not a test. `grep -c blocks-manifest` on both was 0 and 0.
|
|
417
|
+
# A drifted enum ships a vocabulary the model is told about and the browser
|
|
418
|
+
# cannot render, so the generator belongs on the SHIP gate and not only the
|
|
419
|
+
# dev lane.
|
|
420
|
+
#
|
|
421
|
+
# Step 0 has already refused a dirty tree, so what --check diffs is exactly what
|
|
422
|
+
# would ship. It exits non-zero on drift AND on failing to run, and both stop the
|
|
423
|
+
# deploy: an unrun gate is not a pass.
|
|
424
|
+
#
|
|
425
|
+
# THE DRY BRANCH IS EXPLICIT, and it has to be: `run` returns 0 under --dry-run
|
|
426
|
+
# (see its definition above), so `if run ...; then ok "no drift"` would print a
|
|
427
|
+
# GREEN verdict for a check that never executed -- an unrun gate reported as a
|
|
428
|
+
# pass, in the one script where that is the cardinal sin. `gate_start` below
|
|
429
|
+
# already handles DRY the same way: say what would run, record no verdict.
|
|
430
|
+
step "Step 0.5 — generated artifacts"
|
|
431
|
+
if (( DRY )); then
|
|
432
|
+
say " [dry] node .claude/scripts/blocks-manifest.mjs --check"
|
|
433
|
+
elif run "blocks-manifest --check" . node "$ROOT/.claude/scripts/blocks-manifest.mjs" --check; then
|
|
434
|
+
ok "block manifest: no drift"
|
|
435
|
+
else
|
|
436
|
+
tail -5 "$LOG" | sed 's/^/ /'
|
|
437
|
+
die "block manifest DRIFT (or --check could not run) — regenerate with 'node .claude/scripts/blocks-manifest.mjs' and commit all three artifacts"
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
# ── Steps 1+3 — the slow gates, all at once ─────────────────────────────────
|
|
441
|
+
# typecheck ×5, vitest and the astro build share no STATE — the only thing they
|
|
442
|
+
# share is the SDK's dist/, built first if missing. Running them concurrently
|
|
443
|
+
# makes W0+build cost max() instead of sum(). Nothing here has a side effect
|
|
444
|
+
# outside the working tree, so a red gate still stops the deploy before
|
|
445
|
+
# anything ships.
|
|
446
|
+
#
|
|
447
|
+
# They are NOT independent in COST, and the comment here used to claim they
|
|
448
|
+
# were. This box has 10 cores / 4 performance cores; the block above spawns 5
|
|
449
|
+
# tsc, an 8 GiB-heap astro build, and a vitest that forks its own worker pool.
|
|
450
|
+
# Anything inside the suite that shells out to a heavy child competes with all
|
|
451
|
+
# of it — measured 2026-08-08, tests/integration/reconcile.test.ts (which runs
|
|
452
|
+
# a full-repo tsc of its own) went 53s standalone → 376s here and blew its
|
|
453
|
+
# budget, failing the PASSING direction and blocking a deploy over nothing.
|
|
454
|
+
# Test budgets in this repo must therefore be sized for a STARVED run, not a
|
|
455
|
+
# quiet one. If a gate ever needs the box to itself, take it out of this block
|
|
456
|
+
# rather than widening every timeout downstream.
|
|
457
|
+
|
|
458
|
+
# web + channels resolve @oneie/sdk types out of packages/sdk/dist — an unbuilt
|
|
459
|
+
# dist fakes a wall of TS2307, so this one is a real dependency, done serially.
|
|
460
|
+
if ! (( SKIP_TYPECHECK )) && [[ ! -d packages/sdk/dist ]]; then
|
|
461
|
+
step "Step 1.0 — @oneie/sdk build (dist/ missing)"
|
|
462
|
+
run "build @oneie/sdk" packages/sdk bun run build || die "SDK build failed — see $LOG"
|
|
463
|
+
ok "sdk built"
|
|
464
|
+
fi
|
|
465
|
+
|
|
466
|
+
# ── heavy-gate concurrency, priced in MEMORY not cores ──────────────────────
|
|
467
|
+
# The five typechecks are cheap (~1s each, tree-fingerprint cached). The other
|
|
468
|
+
# two are not: the astro build runs with --max-old-space-size=8192 and vitest
|
|
469
|
+
# runs a driver plus 4 forks (~6GB). Both at once wants ~14GB, and this box has
|
|
470
|
+
# 24GB with editors and sessions already resident.
|
|
471
|
+
#
|
|
472
|
+
# Cores were never the binding constraint, so price the gates in memory. What
|
|
473
|
+
# this DOES buy: two gates that want ~14GB no longer overlap on a 24GB box.
|
|
474
|
+
#
|
|
475
|
+
# What it does NOT buy, and an earlier version of this comment wrongly claimed:
|
|
476
|
+
# it is not the fix for the vitest gate HANGING. That was reproduced with the
|
|
477
|
+
# gates serialised, the box at 41% free, no swap thrash and no sockets held —
|
|
478
|
+
# see .claude/commands/deploy.md "The vitest gate hangs". Serialising addresses
|
|
479
|
+
# a slow gate; the bound below is what addresses a parked one.
|
|
480
|
+
#
|
|
481
|
+
# Same reasoning as lib/govern.sh gate_headroom, but priced for THESE two gates
|
|
482
|
+
# rather than a generic ~2GB cycle. A probe that cannot read memory returns a
|
|
483
|
+
# large number — a broken sensor must never silently serialise the pipeline.
|
|
484
|
+
HEAVY_NEED_GB="${DEPLOY_HEAVY_NEED_GB:-14}" # build 8 + vitest ~6
|
|
485
|
+
# shellcheck source=lib/govern.sh
|
|
486
|
+
. "$ROOT/.claude/scripts/lib/govern.sh"
|
|
487
|
+
# This used to be `hw.memsize * memory_pressure "free percentage" / 100`, which
|
|
488
|
+
# is the defect lib/govern.sh documents at length: that percentage counts file
|
|
489
|
+
# cache, purgeable and compressed pages as free. Measured 2026-09-03 it read 74%
|
|
490
|
+
# on a box with 1.3% actually free and 9.4 GB in swap — so this gate overlapped
|
|
491
|
+
# two ~7 GB gates on a machine that had ~3 GB. gate_mem_avail_mb is the honest
|
|
492
|
+
# quantity. Unreadable probe still returns 999: a broken sensor must never
|
|
493
|
+
# silently serialise the pipeline.
|
|
494
|
+
heavy_free_gb() {
|
|
495
|
+
local avail_mb
|
|
496
|
+
avail_mb="$(gate_mem_avail_mb 2>/dev/null || echo "")"
|
|
497
|
+
[ -z "$avail_mb" ] && { echo 999; return 0; }
|
|
498
|
+
echo $(( avail_mb / 1024 ))
|
|
499
|
+
}
|
|
500
|
+
HEAVY_FREE_GB="$(heavy_free_gb)"
|
|
501
|
+
if (( HEAVY_FREE_GB >= HEAVY_NEED_GB )); then HEAVY_PARALLEL=1; else HEAVY_PARALLEL=0; fi
|
|
502
|
+
[[ "${DEPLOY_HEAVY_PARALLEL:-}" == 1 ]] && HEAVY_PARALLEL=1 # escape hatch
|
|
503
|
+
|
|
504
|
+
if (( HEAVY_PARALLEL )); then
|
|
505
|
+
step "Steps 1+3 — gates in parallel (typecheck ×5 · vitest · astro build)"
|
|
506
|
+
say " memory: ${HEAVY_FREE_GB}GB free >= ${HEAVY_NEED_GB}GB needed — heavy gates overlap"
|
|
507
|
+
else
|
|
508
|
+
step "Steps 1+3 — gates (typecheck ×5 parallel · vitest + astro build serial)"
|
|
509
|
+
say " memory: ${HEAVY_FREE_GB}GB free < ${HEAVY_NEED_GB}GB needed — heavy gates serialised"
|
|
510
|
+
say " (overlapping them pages; measured 1145s vs 176s — see deploy.sh)"
|
|
511
|
+
fi
|
|
512
|
+
|
|
513
|
+
# ── the run, streamed into the world ────────────────────────────────────────
|
|
514
|
+
# Until 2026-09-07 this script emitted NOTHING — no signal, no row — and a
|
|
515
|
+
# committed JSON file was a deploy's only trace. `deploy-emit.sh` puts each gate
|
|
516
|
+
# on the same run/event stream /do and the factory already write to, LIVE, so a
|
|
517
|
+
# reader can see a deploy that is happening rather than only one that happened.
|
|
518
|
+
#
|
|
519
|
+
# THREE RULES, and all three are about not making the telemetry load-bearing:
|
|
520
|
+
#
|
|
521
|
+
# 1. IT CAN NEVER FAIL THE DEPLOY. Backgrounded and `|| true`, exactly as
|
|
522
|
+
# _record_run is guarded. A gate that passed while the network was down is
|
|
523
|
+
# still a gate that passed, and a deploy going red because it could not
|
|
524
|
+
# phone home is a worse failure than the one it was reporting.
|
|
525
|
+
# 2. IT CAN NEVER SLOW THE DEPLOY. `&` — the emitter's own --max-time is 8s and
|
|
526
|
+
# that must not land on the critical path of a gate somebody is waiting on.
|
|
527
|
+
# 3. --dry-run EMITS NOTHING. A run row for a deploy that did not happen is the
|
|
528
|
+
# theater /deploy already refuses to render. Checked here, because the
|
|
529
|
+
# emitter cannot know its caller's flags.
|
|
530
|
+
#
|
|
531
|
+
# A dropped emit degrades to a GAP in the trace, never to a lie: the mapper
|
|
532
|
+
# treats an absent stage as absent and has no default frame.
|
|
533
|
+
# bash 3.2 (what macOS ships) closes `$(` at the FIRST `)` it meets — and a
|
|
534
|
+
# case pattern ends in one. So the old form,
|
|
535
|
+
# EMIT_TARGET="$( case "$MODE" in dev) echo dev.one.ie ;; *) echo one.ie ;; esac )"
|
|
536
|
+
# raised `syntax error near unexpected token` at run time and left EMIT_TARGET
|
|
537
|
+
# holding the literal tail ` echo dev.one.ie ;; *) echo one.ie ;; esac )`, which
|
|
538
|
+
# was then passed as --target and written into deploy-runs.json. Measured on the
|
|
539
|
+
# 2026-09-08 production ship, in the log, twice.
|
|
540
|
+
#
|
|
541
|
+
# That is the one outcome the comment above forbids: an emit that degrades to a
|
|
542
|
+
# LIE rather than a gap. deploy-runs.json is the artifact the release path reads
|
|
543
|
+
# to learn what production runs, so a false target there misinforms the next
|
|
544
|
+
# person to ask "what is live". An `if` has no pattern and no closing paren.
|
|
545
|
+
if [ "$MODE" = dev ]; then EMIT_TARGET="dev.one.ie"; else EMIT_TARGET="one.ie"; fi
|
|
546
|
+
EMIT_ENV="$( [[ "$MODE" == dev ]] && echo dev || echo prod )"
|
|
547
|
+
|
|
548
|
+
# deploy.sh's gate names are implementation names (`tsc-one.ie-web`, `vitest`);
|
|
549
|
+
# DEPLOY_SPINE_STEPS are the pipeline's. Anything unmapped emits NOTHING rather
|
|
550
|
+
# than inventing a stage — an unknown stage is refused at the receiver anyway,
|
|
551
|
+
# and a gate the spine has no name for is better absent than mislabelled.
|
|
552
|
+
_spine_stage() {
|
|
553
|
+
case "$1" in
|
|
554
|
+
tsc-*) echo typecheck ;;
|
|
555
|
+
vitest) echo tests ;;
|
|
556
|
+
build) echo build ;;
|
|
557
|
+
*) echo "" ;;
|
|
558
|
+
esac
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
# The verdict CLOSES the run, and it is a field of its own rather than something
|
|
562
|
+
# inferred from the last stage to arrive. `--gates-only` is a complete,
|
|
563
|
+
# successful run that never reaches ship or health; closing on a stage would
|
|
564
|
+
# leave every one of those runs `open` forever in every run list.
|
|
565
|
+
_emit_verdict() {
|
|
566
|
+
local v="$1"
|
|
567
|
+
_emit --verdict "$v" \
|
|
568
|
+
--detail "shipped=${TARGETS[*]:-none}" \
|
|
569
|
+
--detail "wallSec=$(( SECONDS ))" \
|
|
570
|
+
--detail "mode=${MODE}" \
|
|
571
|
+
--detail "heavyParallel=$( (( ${HEAVY_PARALLEL:-0} )) && echo true || echo false )" \
|
|
572
|
+
--detail "memFreeGb=${HEAVY_FREE_GB:-0}"
|
|
573
|
+
# Give the backgrounded emits a moment to land. Bounded and ignored: the run
|
|
574
|
+
# is closing either way, and a deploy must not wait on its own telemetry.
|
|
575
|
+
wait -n 2>/dev/null || true
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_emit() { # ...deploy-emit.sh args
|
|
579
|
+
[[ -x "$ROOT/.claude/scripts/deploy-emit.sh" ]] || return 0
|
|
580
|
+
(( DRY )) && return 0
|
|
581
|
+
( bash "$ROOT/.claude/scripts/deploy-emit.sh" \
|
|
582
|
+
--run "$STAMP" --target "$EMIT_TARGET" --env "$EMIT_ENV" \
|
|
583
|
+
--door "deploy${MODE:+ $MODE}" \
|
|
584
|
+
--sha "$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo '')" \
|
|
585
|
+
"$@" >/dev/null 2>&1 || true ) &
|
|
586
|
+
return 0
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
GATE_NAMES=() GATE_PIDS=() GATE_LOGS=() GATE_RC=() GATE_T=() GATE_DUR=()
|
|
590
|
+
GATE_BOUND="${DEPLOY_GATE_TIMEOUT:-900}" # healthy vitest 176s, build 174s
|
|
591
|
+
# Typechecks were run bare on the claim that they are "cached ~1s each". They
|
|
592
|
+
# are NOT: no tsconfig in this repo sets `incremental`/`tsBuildInfoFile`, so
|
|
593
|
+
# every one is a full check. Measured 2026-08-23: a single one held 1.5GB for
|
|
594
|
+
# 12m, and five run in parallel — ~7.5GB of fan-out that the HEAVY_NEED_GB test
|
|
595
|
+
# below never covered, because it prices only vitest+build. On a cold worktree
|
|
596
|
+
# that is the whole melt (load 52, swap 20GB).
|
|
597
|
+
#
|
|
598
|
+
# So price them the same way: if the box cannot hold the fan-out, route them
|
|
599
|
+
# through the semaphore. deploy.sh holds no slot itself, so the nested calls
|
|
600
|
+
# really do queue (gate-run.sh is re-entrant — this would be cosmetic if an
|
|
601
|
+
# outer slot were held). A warm, healthy box still runs them bare and parallel.
|
|
602
|
+
TSC_EACH_GB="${DEPLOY_TSC_EACH_GB:-2}"
|
|
603
|
+
TSC_NEED_GB=$(( TSC_EACH_GB * ${#SERVICES_TYPECHECK[@]} ))
|
|
604
|
+
if (( HEAVY_FREE_GB >= TSC_NEED_GB )); then TSC_GOVERNED=0; else TSC_GOVERNED=1; fi
|
|
605
|
+
[[ "${DEPLOY_TSC_GOVERNED:-}" == 1 ]] && TSC_GOVERNED=1 # escape hatch
|
|
606
|
+
[[ "${DEPLOY_TSC_GOVERNED:-}" == 0 ]] && TSC_GOVERNED=0
|
|
607
|
+
gate_start() { # <name> <dir> <cmd...>
|
|
608
|
+
local name="$1" dir="$2"; shift 2
|
|
609
|
+
if (( DRY )); then say " [dry] (cd $dir && $*)"; return; fi
|
|
610
|
+
local out="$LOG_DIR/$name-$STAMP.log"
|
|
611
|
+
( cd "$ROOT/$dir" && unset CLOUDFLARE_API_TOKEN && "$@" ) >"$out" 2>&1 &
|
|
612
|
+
GATE_NAMES+=("$name"); GATE_PIDS+=("$!"); GATE_LOGS+=("$out"); GATE_RC+=(-1)
|
|
613
|
+
GATE_T+=("$SECONDS"); GATE_DUR+=(-1)
|
|
614
|
+
# The `start` frame is what makes a run render as IN PROGRESS rather than
|
|
615
|
+
# appearing whole at the end. Emitted per LAUNCH, so a serialised gate's
|
|
616
|
+
# start is the moment it really started, not the moment the batch did.
|
|
617
|
+
{ st="$(_spine_stage "$name")"; [[ -n "$st" ]] && _emit --stage "$st" --status start; } || true
|
|
618
|
+
say " → $name (pid $!)"
|
|
619
|
+
}
|
|
620
|
+
# Heavy gates run under gate-run.sh for its BOUND and its process-group REAP:
|
|
621
|
+
# macOS ships no timeout(1), and killing only the shell reparents the vitest
|
|
622
|
+
# forks to launchd. A hung gate must die on a clock, not outlive the deploy.
|
|
623
|
+
gate_start_governed() { # <name> <dir> <cmd...>
|
|
624
|
+
local name="$1" dir="$2"; shift 2
|
|
625
|
+
if (( DRY )); then say " [dry] (cd $dir && gate-run.sh deploy-$name -- $*)"; return; fi
|
|
626
|
+
local out="$LOG_DIR/$name-$STAMP.log"
|
|
627
|
+
( cd "$ROOT/$dir" && unset CLOUDFLARE_API_TOKEN \
|
|
628
|
+
&& GOVERN_GATE_TIMEOUT="$GATE_BOUND" \
|
|
629
|
+
exec "$ROOT/.claude/scripts/gate-run.sh" "deploy-$name" -- "$@" ) >"$out" 2>&1 &
|
|
630
|
+
GATE_NAMES+=("$name"); GATE_PIDS+=("$!"); GATE_LOGS+=("$out"); GATE_RC+=(-1)
|
|
631
|
+
GATE_T+=("$SECONDS"); GATE_DUR+=(-1)
|
|
632
|
+
# The `start` frame is what makes a run render as IN PROGRESS rather than
|
|
633
|
+
# appearing whole at the end. Emitted per LAUNCH, so a serialised gate's
|
|
634
|
+
# start is the moment it really started, not the moment the batch did.
|
|
635
|
+
{ st="$(_spine_stage "$name")"; [[ -n "$st" ]] && _emit --stage "$st" --status start; } || true
|
|
636
|
+
say " → $name (pid $!, bound ${GATE_BOUND}s)"
|
|
637
|
+
}
|
|
638
|
+
# gate_start_heavy — same, but when memory says the two heavy gates cannot
|
|
639
|
+
# overlap, block until the previous heavy gate has finished. Its exit status is
|
|
640
|
+
# recorded in GATE_RC so the collection loop below never waits on a reaped pid
|
|
641
|
+
# (a second `wait` on the same child returns 127 and would read as a failure).
|
|
642
|
+
HEAVY_IDX=()
|
|
643
|
+
gate_start_heavy() {
|
|
644
|
+
if (( ! DRY )) && (( ! HEAVY_PARALLEL )) && (( ${#HEAVY_IDX[@]} )); then
|
|
645
|
+
local prev="${HEAVY_IDX[${#HEAVY_IDX[@]}-1]}"
|
|
646
|
+
say " … waiting for ${GATE_NAMES[$prev]} before starting $1 (serialised)"
|
|
647
|
+
if wait "${GATE_PIDS[$prev]}"; then GATE_RC[$prev]=0; else GATE_RC[$prev]=1; fi
|
|
648
|
+
GATE_DUR[$prev]=$(( SECONDS - GATE_T[$prev] ))
|
|
649
|
+
fi
|
|
650
|
+
gate_start_governed "$@"
|
|
651
|
+
(( DRY )) || HEAVY_IDX+=( $(( ${#GATE_NAMES[@]} - 1 )) )
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
GATE_T0=$SECONDS
|
|
655
|
+
if ! (( SKIP_TYPECHECK )); then
|
|
656
|
+
if (( TSC_GOVERNED )); then
|
|
657
|
+
say " memory: ${HEAVY_FREE_GB}GB free < ${TSC_NEED_GB}GB needed — typechecks governed (queued)"
|
|
658
|
+
else
|
|
659
|
+
say " memory: ${HEAVY_FREE_GB}GB free >= ${TSC_NEED_GB}GB needed — typechecks bare (parallel)"
|
|
660
|
+
fi
|
|
661
|
+
# NOT routed through tsc-cached.sh, and that is a measurement, not an oversight.
|
|
662
|
+
# Measured 2026-09-01 on a warm box: one.ie/web 4s · api 0s · sync 0s ·
|
|
663
|
+
# channels 2s · pay/backend 2s -- 8s serial, ~4s of wall-clock in parallel, and
|
|
664
|
+
# they finish inside the vitest gate's shadow anyway, so caching them buys zero
|
|
665
|
+
# seconds off the deploy. It would also COST something: tsc-cached takes the
|
|
666
|
+
# per-folder `tsc-<folder>` gate_lock that do-reconcile.sh takes, so a /do cycle
|
|
667
|
+
# typechecking one.ie/web would make this gate queue up to TSC_CACHE_WAIT (900s)
|
|
668
|
+
# and then exit 1 -- a red ship gate caused by lock contention rather than by
|
|
669
|
+
# code. Four seconds is not worth that trade.
|
|
670
|
+
# CONTENT TYPES FIRST, and this line is worth its four seconds.
|
|
671
|
+
#
|
|
672
|
+
# `astro check` and `astro build` sync implicitly; raw `tsc --noEmit` does NOT,
|
|
673
|
+
# and `DataEntryMap` is GENERATED into the gitignored one.ie/web/.astro/. So a
|
|
674
|
+
# freshly-materialised tree carries whatever that directory happened to hold —
|
|
675
|
+
# for .release/, types from before the newest collection existed; for a new
|
|
676
|
+
# worktree, no file at all. Either way tsc rejects the collection key and the
|
|
677
|
+
# gate goes red on code that is fine.
|
|
678
|
+
#
|
|
679
|
+
# Measured three times on 2026-09-06/07, each costing a full gate: twice on
|
|
680
|
+
# .release/ during a promote, once on a fresh worktree. The error names
|
|
681
|
+
# TypeScript and the cause is Astro, which is why it kept being re-diagnosed:
|
|
682
|
+
# src/lib/docs/sections.ts(23,40): error TS2344:
|
|
683
|
+
# Type '"docs"' does not satisfy the constraint 'keyof DataEntryMap'.
|
|
684
|
+
#
|
|
685
|
+
# It must run BEFORE the fan-out rather than inside the one.ie/web gate: the
|
|
686
|
+
# five typechecks start concurrently, and a sync racing them writes the types
|
|
687
|
+
# out from under a tsc that is already reading them.
|
|
688
|
+
if (( want_astro )) && [[ -d one.ie/web ]]; then
|
|
689
|
+
( cd one.ie/web && NODE_OPTIONS=--max-old-space-size=4096 \
|
|
690
|
+
./node_modules/.bin/astro sync >/dev/null 2>&1 ) \
|
|
691
|
+
&& ok "content types synced" \
|
|
692
|
+
|| warn "astro sync failed — a new content collection will read as a tsc error"
|
|
693
|
+
fi
|
|
694
|
+
for svc in "${SERVICES_TYPECHECK[@]}"; do
|
|
695
|
+
if (( TSC_GOVERNED )); then
|
|
696
|
+
gate_start_governed "tsc-$(echo "$svc" | tr / -)" "$svc" bunx tsc --noEmit
|
|
697
|
+
else
|
|
698
|
+
gate_start "tsc-$(echo "$svc" | tr / -)" "$svc" bunx tsc --noEmit
|
|
699
|
+
fi
|
|
700
|
+
done
|
|
701
|
+
fi
|
|
702
|
+
# the suite lives in one.ie/web and gates that worker
|
|
703
|
+
# The vitest gate hangs when stdout is not a TTY (fork spawned, never given
|
|
704
|
+
# work — see deploy.md). `script -qeF` allocates a pty and flushes so the
|
|
705
|
+
# runner matches a green standalone terminal; `--reporter=dot` is the other
|
|
706
|
+
# half of that unstick (default reporter still hung under script).
|
|
707
|
+
# test-cached.sh keys a PASS on the exact inputs -- args + selected test files +
|
|
708
|
+
# `git diff HEAD` by CONTENT + untracked contents + HEAD. deploy refuses a dirty
|
|
709
|
+
# tree, so on the normal path the key collapses to (args, HEAD, test-file set):
|
|
710
|
+
# a full suite already proven green at this exact commit is not run twice. Only
|
|
711
|
+
# passes are memoised, so a red suite re-runs every time until it is green.
|
|
712
|
+
#
|
|
713
|
+
# TEST_CACHE_PTY=1 is not optional here: this gate's stdout is a log file, and the
|
|
714
|
+
# runner hangs on a non-TTY stdout (a fork is spawned and never given work). The
|
|
715
|
+
# wrapper is the same `script -qeF /dev/null` that fixed it, moved inside the memo
|
|
716
|
+
# so the cached and uncached paths run byte-identical commands.
|
|
717
|
+
# The vitest ARGS used to live here, spelled out. They now live in test-full.sh,
|
|
718
|
+
# which /close reaches too -- test-cached keys on argv, so two spellings of the
|
|
719
|
+
# same suite could never share a stamp and a warm tree paid it twice.
|
|
720
|
+
(( SKIP_TESTS || ! want_astro )) || gate_start_heavy vitest one.ie/web \
|
|
721
|
+
env TYPEDB_LANE_NONBLOCKING=1 TEST_CACHE_PTY=1 bash "$ROOT/.claude/scripts/test-full.sh"
|
|
722
|
+
if (( want_astro )); then
|
|
723
|
+
if (( SKIP_BUILD )); then
|
|
724
|
+
[[ -d one.ie/web/dist/server ]] || die "--skip-build but one.ie/web/dist/server/ does not exist"
|
|
725
|
+
else
|
|
726
|
+
gate_start_heavy build one.ie/web bash "$ROOT/.claude/scripts/astro-build-cached.sh"
|
|
727
|
+
fi
|
|
728
|
+
fi
|
|
729
|
+
|
|
730
|
+
TYPECHECK_REPORT="skipped" TESTS_REPORT="skipped" BUILD_REPORT="skipped"
|
|
731
|
+
tc_pass=0 tc_total=0 gate_fail=()
|
|
732
|
+
for i in $(seq 0 $(( ${#GATE_NAMES[@]} - 1 )) ); do
|
|
733
|
+
(( ${#GATE_NAMES[@]} )) || break
|
|
734
|
+
name="${GATE_NAMES[$i]}" glog="${GATE_LOGS[$i]}"
|
|
735
|
+
if (( ${GATE_RC[$i]} >= 0 )); then rc="${GATE_RC[$i]}" # already reaped when serialised
|
|
736
|
+
elif wait "${GATE_PIDS[$i]}"; then rc=0; else rc=1; fi
|
|
737
|
+
GATE_RC[$i]=$rc # record it: the deploy receipt below reads results, not just timings
|
|
738
|
+
(( ${GATE_DUR[$i]} < 0 )) && GATE_DUR[$i]=$(( SECONDS - GATE_T[$i] ))
|
|
739
|
+
# …and onto the stream. ONE hook for typecheck/tests/build — the three gates
|
|
740
|
+
# that are ~95% of the wall clock and carry the metrics deploy-runs.json has
|
|
741
|
+
# no shape for. Deferred to after the `case` below for `tests`, which is the
|
|
742
|
+
# only gate whose reused/waived facts are not known yet at this line.
|
|
743
|
+
_emit_stage="$(_spine_stage "$name")"
|
|
744
|
+
_emit_dur=$(( GATE_DUR[i] < 0 ? 0 : GATE_DUR[i] ))
|
|
745
|
+
cat "$glog" >>"$LOG"
|
|
746
|
+
case "$name" in
|
|
747
|
+
tsc-*)
|
|
748
|
+
tc_total=$((tc_total+1))
|
|
749
|
+
if (( rc )); then bad "${name#tsc-} typecheck FAILED"; gate_fail+=("$name")
|
|
750
|
+
else tc_pass=$((tc_pass+1)); fi ;;
|
|
751
|
+
vitest)
|
|
752
|
+
# The suite now runs as TWO lanes, so the log carries TWO "Tests N passed"
|
|
753
|
+
# lines and `tail -1` reported only the last one — the 19-file typedb lane.
|
|
754
|
+
# A deploy that ran 10940 tests said "143 passed", which understates the
|
|
755
|
+
# gate by two orders of magnitude and reads like a partial run. Sum them.
|
|
756
|
+
TESTS_REPORT="$(awk '
|
|
757
|
+
/Tests +[0-9]+ (passed|failed)/ {
|
|
758
|
+
for (i = 1; i <= NF; i++) {
|
|
759
|
+
if ($i == "passed") p += $(i-1) + 0
|
|
760
|
+
if ($i == "failed") f += $(i-1) + 0
|
|
761
|
+
}
|
|
762
|
+
n++
|
|
763
|
+
}
|
|
764
|
+
# vitest reports UNHANDLED errors on their own line — "Errors 45 errors" —
|
|
765
|
+
# and exits non-zero on them with ZERO failed tests. A verdict that reads
|
|
766
|
+
# only the Tests line printed "11327 passed" for a red run and the flake
|
|
767
|
+
# checker called it GREEN (measured 2026-09-05). Read the Errors line too.
|
|
768
|
+
/Errors +[0-9]+ errors?/ { for (i = 1; i <= NF; i++) if ($i ~ /^errors?$/) e += $(i-1) + 0 }
|
|
769
|
+
END {
|
|
770
|
+
if (n == 0) exit
|
|
771
|
+
if (n > 1) printf "Tests %d passed", p; else printf "Tests %d passed", p
|
|
772
|
+
if (f > 0) printf " | %d failed", f
|
|
773
|
+
if (e > 0) printf " | %d unhandled errors", e
|
|
774
|
+
if (n > 1) printf " (%d lanes)", n
|
|
775
|
+
printf "\n"
|
|
776
|
+
}' "$glog")"
|
|
777
|
+
if (( rc )); then
|
|
778
|
+
bad "${TESTS_REPORT:-vitest failed}"
|
|
779
|
+
say " Failing files (check the Known-Flaky allowlist in .claude/commands/deploy.md):"
|
|
780
|
+
grep -E '^ *(FAIL|❯ .*\.test\.ts.*(failed|×))' "$glog" | sort -u | sed 's/^/ /' | tee -a "$LOG"
|
|
781
|
+
# Is the whole red run just the shared substrate being down? The checker
|
|
782
|
+
# answers by SIGNATURE, and blocks on the first failure it cannot account
|
|
783
|
+
# for — so a real break standing beside a 503 still stops the deploy.
|
|
784
|
+
_flake_out=""
|
|
785
|
+
if (( TYPEDB_FLAKE_WAIVER )); then
|
|
786
|
+
_flake_out="$(bash "$ROOT/.claude/scripts/typedb-flake-check.sh" "$glog" 2>&1)"; _flake_rc=$?
|
|
787
|
+
else
|
|
788
|
+
_flake_out="typedb-flake: waiver disabled (--no-typedb-flake-waiver)"; _flake_rc=1
|
|
789
|
+
fi
|
|
790
|
+
printf '%s\n' "$_flake_out" | sed 's/^/ /' | tee -a "$LOG"
|
|
791
|
+
if (( _flake_rc == 0 )); then
|
|
792
|
+
TESTS_WAIVED=1
|
|
793
|
+
TESTS_REPORT="${TESTS_REPORT:-vitest failed} — WAIVED as TypeDB outage (suite NOT green)"
|
|
794
|
+
ok "test failures waived: shared TypeDB Cloud unavailable"
|
|
795
|
+
else
|
|
796
|
+
gate_fail+=(vitest)
|
|
797
|
+
fi
|
|
798
|
+
elif grep -q 'cache HIT' "$glog"; then
|
|
799
|
+
# Say WHICH it was. A reused pass and a fresh run are both green, but
|
|
800
|
+
# reporting a memo hit as "all pass" is the same class of dishonesty as
|
|
801
|
+
# reporting a fast lane as a full one -- the operator cannot tell whether
|
|
802
|
+
# 10,780 tests just executed or whether an earlier identical tree's
|
|
803
|
+
# result was replayed. Both are sound; only one of them took 20 minutes.
|
|
804
|
+
TESTS_REPORT="full suite REUSED — identical inputs already passed ($(grep -o 'already PASSED.*' "$glog" | tail -1))"
|
|
805
|
+
ok "$TESTS_REPORT"
|
|
806
|
+
else
|
|
807
|
+
TESTS_REPORT="${TESTS_REPORT:-all pass}"; ok "$TESTS_REPORT"
|
|
808
|
+
fi ;;
|
|
809
|
+
build)
|
|
810
|
+
if (( rc )); then bad "astro build FAILED"; gate_fail+=(build)
|
|
811
|
+
else BUILD_REPORT="ok"; ok "astro build clean"; fi ;;
|
|
812
|
+
esac
|
|
813
|
+
# AFTER the case, deliberately: `tests` only learns whether it was a memo hit
|
|
814
|
+
# or a waived failure inside it, and those two booleans are the whole reason
|
|
815
|
+
# this stream exists. Emitting before the case would carry the timing and lose
|
|
816
|
+
# the meaning.
|
|
817
|
+
#
|
|
818
|
+
# `waived` is the one that matters most. On 2026-09-06 a flake waiver let this
|
|
819
|
+
# gate exit 0 on a genuinely red suite, and deploy-runs.json recorded `green`
|
|
820
|
+
# with nothing to distinguish it. A waived pass is now a fact on the run.
|
|
821
|
+
if [[ -n "$_emit_stage" ]]; then
|
|
822
|
+
_emit_args=(--stage "$_emit_stage" --wall-ms "$(( _emit_dur * 1000 ))")
|
|
823
|
+
if (( rc )) && ! { [[ "$_emit_stage" == tests ]] && (( TESTS_WAIVED )); }; then
|
|
824
|
+
_emit_args+=(--status fail --reason "${name}")
|
|
825
|
+
else
|
|
826
|
+
_emit_args+=(--status ok)
|
|
827
|
+
fi
|
|
828
|
+
case "$_emit_stage" in
|
|
829
|
+
tests)
|
|
830
|
+
_emit_args+=(--detail "reused=$( [[ "$TESTS_REPORT" == *REUSED* ]] && echo true || echo false )")
|
|
831
|
+
_emit_args+=(--detail "waived=$( (( TESTS_WAIVED )) && echo true || echo false )") ;;
|
|
832
|
+
typecheck)
|
|
833
|
+
# Which of the five, not just how many — a reader chasing a red gate
|
|
834
|
+
# should not have to open the log to learn the service.
|
|
835
|
+
_emit_args+=(--detail "service=${name#tsc-}") ;;
|
|
836
|
+
esac
|
|
837
|
+
_emit "${_emit_args[@]}"
|
|
838
|
+
fi
|
|
839
|
+
done
|
|
840
|
+
(( tc_total )) && {
|
|
841
|
+
TYPECHECK_REPORT="$tc_pass/$tc_total services clean"
|
|
842
|
+
(( tc_pass == tc_total )) && ok "$TYPECHECK_REPORT"
|
|
843
|
+
}
|
|
844
|
+
if (( ${#GATE_NAMES[@]} )); then
|
|
845
|
+
GATE_WALL="$((SECONDS - GATE_T0))s"
|
|
846
|
+
# Per-gate, not just the aggregate. The gates overlap, so the total is max()
|
|
847
|
+
# over the parallel set and sum() over the serialised heavies -- without the
|
|
848
|
+
# breakdown there is no way to tell which one is actually the critical path,
|
|
849
|
+
# and "make the deploy faster" turns into caching whatever finishes inside
|
|
850
|
+
# vitest's shadow for zero seconds.
|
|
851
|
+
for i in $(seq 0 $(( ${#GATE_NAMES[@]} - 1 )) ); do
|
|
852
|
+
say " ${GATE_NAMES[$i]}: ${GATE_DUR[$i]}s"
|
|
853
|
+
done
|
|
854
|
+
say " gates wall-clock: $GATE_WALL"
|
|
855
|
+
# Repeat the branch HERE, next to the durations it explains. Without it a
|
|
856
|
+
# reader comparing two runs cannot tell a slow gate from a serialised one.
|
|
857
|
+
if (( HEAVY_PARALLEL )); then
|
|
858
|
+
say " heavy gates: OVERLAPPED (${HEAVY_FREE_GB}GB free >= ${HEAVY_NEED_GB}GB)"
|
|
859
|
+
else
|
|
860
|
+
say " heavy gates: SERIALISED (${HEAVY_FREE_GB}GB free < ${HEAVY_NEED_GB}GB) — the 6.5x branch"
|
|
861
|
+
fi
|
|
862
|
+
[[ "$BUILD_REPORT" == ok ]] && BUILD_REPORT="$GATE_WALL (overlapped)"
|
|
863
|
+
fi
|
|
864
|
+
(( ${#gate_fail[@]} )) && die "gates red: ${gate_fail[*]} — nothing deployed"
|
|
865
|
+
|
|
866
|
+
# ── deferred-pin debt ───────────────────────────────────────────────────────
|
|
867
|
+
# verify-fast.sh may DEFER its pinned integration suites mid-plan and record the
|
|
868
|
+
# debt in a machine-keyed ledger. That debt is meant to come due at the close
|
|
869
|
+
# gate. deploy is the LAST place it can come due, and until now it could not:
|
|
870
|
+
# deploy sets FULL_VERIFY=1 but invokes vitest directly, so it never reached the
|
|
871
|
+
# branch in verify-fast.sh that settles the ledger. A production deploy could
|
|
872
|
+
# therefore ship over pins that no run had ever executed.
|
|
873
|
+
#
|
|
874
|
+
# Two rules, both one-directional:
|
|
875
|
+
# · a GREEN full suite PAYS the debt. The suite is a superset of every pin, so
|
|
876
|
+
# reaching here green is the payment -- same reasoning verify-fast uses.
|
|
877
|
+
# · anything else REFUSES. A skipped suite does not pay a debt; neither does a
|
|
878
|
+
# red one (we never get here) nor a `--gates-only` run that skipped tests.
|
|
879
|
+
# An unpaid debt with no run to settle it is an unrun gate, and an unrun gate
|
|
880
|
+
# is not a passing gate.
|
|
881
|
+
DEBT_FILE="${VERIFY_FAST_DEBT_FILE:-${TMPDIR:-/tmp}/one-verify-fast-debt}"
|
|
882
|
+
if [[ -s "$DEBT_FILE" ]]; then
|
|
883
|
+
step "Deferred-pin debt"
|
|
884
|
+
say " ledger: $DEBT_FILE"
|
|
885
|
+
sed 's/^/ /' "$DEBT_FILE" | tee -a "$LOG"
|
|
886
|
+
if (( SKIP_TESTS )) || [[ "$TESTS_REPORT" == "skipped" ]]; then
|
|
887
|
+
die "deferred pins are outstanding and this run did not execute the suite — re-run without --skip-tests"
|
|
888
|
+
fi
|
|
889
|
+
# A waived run is not a green run. The debt exists precisely because the pinned
|
|
890
|
+
# gates were deferred, and a waiver says only that the failures we DID see were
|
|
891
|
+
# substrate — it says nothing about a pin that never got to report.
|
|
892
|
+
if (( TESTS_WAIVED )); then
|
|
893
|
+
die "deferred pins are outstanding and the suite was WAIVED, not green — re-run when TypeDB answers"
|
|
894
|
+
fi
|
|
895
|
+
rm -f "$DEBT_FILE"
|
|
896
|
+
ok "debt settled by the green full suite"
|
|
897
|
+
fi
|
|
898
|
+
if (( GATES_ONLY )); then
|
|
899
|
+
say ""; ok "gates green in ${GATE_WALL:-0s} — nothing shipped (--gates-only)"
|
|
900
|
+
say " Typecheck: $TYPECHECK_REPORT"; say " Tests: $TESTS_REPORT"; say " Log: $LOG"
|
|
901
|
+
# CLOSE THE RUN. This exit is the one --gates-only always takes, and it is the
|
|
902
|
+
# path this feature is exercised by most (every release.sh promote mints its
|
|
903
|
+
# receipt through it). Without this the run would carry its gate frames and
|
|
904
|
+
# sit `open` forever — the precise failure the verdict field was added to
|
|
905
|
+
# prevent, and it is only not a bug because of this line.
|
|
906
|
+
_emit_verdict green
|
|
907
|
+
exit 0
|
|
908
|
+
fi
|
|
909
|
+
|
|
910
|
+
# ── Step 4 — credentials (the real work happens in Step 0.4) ────────────────
|
|
911
|
+
step "Step 4 — credentials"
|
|
912
|
+
ok "auth verified ($CF_AUTH) · API_TOKEN unset"
|
|
913
|
+
say " source: $CF_AUTH_SOURCE"
|
|
914
|
+
_cf_agree || die "services disagree about which Cloudflare account they are deploying to"
|
|
915
|
+
|
|
916
|
+
# ── Step 5 — smoke ──────────────────────────────────────────────────────────
|
|
917
|
+
step "Step 5 — smoke"
|
|
918
|
+
for cfg in api/wrangler.toml sync/wrangler.toml channels/wrangler.toml \
|
|
919
|
+
one.ie/web/wrangler.toml pay/backend/wrangler.toml; do
|
|
920
|
+
[[ -f "$cfg" ]] || die "missing wrangler config: $cfg"
|
|
921
|
+
done
|
|
922
|
+
ok "5/5 wrangler configs present"
|
|
923
|
+
if (( want_astro )); then
|
|
924
|
+
[[ -d one.ie/web/dist/server ]] || die "one.ie/web/dist/server/ missing after build"
|
|
925
|
+
GZIP_HINT="$(grep -Eo 'gzip: *[0-9.]+ *[KM]iB' "$LOG" | tail -1)"
|
|
926
|
+
say " dist/server: $(du -sh one.ie/web/dist/server | cut -f1)"
|
|
927
|
+
fi
|
|
928
|
+
# --env is the decoy trap; assert no config reintroduced an env-scoped target
|
|
929
|
+
if grep -q '^\[env\.production' one.ie/web/wrangler.toml 2>/dev/null; then
|
|
930
|
+
warn "one.ie/web/wrangler.toml grew an [env.production] block — the deploy-target trap is reachable again"
|
|
931
|
+
fi
|
|
932
|
+
|
|
933
|
+
# ── Step 6 — approval ───────────────────────────────────────────────────────
|
|
934
|
+
step "Step 6 — approval"
|
|
935
|
+
TARGETS=()
|
|
936
|
+
(( want_gateway )) && TARGETS+=("api → one-gateway")
|
|
937
|
+
(( want_sync )) && TARGETS+=("sync → one-sync (cron-only)")
|
|
938
|
+
(( want_agents )) && TARGETS+=("channels → channels.one.ie")
|
|
939
|
+
(( want_pay )) && TARGETS+=("pay/backend → one-core-worker")
|
|
940
|
+
(( want_astro )) && TARGETS+=("one.ie/web → one-prod (one.ie)")
|
|
941
|
+
for t in "${TARGETS[@]}"; do say " $t"; done
|
|
942
|
+
if (( ASSUME_YES || DRY )); then
|
|
943
|
+
ok "auto-approved"
|
|
944
|
+
elif [[ "$BRANCH" != "main" ]]; then
|
|
945
|
+
ok "auto-approved (branch $BRANCH ≠ main)"
|
|
946
|
+
else
|
|
947
|
+
read -r -p " deploy to production? [yes/N] " reply
|
|
948
|
+
[[ "$reply" == "yes" ]] || die "aborted at approval"
|
|
949
|
+
fi
|
|
950
|
+
|
|
951
|
+
# ── Step 6.5 — D1 migrations ────────────────────────────────────────────────
|
|
952
|
+
MIG_REPORT="skipped"
|
|
953
|
+
if (( want_astro )) && ! (( SKIP_MIGRATIONS )); then
|
|
954
|
+
step "Step 6.5 — D1 migrations (DB, remote)"
|
|
955
|
+
if run "d1 migrations apply DB --remote" one.ie/web bunx wrangler d1 migrations apply DB --remote; then
|
|
956
|
+
if (( DRY )); then
|
|
957
|
+
MIG_REPORT="dry-run"
|
|
958
|
+
elif tail -40 "$LOG" | grep -q "No migrations to apply"; then
|
|
959
|
+
MIG_REPORT="none to apply"
|
|
960
|
+
else
|
|
961
|
+
MIG_REPORT="$(grep -Eo '[0-9]{4}_[a-z0-9_]+\.sql' "$LOG" | tail -5 | tr '\n' ' ')"
|
|
962
|
+
MIG_REPORT="applied: ${MIG_REPORT:-see log}"
|
|
963
|
+
fi
|
|
964
|
+
ok "$MIG_REPORT"
|
|
965
|
+
else
|
|
966
|
+
die "D1 migrations failed — never ship worker code ahead of its schema"
|
|
967
|
+
fi
|
|
968
|
+
fi
|
|
969
|
+
|
|
970
|
+
# ── Step 6.6 — D1 migrations (channels) ─────────────────────────────────────
|
|
971
|
+
# A SECOND database. The step above runs in one.ie/web, where `DB` is
|
|
972
|
+
# `one-owners`; channels binds `DB` to `claw` and keeps its own
|
|
973
|
+
# channels/migrations/. Nothing applied that directory: the step above never
|
|
974
|
+
# enters it, and channels' own `bun run migrate` was pinned to 0001_init.sql.
|
|
975
|
+
# So 0010_action_bindings.sql shipped with the C6 worker code and its table was
|
|
976
|
+
# never created — and because bindAction's INSERT was fire-and-forget, every
|
|
977
|
+
# button binding failed silently, leaving taps that resolve to nothing. The
|
|
978
|
+
# promise's accept (`ls channels/migrations/*action*.sql`) proved the FILE
|
|
979
|
+
# existed, never the table. Same rule as above: schema before code.
|
|
980
|
+
MIG_CHAN_REPORT="skipped"
|
|
981
|
+
if (( want_agents )) && ! (( SKIP_MIGRATIONS )); then
|
|
982
|
+
step "Step 6.6 — D1 migrations (channels DB, remote)"
|
|
983
|
+
if run "d1 migrations apply DB --remote (channels)" channels bunx wrangler d1 migrations apply DB --remote; then
|
|
984
|
+
if (( DRY )); then
|
|
985
|
+
MIG_CHAN_REPORT="dry-run"
|
|
986
|
+
elif tail -40 "$LOG" | grep -q "No migrations to apply"; then
|
|
987
|
+
MIG_CHAN_REPORT="none to apply"
|
|
988
|
+
else
|
|
989
|
+
MIG_CHAN_REPORT="$(grep -Eo '[0-9]{4}_[a-z0-9_]+\.sql' "$LOG" | tail -5 | tr '\n' ' ')"
|
|
990
|
+
MIG_CHAN_REPORT="applied: ${MIG_CHAN_REPORT:-see log}"
|
|
991
|
+
fi
|
|
992
|
+
ok "$MIG_CHAN_REPORT"
|
|
993
|
+
else
|
|
994
|
+
die "channels D1 migrations failed — never ship worker code ahead of its schema"
|
|
995
|
+
fi
|
|
996
|
+
fi
|
|
997
|
+
|
|
998
|
+
# ── Step 7 — deploy ─────────────────────────────────────────────────────────
|
|
999
|
+
step "Step 7 — deploy"
|
|
1000
|
+
# bash 3.2 on macOS has no associative arrays — two parallel indexed ones
|
|
1001
|
+
BG_NAMES=() BG_PIDS=()
|
|
1002
|
+
hfail=()
|
|
1003
|
+
deploy_bg() { # <name> <dir> <cmd...>
|
|
1004
|
+
local name="$1" dir="$2"; shift 2
|
|
1005
|
+
if (( DRY )); then say " [dry] (cd $dir && ${*})"; return; fi
|
|
1006
|
+
local out="$LOG_DIR/$name-$STAMP.log"
|
|
1007
|
+
( cd "$ROOT/$dir" && unset CLOUDFLARE_API_TOKEN && "$@" ) >"$out" 2>&1 &
|
|
1008
|
+
BG_NAMES+=("$name"); BG_PIDS+=("$!")
|
|
1009
|
+
say " → $name (pid $!, log $out)"
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
# All five upload independently — no shared state, no ordering constraint
|
|
1013
|
+
# (verified 2026-07-08 for the three workers; astro's only dependency is its own
|
|
1014
|
+
# build, already done). Astro is slowest (~30s), so it starts first.
|
|
1015
|
+
# NEVER --env production on any of them — it ships to the one-prod-production decoy.
|
|
1016
|
+
(( want_astro )) && deploy_bg astro one.ie/web bunx wrangler deploy
|
|
1017
|
+
(( want_gateway )) && deploy_bg gateway api bunx wrangler deploy
|
|
1018
|
+
(( want_sync )) && deploy_bg sync sync bunx wrangler deploy
|
|
1019
|
+
(( want_agents )) && deploy_bg channels channels bunx wrangler deploy
|
|
1020
|
+
(( want_pay )) && deploy_bg pay pay/backend bun run deploy
|
|
1021
|
+
|
|
1022
|
+
DEPLOY_FAIL=()
|
|
1023
|
+
for i in $(seq 0 $(( ${#BG_NAMES[@]} - 1 )) ); do
|
|
1024
|
+
(( ${#BG_NAMES[@]} )) || break
|
|
1025
|
+
name="${BG_NAMES[$i]}"
|
|
1026
|
+
if wait "${BG_PIDS[$i]}"; then
|
|
1027
|
+
ok "$name deployed"
|
|
1028
|
+
cat "$LOG_DIR/$name-$STAMP.log" >>"$LOG"
|
|
1029
|
+
else
|
|
1030
|
+
bad "$name FAILED"
|
|
1031
|
+
tail -30 "$LOG_DIR/$name-$STAMP.log" | sed 's/^/ /' | tee -a "$LOG"
|
|
1032
|
+
DEPLOY_FAIL+=("$name")
|
|
1033
|
+
fi
|
|
1034
|
+
done
|
|
1035
|
+
(( ${#DEPLOY_FAIL[@]} )) && die "parallel deploy failed: ${DEPLOY_FAIL[*]}"
|
|
1036
|
+
if ! (( DRY )); then
|
|
1037
|
+
for i in $(seq 0 $(( ${#BG_NAMES[@]} - 1 )) ); do
|
|
1038
|
+
(( ${#BG_NAMES[@]} )) || break
|
|
1039
|
+
git rev-parse HEAD > "$(mark_file "${BG_NAMES[$i]}")"
|
|
1040
|
+
done
|
|
1041
|
+
fi
|
|
1042
|
+
(( want_astro && ! DRY )) && GZIP_HINT="$(grep -Eo 'gzip: *[0-9.]+ *[KM]iB' "$LOG_DIR/astro-$STAMP.log" | tail -1)"
|
|
1043
|
+
|
|
1044
|
+
# ── Step 8 — health ─────────────────────────────────────────────────────────
|
|
1045
|
+
HEALTH_REPORT="skipped"
|
|
1046
|
+
if (( SKIP_HEALTH || DRY )); then
|
|
1047
|
+
step "Step 8 — health (skipped)"
|
|
1048
|
+
else
|
|
1049
|
+
step "Step 8 — health (custom domains only)"
|
|
1050
|
+
probe() { # <label> <url> <assert-substring|-> ; 3 tries, backoff
|
|
1051
|
+
local label="$1" url="$2" want="$3" i body code
|
|
1052
|
+
for i in 1 2 3; do
|
|
1053
|
+
body="$(curl -sL --max-time 20 -w $'\n%{http_code}' "${url}?_t=$(date +%s)" 2>/dev/null)"
|
|
1054
|
+
code="${body##*$'\n'}"; body="${body%$'\n'*}"
|
|
1055
|
+
if [[ "$code" == "200" ]] && { [[ "$want" == "-" ]] || [[ "$body" == *"$want"* ]]; }; then
|
|
1056
|
+
ok "$label 200"; return 0
|
|
1057
|
+
fi
|
|
1058
|
+
sleep $((i * 3))
|
|
1059
|
+
done
|
|
1060
|
+
bad "$label unhealthy (last code=${code:-none})"
|
|
1061
|
+
return 1
|
|
1062
|
+
}
|
|
1063
|
+
# probe all four at once — they are independent hosts; serial cost was the
|
|
1064
|
+
# sum of four round trips plus, on a bad deploy, three backoff sleeps each
|
|
1065
|
+
hp=0 ht=0 PNAMES=() PPIDS=()
|
|
1066
|
+
probe_bg() { probe "$2" "$3" "$4" >"$LOG_DIR/health-$1-$STAMP.log" 2>&1 & PNAMES+=("$1"); PPIDS+=("$!"); ht=$((ht+1)); }
|
|
1067
|
+
(( want_gateway )) && probe_bg gateway "api.one.ie/health" https://api.one.ie/health -
|
|
1068
|
+
(( want_astro )) && probe_bg astro "one.ie/api/health" https://one.ie/api/health '"status":"ok"'
|
|
1069
|
+
(( want_agents )) && probe_bg channels "channels.one.ie" https://channels.one.ie/health -
|
|
1070
|
+
(( want_pay )) && probe_bg pay "pay.one.ie/status" https://pay.one.ie/status '"ok"'
|
|
1071
|
+
for i in $(seq 0 $(( ${#PNAMES[@]} - 1 )) ); do
|
|
1072
|
+
(( ${#PNAMES[@]} )) || break
|
|
1073
|
+
if wait "${PPIDS[$i]}"; then hp=$((hp+1)); else hfail+=("${PNAMES[$i]}"); fi
|
|
1074
|
+
cat "$LOG_DIR/health-${PNAMES[$i]}-$STAMP.log" | tee -a "$LOG"
|
|
1075
|
+
done
|
|
1076
|
+
(( want_sync )) && ok "sync — cron-only, deploy success IS the health signal"
|
|
1077
|
+
HEALTH_REPORT="$hp/$ht HTTP 200"
|
|
1078
|
+
(( want_sync )) && HEALTH_REPORT="$HEALTH_REPORT + sync deploy-confirmed"
|
|
1079
|
+
|
|
1080
|
+
# ── deploy receipt ────────────────────────────────────────────────────────
|
|
1081
|
+
# The last deploy's gate timings and per-worker outcome, read by the operator
|
|
1082
|
+
# deploy dashboard at /u/one/deploy. Written HERE because health is the last
|
|
1083
|
+
# fact it needs. Three honesty rules the data itself forces:
|
|
1084
|
+
#
|
|
1085
|
+
# * gates in this file are only ever GREEN — `die "gates red"` above exits
|
|
1086
|
+
# long before Step 8, so a red gate never reaches a receipt. The page says
|
|
1087
|
+
# that out loud rather than letting "all pass" imply a full audit.
|
|
1088
|
+
# * `healthy` is null for sync. It is never probed (cron-only — the line two
|
|
1089
|
+
# above), and a worker nobody asked must not render healthy OR unhealthy.
|
|
1090
|
+
# * `deployed` comes from BG_NAMES — what actually uploaded — never from
|
|
1091
|
+
# TARGETS, which is only the intent.
|
|
1092
|
+
#
|
|
1093
|
+
# Wrapped in `{ … } || true` so a failed write can never fail a deploy that
|
|
1094
|
+
# has already shipped.
|
|
1095
|
+
_receipt="$ROOT/one.ie/web/src/lib/generated/deploy-receipt.json"
|
|
1096
|
+
{
|
|
1097
|
+
mkdir -p "$(dirname "$_receipt")"
|
|
1098
|
+
{
|
|
1099
|
+
printf '{\n "stamp": "%s",\n "headSha": "%s",\n "mode": "%s",\n' \
|
|
1100
|
+
"$STAMP" "${HEAD_SHA:-unknown}" "$MODE"
|
|
1101
|
+
printf ' "gates": ['
|
|
1102
|
+
_sep=""
|
|
1103
|
+
for i in $(seq 0 $(( ${#GATE_NAMES[@]} - 1 )) ); do
|
|
1104
|
+
(( ${#GATE_NAMES[@]} )) || break
|
|
1105
|
+
if (( ${GATE_RC[$i]} < 0 )); then _res=unrun
|
|
1106
|
+
elif (( ${GATE_RC[$i]} > 0 )); then _res=fail
|
|
1107
|
+
else _res=pass; fi
|
|
1108
|
+
printf '%s\n {"name": "%s", "seconds": %s, "result": "%s"}' \
|
|
1109
|
+
"$_sep" "${GATE_NAMES[$i]}" "${GATE_DUR[$i]}" "$_res"
|
|
1110
|
+
_sep=","
|
|
1111
|
+
done
|
|
1112
|
+
(( ${#GATE_NAMES[@]} )) && printf '\n '
|
|
1113
|
+
printf '],\n "workers": ['
|
|
1114
|
+
_sep=""
|
|
1115
|
+
for i in $(seq 0 $(( ${#BG_NAMES[@]} - 1 )) ); do
|
|
1116
|
+
(( ${#BG_NAMES[@]} )) || break
|
|
1117
|
+
_n="${BG_NAMES[$i]}"
|
|
1118
|
+
if [[ "$_n" == sync ]]; then _h=null # never probed
|
|
1119
|
+
# `${hfail[*]-}` not `${hfail[*]}`: under `set -u` bash 3.2 treats an EMPTY
|
|
1120
|
+
# array as unbound, and hfail is empty exactly when every probe passed —
|
|
1121
|
+
# so a perfect deploy died here (line 933, 2026-09-05), truncated its own
|
|
1122
|
+
# receipt mid-JSON and reported exit 1. Same trap test-lanes.sh already paid.
|
|
1123
|
+
elif [[ " ${hfail[*]-} " == *" $_n "* ]]; then _h=false
|
|
1124
|
+
else _h=true; fi
|
|
1125
|
+
printf '%s\n {"name": "%s", "deployed": true, "healthy": %s}' "$_sep" "$_n" "$_h"
|
|
1126
|
+
_sep=","
|
|
1127
|
+
done
|
|
1128
|
+
(( ${#BG_NAMES[@]} )) && printf '\n '
|
|
1129
|
+
printf ']\n}\n'
|
|
1130
|
+
} >"$_receipt" && say " receipt: $_receipt"
|
|
1131
|
+
} || true
|
|
1132
|
+
fi
|
|
1133
|
+
|
|
1134
|
+
# ── Report ──────────────────────────────────────────────────────────────────
|
|
1135
|
+
step "Step 9 — speed (advisory)"
|
|
1136
|
+
# Post-deploy Lighthouse reading of the LIVE site. ADVISORY: it reports, it
|
|
1137
|
+
# does not block a deploy that already shipped. The ENFORCED ratchet runs
|
|
1138
|
+
# pre-deploy against the local production build (`bun run speed` in one.ie/web),
|
|
1139
|
+
# because live-site numbers swing ~30 points on network jitter and cannot gate.
|
|
1140
|
+
#
|
|
1141
|
+
# Placed here, serial and after the parallel deploys, deliberately: Lighthouse
|
|
1142
|
+
# drives a full Chrome and would fight the Step 1+3 gate fan-out and the machine
|
|
1143
|
+
# governor's slot cap if it ran alongside them.
|
|
1144
|
+
#
|
|
1145
|
+
# Exit 2 = INCONCLUSIVE (could not measure). It is reported as such and NEVER
|
|
1146
|
+
# as a pass — an unrun check is not a green check.
|
|
1147
|
+
if (( DRY )); then
|
|
1148
|
+
echo " (dry run — speed check skipped)" | tee -a "$LOG"
|
|
1149
|
+
elif [[ "${SKIP_SPEED:-0}" == "1" ]]; then
|
|
1150
|
+
echo " (SKIP_SPEED=1 — skipped)" | tee -a "$LOG"
|
|
1151
|
+
else
|
|
1152
|
+
sp_out="$LOG_DIR/speed-$STAMP.log"
|
|
1153
|
+
sp_cmd=(node "$(dirname "${BASH_SOURCE[0]}")/speed-check.mjs" --prod --runs 3)
|
|
1154
|
+
# The verdict, from the exit code. One function, used by both the inline and
|
|
1155
|
+
# the detached path, so the two can never phrase the same code differently.
|
|
1156
|
+
# NOTE: bad() only PRINTS in this script (die() is what exits). That is
|
|
1157
|
+
# correct here — Step 9 runs AFTER the deploy has shipped, so failing the
|
|
1158
|
+
# pipeline could not un-ship it. This step reports; it does not block. The
|
|
1159
|
+
# blocking gate is `bun run speed` pre-deploy against the local build.
|
|
1160
|
+
# warn() is used so a red x is never mistaken for a blocked deploy.
|
|
1161
|
+
speed_verdict() {
|
|
1162
|
+
grep -E 'perf=|faster|SLOWER|performance:|advisory only' "$sp_out" | sed 's/^/ /' | tee -a "$LOG" || true
|
|
1163
|
+
case "$1" in
|
|
1164
|
+
0) ok "speed: nothing got slower (live site)" ;;
|
|
1165
|
+
1) warn "speed: REGRESSION on the live site — ADVISORY, deploy not blocked — $sp_out" ;;
|
|
1166
|
+
2) warn "speed: INCONCLUSIVE (not a pass) — ADVISORY — $sp_out" ;;
|
|
1167
|
+
# 3 = regressions found, deliberately not gating (--prod). Distinct from 0
|
|
1168
|
+
# so it can never render as "nothing got slower" — which it did, twice, in
|
|
1169
|
+
# the same log that printed the regression two lines above.
|
|
1170
|
+
3) warn "speed: REGRESSION on the live site — advisory, not gating — $sp_out" ;;
|
|
1171
|
+
# 124 is run_bounded's own code: Lighthouse hung past the cap and was
|
|
1172
|
+
# reaped. Reported as unrun, never as a pass.
|
|
1173
|
+
124) warn "speed: TIMED OUT (not a pass) — ADVISORY — $sp_out" ;;
|
|
1174
|
+
*) warn "speed: unexpected exit $1 — ADVISORY — $sp_out" ;;
|
|
1175
|
+
esac
|
|
1176
|
+
}
|
|
1177
|
+
if [[ "${SPEED_SYNC:-0}" == "1" ]]; then
|
|
1178
|
+
# Inline — the operator wants the number before leaving the terminal.
|
|
1179
|
+
run_bounded "${SPEED_TIMEOUT:-600}" "${sp_cmd[@]}" >"$sp_out" 2>&1
|
|
1180
|
+
speed_verdict $?
|
|
1181
|
+
else
|
|
1182
|
+
# DETACHED, by default. Measured 2026-09-05 on deploy-20260904-173517: the
|
|
1183
|
+
# pipeline was 227s wall-clock, and 166s of it was THIS step — three
|
|
1184
|
+
# Lighthouse runs over six routes against a site that had already shipped
|
|
1185
|
+
# and already passed health at +61s. The number is advisory by design, so
|
|
1186
|
+
# nothing downstream waits on it; the only thing that waited was the
|
|
1187
|
+
# operator. The verdict is appended to $LOG when it lands, in the same
|
|
1188
|
+
# words the inline path uses, and the process is bounded so a hung Chrome
|
|
1189
|
+
# cannot outlive the deploy by more than SPEED_TIMEOUT.
|
|
1190
|
+
(
|
|
1191
|
+
run_bounded "${SPEED_TIMEOUT:-600}" "${sp_cmd[@]}" >"$sp_out" 2>&1
|
|
1192
|
+
rc=$?
|
|
1193
|
+
{ echo ""; echo "══ Step 9 — speed (detached, landed $(date -u +%FT%TZ))"; } >>"$LOG"
|
|
1194
|
+
speed_verdict $rc >>"$LOG" 2>&1
|
|
1195
|
+
) >/dev/null 2>&1 </dev/null &
|
|
1196
|
+
disown 2>/dev/null || true
|
|
1197
|
+
say " speed: measuring the live site in the background (pid $!, ~3 min)"
|
|
1198
|
+
say " verdict lands in $LOG · raw: $sp_out · inline: SPEED_SYNC=1"
|
|
1199
|
+
fi
|
|
1200
|
+
fi
|
|
1201
|
+
|
|
1202
|
+
step "Report"
|
|
1203
|
+
say " Mode: $MODE"
|
|
1204
|
+
say " Branch: $BRANCH @ $HEAD_SHA"
|
|
1205
|
+
say " Typecheck: $TYPECHECK_REPORT"
|
|
1206
|
+
say " Tests: $TESTS_REPORT"
|
|
1207
|
+
say " Build: $BUILD_REPORT"
|
|
1208
|
+
say " Migrations: $MIG_REPORT"
|
|
1209
|
+
say " Deployed: ${TARGETS[*]:-none}"
|
|
1210
|
+
say " Health: $HEALTH_REPORT"
|
|
1211
|
+
[[ -n "${sp_out:-}" && "${SPEED_SYNC:-0}" != "1" ]] && say " Speed: detached — $sp_out"
|
|
1212
|
+
(( ${#SKIPPED_UNCHANGED[@]} )) && say " Unchanged: ${SKIPPED_UNCHANGED[*]} (skipped, --changed)"
|
|
1213
|
+
[[ -n "${GZIP_HINT:-}" ]] && say " Bundle: $GZIP_HINT"
|
|
1214
|
+
say " Log: $LOG"
|
|
1215
|
+
|
|
1216
|
+
# The record the /deploy page renders. It runs on BOTH exits — a degraded deploy
|
|
1217
|
+
# is the run most worth having a timing for, and a record that only ever holds
|
|
1218
|
+
# green runs is a record that cannot be read for a trend. Guarded so a recorder
|
|
1219
|
+
# fault can never turn a good deploy into a bad exit: `|| true`, and the phases
|
|
1220
|
+
# come from arrays the gates already filled.
|
|
1221
|
+
_record_run() {
|
|
1222
|
+
local verdict="$1"
|
|
1223
|
+
[[ -x "$ROOT/.claude/scripts/deploy-record.sh" ]] || return 0
|
|
1224
|
+
(( DRY )) && return 0
|
|
1225
|
+
local -a args=(--door "deploy${MODE:+ $MODE}" --target "${TARGETS[*]:-—}"
|
|
1226
|
+
--branch "$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
|
|
1227
|
+
--sha "$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo '')"
|
|
1228
|
+
--verdict "$verdict" --wall "$(( SECONDS ))"
|
|
1229
|
+
--note "heavy=$( (( ${HEAVY_PARALLEL:-0} )) && echo parallel || echo serial ) memFreeGb=${HEAVY_FREE_GB:-?} needGb=${HEAVY_NEED_GB:-?}")
|
|
1230
|
+
local i st
|
|
1231
|
+
for i in "${!GATE_NAMES[@]}"; do
|
|
1232
|
+
# -1 means the gate never reported. It is recorded as `unrun`, never as a
|
|
1233
|
+
# pass and never as a zero — the page draws it as an outline for that reason.
|
|
1234
|
+
if [[ "${GATE_RC[$i]}" == 0 ]]; then st=pass
|
|
1235
|
+
elif [[ "${GATE_RC[$i]}" == -1 ]]; then st=unrun
|
|
1236
|
+
else st=fail; fi
|
|
1237
|
+
args+=(--phase "${GATE_NAMES[$i]}:$(( GATE_T[i] - GATE_T0 )):$(( GATE_DUR[i] < 0 ? 0 : GATE_DUR[i] )):gate:$st")
|
|
1238
|
+
done
|
|
1239
|
+
bash "$ROOT/.claude/scripts/deploy-record.sh" "${args[@]}" >/dev/null 2>&1 || true
|
|
1240
|
+
# THE TRUNK LEG. The row lands in the PRIMARY worktree, where a commit can
|
|
1241
|
+
# pick it up — and nothing commits it, so /deploy (which renders that file at
|
|
1242
|
+
# BUILD time) never sees it. Two production ships were lost that way in two
|
|
1243
|
+
# days. It is not automated: main receives merges, never edits, and a
|
|
1244
|
+
# recorder that pushed its own row would invent a door past that loop and
|
|
1245
|
+
# need credentials at ship time. So it is made impossible to miss instead —
|
|
1246
|
+
# printed here, at the moment it becomes true. Never fatal: a LOGGING debt
|
|
1247
|
+
# must not fail a ship.
|
|
1248
|
+
local pend; pend="$(bash "$ROOT/.claude/scripts/deploy-record.sh" --pending 2>/dev/null)" || true
|
|
1249
|
+
[[ -n "$pend" ]] && say "$pend"
|
|
1250
|
+
return 0
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (( ${#hfail[@]} )); then
|
|
1254
|
+
_emit --stage health --status fail --reason "unhealthy: ${hfail[*]}"
|
|
1255
|
+
_emit_verdict red
|
|
1256
|
+
_record_run degraded
|
|
1257
|
+
# A degraded ship is an outcome. Close it as one.
|
|
1258
|
+
# --only dims,feedback: `deploy` is a label, not a plan slug — without --tid the
|
|
1259
|
+
# task leg resolves it through the `slug:` tag door and would close a plan named
|
|
1260
|
+
# deploy if one ever existed. The deploy record is deploy-record.sh's job; what
|
|
1261
|
+
# is true to write here is the pheromone.
|
|
1262
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" deploy --status failed --only dims,feedback || true
|
|
1263
|
+
say ""
|
|
1264
|
+
bad "degraded — unhealthy: ${hfail[*]}"
|
|
1265
|
+
say " rollback: cd one.ie/web && bunx wrangler rollback --name one-prod"
|
|
1266
|
+
exit 1
|
|
1267
|
+
fi
|
|
1268
|
+
_emit --stage health --status ok --detail "probes=${#TARGETS[@]}"
|
|
1269
|
+
_emit_verdict green
|
|
1270
|
+
_record_run green
|
|
1271
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" deploy --only dims,feedback || true
|
|
1272
|
+
say ""
|
|
1273
|
+
ok "deploy:success"
|