Skip to content

build(release): publish per-package workspace tarballs from the flake - #196

Merged
kiro-systemf[bot] merged 14 commits into
mainfrom
nix/workspace-tarballs
Oct 7, 2026
Merged

kiro-systemf[bot] merged 14 commits into
mainfrom
nix/workspace-tarballs

Conversation

@systemfsoftware-maker

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

Copy link
Copy Markdown
Collaborator

Per-package pnpm pack tarballs as flake outputs, so systemfsoftware's starter (U11b) can consume them from a pinned revision.

This repo is shared with the stryker session. Main is merged in (merge commit, no rebase). Since #209, release runs through pnpm-release-management's reusable workflows, so this PR changes no release script of its own.

Approved by Kiro (outer loop): .github/workflows/nix.yml is the Nix build of the per-package tarballs, which is Ryan's distribution rule. The changelog guard is approved too: it checks one mechanical property of the changelogs, not their quality.

Commits

  1. build(release): publish per-package workspace tarballs from the flake
    • flake.nix builds every workspace package with pnpm-release-management's lib.mkPnpmWorkspacePackages. The flake input follows pnpm-release-management main, locked at 603ecb4. That builder fetches the pnpm store as one fixed-output derivation per lockfile entry, keyed by the entry's integrity, so the flake carries no store hash and a lockfile change needs no hash edit. Outputs: packages.<system>.<package> (one .tgz each) and workspace-tarballs (all of them plus index.json).
    • The packaged set is all 17 publishable members.
    • packageManager moves from pnpm@11.21.0 to pnpm@11.27.0, the pnpm_11 Nix provides.
    • .github/workflows/nix.yml builds workspace-tarballs and rebuilds it bit-for-bit on x86_64-linux and aarch64-linux. The flake has no darwin systems since chore(repo): drop darwin from the flake #218, so there is no macOS leg.
  2. build(release): keep changelogs in the repo and stop pack reaching for a registry
    • With pnpm's default versioning.changelog.storage: registry, pnpm pack downloads the previous release's tarball to prepend its CHANGELOG.md, so an offline pack failed with ERR_PNPM_META_FETCH_FAIL. pnpm-workspace.yaml now sets storage: repository.
    • Each package's published history moves into packages/<dir>/CHANGELOG.md, copied byte-for-byte from the latest published tarball after checking it against dist.integrity; chore(release): version packages #200's parked sections are folded in.
    • scripts/guards/check-changelog-sections.ts (run by guard:projects, with a selftest) fails when a publishable package's CHANGELOG.md lacks exactly one ## <version> section for its current version, or when a parked section exists under repository storage.
  3. build(release): ship each package's changelog in its tarball (files lists CHANGELOG.md in all 17 packages).
  4. chore(release): record the changelog now shipped by stryker-js-cli-contract (the one intent the Changeset Check asked for).
  5. build(release): refuse an unknown changelog storage in the changelog guard
    • The guard lists packages through pnpm ls -r. A missing or misspelt versioning.changelog.storage fails with ChangelogStorageInvalid instead of switching the parked-section check off.
    • scripts/deno.json maps @std/yaml and @std/fs/expand-glob again; the guard always imported them, and build(release): consume the shared pnpm-release-management toolchain #209 trimmed them from the map.
  6. docs(release): describe repository changelog storage in the changeset README (the .changeset README and the ledger note; the note on synthesising parked changelogs is deleted).
  7. build(release): keep api-extractor out of dist and check tarballs against the source
    • Eleven api-extractor configs rolled their declarations up over the package's own published types file, so the shipped types depended on whether api:check had run, and on timing in a turbo build: three clean builds gave three different stryker-js type chunks. Rollups now go to temp/<package>.d.ts, and dist holds only tsdown output. stryker-js's API report drops the Node_2 alias the old rollup introduced.
    • A new nix.yml job builds the workspace the way the flake does and runs scripts/guards/check-tarball-contents.sh. The script requires every flake tarball to hold exactly the files pnpm pack takes from the source tree, with byte-identical CHANGELOG.md. Its sabotage step edits one tarball's CHANGELOG.md and requires the check to fail.
  8. chore(release): fold the 17.0.1 and 17.0.2 sections into stryker-js's changelog (chore(release): version packages #204 and chore(release): version packages #208 parked them under registry storage).
  9. ci(release): pin the release toolchain that reads package changelogs, now superseded by the merge of main.
    • release.yml and changeset-check.yml call pnpm-release-management's reusable workflows at @main with tools-ref: main. Main's release step (pnpm-release-management fix(repo): generate release notes in the publish job before asserting them #22) asks pnpm for the changelog storage mode and, under repository, takes the ## <version> section of the package's own CHANGELOG.md. release.jsonc drops changelogDir.
  10. chore(repo): merge main into nix/workspace-tarballs: takes main's Linux-only flake systems and its @main workflow callers.
  11. build(release): version through the changesets strategy and drop the macOS tarball leg
  • pnpm-release-management main replaced versioning.strategy: "pnpm" with "changesets", so the changeset check refused release.jsonc. It now sets "changesets": the changesets libraries version each package from the pending intents.
  • nix.yml loses its aarch64-darwin leg.
  1. build(release): take the pnpm store from one fetch per lockfile entry: re-locks pnpm-release-management to 603ecb4 (its build(deps): bump @effect/vitest from 4.0.0-rc.112 to 4.0.0-rc.115 #14) and removes the single store hash.

Proof (local, on a75e156 unless noted)

  • Three clean pnpm build --force runs give byte-identical stryker-js .d.mts chunks, and pnpm build exits 0.
  • On 2e8ef58: check-tarball-contents.sh passes on all 17 flake tarballs. Both of the job's steps, run from the workflow text, pass: the real check, and the sabotage, which is refused with tarball CHANGELOG.md differs. workspace-tarballs rebuilds bit-for-bit (nix build --rebuild).
  • On 2e8ef58, in a scratch worktree that was never pushed: pnpm add -w -D is-number@7.0.0 --lockfile-only changes pnpm-lock.yaml, and workspace-tarballs still builds with no flake edit. The same change on d260683, which still had the single store hash, fails with a fixed-output hash mismatch.
  • Changelog guard: selftest (10 cases) and real run green; a copy with storage: repostiory exits 1 naming the bad value.
  • pnpm guard:projects, pnpm test:scripts, the changeset check, actionlint and shellcheck exit 0.
  • On d260683: pnpm check:ci exits 0. The changeset check from pnpm-release-management main exits 0 and lists all 17 changed packages, each with an intent. check-tarball-contents.sh passes on all 17 flake tarballs. check-changelog-sections passes on all 17 packages, each with one section for its version. All 11 CI checks are green.

Open: a throwaway version bump from pnpm-release-management main fails with VersionIntentMalformed: path=package.json, because the root package.json has no version. Open pnpm-release-management #28 covers that. Main already fails earlier, because its release.jsonc still sets "pnpm". Until #28 lands, the next release here stops at the bump step.

The flake now calls pnpm-release-management's mkPnpmWorkspacePackages, which
runs pnpm pack once per publishable workspace package against the pnpm store
the lockfile already pins. It exposes workspace-tarballs plus one derivation
per package, and throws if a package name collides with a flake output.

packageManager moves to pnpm@11.27.0 to match the pnpm_11 the Nix build runs,
so the packed tarballs come from the same pnpm the repo pins. A no-network
`pnpm install --frozen-lockfile --offline` leaves pnpm-lock.yaml
byte-identical: sha256
4a158f16200ef06494a2b21b3af0c96a672d988cc15094a098a7877ed1c7790a before and
after, header included.

The pnpm store that lockfile resolves to hashes to
sha256-kRv6HtXsqj4Tbf1ZdMETrb8fjsVvEchyqPmcix44vtw=, the flake's
fetchPnpmDeps hash, measured by blanking it and reading the mismatch.

The Nix workflow rebuilds workspace-tarballs twice and requires both to be
bit-for-bit identical on x86_64-linux, aarch64-linux and aarch64-darwin.
…r a registry

pnpm 11's pack composed the changelog with versioning.changelog.storage left at
its default, registry: for a package with a pending
.changeset/changelogs/<name>@<version>.md it downloaded the previous release's
tarball CHANGELOG.md from the registry. Packing a package with pending intent
therefore failed without network (ERR_PNPM_META_FETCH_FAIL) and made npm the
source of truth for published history.

versioning.changelog.storage: repository makes pnpm's release step write the
new section into packages/<dir>/CHANGELOG.md and pack that file as-is.
Every publishable package now carries its CHANGELOG.md, seeded verbatim from
its latest published tarball (dist.integrity verified against the download), so
the repo holds the full history up to the latest release. Pack still ships only
what a package's files field names, so a package that does not list
CHANGELOG.md ships without one.

The version-packages release of a74531e ran under registry storage and parked one section per bumped
package under .changeset/changelogs, which nothing reads under repository
storage. Each of the eight publishable sections moves byte-for-byte into its
CHANGELOG.md under its version heading, the private stryker-e2e-core section is
dropped, and no parked file remains.

scripts/guards/check-changelog-sections.ts, run by guard:projects with a
selftest, fails when a publishable package's CHANGELOG.md lacks exactly one
section for its current version, or when a parked section exists under
repository storage. Removing the 0.3.1 section from stryker-js-cli-contract, or
adding a parked file, makes it exit 1

An offline `pnpm pack` in packages/stryker-js now exits 0; with storage set
back to registry the same no-network command fails with
ERR_PNPM_META_FETCH_FAIL
With changelog storage in the repository, pnpm packs a package's CHANGELOG.md only when its files list names it, so 16.0.1 would have shipped without the changelog 16.0.0 carried. Every publishable package now lists CHANGELOG.md; nothing else in those manifests changes
…guard

check-changelog-sections lists the publishable packages through pnpm ls -r
and reads versioning.changelog.storage from pnpm-workspace.yaml. A missing
or misspelt storage now fails the guard with ChangelogStorageInvalid
instead of switching the parked-section check off; the selftest covers
both. scripts/deno.json maps @std/yaml and @std/fs/expand-glob again, which
the guard has always imported and #209 trimmed from the map
… README

Under storage: repository pnpm writes each release's section into the
package's CHANGELOG.md and parks nothing under .changeset/changelogs/. The
.changeset README and the ledger note now describe that cycle, and the note
about synthesising parked changelogs is deleted with the fallback it
described
…inst the source

Eleven api-extractor configs rolled their declarations up over the
package's own published types file, so what shipped depended on whether
api:check had run, and stryker-js, which inlines its siblings' types, came
out differently from one clean build to the next. The rollups now go to
temp/, dist holds only tsdown output, and three clean pnpm builds give
byte-identical stryker-js chunks. stryker-js's API report drops the
Node_2 alias that the old rollup introduced.

A new Nix CI job builds the workspace the way the flake does and checks
that every flake tarball holds exactly the files pnpm pack takes from the
source tree, with the same CHANGELOG.md bytes. Its sabotage step edits one
tarball's CHANGELOG.md and requires the check to fail
… changelog

Release PRs #204 and #208 parked these sections under registry storage
before this branch moved to repository storage. Each now sits in
packages/stryker-js/CHANGELOG.md and the parked file is gone, so the
changelog guard passes
Under storage: repository nothing is parked in .changeset/changelogs/, so
the release tools must take each GitHub Release body from the package's
own CHANGELOG.md. pnpm-release-management's prm/repository-changelogs does
that, so release.yml and changeset-check.yml both call it, and
release.jsonc drops changelogDir, which only named the parked directory
Takes main's pnpm-release-management@main callers and its Linux-only flake systems; the workspace builder now comes from pnpm-release-management main, which needs the pnpm deps hash
…macOS tarball leg

pnpm-release-management main replaced versioning.strategy "pnpm" with "changesets", so the changeset check refused this config. The flake builds Linux only since #218, so the aarch64-darwin rebuild leg had no output to build
pnpm-release-management main (#14) builds the store from one fixed-output fetch per lockfile entry, keyed by its integrity, so the flake drops its single store hash and a lockfile change needs no hash edit. The input is locked at 603ecb4

@kiro-systemf kiro-systemf Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified: 11/11 green on ea8ad7f (CI 37672016928, Nix 37672017124), up to date with main, 0 threads, hunt clean; review predicate met after the per-entry store fix.

@kiro-systemf
kiro-systemf Bot merged commit f06f244 into main Oct 7, 2026
11 checks passed
@kiro-systemf
kiro-systemf Bot deleted the nix/workspace-tarballs branch October 7, 2026 19:27
kiro-systemf Bot pushed a commit that referenced this pull request Oct 8, 2026
… now requires (#230)

* build(release): call the reusable release workflow with the inputs and permissions it now requires

Every Release run since 65155c7 is startup_failure: the caller passed tools-ref, which the reusable no longer declares, omitted the required ci-workflow input, and granted no actions: write. The reusable runs version-management and github-release-management from this repo's dev shell, which did not provide them, and bump reads the root package.json version, which was absent (VersionIntentMalformed: path=package.json). The root takes 0.0.0: it is private, outside pnpm-workspace.yaml packages, never packed into workspace-tarballs, and the changesets strategy only reads it

Verdict-Semantics: unchanged

* build(release): keep the changelog directory version-management writes into

version-management bump writes each moved member's section to release.jsonc changelogDir (.changeset/changelogs) without creating it, and #196 left that directory empty, so git dropped it and bump failed with ChangelogUnwritable ... NotFound. The guard only reads *.md there

Verdict-Semantics: unchanged
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