Skip to content

feat(ci): alchemy CI and deploy shape from the starter, effect 4.1.0 (SYS-900) - #14

Merged
systemfsoftware-bot merged 7 commits into
mainfrom
sys-900-alchemy-ci
Oct 30, 2026
Merged

systemfsoftware-bot merged 7 commits into
mainfrom
sys-900-alchemy-ci

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Ports the starter's alchemy CI and deploy shape (systemfsoftware/effect-endgame-starter-kit#104, merged as d65a7a754) onto frontend: check.yml and deploy.yml from the starter, credentials as code, effect 4.1.0. SYS-900 roll-out #1. Light lane: the starter PR is the plan, and review is the driver's.

frontend publishes, so the release workflow stays. .github/workflows holds check.yml, deploy.yml and release.yml.

Packages that keep publishing

  • @systemfsoftware/effect-atom 6.0.0
  • @systemfsoftware/effect-atom-react 2.0.0
  • @systemfsoftware/storybook-gherkin 5.0.0
  • @systemfsoftware/oxlint-config-frontend 0.1.0

The 7 pending changesets are kept, plus one added for the effect 4.1.0 bump (8 in all). .changeset/, release.jsonc and commitlint.config.ts match main except for the two intended changesets edits (the added effect-4-1-0-resolved.md, and one sentence in effect-4-0-5-peer-floor.md put in the past tense). release.yml differs from main by one line, below.

Proof

deploy.yml is byte-identical to the starter's (SYS-900 Done-when)

$ git diff --no-index <(git show starter/main:.github/workflows/deploy.yml) .github/workflows/deploy.yml
$ echo $?
0

Empty output.

Workflows

.github/workflows:
check.yml
deploy.yml
release.yml

 57 .github/workflows/check.yml
 81 .github/workflows/deploy.yml
 25 .github/workflows/release.yml
163 total

