Skip to content

feat(release): the changeset check compares unreleased packages' tarballs with their release tags - #66

Open
systemfsoftware-maker wants to merge 8 commits into
mainfrom
feat/pr-time-tag-digests
Open

systemfsoftware-maker wants to merge 8 commits into
mainfrom
feat/pr-time-tag-digests

Conversation

@systemfsoftware-maker

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

Copy link
Copy Markdown
Collaborator

Summary

A pull request could give a changed, already-released package a none intent. The changeset check accepted it, and only github-release-management plan on main later found that the package's tarball no longer matched the integrity its release tag records (TagIntegrityMismatch), so the Release run on main went red.

The reusable changeset check now catches this before merge, with plan's own comparison:

  • github-release-management digests <base> lists the released packages (publishable, tagged at their current version) that the change evidence names and the pending intents do not release.
  • With --tarballs <dir> it compares each one's flake-built tarball with its recorded integrity, using plan's rules, and refuses a mismatch with tag digests refused: <package>@v<version> changed without a release - recorded …, built …, first differing file …; give <package> a real bump in its intent, or revert its change.
  • changeset-check.yml runs the list after the intent verdict and builds the caller's .#workspace-tarballs only when the list is non-empty. A caller whose flake lacks that output fails with tag digests: cannot build .#workspace-tarballs to compare <packages> with their release tags - the caller's flake must expose workspace-tarballs, as release.yml requires.

plan is unchanged in behaviour: its per-member integrity resolution moved verbatim into released-tarballs.ts (releasedTarballChecks, unreleasedOf, ledgerEntries), and both plan and the new unreleasedTarballsCell call it. Repositories whose evidence names no such package see one note line and no build.

Scope boundary, stated in the note itself: under "strategy": "paths" a root-level or lockfile-only change can alter a released tarball without the evidence naming the package. That package is not compared here (pr-gate-runs-only-what-the-diff-affects), and plan still refuses it after merge. A vacant list now says tag digests: the change evidence names no released package that changes without a release.

Red before, green after

  • 8467248 (the e2e phase alone, unchanged product code): CI's check job failed in e2e/tests/pipeline.integration.test.ts at the new phase, expected +0 to be 1 (no failing step: the job passed the wrong none), run 38465179714.
  • efe46ec: the same phase passes, plus the R3 rerun without the flake output.

Real-tool evidence (Nix-built tools, scratch clone of this repository)

