Skip to content

feat(guard): standing CI contract guard (Spec 4) - #1

Merged
odenirdev merged 8 commits into
mainfrom
worktree-spec-4-contract-guard
Jun 12, 2026
Merged

feat(guard): standing CI contract guard (Spec 4)#1
odenirdev merged 8 commits into
mainfrom
worktree-spec-4-contract-guard

Conversation

@odenirdev

Copy link
Copy Markdown
Contributor

Summary

Turns the one-off contract-check into a standing, both-package, render-deep CI contract guard (scripts/contract-guard.mjs). Each run materializes two engine versions by construction, publishes them to an ephemeral Verdaccio, stages the real adopter (apps/cms + apps/web) at the baseline, runs pnpm update @press/*, and fails on any of three leak classes:

  • File leak — a Project-zone file changed on disk (content-hash of apps/** + assert-no-engine-in-host)
  • Boot leak — host builds but no longer boots (/_health 204 smoke)
  • Behavioral leak — boots & disk-clean, but the adopter's custom block / whitelabel <head> stops rendering (seeded e2e render)

Composed from single-purpose libs under scripts/lib/ (sh, registry, baseline, leak-snapshot + unit tests, adopter-cycle) and the existing seed-e2e/e2e-check/assert-no-engine-in-host. Wired into GitHub Actions on packages/** PRs + manual dispatch.

Notable implementation decisions (vs. the spec's first draft)

  • Content-addressed publish labels (X.Y.Z-base.<srcHash> / -contract.<srcHash>, hashed on engine source) so baseline/candidate are always distinct, always-fresh artifacts — a non-ephemeral registry can never serve a stale tarball, and an uncommitted regression is published as a genuinely new candidate.
  • pregenerateTypes writes @press/web's gitignored generated.ts before publishing, so every tarball ships the types the adopter imports.
  • Clean-tree baseline: a dirty packages/ builds the baseline from a pristine tag worktree, so a regression is tested as vN+1 against an untouched vN.
  • Re-run hygiene (local only): clear apps/web/.next per build; kill any orphaned web server on :3000 (e2e-check leaks it on assertion failure).

Acceptance (proven end-to-end)

  • AC1 real cycle green — CONTRACT HELD on bootstrap, fast-path, and clean-worktree paths, exit 0, clean tree.
  • AC2 catches a leak — dropping the custom-block render path fails the post-update behavioral step (callout message missing), exit 1, while disk + boot pass; reverting restores green.
  • AC3 CI gated/required — .github/workflows/contract-guard.yml on packages/** + dispatch, Node 20 / pnpm 10, full history + tags, Verdaccio in-job.
  • AC4 bootstrap honest — first pre-tag run logged BOOTSTRAP …, exit 0.
  • AC5 local repro — re-runnable from a clean tree.

First engine release tag engine-v0.3.2 pushed (@press/cms@0.3.2 + @press/web@0.1.0). contract-check.mjs folded in and removed.

⚠️ Reviewer action required

  • Enable guard as a required status check on main (repo branch-protection setting) once it has run on a PR — the workflow file alone cannot enforce this (Spec 4 §6).
  • Base diverges: this branch forks before the concurrent Spec 3 work (it keeps @press/web@0.1.0); main now has @press/web@0.2.0 + host-template. Expect to reconcile packages/press-web on merge.

Test Plan

  • Unit: pnpm --filter @press/web test (33, incl. leak-snapshot) + pnpm --filter @press/cms test (4) — green
  • node scripts/contract-guard.mjsCONTRACT HELD, exit 0, clean tree (bootstrap, fast-path, worktree-path)
  • Negative test → E2E FAIL: callout message missing from HTML, exit 1; revert → green
  • CI: confirm the workflow runs on this PR, then enable the required check

🤖 Generated with Claude Code

odenirdev and others added 8 commits June 12, 2026 18:30
Both-manifest allowed delta (Spec 4 §4.4); independent of git cleanliness so
the workspace:*→pinned baseline rewrite doesn't pollute leak detection.

Also extends @press/web vitest.config include pattern to cover
scripts/lib/**/*.test.mjs so the guard's pure-logic tests run through
the existing vitest harness without adding a new runner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ns by construction)

