Skip to content

feat: add bin/smoke for post-promotion checks - #1023

Merged
rainhead merged 2 commits into
mainfrom
add-deploy-smoke-checks
Aug 16, 2026
Merged

rainhead merged 2 commits into
mainfrom
add-deploy-smoke-checks

Conversation

@rainhead

Copy link
Copy Markdown
Collaborator

Deploys promote one prebuilt artifact across dev, staging and production, so the failure worth catching is an app serving another environment's data or configuration — the bug fixed in #1019, which reached production and needed a rollback.

Nothing checks for it today. CI only runs the Elixir suite against containers. The health-check job in heroku.yaml fires on GitHub deployment events, and promotions stopped producing those:

orcasite       last deployment record: 2025-11-15  (472133c)
orcasite-beta  last deployment record: 2025-11-15  (472133c)

Review apps still create them, so the workflow works — it just never fires for the environments that matter. And even when it did, it fetched / and accepted any success status, which would have passed happily while production listed development's hydrophones.

Approach

The checks are self-configuring rather than hardcoded, so one command works against any environment with no per-app expectations to maintain:

bin/smoke https://live.orcasound.net

It asks the app's own API which feeds it has, then confirms /listen lists exactly those, that one of them renders, and that feeds the API does not have return 404. A page rendered against a different environment disagrees with the API of the one serving it — which is precisely the bug, expressed as an invariant instead of a fixture.

It also checks that the injected runtime config reports a bucket, that the config script reaches the served HTML, that no absolute API host is baked into the client bundle, and that /seed is absent unless EXPECT_SEED_PAGE=1.

Pure bash and curl, no dependencies, shellcheck clean, exits non-zero on failure.

Verified against all three environments

Dev passes fully. Beta and production pass everything except the seed page:

Seed page
  FAIL /seed -> 200 (expected 404)

That is correct, not a bug in the script: #1022 is merged but not yet promoted, so beta and production still serve a build that baked the page's visibility in. It should go green once you promote — which makes it a decent first exercise of the thing.

Not automated

Deliberately a script rather than a workflow, since promotions emit no events a workflow could hang off. Run it by hand after promoting. If you would rather have it wired into CI against a fixed URL, or run on a schedule, happy to add that.

🤖

Deploys promote one prebuilt artifact across dev, staging and production, so
the failure worth catching is an app serving another environment's data or
configuration. Nothing checked for it: CI only runs the Elixir suite against
containers, and the health-check job in heroku.yaml fires on GitHub
deployment events, which promotions stopped producing in November 2025 --
its last records for orcasite and orcasite-beta are from 2025-11-15. Even
when it ran it fetched / and accepted any success status, which would have
passed while production listed development's hydrophones.

The checks are self-configuring rather than hardcoded, so one command works
against any environment: ask the app's own API which feeds it has, then
confirm /listen lists exactly those, that one of them renders, and that
feeds the API does not have return 404. A page rendered against a different
environment disagrees with the API of the one serving it. Also checks that
the injected runtime config reports a bucket, that the config script reaches
the served HTML, that no absolute API host is baked into the client bundle,
and that the seed page is absent unless EXPECT_SEED_PAGE=1.

Verified against all three environments. Dev passes. Beta and production
currently fail only on the seed page, correctly: #1022 is merged but not yet
promoted, so they still serve a build that baked its visibility in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rainhead
rainhead requested a review from a team as a code owner August 16, 2026 17:17
@rainhead
rainhead requested a review from paulcretu August 16, 2026 17:17
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@rainhead, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 31 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 127ef76c-08b2-42ad-807f-58bb7935fdea

📥 Commits

Reviewing files that changed from the base of the PR and between e125549 and cc2b288.

📒 Files selected for processing (2)
  • README.md
  • bin/smoke

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Review found the script passed on the bug it was written for.

