Publish Transaction
Buildchain release promotion is not just tag movement. A release can also publish external artifacts: npm packages, Python wheels, OCI images, binary archives, metadata manifests, or site deployment records. Those side effects are harder than Git refs because most registries are append-only: a failed rerun must know which artifacts already exist, which are still missing, and whether any existing artifact conflicts with the release material.
Buildchain v2 models that work as a release transaction.
Why This Exists
The old ABV workflow made Git refs the visible release authority. That was enough when “release” meant “create a version commit, move tags, and let downstream jobs react.” It is not enough when a single publish run must also upload packages and images.
The failure mode to avoid is:
- publish an external artifact;
- fail before moving the exact release tag or floating channel refs;
- rerun from a new job id with no memory of the artifact;
- either republish something different or move refs without proving the already-published artifact matches the release.
The transaction gives reruns a stable identity and a machine-readable state so Buildchain can resume safely. The identity is:
repository + version + source_sha + target_ref
It is not the GitHub Actions run id.
Durable State
actions/promote-buildchain-ref stores release transaction state in a
machine-managed Git branch:
refs/heads/buildchain/release-state/<version>
The branch contains:
state.json
evidence.json # present after publish evidence exists
The local .buildchain/release-state/... and
.buildchain/release-evidence/... files are working copies. They are useful for
local inspection and lifecycle commands, but they are not the durable truth for
GitHub-hosted reruns. On action startup, Buildchain reads the durable state ref
first, restores the local working copies, and only then decides whether to
publish, repair, or finalize.
Every meaningful state transition is written to the durable ref before public release refs move. Release-state GitHub API reads and writes use retry/backoff for transient service failures such as HTTP 5xx responses, connection resets, timeouts, and “other side closed” socket failures. If the durable write still cannot be persisted after retries, the action fails closed.
Durable release-state refs reserve their exact version even when the public exact
tag was never created. If a later machine run sees a failed or repair-required
state for vX.Y.Z-alpha.N and cannot resume it with the same transaction
identity, alpha version selection must advance to the next prerelease instead
of reusing or overwriting that failed transaction slot.
Lifecycle
Repositories declare publish work in .buildchain/buildchain.toml:
[publish]
mode = "publish-final-version"
auth = "trusted-publishing"
dist_tag = "latest"
package_set_order = "platforms-first-main-last"
main_package = "@kungfu-tech/libnode"
[lifecycle.publish]
commands = [
"python scripts/publish_wheels.py",
"node scripts/publish-images.mjs",
"node scripts/write-publish-evidence.mjs",
]
actions/promote-buildchain-ref runs lifecycle.publish only when
publish-transaction: "true" is set or when a publish-command input is
provided. The action sets:
BUILDCHAIN_VERSION
BUILDCHAIN_CHANNEL
BUILDCHAIN_SOURCE_SHA
BUILDCHAIN_TARGET_REF
BUILDCHAIN_RELEASE_STATE
BUILDCHAIN_EVIDENCE_DIR
BUILDCHAIN_RELEASE_SHA
BUILDCHAIN_RELEASE_MATERIAL_SHA
BUILDCHAIN_PUBLISH_TOOLING_SHA
BUILDCHAIN_PUBLISH_EVIDENCE
BUILDCHAIN_REQUIRED_ARTIFACTS
BUILDCHAIN_PUBLISH_MODE
BUILDCHAIN_PUBLISH_AUTH
BUILDCHAIN_NPM_DIST_TAG
BUILDCHAIN_PACKAGE_SET_ORDER
BUILDCHAIN_PACKAGE_SET_MAIN_PACKAGE
Buildchain itself uses this contract for npm publishing:
[lifecycle.publish]
command = "node scripts/npm-publish-transaction.mjs"
That script validates that package.json matches BUILDCHAIN_VERSION, runs
npm publish --access public --tag <BUILDCHAIN_NPM_DIST_TAG> through npm Trusted
Publishing, and writes npm artifact evidence before the promotion action moves
public refs.
BUILDCHAIN_RELEASE_MATERIAL_SHA is the source material whose artifacts must
match. BUILDCHAIN_PUBLISH_TOOLING_SHA identifies the publishing code. A repair
run may change tooling, but material drift fails closed.
Post-Publish Requirements And Artifact Provenance
publish-required-artifacts-json is a pre-publish family declaration, not a
request to guess registry digests. A descriptor must include kind + name; it
may omit ref and digest. The action resolves a missing ref to the exact
BUILDCHAIN_VERSION. Registries whose exact refs add a stable prefix or suffix
may instead declare a ref_template containing exactly one {version}, such
as v{version}. The template is expanded only after exact version selection,
so a resumed alpha transaction receives the newly selected prerelease rather
than the checked-out version. Declaring both ref and ref_template, using
another placeholder, or leaving unmatched braces fails before
lifecycle.publish. The action exports the normalized exact refs as
BUILDCHAIN_REQUIRED_ARTIFACTS, runs lifecycle.publish, and then requires the
final evidence to contain every exact member with a non-empty digest. Existing
callers may continue supplying exact refs and digests.
OCI publishers can opt into strict per-artifact provenance by adding
action: built or action: reused. Those artifacts carry two separate
coordinates:
content: the version, ref, source SHA, and material SHA that produced the immutable content;release: the exact current version/ref, target ref, source SHA, and release material SHA that bind that content into this release.
This distinction permits truthful cross-version reuse without claiming that an
old OCI config was rebuilt from current material. For an OCI artifact with an
action, final evidence also requires platform, positive contract_major, and
verification containing a public manifest result, exact ref and digest,
platform, contract major, optional parent digest, evidence location, and a
passed named smoke policy. Buildchain cross-checks those values against the
artifact and current transaction. Missing family members and ref, digest,
content, release, or verification conflicts enter repair_required before
public refs move.
Example reused OCI evidence entry (the pre-publish requirement may omit
ref, digest, release, and the observed verification values):
{
"group": "image",
"kind": "oci",
"name": "ghcr.io/kungfu-systems/base-linux",
"ref": "1.2.0-alpha.3",
"digest": "sha256:...",
"action": "reused",
"platform": "linux/amd64",
"contract_major": 1,
"content": {
"version": "1.1.9",
"ref": "1.1.9",
"source_sha": "<source-sha>",
"material_sha": "<material-sha>"
},
"release": {
"version": "1.2.0-alpha.3",
"ref": "1.2.0-alpha.3",
"target_ref": "alpha/v1/v1.2",
"source_sha": "<current-source-sha>",
"material_sha": "<current-material-sha>"
},
"verification": {
"public_manifest": true,
"ref": "1.2.0-alpha.3",
"digest": "sha256:...",
"platform": "linux/amd64",
"contract_major": 1,
"evidence": "registry-inspect.json",
"smoke": {
"policy": "manifest-contract",
"passed": true,
"evidence": "smoke.json"
}
}
}
Release Modes And Auth
Buildchain distinguishes two npm release modes:
| Mode | Use | Auth | npm operation |
|---|---|---|---|
publish-final-version |
normal alpha or stable publication | trusted-publishing |
`npm publish --tag <alpha |
promote-existing-version |
same-version alpha-to-latest recovery | npm-token |
npm dist-tag add <pkg>@<version> latest |
The normal libnode path is publish-final-version: publish an alpha package set
such as 22.22.3-kf.3-alpha.0 with the alpha dist-tag, then publish a
distinct final package set such as 22.22.3-kf.3 with the latest dist-tag.
GitHub-hosted npm Trusted Publishing can authorize those npm publish calls
when the workflow grants id-token: write.
promote-existing-version is deliberately separate. npm Trusted Publishing does
not authorize arbitrary registry-management operations such as npm dist-tag add; it authorizes publish-time package provenance. Therefore same-version
promotion must declare auth = "npm-token". Buildchain runs an npm token
preflight with npm whoami before it writes any release transaction state or
moves a dist-tag. Missing token auth fails early with a contract error instead
of a late E401 after publish evidence has started to move.
For package sets, package_set_order = "platforms-first-main-last" makes the
main package the visibility gate. Platform package side effects are planned or
retried first, and the main package or main dist-tag move happens last.
When the transaction reaches complete, actions/promote-buildchain-ref
generates .buildchain/release-passport/buildchain.release.json and persists
the release-passport/* files into the durable buildchain/release-state/...
ref. The passport is the stable release artifact for agents and people: it
links the package set, npm publish evidence, dist-tag evidence, build summary,
platform artifact manifests, trusted publishing metadata, release-state ref,
durable release-state SHA, and transaction result in one schema. Consumer
repositories can set release-passport-product-name so the passport names their
product instead of the Buildchain default.
Evidence
The publish lifecycle must write JSON evidence. Buildchain validates common fields and required artifact identities before final refs move.
{
"schema": 1,
"version": "2.0.11",
"channel": "release",
"source_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"release_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"target_ref": "release/v2/v2.0",
"release_material_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"publish_tooling_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"artifacts": [
{
"group": "node",
"kind": "npm",
"name": "@kungfu-systems/example",
"ref": "2.0.11",
"digest": "sha256:..."
},
{
"group": "image",
"kind": "oci",
"name": "ghcr.io/kungfu-systems/example",
"ref": "2.0.11",
"digest": "sha256:..."
}
]
}
The generic contract is intentionally small:
version,channel,source_sha,release_sha, andtarget_refmust match the promotion run;- dist-tag promotion evidence is written beside the publish evidence as
dist-tag-evidence.jsonand is referenced from the generated passport; - required artifacts must appear in evidence;
- evidence used by a GitHub-hosted rerun must either be stored in the durable state ref or be reconstructed by a machine-verifiable consumer command;
- existing artifacts with the same identity and digest are accepted on rerun;
- missing artifacts can be published by the next run;
- an existing artifact with a different digest puts the transaction into
repair_required.
Artifact identity is group + kind + name + ref. A required artifact that omits
group matches any group with the same kind + name + ref.
Registry Truth Contract
Buildchain owns transaction orchestration, finalization ordering, durable state, and generic evidence validation. It does not embed registry clients for npm, PyPI, GHCR/OCI, GitHub Releases, S3, Conan, CMake packaging, or project-specific download pages.
Consumer lifecycle.publish commands own registry truth. A valid consumer stage
must be idempotent and machine-verifiable:
- inspect the target registry before publishing;
- accept an existing exact artifact only when version, identity, digest, and release-source binding match;
- publish missing required artifacts;
- reject conflicting existing artifacts and write evidence that lets Buildchain
move the transaction to
repair_required; - write
BUILDCHAIN_PUBLISH_EVIDENCEafter every successful inspect/publish cycle; - leave floating aliases such as npm dist-tags, PyPI stable markers, OCI floating tags, GitHub Release “published” status, or download-page stable links to a finalization step after Buildchain evidence validation.
The first-class adapter surface is command-based. Projects may wrap npm, PyPI, GHCR/OCI, GitHub Release assets, archives, SBOMs, provenance, or checksums however they need, as long as they emit the common evidence contract.
States
The state machine is:
prepared -> publishing -> published -> finalizing -> complete
| | |
v v v
publish_failed repair_required failed_permanently
|
v
abandoned
Supported states:
| State | Meaning |
|---|---|
prepared |
Transaction identity was created, but publish has not started. |
publishing |
Publish lifecycle is running or may have been interrupted. |
publish_failed |
Publish command failed before valid evidence was produced. |
published |
Evidence is valid; refs have not necessarily finalized. |
finalizing |
Buildchain is moving exact/floating refs or needs a later run to do it. |
complete |
Required evidence is valid and refs have finalized. |
repair_required |
Existing evidence or artifact state conflicts with expected release material. |
abandoned |
A human or controlled process abandoned this transaction, usually because a newer version supersedes it. |
failed_permanently |
Recovery should not continue without explicit override. |
repair_required, abandoned, and failed_permanently fail closed unless the
operator passes an explicit override. That override is for controlled repair
runs, not normal retry behavior.
Ref Ordering
When publish transactions are enabled, promotion order is:
- verify target source and governance;
- create or reuse the version-state release commit;
- acquire or resume the release transaction;
- run
lifecycle.publishor accept already-valid evidence; - validate evidence and required artifacts;
- move exact release/prerelease tag;
- move floating tags and channel refs;
- mark the transaction
complete.
When a protected channel requires a generated version-state pull request, the
first run can stop at finalizing after registry publication. If the reviewed
merge commit later contains that exact transaction release material but the
exact tag is still absent, a retry performs finalization only: it reloads the
same durable source, release material, tooling, evidence, version, and target
bindings; creates the exact and floating tags at the transaction release SHA;
and completes the passport from the transaction source tree. It does not rerun
the provider mutation and does not authorize the newer composite channel tree
as published material. A different source tree still requires a new version and
a fresh release candidate.
Deferred binary dispatch, controller-evidence bundling, and any consumer
publication commit are skipped while finalization-needed=true. They run only
after the exact public tag and complete release passport exist.
Consumer products that expose a signed well-known channel can opt into one
additional, deliberately final step with publication-commit-command. Before
that command runs, Buildchain has already completed the transaction, created
the public GitHub Release, and uploaded every release-passport file plus the
explicit PR-stage payload files selected by
github-release-payload-patterns. The command is therefore a commit point for
discovery authority, not another artifact publisher.
The command receives the exact version, source SHA, release SHA, release tag,
release passport path, and downloaded payload directory through
BUILDCHAIN_PUBLICATION_COMMIT_*. Optional consumer-owned dispatch/API
credentials and private signing material are exposed separately as
BUILDCHAIN_PUBLICATION_COMMIT_TOKEN and
BUILDCHAIN_PUBLICATION_COMMIT_SIGNING_KEY; Buildchain never logs, persists,
or interprets either value. The command must write
.buildchain/publication-commit/evidence.json (or another declared path below
.buildchain/) with this contract:
{
"schema": "kungfu-buildchain-publication-commit-evidence/v1",
"status": "passed",
"identity": {
"version": "4.0.0-alpha.2",
"sourceSha": "<source-sha>",
"releaseSha": "<release-sha>",
"releaseTag": "v4.0.0-alpha.2"
},
"publication": {
"url": "https://example.test/.well-known/product/alpha.json",
"payloadRoot": "sha256:<64-lowercase-hex>"
},
"readback": {
"status": "passed",
"url": "https://example.test/.well-known/product/alpha.json",
"payloadRoot": "sha256:<same-root>"
},
"recovery": {
"previousAuthority": "preserved",
"rollbackReference": "sha256:<previous-root>"
}
}
Buildchain rejects stale evidence, identity drift, non-public or mutable URLs,
read-back root drift, and missing recovery evidence. It also rejects
standalone-binary-distribution=true with a final commit command because that
would queue product mutations after the authority moved. On any command or
read-back failure, the consumer must leave the previous well-known document
authoritative; Buildchain does not retry the command behind a successful
receipt.
If protected branch finalization is interrupted after publish evidence is
valid, the transaction can stop in finalizing and output
finalization-needed=true. A later run resumes from the same transaction state
and completes ref movement without republishing matching artifacts. New
Buildchain-managed promotions first try to finish generated version-state
bookkeeping with the promotion token directly. Before patching a protected
generated bookkeeping ref, Buildchain emits every configured required check on
the exact generated version-state commit so branch protection can
validate the automation path without a second build, then uses the generated
ref update token for the protected ref PATCH. If release finalization
bookkeeping is still rejected, Buildchain creates or reuses a same-repository
buildchain/version-state/* PR and leaves the transaction resumable with
finalization-needed=true. Strict alpha uses the same protected PR fallback
for both its target channel and subsequent dev reconciliation. A later
idempotent run continues only after the provider shows that the PR reached the
protected branch. The reusable wrapper binds that token to the run-scoped
github.token and rejects user, team, or alternate App bypass actors.
If finalization fails after an exact Git tag, a channel branch, or dev/alpha
sync ref has already moved, the next run reads the durable finalizing state
and continues from the recorded transaction. The current workflow SHA may be a
generated version-state commit, or a historical version-state merge commit, that
contains or corresponds to the transaction’s release_material_sha; it does not
have to equal the original source_sha or the transaction release_sha. Exact
tags are accepted when they already point at the transaction release/material
SHA or the finalized channel head. Floating
channel tags and dev/alpha refs are then retried idempotently, and the
transaction is marked complete only after those public refs are consistent.
Writing complete clears any stale failure value from earlier attempts, so
the durable state.json represents the successful final state instead of the
last transient error seen before a rerun.
An exact tag at an unrelated SHA is still a material conflict and blocks
recovery.
For anchored package versions, the package version and internal line tag are
separate transaction coordinates. A retry can correct a stale internal tag on
an unfinished published or finalizing transaction only when its validated
evidence and complete artifact set match the same package version, source,
release material, and target. Buildchain additionally requires that the stale
tag does not already point at the transaction and that the newly selected tag
is unclaimed or already points at accepted release material. No registry publish
command is rerun during this exact-tag rebind.
Governed retries distinguish unrelated channel advancement from advancement
made by their own durable transaction. An unrelated descendant remains an
auditable superseded-promotion no-op. When the target ref is exactly the
recorded release_sha for the requested source, target, and expected version,
Buildchain resumes finalization, restores publish evidence, and emits the
release-passport paths needed by downstream controller receipts.
Publication authority planning applies the same occupied-version rule as the later mutation step. If a current alpha transaction already contains published material and regenerating version state would create new release material, the planner advances to the next alpha before sealing authority. It never seals the old published version and then lets the publisher discover a different version inside the mutation boundary.
If finalization fails after an exact Git tag is created, the next run reads the
durable finalizing state, verifies the exact tag points at the recorded
release SHA, and retries the remaining floating refs. An exact tag at a
different SHA is a material conflict and blocks recovery.
CLI Recovery
Local recovery commands operate on the same state/evidence files:
node scripts/release-transaction.mjs inspect --version v2.0.11
node scripts/release-transaction.mjs recover --version v2.0.11
node scripts/release-transaction.mjs finalize --version v2.0.11
node scripts/release-transaction.mjs abort --version v2.0.11 --superseded-by v2.0.12
The CLI is a diagnostic and local repair surface. It reports the durable
state_ref, but remote durable-ref writes and public Git ref finalization are
owned by actions/promote-buildchain-ref, because that action runs inside the
same governed GitHub permissions and branch-protection checks as release
promotion. In other words, CLI finalize can mark the local transaction state
complete after valid evidence; the machine-operated public finalization path is
to rerun the promotion action.
When no state file exists, creation commands also require:
--repository kungfu-systems/buildchain \
--source-sha <sha> \
--release-sha <sha> \
--target-ref release/v2/v2.0 \
--channel release
Build-Images Follow-Up
build-images should consume this contract rather than inventing a separate
workflow rule. The expected integration shape is:
- image build writes OCI digests into publish evidence;
- required image families are passed through
publish-required-artifacts-jsonbefore their final digests are known; - mixed built/reused evidence preserves content provenance separately from the current release binding;
- reruns check GHCR or the target registry and accept existing images only when tag and digest match;
- preview or alpha image tags remain non-stable until the transaction evidence validates;
- production image aliases move only after all required image artifacts are present and the Buildchain exact release tag has finalized.