Synthetic X.Y.Z-contract.<sha> candidate guarantees a real code delta even
without a version bump; worktree-from-tag builds the baseline (Spec 4 §4.1/§4.3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…store

Reuses seed-e2e + e2e-check + assert-no-engine-in-host; green-baseline seeds and
renders a real entry so a missing DZ component can't 204-false-pass (Spec 4 §4.2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Composes registry/baseline/leak-snapshot/adopter-cycle into the §4.2 cycle;
bootstrap (no tag) runs harness-only and says so (Spec 4 AC1 harness, AC4).

Two integration fixes surfaced while running the real cycle:
- pregenerateTypes(): sync @press/web's generated.ts ON DISK before any publish,
  so every tarball ships it (generated.ts is gitignored, absent in fresh trees);
  without it the baseline build fails on the missing CustomCallout export.
- run-scoped synthetic version labels (X.Y.Z-base.<sha> / -contract.<sha>) for
  baseline and candidate, so the guard always controls the exact tarball content
  and never reuses a stale version from a shared/non-ephemeral registry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hardening surfaced by the AC2 negative test and local re-runs. None of these
manifest in CI (fresh checkout + empty registry); all broke local re-runs:

- Content-addressed publish labels (X.Y.Z-base.<hash> / -contract.<hash>) keyed
  on the engine SOURCE hash, not the commit sha. An uncommitted regression now
  publishes a genuinely new candidate instead of colliding with a prior clean
  tarball under the same -contract.<sha>; a shared/non-ephemeral registry can
  never serve a stale tarball for a reused label.
- Baseline builds from a CLEAN tag worktree whenever the engine tree is dirty
  (not just when tag != HEAD), so the negative test's uncommitted regression is
  tested as vN+1 against a pristine vN — never leaked into the baseline. The
  worktree reuses the pregenerated generated.ts (gitignored, absent in a fresh
  checkout) so the baseline tarball ships the same types as the candidate.
- Clear apps/web/.next before each build (next build is incremental and keyed by
  source path, not @press/web version) so a prior engine's transpilation can't
  render stale.
- stopWeb(): kill the orphaned next-server on :3000. e2e-check calls process.exit
  on assertion failure, skipping its finally, so the web child leaks and the next
  run fetches stale HTML. The guard now owns the web lifecycle (free :3000 before
  each render + in teardown).
- restoreAdopter fails the process (exitCode=1) if the workspace can't be restored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gated trigger, Node 20 / pnpm 10, full history+tags for the baseline build,
ephemeral Verdaccio in-job (no external registry); logs uploaded on failure.
Required-check wiring on main is a repo setting (noted in the PR). (Spec 4 §6, AC3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…heck; record results

Removes the CMS-only contract-check.mjs (the generalized guard is now the single
source of the allowed delta); README documents the minimal update path, the three
leak classes, and the negative-test repro; spec §11 records AC1–AC5 + the
content-addressed-label / re-run-hygiene deltas decided during execution. (Spec 4 §8)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@odenirdev
odenirdev merged commit 83f8330 into main Jun 12, 2026
1 check failed
@odenirdev
odenirdev deleted the worktree-spec-4-contract-guard branch June 12, 2026 23:32
odenirdev added a commit that referenced this pull request Jul 4, 2026
…al) (#9)

* feat(cms): enhance component-picker UX with icons and human-readable labels

- Added `info.icon` to every engine component JSON to improve the admin "Pick one component" dialog, replacing the generic grid fallback with specific icons for each block.
- Introduced a new admin bundle (`./strapi-admin`) to register human-readable category labels for the component picker, resolving titles through react-intl.
- Implemented placement-scoped adopter categories for custom blocks, allowing specific components to be admitted into designated Dynamic Zones.
- Seeded a "Privacy Policy" page template at bootstrap, ensuring it is created only once and respects existing pages on the same slug.
- Updated various component JSON files to include icons for better visual representation in the admin interface.

* feat: cookie-consent as the first engine plugin (new 'plugin' canonical)

Introduces the engine plugin family and ships cookie consent as plugin #1.

press-web (major):
- Entity gains 'plugin' (additive); new PressPlugin<Id> contract with a
  synthetic urn:plugin:{id} identity, mirroring urn:site-setting:default
- ResolvedPressConfig gains the REQUIRED plugins named map
  (plugins.cookieConsent) — resolved TOTAL even when the CMS is unreachable
- mapCookieConsent fails OPEN (banner enabled + default copy on CMS outage);
  hasConsent stays independently fail-closed (no decision => false)
- CookieConsentBanner (first stateful client component), useConsent hook,
  consent store over a versioned first-party cookie (press_consent, 180d),
  and buildConsentBootstrapScript() for pre-paint anti-flash — the visitor's
  decision is never read via cookies() in the RSC tree (static/ISR intact)
- Closed consent categories (necessary | analytics | marketing): editors
  toggle and re-word, never rename keys

press-cms (minor):
- press.cookie-consent + press.cookie-category config components (injected,
  never DZ-admitted), cookieConsent attribute on Site Settings, deep populate
  (nested categories + privacy page slug)
- seedCookieConsent: run-once (cookieConsentSeeded flag), booleans only —
  default copy lives web-side; editor disable respected forever; the flag is
  not set when the record is missing so a broken bootstrap order self-heals

Tests: 8 interactive banner tests via a minimal act()+createRoot jsdom
harness — deliberately not @testing-library/react, whose only materialized
copy under the Strapi-required node-linker=hoisted layout is the react-19
variant and cannot render this package's react-18 elements.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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