Skip to content

ci: gate PRs, and actually enforce the cross-origin invariant - #16

Merged
openipc-ai merged 1 commit into
mainfrom
ci/gate-prs-and-bundle-invariant
Aug 27, 2026
Merged

ci: gate PRs, and actually enforce the cross-origin invariant#16
openipc-ai merged 1 commit into
mainfrom
ci/gate-prs-and-bundle-invariant

Conversation

@openipc-ai

Copy link
Copy Markdown
Contributor

The headline is not "PRs have no CI"

tests/bundle.test.ts — the guard behind what CLAUDE.md calls "the one architectural invariant" — has been running nowhere. Not on PRs, not on main, not on the nightly.

It skips itself when dist/ is absent, and the only job that creates dist/ never runs tests:

job in pages.yml runs dist/ present? bundle test
test npm ci && npm test no skipped
build npm ci && npm run build yes (creates it) never invoked — no test step
deploy publishes

CLAUDE.md asserted it "gates in CI (after npm run build)". No job did that. Separately, pages.yml triggers only on push to main, cron, and dispatch — so PRs got no CI at all.

Verified by mutation, not by inspection: pointing sizesUrl() at a releases/download/... URL now fails the suite (exit 1), and passes again on revert. Two earlier attempts at that mutation passed — once because Vite tree-shook the unreachable constant, once because tsc -b failed and && short-circuited, leaving a stale dist/ for the test to validate. Both traps are now written into CLAUDE.md.

The change

New ci.yml, pull_request only: npm cinpm run build:appnpm test.

build:app is tsc -b && vite build with no prebuild lifecycle hook, so PR CI needs no gh CLI, no GH_TOKEN, no API calls and no downloads — 2.0s of build, 0.5s of tests, npm ci dominating. The prebuild stays out of PR CI deliberately: it's slow, needs a token, and its logic is already covered by the mocked unit tests plus tests/live.test.ts under RUN_LIVE_TESTS=1.

package.json keeps build as the sole caller of the prebuild hook, with no explicit npm run prebuild && anywhere, so the double-invocation bug of 45c1740 can't return. Verified in isolation: npm run build fires it exactly once, npm run build:app not at all.

Why a separate workflow, not a pull_request trigger on pages.yml:

  1. pages.yml's concurrency: group: pages is repo-wide. PRs sharing it would cancel in-flight deploys — merging perf(prebuild): batch asset fetches into parallel curl invocations #15 killed fix(prebuild): page /releases at 20, not the per_page=100 cap #14's build 38 minutes in, exactly this way.
  2. pages.yml holds pages: write / id-token: write. A PR run should never carry those. ci.yml is contents: read only, so a PR structurally cannot deploy, and fork PRs work unchanged.

pages.yml also gains:

  • npm test after npm run build, so main and the nightly get the invariant too (~2s).
  • cancel-in-progress: false. The build produces the entire data set; cancelling one discards its downloads and leaves the site stale. Queueing is affordable now that perf(prebuild): batch asset fetches into parallel curl invocations #15 puts the build in the ~13 min range.

Verification

  • Both workflow files parse; jobs and concurrency asserted.
  • Exact PR sequence run locally from a clean tree: build ok, 111 passed / 3 skipped. Before this change the same command gave 106 passed with bundle.test.ts among the skips — the 5 invariant tests now actually execute.
  • Mutation test as described above.

🤖 Generated with Claude Code

https://claude.ai/code/session_013R9YDQbB9GbMYWuyXUoMBM

tests/bundle.test.ts was running nowhere. It skips itself when dist/ is
absent, and the only job that creates dist/ (build) never ran tests --
the test job runs npm test with no bundle present. So the guard behind
"the one architectural invariant" in CLAUDE.md has been silently inert
on main and on the nightly, and PRs had no CI at all: pages.yml
triggers only on push to main, cron and dispatch.

Verified by mutation: pointing sizesUrl() at a releases/download URL
now fails the suite (exit 1), and passes again when reverted.

New ci.yml, pull_request only:
  npm ci -> npm run build:app -> npm test

build:app is tsc -b && vite build with no prebuild lifecycle hook, so
PR CI needs no gh CLI, no GH_TOKEN, no API calls and no downloads --
2.0s of build, 0.5s of tests. The prebuild stays out of PR CI on
purpose: its logic is covered by the mocked unit tests plus
tests/live.test.ts under RUN_LIVE_TESTS=1. package.json keeps `build`
as the sole caller of the prebuild hook, with no explicit
`npm run prebuild &&`, so the double-invocation bug of 45c1740 cannot
return; verified that `npm run build` fires it exactly once and
`npm run build:app` not at all.

