Skip to content

ops: pin, badge, de-index, measure and expire preview deployments - #111

Merged
joelpeace48-cell merged 2 commits into
FinesseStudioLab:mainfrom
defimomof2:ops/preview-hardening
Aug 30, 2026
Merged

ops: pin, badge, de-index, measure and expire preview deployments#111
joelpeace48-cell merged 2 commits into
FinesseStudioLab:mainfrom
defimomof2:ops/preview-hardening

Conversation

@defimomof2

Copy link
Copy Markdown
Contributor

What this changes

Completes the four acceptance criteria on #78 that were not covered by the initial preview workflow: previews are pinned to testnet and staging, visibly badged, kept out of search indexes, measured with Lighthouse, and cleaned up when a pull request closes.

Closes #78

Stacked on fix/duplicate-main-landmark. The badge is rendered from the same JSX block that PR fixes, so please merge that one first. This branch is based on it, not on main.

Why

#101 delivered the first item on the issue's list — a preview per pull request with the URL commented back. The other four were never built:

Acceptance criterion Before Now
Preview per PR with the URL commented #101 unchanged
Previews point at testnet and staging only
Preview domains not indexed
Lighthouse scores posted to the PR
Previews cleaned up automatically

Previews point at testnet and staging only

The workflow now builds with NEXT_PUBLIC_DEPLOY_ENV=preview, NEXT_PUBLIC_STELLAR_NETWORK=testnet and a staging backend from a new STAGING_BACKEND_URL secret.

Setting those in the workflow is not sufficient on its own: a workflow that is edited later, or a preview built through some other path, would silently deploy something wired to production. So lib/deploy-env.ts re-checks at build time and throws. Verified both directions:

NEXT_PUBLIC_DEPLOY_ENV=preview NEXT_PUBLIC_STELLAR_NETWORK=public npm run build
  → Error: Preview builds must use testnet, got NEXT_PUBLIC_STELLAR_NETWORK="public".

NEXT_PUBLIC_DEPLOY_ENV=preview NEXT_PUBLIC_STELLAR_NETWORK=testnet \
  NEXT_PUBLIC_BACKEND_URL=https://staging.example npm run build
  → builds

It also falls back to Vercel's own NEXT_PUBLIC_VERCEL_ENV when the explicit variable is missing, because the cost of failing to recognise a preview is an indexed preview.

Badged, and not indexed

components/preview-badge.tsx renders a bar naming the environment and network, and returns null anywhere else, so a reviewer or a stakeholder handed a link cannot mistake it for the live site.

app/robots.ts serves Disallow: / on a preview. That alone is not enough — robots.txt is advisory and does not stop a page being indexed when something links to it — so next.config.ts also sends X-Robots-Tag: noindex, nofollow on previews only.

Verified against real builds, both directions:

Preview Production
robots.txt Disallow: / Allow: /
X-Robots-Tag noindex, nofollow absent
Badge renders absent

While checking this I noticed production robots.txt was advertising /sitemap.xml, which does not exist — the app has no sitemap route. Pointing crawlers at a 404 is worse than omitting the line, so it is dropped. Adding a real sitemap would mean depending on lib/site-map.ts, which belongs to a different change.

Lighthouse

A job runs Lighthouse against the deployed preview and comments performance, accessibility, best-practices and SEO for / and /docs, updating a single marked comment rather than appending one per push.

It runs through npx @lhci/cli rather than a third-party action deliberately: that job holds pull-requests: write, and the fewer external actions running with that token, the smaller the surface.

Cleanup

.github/workflows/preview-cleanup.yml runs on pull_request: closed and removes the deployments belonging to that branch, so previews stop accumulating. Guarded on VERCEL_TOKEN like the deploy workflow, so it reports a skip notice instead of failing when credentials are absent.

How it was verified