Case Result
Intent only, no packed change list empty, exit 0
adoption-adapter test file touched, tarball unchanged list empty (the turbo evidence does not name it)
adoption-adapter source comment touched listed with process-adapter; verify refused tag-annotation-lightweight for process-adapter@v0.0.0 (plan's refusal, as expected for the lightweight tags)
Comment-only change to two packages with annotated tags 2 package(s) match their release tags, exit 0 (flake build 18-22 s)
Real code change to tarball-adapter tag digests refused: @systemfsoftware/tarball-adapter@v0.0.0 changed without a release - recorded sha512-VFDI…, built sha512-FHQF…, first differing file package/dist/index.d.ts; …, exit 1
This branch, digests origin/main packages= empty, exit 0, 2.0 s

Checks on efe46ec

From the GitHub Actions REST API (run_watch), job start to finish:

Job Time
Changeset / a publishable-package change needs an intent 1m55s
CI / check 4m27s
E2E / containerised release pipeline 6m12s (tests 187 s)
Sandbox / tarballs rebuild bit-for-bit 2m03s
Sandbox / sandbox proofs 8m46s

In this repository's own Changeset job the gate takes 1.6 s and the new digests step 1.7 s; the job's first verdict step is unchanged, and the tarball build runs only when the list is non-empty. actionlint 1.7.12 on changeset-check.yml reports only the two job.workflow_repository/job.workflow_sha findings that main also reports.

Commits

Commit What
f3247bf, 64d88e8 plan and its doc-review dispositions
8467248 e2e phase alone (red on unchanged product code)
28c7f7f plan's released-tarball checks extracted, behaviour unchanged
d3023a3 the cell, digests command, workflow steps, README, intent
13dd9b1 ce-simplify-code: evidenceOf, taskOf, ChangeEvidenceRefusal schema, evidenceRefusalLines single-sourced
a635b6c code-review fixes
efe46ec cross-model review fix

Code review receipt

Report-only ce-code-review, FULL path. Base ef8970e78a5013fe9f8ec7daaebebce1ab23109e (main), reviewed head 13dd9b17aa6c658c76ac893791498cab54ba16c6, run 20261013-210000-ee16177c, 1135 changed lines.

  • Reviewers: correctness, testing, maintainability, api-contract, reliability, security, project-standards, learnings-researcher (pack grader), and the cross-model adversarial peer.
  • Cross-model peer: openrouter / deepseek/deepseek-v4.1-flash through omp (--no-tools, thinking high). The saved session transcript records one assistant turn, stopReason: stop, no model change. 1 P2 finding.
  • Pack resolver (packs-resolve.py, repository config): roots=6, errors=[], warnings=[], all from https://github.com/systemfsoftware/systemfsoftware.git, path compound-packs:
    • boundary-testing at 8d071180798f9ea82ef5a4fd877152dacc02f6b6
    • cell-architecture at 8d071180798f9ea82ef5a4fd877152dacc02f6b6
    • ci-wall-clock at 8d071180798f9ea82ef5a4fd877152dacc02f6b6
    • mutation-discipline at 8d071180798f9ea82ef5a4fd877152dacc02f6b6
    • package-topology at 8d071180798f9ea82ef5a4fd877152dacc02f6b6
    • schema-laws at 8d071180798f9ea82ef5a4fd877152dacc02f6b6
  • Rules: 53 graded. 25 matched (21 comply, 4 violate at the reviewed head, all 4 fixed in a635b6c), 28 not matched.

Matched pack rules

Cited lines are at the reviewed head 13dd9b1; each was checked against that tree.

Pack Rule Verdict File:line Final head
boundary-testing no-mocks-on-internal-glue.md comply apps/github-release-management/tests/unreleased-tarballs.integration.test.ts:56 -
boundary-testing port-fakes-pass-the-port-contract.md violate apps/github-release-management/tests/unreleased-tarballs.integration.test.ts:150 fixed in a635b6c: the feature now composes WorkspaceStoreLive, ChangesetsPortLive and ChangeEvidenceLive (unreleased-tarballs.integration.test.ts:122), no port double remains
boundary-testing real-system-oracles.md comply apps/github-release-management/tests/unreleased-tarballs.integration.test.ts:80 -
cell-architecture pure-decision-workflows.md violate packages/github-release-engine/src/compare-unreleased-tarballs.workflow.ts:54 fixed in a635b6c: the decide body is one Match.tagsExhaustive over command.targets (compare-unreleased-tarballs.workflow.ts:57), no if
cell-architecture sandwich-phase-order.md comply packages/github-release-engine/src/unreleased-tarballs.ts:104 -
cell-architecture decode-never-cast.md comply apps/github-release-management/src/boundary.ts:73 -
cell-architecture four-channel-contracts.md comply packages/github-release-engine/src/unreleased-tarballs.ts:104 -
cell-architecture service-and-layer-boundaries.md comply apps/github-release-management/src/main.ts:132 -
cell-architecture single-namespace-barrel.md comply packages/github-release-engine/src/index.ts:11 -
ci-wall-clock flake-outputs-build-once-on-main.md comply .github/workflows/changeset-check.yml:97 -
ci-wall-clock light-checks-run-the-pinned-tool.md comply .github/workflows/changeset-check.yml:101 -
ci-wall-clock no-dev-shell-on-verdict-lanes.md comply .github/workflows/changeset-check.yml:97 -
ci-wall-clock one-stuck-job-backstop.md comply .github/workflows/changeset-check.yml:101 -
ci-wall-clock pr-gate-runs-only-what-the-diff-affects.md comply .github/workflows/changeset-check.yml:92 -
ci-wall-clock setup-under-a-minute.md comply .github/workflows/changeset-check.yml:97 -
package-topology enumerated-entry-modules.md comply packages/github-release-engine/src/index.ts:11 -
package-topology import-time-inertness.md comply apps/github-release-management/src/main.ts:132 -
package-topology one-access-path.md comply packages/release-language/src/index.ts:1 -
package-topology public-signature-types.md comply packages/github-release-engine/src/index.ts:52 -
package-topology surface-changes-are-versioned.md comply .changeset/unreleased-tarballs-compared-at-pull-request.md:2 -
schema-laws tagged-unions-over-state-by-presence.md violate packages/github-release-engine/src/compare-unreleased-tarballs.workflow.ts:41 fixed in a635b6c: one required targets union (compare-unreleased-tarballs.workflow.ts:48)
schema-laws cross-field-invariants-as-struct-checks.md violate packages/github-release-engine/src/compare-unreleased-tarballs.workflow.ts:52 fixed in a635b6c: the packages/comparison pairing is a union variant, nothing to cross-check
schema-laws data-only-schema-classes.md comply packages/github-release-engine/src/compare-unreleased-tarballs.workflow.ts:38 -
schema-laws invariants-as-refinements.md comply packages/github-release-engine/src/unreleased-tarballs.ts:53 -
schema-laws tests-own-no-schemas.md comply apps/github-release-management/tests/unreleased-tarballs.integration.test.ts:36 -
Not matched (28), each with its reason
  • boundary-testing / fake-and-real-store-laws.md: no store with a declared law suite is faked; the rule states "A port with no law suite is not a store, and the store rules do not apply to it", which is the case for WorkspaceStore/ChangesetsPort here.
  • boundary-testing / pin-dependency-semantics.md: no third-party driver/SDK is adopted, upgraded or wrapped, and no vendor protocol assumption is documented.
  • boundary-testing / staged-protocol-evidence.md: no new multi-step connection/handshake/readiness protocol is modelled; git and tarball access go through existing adapters.
  • cell-architecture / callable-vs-resource-syntax.md: no new ADT/blueprint/handle is given callable or interface-object invocation syntax.
  • cell-architecture / handle-state-privacy.md: no per-instance state or driver token is stored on a handle/service; no storage mechanism chosen.
  • cell-architecture / pipeable-dual-parity.md: no combinator or fluent operation is authored on a blueprint/handle.
  • cell-architecture / pipeline-composition.md: the new cell is run directly by the CLI; no two cells are composed with Cell.andThen-style combinators.
  • cell-architecture / resource-vs-handle-duality.md: no cold blueprint vs runtime handle separation is designed.
  • cell-architecture / scoped-lifecycle-boundaries.md: no acquired external target with a bounded lifetime, finalizer, or start/stop lifecycle is added.
  • cell-architecture / staged-lawful-builders.md: no blueprint/builder algebra is authored.
  • cell-architecture / store-serializable-unit-of-work.md: no read-decide-save against shared mutable state, and no serializable transaction or concurrency mechanism is chosen.
  • cell-architecture / store-unit-of-work-handle.md: no store port whose reads feed a save, and no unit-of-work handle is declared.
  • ci-wall-clock / gates-share-one-job.md: no pull-request gate job is split or matrixed; the modified job is a changeset check, which the pack carves out ("The commit-message, changeset and PR-title checks … are not gates here: they stay install-free jobs of their own").
  • ci-wall-clock / heavy-lanes-state-their-daily-minutes.md: no heavy lane is added or re-budgeted; the added step is a conditional build inside an existing light check job.
  • ci-wall-clock / heavy-suites-judge-every-main-commit.md: no postsubmit/main-only suite is added, moved, or removed.
  • ci-wall-clock / no-lane-grades-the-repos-own-docs.md: no CI job or check takes the repo's own docs, plans or layout as its subject.
  • ci-wall-clock / red-main-reverts-its-culprit-same-day.md: no postsubmit on-failure handling, P0 filing, or revert automation is added or changed.
  • ci-wall-clock / release-promotes-the-staging-proven-build.md: release.yml, the tag/publish job and the image rollout are untouched; the change adds a presubmit comparison that reuses the same derivation.
  • mutation-discipline / kill-survivors-with-contract-properties.md: no mutation report or surviving mutant is in scope; the new test is a behaviour test of the feature, not a test written to kill a reported mutant.
  • mutation-discipline / suppress-by-shape-in-an-ignorer.md: no Stryker ignorer, disable comment or mutator exclusion is added or changed.
  • mutation-discipline / unkillable-survivors-are-dead-code.md: no mutation baseline row is added, and no survivor reason about unreachable code is recorded.
  • package-topology / condition-branch-agreement.md: no condition branch, types-build change, or tsdown entry restructuring is made.
  • package-topology / declared-entry-points.md: no exports map entry, subpath or tsdown entry list changes; /render and the packages' existing specifiers are reused as-is.
  • schema-laws / arbitrary-filter-floors.md: no refinement or filter is added to a schema that property tests draw from, and no arbitrary/rejection-sampling failure is in scope.
  • schema-laws / law-failure-is-a-codec-defect.md: no generated round-trip or encode-stability law fails, and no transformation/composition/custom encoding is authored.
  • schema-laws / recursive-schema-suspend.md: no self-referential or mutually recursive schema is authored; no Schema.suspend/recursion-budget point arises.
  • schema-laws / refusals-beside-generated-laws.md: no new refinement widens a rejection boundary that generated laws would leave green; NonEmptyArray and the literals are stock refinements used, not authored.
  • schema-laws / rich-type-over-foreign-encoded.md: no third-party wire shape (Node error, HTTP status line, OTLP span, vendor response) is decoded or reshaped.

Findings and dispositions

# Sev Source Finding Disposition
1 P2 project-standards; packs schema-laws ×2, cell-architecture UnreleasedTarballsCommand encoded its state by an empty list and an optional comparison that the workflow probed with if Fixed a635b6c: one targets tagged union, matched exhaustively
2 P2 packs boundary-testing inline WorkspaceStore, ChangeEvidencePort, ChangesetsPort doubles in the feature Fixed a635b6c: real pnpm workspace, real intent file, real paths evidence over git; 13/13 app tests
3 P2 testing no turbo-strategy row for digests Rejected: routing is the one shared evidenceOf. This repository gates with turbo, so its own Changeset job runs digests through the turbo branch on every pull request (run 38470181768, step exit 0 in 1.7 s)
4 P2 DeepSeek peer under paths, un-evidenced tarball changes pass the check Fixed (wording) efe46ec: the vacant note names the evidence as its scope. Widening to every released package is rejected: it builds tarballs on every pull request, which pr-gate-runs-only-what-the-diff-affects and setup-under-a-minute forbid; plan still refuses after merge
5 P3 testing; reliability, maintainability, correctness, standards (gaps) R3's cannot build .#workspace-tarballs line untested Fixed a635b6c: the e2e stub fails like nix when the output is absent; the AE1 phase reruns the job without it and asserts the line, including the packages output value. CI E2E passed on efe46ec
6 P3 maintainability released-candidate selection and ledger default copied from plan Fixed a635b6c: unreleasedOf and ledgerEntries, called by both
7 P3 api-contract, correctness plan doc's KTD2/KTD3/U2 drifted from what shipped Fixed a635b6c: a new dated plan supersedes it
8 P3 correctness refusal could name an empty first differing file Fixed a635b6c: says every packed file matches, so the archive metadata differs
9 P3 maintainability ComparisonRefusal declared twice Fixed a635b6c: the cell imports the workflow's type
10 - found while fixing #2 git archive --mtime=@0 is read as "now", so the feature flaked across a second boundary Fixed a635b6c: calendar mtime; reproduced and proven with tar -tv
11 - DeepSeek peer (gap) the fixture modelled none as an empty release list Fixed a635b6c: a real none intent through ChangesetsPortLive
12 - DeepSeek peer (gap) refusal assertion skipped the integrities Fixed efe46ec: asserts both and that they differ
13 - DeepSeek peer (residual) this PR's changeset-engine: none might trip its own check Rejected, measured: count-deleted-packages.md releases changeset-engine as patch, and the check exits 0 on this branch locally and in CI
14 - DeepSeek peer (residual) gate and plan may project releases differently Fixed a635b6c: both call unreleasedOf over ChangesetsPort.plan()
15 - security, reliability (residual) verify step builds PR-controlled flake code Rejected: the Sandbox lane already builds this derivation from every pull request's tree (nix build --rebuild .#workspace-tarballs); no permission is added
16 - security (gap) package names reach $GITHUB_OUTPUT Rejected: names are PackageName-decoded (no whitespace, newline or =) and the workflow reads the output only through env:
17 - reliability (residual) git ls-remote has no explicit timeout Rejected: the job's timeout-minutes: 60 is the backstop one-stuck-job-backstop prescribes; plan makes the same remoteTags call
18 - testing, reliability, correctness (gaps) lightweight tag, missing tarball, two packages, matched render untested here Rejected, measured: the refusals are plan's verbatim-moved code; the real-tool table above exercises the lightweight refusal, two matched packages and the matched line
19 - maintainability, correctness (residual) GateRefusal arms unreachable; tags only <name>@v<version> Rejected: the arms are required for exhaustive matching; the tag rule is plan's, unchanged here
20 - api-contract (residual) callers on @main take the new step without a version boundary Rejected: callers pin the reusable workflow by SHA, and the README documents the step and the workspace-tarballs requirement
21 - correctness (gap) list mode failing on an evidence refusal untested Rejected: the intent-verdict step runs first with the same evidence and fails first

No P0 or P1. With every finding fixed or rejected on evidence, the validator stage had no open finding to judge.

Delta after review (13dd9b1..efe46ec)

One named correctness lens through omp on openrouter / deepseek/deepseek-v4.1-flash (tools off), given the outcome, the delta and the touched files. The saved transcript records one assistant turn, stop. 0 findings. Its residuals are covered above: the e2e tarball directory ends the phase in the same state it did before; no consumer reads the removed command fields (typecheck 32/32); R3's string and ChangesetsPortLive with a none intent are proven by CI E2E and the 13/13 app tests.

ce-simplify-code

Three lenses (reuse, quality, efficiency) over the settled diff. Applied in 13dd9b1: one evidenceOf and taskOf in release-language for both cells and apps, ChangeEvidenceRefusal as a schema, released-tarballs taking IntegrityCandidate, one dispatch in the digests render. Two lint gates moved the schema into Gate.schema.ts and taskOf beside the port.

ce-compound

Learning recorded and validated, held for a docs-only pull request after this one merges: docs/solutions/test-failures/git-archive-mtime-at-zero-means-now.md (git archive reads --mtime=@0 as now; a pin is proven by reading it back). validate-doc-claims, validate-frontmatter, the solution-doc checker and dprint all exit 0.

Privacy

Before each push and on this body: a scan of the diff, commit messages, branch name and body against all 54 of the org's private repository names, 0 hits. Mutation testing was not run locally.

…ls against their release tags

A changed publishable package passes the changeset gate once any intent names it, none included, and only plan on main compares its tarball with what its release tag records. The plan moves that comparison to pull-request time for the packages the diff affects and will not release, reusing plan's own resolution, and builds the flake tarballs only when such a package exists.
Coherence and feasibility found that the release app does not yet compose change evidence, that the refusal line had two specs, and that the scope wording forbade the extraction it asks for; the plan now adds ChangeEvidenceLive to the digests command, makes the digests command render one refusal line, and limits the scope to plan's behaviour. A caller with tags but no workspace-tarballs output now fails with a named line. Assumption 2 cites a cross-tree measurement: adoption-adapter's tarball built at main matches the integrity its tag recorded at 524505a. The DeepSeek peer's Sandbox-lane question is answered in KTD4: consumers run the reusable check, not this repository's sandbox.yml.
…the changeset check

The changeset check passes a pull request whose intent gives a changed, released package a none bump, and only plan on main later finds that its tarball no longer matches the integrity its release tag records. The new phase changes @e2e/alpha after its 1.1.0 release under a none intent and runs the reusable check job over the stubbed flake tarballs; it expects one failing step that names @e2e/alpha@v1.1.0. On this commit the job passes, so the phase fails.
@systemfsoftware-maker systemfsoftware-maker changed the title feat/pr time tag digests feat(release): the changeset check compares unreleased packages' tarballs with their release tags Oct 13, 2026
…ne function

The per-member resolution plan ran before verifyIntegrity (ledger state, ledger tag commit, annotation, tarball digest) moves unchanged into releasedTarballChecks, so a second caller compares a released package's tarball by exactly plan's rules. plan's behaviour and tests are unchanged.
…alls with their release tags

github-release-management digests <base> lists the released packages the change evidence names and the pending intents do not release. With --tarballs it compares each one's tarball with the integrity its release recorded, and refuses a mismatch with one line that names the tag, both integrities, the first differing file and the fix. The reusable changeset check runs the list after the intent verdict and builds the caller's .#workspace-tarballs only when the list is non-empty, so a pull request that releases what it changes pays one evidence pass and no build.

The decision lives in compare-unreleased-tarballs.workflow.ts; the change-evidence refusal lines move to process-adapter's evidenceRefusalLines so both apps print them from one place.
…k and the evidence refusal schema

Simplify review: both cells routed change evidence by strategy with the same five lines, both apps derived the gate task the same way, and the release app re-derived the evidence refusal union. release-language now exports evidenceOf and taskOf beside the port and ChangeEvidenceRefusal as a schema; the cells, both apps and the render use them. released-tarballs takes the existing IntegrityCandidate, and the digests render dispatches once. Behaviour and refusal lines are unchanged.
… test it over real adapters

Code review: the command encoded its state by an empty list and an optional comparison that the workflow probed; it now carries one tagged targets union that the workflow matches. plan and the cell share unreleasedOf and ledgerEntries instead of copying the candidate selection. The feature runs over a real pnpm workspace, intent file and paths evidence instead of inline port doubles, and its archives pin a calendar mtime because git reads @0 as now, which made a row flaky. The e2e phase now also proves the named line when the caller's flake has no workspace-tarballs output. The refusal says when every packed file matches instead of naming an empty file. The plan is superseded by one that matches what shipped.
…evidence names

Cross-model review (DeepSeek peer): under paths, a root-level or lockfile-only change can alter a released tarball without the evidence naming the package, so 'no released package changes without a release' overclaimed. The note now names the evidence as its scope. The refusal test also asserts both integrities and that they differ.
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