Skip to content

canonical-artifact.json is a derived file under version control, and it serialises every branch that touches dist #648

Description

@MongLong0214

installer/canonical-artifact.json records checksums over all of dist/. Because dist/ is tracked (274 files) and every branch rebuilds it, any two branches that touch dist conflict on this file by construction — not on the source change, on the manifest.

The cost is not linear. Draining N concurrent dist-touching branches forces N(N-1)/2 rebuild-and-recommit cycles, because each merge invalidates the manifest of every branch still open. With four branches that is six forced canonical rebuilds that carry no review value.

What the manifest asserts, and who already proves it

Its fields are format, builder, runtimeAssets, source, artifact. builder, format and runtimeAssets are constants read from scripts/canonical-artifact-contract.mjs — source. source.sha256 digests the declared inputs, and artifact digests the built bundle. Both are computed from the tree. Every field is derivable from the commit it sits in.

Meanwhile .github/workflows/ci.yml already proves the property that matters, without reference to the manifest:

npm run build:canonical
find dist -type f -print0 | sort -z | xargs -0 sha256sum > "$first"
npm run build:canonical
find dist -type f -print0 | sort -z | xargs -0 sha256sum > "$second"
cmp "$first" "$second" || exit 1
git diff --exit-code -- dist/ installer/canonical-artifact.json

The canonical builder runs twice and must produce identical bytes, then the committed dist/ must equal that rebuild. That is the integrity claim, and it needs no manifest.

Why the committed copy cannot catch what it appears to catch

artifact:verify compares the committed manifest against a manifest recomputed from the tree. But the manifest is written by npm run artifact:manifest from whatever is in dist/ — the same step that produced dist, in the same command. It therefore cannot detect a bad dist; it can only ever confirm itself.

The one failure it does catch is rebuilt dist, forgot to refresh the manifest — a workflow slip that exists only because the manifest is committed. The check is paying for its own existence.

Where the manifest genuinely earns its place

At the tag. release.yml checks out the release commit, rebuilds canonically, and verifies — and a published statement of which builder image, which platform, which source inputs produced the shipped bundle is real supply-chain value a consumer can read. That value is needed at release, which is exactly where it is not conflicting with anything.

Nothing reads it at install time: install.sh and install.ps1 do not reference it.

Proposed shape

  • keep the canonical rebuild and git diff --exit-code -- dist/ on every branch — that is the real check and it is manifest-free
  • generate the manifest at release from the tag, and publish it as a release asset rather than tracking it
  • branches then never touch the file, and the conflict class disappears

Before acting, one thing to check

The grep above covers this repository only. If any downstream consumer — a marketplace, a mirror, an external verifier — reads installer/canonical-artifact.json at a path inside a tag, then it must remain a tracked file at tags and only the per-branch refresh should move. That question decides between the two shapes and I could not answer it from inside this repo.

Ownership

I designed this in #605 and got it wrong. I put a derived artifact under version control, which is the same shape ADR-0003 rules against for the SQLite index: derived state is a cache, not a source of truth. The provenance goal was right; binding it to every feature branch commit was not.

Not urgent — F-001 is the open P0 and this comes after it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions