Skip to content

feat: add a reusable release workflow for Foundry packages - #16

Merged
toastygm merged 1 commit into
mainfrom
feat/12_reusable-release-workflow
Sep 1, 2026
Merged

feat: add a reusable release workflow for Foundry packages#16
toastygm merged 1 commit into
mainfrom
feat/12_reusable-release-workflow

Conversation

@toastygm

@toastygm toastygm commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

feat: add a reusable release workflow for Foundry packages

Six repositories carried near-identical copies of the changesets release
sequence — sohl, thalorna, kethira, harn-adventures, harn-ensemble and hm3 —
differing in a package name, one npm script and one word in two filenames.

That duplication had already produced its own failure twice. changesets/action
renamed every multi-word output to kebab-case at v2; a stale v1 name resolves to
the empty string, so '' == 'false' is always false, the gated step is skipped
and the job reports green while releasing nothing. One dependency bump
introduced that into every copy at once, and it was found in three only because
someone went looking after noticing it in one. Separately, harn-ensemble's job
still announces itself as sohl-kethira-basic.

The sequence now lives here once. Only two axes genuinely differ, so only two
are inputs: package-kind (system/module, which names both release assets)
and build-script. post-release-script is the escape hatch for the one
caller — sohl — that does something no other caller does, and it runs after the
Release exists so a package's follow-on work can never be why the Release was
not cut. build:pack-release is fixed: an input for an axis nothing varies on
is a knob that invites divergence.

Two guards that were missing or wrong:

  • The already-released check now branches on git ls-remote's exit code rather
    than on truthiness (The duplicate-release guard fails open: an unreachable remote reads as "this version is untagged" #13). The obvious if conflates "no matching ref" (2)
    with "could not reach the remote" (128), so a transient failure read as this
    version is untagged
    and proceeded to release. Every other guard in the file
    fails closed; that one failed open and did the irreversible thing.
  • The two release assets are checked non-empty before the Release is created. A
    Release is published the instant it exists and is what every installed copy
    updates from, so a pack script that failed quietly would produce a tag, a
    Release, and nothing to install.

The README records the standing rule the rename defect calls for — re-check
every steps.<id>.outputs.* against the pinned major's action.yml whenever
that pin moves — the adoption order, and the two-run success signal, since
"green while doing nothing" is the shape being guarded against and a single
green run does not disprove it.

No repository is migrated here. This adds the workflow; adoption is one
repository at a time, each verified by a real release. #13 stays open until
then: the six repositories still carry the fail-open guard in their own copies,
and centralising the fix does not reach them until they call this.

Closes #12
Refs #13

Six repositories carried near-identical copies of the changesets release
sequence — sohl, thalorna, kethira, harn-adventures, harn-ensemble and hm3 —
differing in a package name, one npm script and one word in two filenames.

That duplication had already produced its own failure twice. `changesets/action`
renamed every multi-word output to kebab-case at v2; a stale v1 name resolves to
the empty string, so `'' == 'false'` is always false, the gated step is skipped
and the job reports green while releasing nothing. One dependency bump
introduced that into every copy at once, and it was found in three only because
someone went looking after noticing it in one. Separately, harn-ensemble's job
still announces itself as `sohl-kethira-basic`.

The sequence now lives here once. Only two axes genuinely differ, so only two
are inputs: `package-kind` (`system`/`module`, which names both release assets)
and `build-script`. `post-release-script` is the escape hatch for the one
caller — sohl — that does something no other caller does, and it runs after the
Release exists so a package's follow-on work can never be why the Release was
not cut. `build:pack-release` is fixed: an input for an axis nothing varies on
is a knob that invites divergence.

Two guards that were missing or wrong:

- The already-released check now branches on `git ls-remote`'s exit code rather
  than on truthiness (#13). The obvious `if` conflates "no matching ref" (2)
  with "could not reach the remote" (128), so a transient failure read as *this
  version is untagged* and proceeded to release. Every other guard in the file
  fails closed; that one failed open and did the irreversible thing.
- The two release assets are checked non-empty before the Release is created. A
  Release is published the instant it exists and is what every installed copy
  updates from, so a pack script that failed quietly would produce a tag, a
  Release, and nothing to install.

The README records the standing rule the rename defect calls for — re-check
every `steps.<id>.outputs.*` against the pinned major's `action.yml` whenever
that pin moves — the adoption order, and the two-run success signal, since
"green while doing nothing" is the shape being guarded against and a single
green run does not disprove it.

No repository is migrated here. This adds the workflow; adoption is one
repository at a time, each verified by a real release. #13 stays open until
then: the six repositories still carry the fail-open guard in their own copies,
and centralising the fix does not reach them until they call this.

Closes #12
Refs #13
@toastygm
toastygm merged commit 535c64e into main Sep 1, 2026
1 check passed
@toastygm
toastygm deleted the feat/12_reusable-release-workflow branch September 1, 2026 23:35
toastygm added a commit that referenced this pull request Sep 1, 2026
#18)

#16 said the `build-script` input existed because "five of the six callers use
`build:noci` and sohl, whose `build` does not reinstall, passes `build`". The
second half is false. In all six repositories `build` is literally
`npm ci && npm run build:noci`, sohl included — so it does reinstall, and
sohl's release has been running a redundant `npm ci` on top of the install two
steps earlier.

Nothing varies on this axis at all: every caller can take the `build:noci`
default, sohl included. The input stays anyway, but on honest grounds — it is
the same named-script seam deploy-package-site.yml makes with `build:site`, so
a package whose script is named differently can call this without editing it.
An input every caller leaves alone is a seam; the one worth refusing is an
input that invites callers to differ where they currently agree, which is why
`build:pack-release` is still fixed.

The sohl caller example loses its `build-script: build` line accordingly. Left
in, it would have instructed the sixth adoption to carry the redundant install
across deliberately.

No behaviour change: the default was already `build:noci` and the only adopter
so far does not pass this input.

Co-authored-by: Tom Rodriguez <tmrodrig@gmail.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.

Five repositories carry near-identical release workflows, so one dependency rename broke several at once

1 participant