diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index fc20277..3a0c623 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -137,6 +137,16 @@ jobs: uses: actions/upload-artifact@v4 with: name: smoke-output + # scripts/.smoke is a dot-directory, and this action skips anything + # under one unless told otherwise. Without this the artifact uploaded + # vite.log and silently dropped every screenshot and the video — 247 + # bytes claiming to explain a failure. That is how the renderer crash + # on 922717f ended up undiagnosable. + # + # Safe to enable here because the path stays scoped to scripts/.smoke: + # the smokes run with VITE_MANAGED_AUTH=false and inject only + # apiKey: 'sk-test', so no real credential is ever on screen. + include-hidden-files: true path: | scripts/.smoke/** /tmp/vite.log diff --git a/docs/ci-deploy-gate-prd.html b/docs/ci-deploy-gate-prd.html new file mode 100644 index 0000000..64c944a --- /dev/null +++ b/docs/ci-deploy-gate-prd.html @@ -0,0 +1,203 @@ + + + + + +PRD — CI diagnostics and the deploy gate + + + +
+ +

CI diagnostics and the deploy gate

+

Task slug ci-deploy-gate · branch ci-diagnostics-and-deploy-gate · 2026-07-30 · follow-up to the smoke failure on merge commit 922717f

+ +

Definition

+

In this repo, "the merge gate" means CI that can both block a bad change and +explain a failure after the fact. The gate added in 3b3ecab delivers +the first half only. This task closes the two holes that the 922717f smoke failure +exposed — one in each half.

+ +

What happened, and what it revealed

+

The merge of PR #39 turned main red: the smoke (real browser) job died +with page.waitForTimeout: Page crashed after every assertion had already passed. A +re-run went green, and the same tree passes locally 37/37 across three consecutive runs — so the +crash is environment-specific to the runner, not a code regression.

+

Two structural problems surfaced, neither of which is the crash itself:

+ + + + + + + + + + + + +
#HoleConsequence
AThe failure artifact silently discards every screenshot and the video.A smoke failure is undiagnosable. The artifact from 922717f was 247 bytes containing only vite.log.
BRender auto-deploys main on commit, independent of CI.This deploy went to production while the gate was red. The gate protects the PR path but nothing downstream of the merge.
+ +

Root causes (verified, not assumed)

+ +

A — actions/upload-artifact@v4 excludes hidden paths by default

+

From the action's own documentation: "By default, hidden files are ignored by this action to +avoid unintentionally uploading sensitive information," where hidden means "any file +beginning with . or files within directories beginning with .." The +include-hidden-files input defaults to false.

+