check.yml + deploy.yml are 138 lines (under 300). actionlint .github/workflows/*.yml exits 0. ci.yml, release-gate.yml, changeset-check.yml, commitlint.yml, previews.yml and .github/actions/ are deleted.

Release wiring: the two differences from main and from the starter

  1. release.yml, one line: ci-workflow: ci.yml is now ci-workflow: check.yml. The shared release workflow runs gh workflow run <ci-workflow> on the release PR branch so the release PR gets its CI. ci.yml no longer exists, and check.yml has workflow_dispatch. Without this the release PR would never get a check status.

  2. check.yml, one added step (the only difference from the starter's check.yml): the change-intent gate, folded in from changeset-check.yml.

    - name: Require a change intent for every publishable-package change
      if: github.event_name == 'pull_request' && !(github.head_ref == 'changeset-release/main' && github.event.pull_request.head.repo.full_name == github.repository)
      env:
        BASE_SHA: ${{ github.event.pull_request.base.sha }}
      run: nix develop --command changeset-management check "$BASE_SHA"

    Why a step and not a separate workflow: changeset-check.yml was a thin caller of the shared reusable workflow, whose only job runs changeset-management check <base>. The release flow (release.yml) never calls it; it is only a pull-request gate. A reusable workflow cannot be a step, but the devShell already ships changeset-management, so the step runs the same command, against the same base ref. The only skip is a same-repo PR from changeset-release/main — that PR consumes the intents it names. A fork PR runs the gate even from a branch carrying that name: the skip requires both the branch name and github.event.pull_request.head.repo.full_name == github.repository. The required context stays check alone and .mergify.yml requires only check. commitlint.yml stays deleted; commitlint runs in the commit-msg hook and in the check.yml step from the starter.

    Proof that the step's script enforces the rule (replay of the step's command, changeset-management check <base-sha>, against a fixture checkout of this tree):

    • A commit touching only packages/atom/effect-atom/package.json (version bump) with no intent naming it:
      ::error::This change moves 2 publishable package(s) that no intent names: @systemfsoftware/effect-atom, @systemfsoftware/effect-atom-react
      
      exit 1 — the step fails the PR.
    • The same change with a pending intent naming @systemfsoftware/effect-atom present:
      changeset gate: 2 publishable package(s) changed, each named by an intent — @systemfsoftware/effect-atom, @systemfsoftware/effect-atom-react
      
      exit 0 — the step passes.
    • A commit touching no publishable package: changeset gate: no publishable package changed (6 member(s)), exit 0.

Run locally on this branch: changeset-management check origin/main exits 0 ("4 publishable package(s) changed, each named by an intent"). It would fail without the restored intents, because the effect bump touches all four packages.

pnpm why effect --depth 0

effect@4.1.0
├── @effect/opentelemetry@4.1.0
├── @effect/platform-node@4.1.0
├── @effect/platform-node-shared@4.1.0
├── @effect/sql-d1@4.1.0
├── @endgame/site@0.0.0 (dependencies)
├── alchemy@2.0.0-beta.84 [deduped]
… (49 dependents in all)

Found 1 version of effect

pnpm test:sandbox and pnpm check:sfs-sources (run on the first commit's tree; no later commit touches what they exercise)

ok | 1 passed (7 steps) | 0 failed (6s)          # test:sandbox, tail; exit 0
check-sfs-sources: all 38 @systemfsoftware/* packages resolve from the .sfs-deps tarballs our flake inputs build   # exit 0

Also run locally, all exit 0: pnpm format:check, pnpm typecheck, pnpm lint, pnpm test (unit and browser projects), and the CI command pnpm exec turbo run format:check lint typecheck test --affected.

CONST-W3 declaration

This PR replaces its own grading instrument, and two follow-up edits (made in commits pushed after the first complete review, at the conductor's ruling) edit that instrument further. Declared per CONST-W3:

  • The gates ruleset's required status checks go from the ten contexts of the old six-workflow tree (check (format), check (lint), check (typecheck), check (test), check (dist), check (sfs-sources), journeys, rules, lint PR commits, changeset · shared tooling) to the single context check of the new tree. The change on GitHub is applied by the lead (operator step 3 below, a PUT by ruleset id), never by this branch.
  • The check.yml intent-gate condition (commit f354a83): the skip is now the negated pair !(github.head_ref == 'changeset-release/main' && head.repo.full_name == github.repository), so only a same-repo release PR skips the gate and every fork PR runs it.
  • The turbo.json gate edges (commits 5608b77, 77bfd52): typecheck now dependsOn test:types, and lint now dependsOn lint:tsgo, keeping those checks under the owner's single command after the old per-part CI jobs were deleted.

Authorities for all three: the owner's SYS-900 order, relayed by the lead on 2026-10-30 03:41Z, ordering the starter's CI/deploy shape onto this repo; conductor rulings ruling-fe14d (B3) and ruling-fe14e (B2, receipt#5, pre#0) under SYS-900, which ordered these exact edits.

Checks that stay out of CI

attw guards the published type shape, so it runs in the release workflow's new types job (pnpm attw, all four packages) before the shared release job, not in check. Verified locally: pnpm attw exits 0.

Dropped from CI on purpose, per the owner's ruling; each still runs locally through pnpm check:ci:

  • storybook-gherkin test:browser — needs Storybook and Playwright against a built bundle; the conformance suite in pnpm test still covers the spec engine.
  • check:sfs-sources — proved at install time: CI's offline install resolves every @systemfsoftware/* from the Nix-built store, which is the property it checks.
  • test:sandbox — the sandbox-proof suite exercises the Nix sandbox itself, not the workspace; it runs on the host locally.
  • journeys — end-to-end Playwright against a live dev server; e2e runs locally/CI-on-demand, not per-PR (OP28).

Kept under the owner's single command via turbo wiring: test:types is a dependsOn of typecheck, and lint:tsgo is a dependsOn of lint. (An earlier revision of this section wrongly said lint:tsgo was already wired; it was not, and turbo run lint skipped it. Both are wired now.)

Proof — pnpm exec turbo run typecheck lint --dry=json schedules both under the matching task (6 packages each; 39 tasks total):

["@endgame/site#test:types","@endgame/site-e2e#test:types","@systemfsoftware/effect-atom#test:types","@systemfsoftware/effect-atom-react#test:types","@systemfsoftware/oxlint-config-frontend#test:types","@systemfsoftware/storybook-gherkin#test:types"]
["@endgame/site#lint:tsgo","@endgame/site-e2e#lint:tsgo","@systemfsoftware/effect-atom#lint:tsgo","@systemfsoftware/effect-atom-react#lint:tsgo","@systemfsoftware/oxlint-config-frontend#lint:tsgo","@systemfsoftware/storybook-gherkin#lint:tsgo"]

The lint:tsgo edge itself, from pnpm exec turbo run lint --dry=json piped through jq '[.tasks[] | select(.taskId == "@systemfsoftware/effect-atom#lint" or .taskId == "@systemfsoftware/effect-atom#lint:tsgo") | {taskId, dependencies}]':

[
  {
    "taskId": "@systemfsoftware/effect-atom#lint",
    "dependencies": [
      "//#format:check",
      "//#lint:suppressions",
      "@systemfsoftware/effect-atom#build",
      "@systemfsoftware/effect-atom#generate",
      "@systemfsoftware/effect-atom#lint:tsgo",
      "@systemfsoftware/oxlint-config-frontend#build"
    ]
  },
  {
    "taskId": "@systemfsoftware/effect-atom#lint:tsgo",
    "dependencies": []
  }
]

The tsgo-0-63-brands intent is now major for effect-atom, effect-atom-react and storybook-gherkin: renaming the payload fields of published error classes is breaking.

What differs from the starter

  • apps/site/stacks/github.ts is the one intended difference in the stack. It lowercases GITHUB_REPOSITORY before naming the stack (process.env['GITHUB_REPOSITORY']?.toLowerCase()), so Owner/Repo and owner/repo do not make two stacks (SYS-910). Every other line is the starter's.
  • check.yml and release.yml: the two release-wiring differences above.
  • turbo.json: the starter's edits apply as written. frontend's typecheck already depended on build, so it keeps that and gains //#typecheck:node.
  • pnpm-workspace.yaml: the starter's lines, copied verbatim for alchemy, @alchemy.run/cloudflare and @alchemy.run/github, plus blockExoticSubdeps, the minimumReleaseAgeExclude entries and the rolldown override. frontend also pins @effect/sql-d1, which the starter does not; it moves to 4.1.0 with the rest. @effect/tsgo stays at 0.63.0. @alchemy.run/frontend-frameworks is dropped: apps/site declared it and imported nothing from it.
  • .mergify.yml: both condition lists require check-success = check instead of the ten deleted contexts; base, -draft, -label = hold, the kiro approval and #changes-requested-reviews-by = 0 are unchanged. Mergify config is not in #104; this is the owner-ordered fix for this roll-out.
  • AGENTS.md:34 describes three workflows, and the README keeps its Changesets row, because frontend still releases. commitlint.config.ts keeps its changesets comments for the same reason (the starter's edit calls releases plain main deploys, which is false here).
  • Everything else in #104 applies as written: bin/cloud egress for api.github.com, .github/rulesets/gates.json and scripts/check-branch-rules.ts (and its test) requiring only check, the check-env script, and the STRATEGY.md mutation line. The starter's plan document is not copied.

What the effect 4.1.0 bump broke, and the fix

  1. Typecheck. KeyValueStore.makeStringOnly now requires keys. The three test stores in packages/atom/effect-atom/tests/Atom.integration.test.ts gain keys: Effect.sync(() => Array.from(storage.keys())).
  2. Unit test. Atom.integration.test.ts › "Two requests for the next batch at the same time both arrive once the signal comes" failed deterministically with a kernel deadlock, because under 4.1.0 two concurrent Atom.pull requests can finish out of order and leave the atom on the shorter batch. packages/atom/effect-atom/src/atom.blueprint.ts: an accumulating pull now keeps the longest batch it has delivered and drops a late, shorter one. This is a behavior change in a published package (effect-atom); .changeset/effect-4-1-0-resolved.md is the change intent for it and for the bump, at patch.
  3. Browser tests. 11 files in effect-atom and effect-atom-react failed to import with process is not defined: @effect/platform-node-shared 4.1.0 reads process.platform at module load, and the browser projects import it through @systemfsoftware/vitest. The two browser projects' vitest configs now define process.platform. The proper fix is in the @systemfsoftware/vitest tarball, which this repo cannot change without bumping its flake input; the define is the in-repo stop-gap.
  4. Lint passed with no change.

Baseline check: on origin/main the same suites pass (350 tests in effect-atom), so both failures are from the bump.

Operator steps (the lead's; no value is printed or committed)

  1. Bootstrap on the host, outside every sandbox, from apps/site. The operator supplies the values from Doppler:
    CLOUDFLARE_API_TOKEN=… CLOUDFLARE_ACCOUNT_ID=… GITHUB_ACCESS_TOKEN=… \
      GITHUB_REPOSITORY=systemfsoftware/frontend SITE_DOMAIN=<hostname> \
      pnpm exec alchemy deploy --config stacks/github.ts --stage ci-credentials
    
    The env names and their scopes are in the README, "Credentials for CI". Afterwards confirm both secrets with gh secret list (names only).
  2. Set the repository variable ALCHEMY_DEPLOY=true, and SITE_DOMAIN to the same hostname.
  3. Update the gates ruleset (id 25612455) by PUT, not POST: gh api repos/systemfsoftware/frontend/rulesets/25612455 -X PUT --input .github/rulesets/gates.json, so it requires only check (the installed ruleset still names the ten old contexts and would hold this PR forever). Then run pnpm check:branch-rules to confirm the live ruleset and scripts/check-branch-rules.ts agree. Do this once this PR's check run is green and before merging.
  4. Observe the Done-when on frontend: a PR shows the preview-URL comment, its stage is destroyed when it closes, and a push to main deploys in one job.

…(SYS-900)

CI is two workflows, check.yml and deploy.yml, both copied byte for byte from
the starter's #104. deploy.yml is dormant until the repository variable
ALCHEMY_DEPLOY is true. Credentials are code: apps/site/stacks/github.ts mints
a scoped Cloudflare token into repository secrets, and the app stack posts the
preview URL as a comment.

- effect and every published @effect/* package at 4.1.0; alchemy and its
  cloudflare and github packages pinned to the starter's pkg.alchemy.run
  tarballs; @alchemy.run/frontend-frameworks dropped (unused)
- turbo.json, bin/cloud, package.json scripts check-env/deploy/destroy and
  the commitlint comments as in the starter
- the required status context is now `check` alone; the release gate,
  changesets, release.jsonc, the mutation shard planner and both composite
  actions are removed; STRATEGY.md and AGENTS.md job-shape lines updated
- stacks/github.ts lowercases GITHUB_REPOSITORY before naming the stack, so
  Owner/Repo and owner/repo are one stack

effect 4.1.0 fallout fixed here:
- KeyValueStore.makeStringOnly requires `keys`; the three test stores in
  Atom.integration.test.ts gain it
- concurrent Atom.pull requests could finish out of order and leave the atom
  on the shorter batch; an accumulating pull now keeps the longest batch
- @effect/platform-node-shared 4.1.0 reads process.platform at module load and
  the browser test projects import it, so those two projects define it
The queue conditions still named the ten contexts the old workflows reported
(check (format), journeys, rules, lint PR commits, changeset ...). After the
two-workflow cutover none of them reports, so Mergify would never queue a PR.
Both queue_conditions and merge_conditions now require check-success = check,
the one context the ruleset requires; the other conditions are unchanged.
…-900)

frontend publishes @systemfsoftware/effect-atom, effect-atom-react,
storybook-gherkin and oxlint-config-frontend, so the release flow stays.

- restore .changeset/ (README, ledger and the seven pending intents),
  release.jsonc and commitlint.config.ts exactly as on main
- restore release.yml with one line changed: ci-workflow is now check.yml,
  because ci.yml is gone and the release flow dispatches that workflow on the
  release PR branch (check.yml has workflow_dispatch)
- fold the change-intent gate into check.yml as a PR step that runs the
  devShell's changeset-management; a reusable workflow cannot be a step, and
  the release flow only needs the gate on pull requests. It skips the release
  PR branch, as the reusable workflow did, so changeset-check.yml stays
  deleted and the required context stays `check` alone
- restore the Changesets row in the README toolchain table
- AGENTS.md CI line now names three workflows
… (SYS-900)

The 4.1.0 bump changes what the four published packages resolve against, so
they get one change intent at patch, the bump the policy gives a dependency
floor raise. The published peer range on effect stays ^4, and the intent says
so; effect-atom's entry names the pull-atoms-keep-the-longest-batch fix.

effect-4-0-5-peer-floor.md's opening line claimed Effect now resolves to
4.0.5, which this bump makes false; it now states it in the past tense. Its
intent is untouched.
…(SYS-900)

CONST-W3 declaration. This PR replaces the grading instrument it is graded by:
the gates ruleset's required status checks go from the ten contexts of the old
six-workflow tree (check (format), check (lint), check (typecheck), check
(test), check (dist), check (sfs-sources), journeys, rules, lint PR commits,
changeset · shared tooling) to the single context `check` of the new
two-workflow tree. The authority is the owner's SYS-900 order, relayed by the
lead on 2026-10-30 03:41Z, which orders the starter's shape onto this repo;
the owner's review rules re-affirm it. The ruleset change on GitHub is
applied by the lead, not by this branch: PUT .github/rulesets/gates.json to
the installed `gates` ruleset (id 25612455) so it requires only `check`,
before merging.

- turbo typecheck now depends on test:types, so the owner's single command
  keeps running the type-level tests; lint already ran lint:tsgo and that
  wiring is unchanged
- release.yml gains a types job running pnpm attw (the published type shape
  of all four packages) before the shared release job, which now needs it;
  attw guards the publish, not the pull request, so it is not in check
- README section 5 documents updating an installed ruleset by PUT to its id
  and pnpm check:branch-rules, instead of only creating one
…sgo (SYS-900)

- tsgo-0-63-brands.md: the field renames are breaking, so the intent is now
  major for the three packages it names
- check.yml intent gate: a fork branch named changeset-release/main no longer
  skips the gate (the skip also requires the PR to come from this repository);
  the step still runs changeset-management check <base-sha>, which enforces
  the same rule the deleted changeset-check.yml did on main
- turbo lint now depends on lint:tsgo, so the owner's single command runs it;
  the PR body previously claimed it was already wired, which was wrong
The gate's skip ANDed the repository guard onto the whole condition, so every
fork PR skipped the intent gate. The skip is now the negation of the pair: a
branch named changeset-release/main from this repository skips, and a fork PR
- even from a branch with that name - runs the gate.

@kiro-systemf kiro-systemf Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conductor verdict: approved at f354a83. The review (rv-frontend-14-f354a83-1793378611) is complete, and CI check is green.

  • C1 (P0): the CONST-W3 declaration now names the intent-gate condition and both turbo edges, with their authorities.
  • B1 (P3): the lint -> lint:tsgo edge is shown by dry-run output.
  • The four P2s went to SYS-916.
  • Two wording corrections go into the PR body before the merge: the edits were made at the conductor's ruling, not the driver's, and the clause 're-affirmed by the owner's review rulings on this PR' comes out.
  • Merge needs the lead's PUT of the gates ruleset to require only the 'check' context (operator step 3). The check job's context name is 'check'.

@systemfsoftware-bot
systemfsoftware-bot merged commit 369bdc7 into main Oct 30, 2026
5 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.

2 participants