ci.yml is a separate workflow rather than a pull_request trigger on
pages.yml for two reasons. pages.yml's `concurrency: group: pages`
is repo-wide, so PRs sharing it would cancel in-flight deploys --
merging #15 killed #14's build 38 minutes in exactly this way. And
pages.yml holds pages:write / id-token:write, which a PR run should
never carry; ci.yml is contents:read only, so a PR structurally cannot
deploy, and fork PRs work unchanged.

pages.yml also gains `npm test` after `npm run build`, so main and the
nightly get the invariant too (~2s), and switches to
cancel-in-progress: false. The build produces the entire data set;
cancelling one discards its downloads and leaves the site stale, and
queueing is affordable now that batched fetches put the build in the
~13 minute range.

CLAUDE.md corrected -- it claimed the bundle test "gates in CI (after
npm run build)", which was not true of any job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013R9YDQbB9GbMYWuyXUoMBM
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Gate pull requests and enforce production bundle invariants

🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Gate pull requests with a read-only app build and complete test suite.
• Run bundle invariants after builds on pull requests, main, and nightly.
• Queue Pages builds to avoid wasting downloaded deployment data on cancellation.
Diagram

graph TD
  PR["Pull Request"] -->|triggers| Verify["PR Verify"] -->|runs| AppBuild["App Build"] -->|validates| PRTests["Bundle Tests"]
  Main["Main / Nightly"] -->|triggers| FullBuild["Full Build"] -->|validates| DeployTests["Bundle Tests"] -->|gates| Pages["Pages Deploy"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add pull requests to pages.yml
  • ➕ Keeps all CI and deployment logic in one workflow.
  • ➕ Avoids introducing a second workflow file.
  • ➖ Exposes PR runs to deployment-oriented permissions and job structure.
  • ➖ Requires careful concurrency separation to prevent PRs cancelling deployments.
  • ➖ Adds conditional complexity to skip prebuild and deploy stages.
2. Run the full prebuild on pull requests
  • ➕ Exercises the exact deployment build path before merge.
  • ➕ Validates live data aggregation alongside the application bundle.
  • ➖ Requires credentials, GitHub CLI access, network calls, and downloads.
  • ➖ Slows PR feedback and is less reliable for fork pull requests.
  • ➖ Duplicates coverage already provided by mocked and opt-in live tests.
3. Extract reusable build-and-test workflow
  • ➕ Centralizes post-build test ordering across PR and Pages workflows.
  • ➕ Reduces future drift between validation paths.
  • ➖ Must parameterize credentials and app-only versus full builds.
  • ➖ Adds abstraction for two short workflows with intentionally different security boundaries.

Recommendation: Keep the dedicated read-only PR workflow and separate app-only build script. This preserves fast fork-safe checks and isolates deployment permissions and concurrency; a reusable workflow is only warranted if the two validation paths grow substantially.

Files changed (4) +60 / -4

Bug fix (1) +11 / -1
pages.ymlValidate deployment bundles and queue Pages runs +11/-1

Validate deployment bundles and queue Pages runs

• Runs the test suite after the full production build so bundle invariants execute against dist. Pages runs now queue instead of cancelling in-progress data builds.

.github/workflows/pages.yml

Documentation (1) +6 / -2
CLAUDE.mdDocument build modes and bundle-test CI requirements +6/-2

Document build modes and bundle-test CI requirements

• Documents the app-only build command, both workflow paths, and the requirement to test after building. It also warns that stale dist output and failed builds can produce misleading local results.

CLAUDE.md

Other (2) +43 / -1
ci.ymlAdd isolated pull-request build and test gate +41/-0

Add isolated pull-request build and test gate

• Introduces a pull-request-only workflow with read-only permissions and per-ref concurrency. It installs dependencies, builds the app without prebuild downloads, then runs tests against the generated bundle.

.github/workflows/ci.yml

package.jsonSplit app compilation from the prebuild lifecycle +2/-1

Split app compilation from the prebuild lifecycle

• Adds build:app for TypeScript and Vite compilation without data aggregation. The build script delegates to it while preserving npm's single automatic prebuild lifecycle invocation.

package.json

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@openipc-ai
openipc-ai merged commit d0264f7 into main Aug 27, 2026
1 check passed
@openipc-ai
openipc-ai deleted the ci/gate-prs-and-bundle-invariant branch August 27, 2026 17:14
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.

1 participant