npm run lint      ✔ no warnings or errors
npx tsc --noEmit  ✔ clean
npm test          ✔ 122 passed — 9 new covering env resolution and the guard
npm run build     ✔ exit 0
npx playwright test   ✔ 101 passed, chromium and mobile-safari

The nine new unit tests cover both directions of the guard, including that a correctly configured preview and any production build pass through untouched — a guard that rejects everything would also have made the first check above pass.

One pre-existing failure is unrelated to this branch: landing page is navigable by keyboard alone fails locally under mobile-safari on an unmodified main as well, and passes in CI. It looks like a local WebKit version difference, so I have left it rather than change a test that is green in CI.

  • Existing tests pass
  • New tests cover the change
  • No change to a public contract interface

Risk

The workflow changes are inert until a maintainer adds STAGING_BACKEND_URL alongside the existing three secrets — documented in the README, including the fork behaviour. Until then previews skip with a notice, exactly as before.

The one thing worth a maintainer's eye is the production-host list in lib/deploy-env.ts. It currently matches on mainnet, horizon.stellar.org and api.modeltrace; if your real production origins differ, that list should be corrected, or the guard will not catch a preview pointed at them.

app/layout.tsx renders two <main> elements and {children} twice. Every
page therefore renders its entire content twice, and the skip link — whose
target is the second <main> — lands below the duplicate rather than at the
start of the content.

It came from a merge that resolved a conflict in the layout body by
keeping both sides: the analytics branch's <main> and the accessibility
branch's <main id="main" tabIndex={-1}>. Keeping the latter preserves the
skip-link target, and the footer and analytics components move back below
it where they belong.

This is the second time the same block has been broken this way, and
nothing caught it: typecheck and lint are both happy with two <main>
elements, and duplicate landmarks are a best-practice axe rule rather than
a WCAG A/AA one, so the accessibility suite does not fail either.
tests/unit/layout-landmarks.test.ts closes that gap by asserting the
element count and the skip-link wiring at the source. Confirmed it fails
against the current layout rather than passing vacuously.
Closes FinesseStudioLab#78

FinesseStudioLab#101 delivered the first of the five acceptance criteria on this issue —
a preview per pull request with the URL commented. The remaining four were
never built. This adds them.

Previews point at testnet and staging only. The workflow builds with
NEXT_PUBLIC_DEPLOY_ENV=preview, NEXT_PUBLIC_STELLAR_NETWORK=testnet and a
staging backend. Because a workflow that forgets one of those would deploy
a preview wired to production, lib/deploy-env.ts re-checks them and throws
during the build instead. Verified: a preview build with the network set
to "public" fails with "Preview builds must use testnet".

Previews are visibly badged. components/preview-badge.tsx renders a bar
naming the environment and network, and nothing at all outside a preview.

Preview domains are not indexed. app/robots.ts serves Disallow: / on a
preview and the normal policy elsewhere. robots.txt is advisory and does
not stop a page being indexed when something links to it, so next.config.ts
also sends X-Robots-Tag: noindex, nofollow on previews. Production keeps
Allow: / and sends no such header.

Lighthouse scores are posted. A job runs it against the deployed preview
and comments performance, accessibility, best-practices and SEO for / and
/docs, updating one comment in place rather than appending per push. It
runs through npx rather than a third-party action, since that job holds
pull-requests: write.

Previews expire. preview-cleanup.yml removes a pull request's deployments
on close or merge, guarded on the token like the deploy workflow.

The README's secrets table gains STAGING_BACKEND_URL and describes the new
behaviour. Nine unit tests cover the environment resolution and the
production-target guard, including that a correctly configured preview and
any production build both pass untouched.
@joelpeace48-cell

Copy link
Copy Markdown
Contributor

@defimomof2 nice job

@joelpeace48-cell
joelpeace48-cell merged commit 85bdb76 into FinesseStudioLab:main Aug 30, 2026
4 checks passed
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.

ops: preview deployments for every pull request

2 participants