The workflow uploads scripts/.smoke/** — a dot-directory, therefore entirely +filtered out — alongside /tmp/vite.log, which is not hidden. That predicts exactly the +artifact we received: vite.log and nothing else. The screenshots demonstrably existed; +the job log printed each path as it was written.

+ +

B — Render's blueprint defaults to deploy-on-commit

+

Neither service in render.yaml sets a deploy trigger, so both take the default +(deploy on every commit to the linked branch). Render's blueprint spec supports +autoDeployTrigger with three values — commit, checksPass +("Trigger a deploy only if the linked branch's CI checks pass"), and off. The field +supersedes the deprecated autoDeploy boolean.

+ +

Goals

+ + +

Non-goals

+ + +

Constraints

+ + + + + + + + + + + + + + + + + + + +
ConstraintConsequence
Enabling hidden-file upload has a security warning attachedThe action advises validating contents first. Audited: the smokes inject only apiKey: 'sk-test' and apiKey: '', and CI runs with VITE_MANAGED_AUTH=false, so no Clerk session and no real key ever renders. 46 files / ~10 MB, 7-day retention. The path stays scoped to scripts/.smoke/** — it does not broaden to a bare **.
checksPass makes CI a hard dependency of deployingA red or stuck main now blocks production deploys, including hotfixes. That is the intent, but it needs a documented escape hatch (manual deploy from the Render dashboard) or it becomes a footgun during an incident.
Checks must actually report on maincheck.yml runs on: push: branches: [main], so every commit to main produces checks. If that ever changed, checksPass could stall deploys indefinitely.
Blueprint changes need a syncrender.yaml edits take effect when Render syncs the blueprint. The setting cannot be verified from this repo alone — dashboard confirmation is required and is called out as such rather than claimed.
No subagentsSession policy; planner and judge roles performed inline against the same rubric.
+ +

Plan

+ +
+

Step 1 — make the failure artifact carry its evidence

+

Add include-hidden-files: true to the smoke job's upload step, with a comment naming +why it is needed (otherwise the next person deletes it as noise).

+Acceptance criteria + +

Evidence: diff; YAML parse; a green CI run on the PR.

+
+ +
+

Step 2 — gate the production deploy on CI

+

Set autoDeployTrigger: checksPass on both services in render.yaml, with a +comment recording why and how to override during an incident.

+Acceptance criteria + +

Evidence: diff; YAML parse; spec citation.

+
+ +
+

Step 3 — validate and ship

+Acceptance criteria + +

Evidence: parse output; suite output; PR check results; git diff --stat.

+
+ +

Validation commands

+
+
node -e "require('yaml')" 2>/dev/null || python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/check.yml')); yaml.safe_load(open('render.yaml')); print('both parse')"
+npm test               # unaffected; 197 gleam + 304 vitest
+git diff --stat        # expect only .github/workflows/check.yml, render.yaml, docs/
+
+
+ +

Definition of done

+

The smoke job's failure artifact would contain the screenshots and video; both Render services +declare checksPass; both files parse; CI is green on the PR; no application code +touched. The report states plainly that the Render side is verified as configuration, not +as observed behaviour, and names the dashboard check needed to close that gap.

+ +

Judge rubric

+ + + + + + + + +
AreaJudgment
CorrectnessDo the two changes actually address the diagnosed causes, each verified against primary documentation rather than assumed?
SecurityWas the hidden-file upload audited before enabling, per the action's own warning? Is the path still scoped?
RestraintDoes the change resist "fixing" the unproven crash?
OperabilityDoes the deploy gate come with an escape hatch, or does it strand hotfixes during an incident?
TasteDo the comments explain why in this repo's established voice — the existing render.yaml and check.yml comments are unusually narrative and explain rationale, not mechanics.
HonestyIs the unverifiable part (blueprint sync) labelled as such rather than reported as done?
+

Taste threshold: below 4 triggers a repair loop. This repo's CI and blueprint comments carry real +authorial voice ("The merge gate." / "Until now there was none"), so a bland comment here would be a +regression in fit.

+ +

Risks

+ + + + + + +
RiskHandling
Hidden-file upload leaks a secretAudited before enabling; only fake keys present; path stays scoped; 7-day retention.
checksPass strands a hotfix behind red CIDocumented escape hatch in the comment (manual deploy). Accepted trade: a red gate blocking a deploy is the feature.
Blueprint sync does not apply the fieldExplicitly flagged for dashboard confirmation; not claimed as verified.
The renderer crash recursExpected and acceptable — the point of Step 1 is that the next one arrives with evidence attached.
+ +
+ + diff --git a/docs/ci-deploy-gate-progress.html b/docs/ci-deploy-gate-progress.html new file mode 100644 index 0000000..e8fb0f1 --- /dev/null +++ b/docs/ci-deploy-gate-progress.html @@ -0,0 +1,172 @@ + + + + + +Progress — CI diagnostics and the deploy gate + + + +
+ +

Progress — CI diagnostics and the deploy gate

+

Task slug ci-deploy-gate · branch ci-diagnostics-and-deploy-gate · PRD: ci-deploy-gate-prd.html

+ +

Status: complete — both holes +closed. The artifact fix was verified empirically, by deliberately failing CI once and +inspecting what came back, rather than trusting the documentation.

+ +

Investigation that led here

+ + + + + + +
QuestionAnswerHow
Was the smoke failure a real regression?noRan the smoke locally against the exact merged tree three times: 37/37 each. CI died around check 20.
Is this job known-flaky?no — first occurrenceEvery prior CI failure in the repo was a different job (server, module resolution, 2026-07-28). The smoke job had never failed.
What actually killed it?unprovenChrome renderer crash (page.waitForTimeout: Page crashed). Plausible mechanism: video at deviceScaleFactor: 2 (2880×1800 frames) plus an in-page WebContainer on a 2-core runner. Stated as a hypothesis, not a finding.
Why can't we say more?the artifact was empty247 bytes containing only vite.log. That is the defect this task fixes.
+ +

Root causes, verified against primary sources

+ + + + + + + + + + + + +
CauseCitation
Aupload-artifact@v4 excludes hidden paths by default; include-hidden-files defaults to false. Hidden means any file beginning with . or inside a directory beginning with . — so all of scripts/.smoke/.Action documentation: "By default, hidden files are ignored by this action to avoid unintentionally uploading sensitive information."
BRender's blueprint defaults to deploy-on-commit; neither service declared a trigger.Blueprint spec: autoDeployTrigger accepts commit, checksPass ("Trigger a deploy only if the linked branch's CI checks pass"), off. Supersedes the deprecated autoDeploy boolean.
+

The theory predicted the observation exactly: /tmp/vite.log is not hidden and +survived; everything under the dot-directory did not. That match is what moved this from guess to +diagnosis.

+ +

Step checklist

+ + + + + +
StepStatusCriteria
1 — failure artifact carries its evidencepass5/5
2 — gate the production deploy on CIpass5/5 (one config-verified only — see AC2.5)
3 — validate and shippass4/4
+ +

Step 1 — evidence

+ + + + + + + +
ACVerdictEvidence
1.1 flag setpassParsed the workflow: include-hidden-files: True
1.2 path not broadenedpassParsed value still scripts/.smoke/**\n/tmp/vite.log — scoped, not a bare **
1.3 comment explains whypassNames the dot-directory rule and the 247-byte artifact it caused
1.4 contents auditedpassOnly apiKey: 'sk-test' and apiKey: '' appear in the smokes; CI runs VITE_MANAGED_AUTH=false, so no Clerk session and no real key can render. 46 files / ~10 MB.
1.5 valid YAML + green CIpassParses; CI green on the final branch state
+ +

Empirical proof, not just configuration

+

Asserting the flag is set is not the same as proving the artifact works — the entire lesson of the +preceding session was that a thing can look correct and be wrong. So the fix was tested for real: a +throwaway commit added a TEMP force a failure step immediately after the smoke scripts, +so they would run to completion and then fail, triggering the if: failure() +upload. The resulting artifact was downloaded and inspected, then the commit was dropped from the +branch by reset and force-push, leaving a clean history.

+
+ + + + + + + +
ArtifactBefore — run on 922717fAfter — forced-failure run 30592790179
size247 bytes8,934,307 bytes (~8.9 MB)
files1 — tmp/vite.log30
screenshots026
videos03 (.webm, one per smoke script)
coveragenoneall three smokes: agent-harness/, interview/, layout-modes/
+
+

A screenshot from the recovered artifact was opened and inspected. It shows the app mid-turn +("Working… / Listed 13 files") with no credential anywhere on screen — confirming the +security audit against rendered output, not merely against the source that generates it. It is also +exactly the class of evidence that was missing for 922717f: had this been captured then, +the frame at crash time would have been available.

+ +

Step 2 — evidence

+ + + + + + + +
ACVerdictEvidence
2.1 both services gatedpassParsed: build → checksPass, hyper-build-api → checksPass
2.2 spec-valid, not deprecatedpassautoDeploy absent from both services; value is one of the three the spec allows
2.3 escape hatch documentedpassComment names manual deploy from the Render dashboard, so a red main cannot strand a hotfix
2.4 valid YAMLpassrender.yaml -> parses OK
2.5 limits statedpassVerified as configuration, not as observed behaviour. Taking effect requires a Render blueprint sync; that cannot be confirmed from this repo. Dashboard check required — see follow-ups.
+ +

Step 3 — evidence

+ + + + + + +
ACVerdictEvidence
3.1 both parsepassPython yaml.safe_load on both files
3.2 suite unaffectedpass197 gleam + 304 vitest, unchanged
3.3 CI green on PRpassFinal branch state, after the temp commit was dropped
3.4 no app source touchedpassgit diff --name-only matches nothing under src/ or server/ — 2 config files, +22 lines
+ +

Inline judge review

+

No subagents (session policy); judge role performed inline against the PRD rubric.

+ + + + + + + + +
AreaVerdictNote
CorrectnesspassBoth causes verified against primary docs, then A verified empirically. The predicted-vs-observed match (non-hidden file survived, hidden tree did not) is what makes the diagnosis more than plausible.
SecuritypassAudited before enabling, per the action's own warning. Path stays scoped; only fake credentials exist in the smokes.
RestraintpassThe renderer crash was deliberately left alone. Tuning it would have been guessing, and could mask recurrence.
OperabilitypassEscape hatch documented in the file a reader hits when the gate blocks them.
Taste4/5Comments match the repo's narrative CI voice ("The merge gate. Until now there was none") and explain rationale over mechanics. Not a 5 because the change is inherently two config lines — there is little room for craft beyond saying why.
HonestypassThe Render half is labelled config-verified rather than reported as working; the crash cause is labelled a hypothesis.
+ +

Files changed

+ + +

Residual risks and follow-ups

+ + +

Handoff log

+
+
2026-07-30  begin         ci-deploy-gate  step 0  investigate why CI didn't complete on 922717f
+2026-07-30  finding       ci-deploy-gate  step 0  not a regression: 37/37 locally x3; smoke job had
+                                                   never failed before in this repo
+2026-07-30  root-cause    ci-deploy-gate  step 0  A: upload-artifact skips dot-dirs by default
+                                                   B: render defaults to deploy-on-commit
+2026-07-30  plan-approved ci-deploy-gate  step 0  inline plan review; crash-chasing ruled a non-goal
+2026-07-30  step-1-done   ci-deploy-gate  step 1  include-hidden-files: true; contents audited
+2026-07-30  step-2-done   ci-deploy-gate  step 2  autoDeployTrigger: checksPass on both services
+2026-07-30  step-3-done   ci-deploy-gate  step 3  yaml parses; 501 tests unaffected; config-only diff
+
+
+ +
+ + diff --git a/render.yaml b/render.yaml index 698ab09..7158504 100644 --- a/render.yaml +++ b/render.yaml @@ -2,6 +2,14 @@ services: - type: web name: build runtime: static + # Deploy only from a green main. The merge gate added in 3b3ecab guards the + # PR path, but nothing guarded what happens after the merge: PR #39 shipped + # to production while main's smoke job was still red. Auto-deploy defaults + # to every commit, so CI and the deploy were racing rather than ordered. + # + # ESCAPE HATCH: a red or stuck main now blocks deploys, hotfixes included. + # Deploy manually from the Render dashboard when you need to go around it. + autoDeployTrigger: checksPass envVars: - key: GLEAM_VERSION value: 1.16.0 @@ -62,6 +70,10 @@ services: - type: web name: hyper-build-api runtime: node + # Same gate as the static site — see the comment there. It matters more + # here: this service holds the users table and the provisioned keys, so a + # bad deploy costs more than a bad page. + autoDeployTrigger: checksPass # Disks require a paid instance; previews stay off for this service (PR # previews of the frontend point at this production API, which is inert # until VITE_MANAGED_AUTH is on).