The feed comparison only asserted that every feed the API reports appears on
/listen. The failure in #1019 was the opposite shape: production served a
page built from development's feed list, which is a superset of its own,
since dev seeds from prod and adds test hydrophones. Nothing was missing, so
the check reported ok. It only fires today because dev happens to lack two
feeds prod has -- drift, not detection. Now compares both ways by extracting
slugs from the page's own links, so extra feeds fail too. Also fixes
substring matching: grep for a bare slug matched inside a longer one, and
interpolated it as a regex.

The bundle scan truncated to `head -12`. Sorted order puts the app chunks
last, so it kept twelve numeric vendor chunks and dropped _app, main,
webpack and the page chunk -- exactly where inlined NEXT_PUBLIC_* values
land. It now scans all 24, fails when a chunk cannot be fetched rather than
counting it clean, and looks for baked bucket names as well as API hosts.

Also: retries and redirect following, since this runs while dynos restart
and an http:// argument otherwise failed every check with no explanation;
/listen fetched once instead of three times; an empty feed list distinguished
from an unreachable API; colour gated on a tty and NO_COLOR; and the two
piped `grep -q` calls on page HTML replaced with variable matching, which
would have failed spuriously once the page outgrew the pipe buffer and
pipefail saw curl's EPIPE.

The hardcoded dev-only slugs stay, now overridable via ABSENT_SLUGS and
documented as deliberate operator knowledge rather than presented as
self-configuring: a nonsense slug cannot catch detail pages prerendered from
another environment, having never been prerendered anywhere.

Verified against all three environments. Dev passes. Beta and production
still fail only the seed page, correctly, pending promotion of #1022.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rainhead

Copy link
Copy Markdown
Collaborator Author

Pushed cc2b288. A review found the script passed on the bug it was written for, which is worth spelling out.

The feed comparison was one-directional. It asserted only that every feed the API reports appears on /listen. The #1019 failure is the opposite shape: production served a page built from development's feed list, which is a superset of its own — dev seeds from prod and adds test hydrophones. Nothing was missing, so the check reported ok. Simulated against real slugs:

OLD check (page contains every API feed?)  -> PASSES (bug undetected)
NEW check (two-way diff)
  missing: none
  extra:   paul-test rpi-steve-test        -> FAILS correctly

It only fired today because dev happens to lack andrews-bay and mast-center. That's drift, not detection — one re-seed from prod and it would have gone blind. Now it extracts slugs from the page's own /listen/<slug> links and diffs both ways, which also fixes substring matching (a bare grep for one slug matched inside a longer one, and interpolated the slug as a regex).

The bundle scan was truncated to the wrong chunks. head -12 on a sorted list keeps numeric vendor chunks and drops _app, main, webpack and the page chunk — precisely where inlined NEXT_PUBLIC_* values land. Confirmed on production: 24 chunks referenced, and the 12 dropped were exactly those. It now scans all 24, fails when a chunk can't be fetched rather than counting it clean, and looks for baked bucket names as well as API hosts. Re-ran the complete scan by hand against beta and production first: both genuinely clean, so the earlier conclusion held even though the evidence for it didn't.

Also fixed: retries and redirect following (this runs while dynos restart, and an http:// argument previously failed every check with no explanation); /listen fetched once rather than three times; an empty feed list distinguished from an unreachable API; colour gated on a tty and NO_COLOR; and two piped grep -q calls replaced with variable matching — grep exits on first match, killing curl with EPIPE, which pipefail would report as a failed check once the page outgrew the pipe buffer.

The hardcoded dev-only slugs stay, but now overridable via ABSENT_SLUGS and documented as deliberate operator knowledge rather than described as self-configuring. A nonsense slug can't catch detail pages prerendered from another environment, having never been prerendered anywhere. The README's "no hardcoded expectations" claim was overstated and is corrected.

Verified against all three environments. Dev passes fully; beta and production still fail only the seed page, correctly, pending promotion of #1022.

🤖

@rainhead
rainhead temporarily deployed to orcasite-pr-1023 August 16, 2026 17:28 Inactive
@rainhead
rainhead merged commit 96f8bb0 into main Aug 16, 2026
9 checks passed
@rainhead
rainhead deleted the add-deploy-smoke-checks branch August 16, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants