status: draft period: ongoing theme: buildchain-reusable-build doc_type: technical-reference source_level: local-files confidence: high sensitivity: public evidence_grade: A review_state: unreviewed last_reviewed: 2026-08-03 ai_provenance: model_family: GPT-5 product: Codex generated_at: 2026-08-03 invisible_context: not asserted
Reusable Build Surface
Buildchain v3 provides a reusable build workflow for repositories that need
Buildchain’s release semantics but cannot be described as a simple Node package.
The first target shape is libnode: expensive native builds, multiple operating
systems, self-hosted runner labels, and release artifacts that must be auditable.
For Linux release artifacts, the build workflow can hand the sealed artifact,
platform manifest, and Release Passport to the separate GitHub-hosted keyless
attester. The compiler runner remains the recorded build identity; the attester
only signs and verifies immutable data. See
github-artifact-attestation.md.
Automatic Channel Router
The preferred consumer surface is one reusable workflow call. Consumers keep this configuration for both alpha development and stable release work:
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/build.yml@v3
permissions:
contents: read
issues: write
id-token: write
with:
working-directory: .
artifact-name: libnode
runner-preset: kungfu-v4-self-hosted
publish-channel: none
secrets: inherit
buildchain-channel defaults to auto. Selection uses this precedence:
- an explicit
buildchain-refprotected authority, train, SHA, or official channel; - an explicit
buildchain-channel: alpha|stable; publish-channel: alpha|release|major;- GitHub release prerelease metadata;
- a canonical semver tag;
- non-release PR, push, dispatch, schedule, and workflow-run events default to alpha.
The resolved runtime is vN-alpha for development and prerelease intent and
vN for stable release intent. Unknown custom publish channels, malformed
release events, and non-semver release-like tags fail before the build matrix;
they never guess alpha for a stable release.
The router automatically selects .buildchain/alpha-contract-lock.json for
alpha and .buildchain/contract-lock.json for stable. A repository can override
the common path with buildchain-contract-lock-path, or override one channel
with buildchain-alpha-contract-lock-path /
buildchain-stable-contract-lock-path.
Only repositories changing the default policy need extra routing input:
with:
buildchain-channel: stable
During v3 prerelease evaluation windows, canaries use build.yml@v3-alpha.
The same router then selects v3-alpha or stable v3 as the runtime.
Production consumers use build.yml@v3; this keeps the routing shell itself on
a stable ref.
The router is generated from .build.yml’s input/output surface. Run
node scripts/generate-channel-build-workflow.mjs after changing the advanced
build workflow; inventory and unit tests reject a stale generated router.
Advanced Workflow
Consumers that need direct workflow-shell or runtime control call the advanced surface:
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v3
with:
working-directory: .
artifact-name: libnode
runner-preset: kungfu-v4-self-hosted
linux-container-preset: kungfu-verify
artifact-name-template: "{artifact}-{platform}-{sha}"
artifact-paths: |
dist
build/stage
pre-upload-transport-smoke-scenario-path: .buildchain/auditable-demo.json
pre-upload-transport-smoke-artifact-root: .
expected-artifacts-json: >-
{"minFiles":2,"requiredPaths":["dist/libnode.tar.gz","dist/checksums.txt"]}
process-summary-path: .buildchain/diagnostics/process-summary.json
release-candidate: true
publish-channel: release
publish-source-ref: publish-gate/release/v22/v22.22/22.22.3-kf.0
For a standalone Linux binary demo, the optional pre-upload transport smoke
uses the same declarative scenario as the later capture workflow. Buildchain
copies the distribution containing the scenario metadata, strips file execute
bits to model GitHub Artifact transport, restores only the declared
digest-bound executable closure, and runs transportSmoke before either the
GitHub Artifact or S3 relay upload step. The smoke must be non-interactive and
is hard-capped at 60 seconds. Omitting the input preserves the ordinary build
surface; enabling it requires a scenario with transportSmoke.
runner-preset is the stable first-class surface for known runner fleets:
| Preset | Platforms |
|---|---|
github-hosted |
ubuntu-24.04, macos-latest, windows-2022 |
kungfu-v4-self-hosted |
Kungfu Linux x64, macOS ARM64, and Windows x64 self-hosted runner labels |
kungfu-v4-native |
Kungfu Linux x64, Linux ARM64, macOS ARM64, and Windows x64; Linux ARM64 uses GitHub-hosted ubuntu-24.04-arm |
custom |
Requires platforms-json |
Set self-hosted-offline-fallback: true to inspect every exact-label
self-hosted lane from the trusted Buildchain workflow shell before the matrix
starts. A lane with no matching online runner is replaced independently by its
supported GitHub-hosted runner: Kungfu Linux x64 uses ubuntu-24.04, macOS ARM64
uses macos-15, and Windows x64 uses windows-2022. Online-but-busy runners
remain online and keep their declared self-hosted route. Organization-owned
repositories inspect organization runner inventory so selected-repository runner
groups are not mistaken for an empty repository runner inventory. If the
inventory token, permission, or API is unavailable, Buildchain preserves the
original matrix instead of guessing that the fleet is offline. The public
workflow output runner-routing-json records only de-identified counts,
inventory scope, and routing decisions.
with:
runner-preset: kungfu-v4-native
self-hosted-offline-fallback: true
secrets:
BUILDCHAIN_PROMOTION_TOKEN: ${{ secrets.KUNGFU_GITHUB_TOKEN }}
Callers can still provide a custom matrix with platforms-json. Each platform
object has:
| Field | Meaning |
|---|---|
id |
Stable artifact/platform key, such as linux-x64 |
name |
Human-readable job name |
runner |
JSON string passed to runs-on after fromJSON |
The runner field is intentionally a JSON string so callers can pass either GitHub-hosted runners or multi-label self-hosted runners without Buildchain guessing the labels.
Only include platforms that should run. GitHub schedules matrix jobs before steps execute, so a disabled entry with unavailable runner labels can still block the workflow queue.
fail-fast defaults to false, preserving the diagnostic behavior that
collects every platform result. Required promotion callers can set it to true
to cancel sibling native, container, and relay matrix lanes after the first
failure. This input changes scheduling only: it does not reduce the declared
platform matrix, turn cancellation into a pass, or alter artifact and release
admission.
Linux Job Containers
Linux build platforms can run inside a digest-pinned job container while macOS and Windows keep using native runners. This is the recommended way to remove moving Linux runner prerequisites from Buildchain consumers: the Linux host only needs a GitHub Actions runner, Docker, and network access; common verification tools come from the image contract.
Use the Kungfu verification image for lifecycle stages that need Git, jq, Python, uv, and fnm, but do not need native compilation:
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v3
with:
runner-preset: kungfu-v4-self-hosted
linux-container-preset: kungfu-verify
kungfu-verify resolves to:
ghcr.io/kungfu-systems/build-images/kungfu-verify@sha256:11f0ba64267ce88174a4f73a9bf833ff4e9c59cd16ec3d08a6432a06c2be6fb1
Callers that own a different Linux image can pass it explicitly:
with:
linux-container-preset: custom
linux-container-image: ghcr.io/example/project-build@sha256:<digest>
linux-container-image should be pinned by digest. A floating tag makes the
runner surface mutable and weakens Buildchain’s release evidence.
The workflow splits the matrix into two build jobs:
- Linux platforms go to
build-linux-containerwhen a Linux container is configured. - All other platforms go to
build-native.
Artifact names, manifest paths, expected artifact checks, publish-source locks, and aggregate summaries are the same in both jobs. The split is an execution detail, not a different artifact contract.
Native Rust Toolchains
Native lifecycle jobs can request an isolated Rust installation instead of depending on a self-hosted runner user’s PATH:
with:
setup-rust: true
rust-toolchain: "1.96.0"
rustup-dist-server: "https://rsproxy.cn"
rustup-update-root: "https://rsproxy.cn/rustup"
cargo-registry-index: ${{ vars.BUILDCHAIN_CARGO_REGISTRY_INDEX }}
setup-rust defaults to false, so existing consumers are unchanged. When it
is enabled, Buildchain installs rust-toolchain before the install, build, and
verify lifecycle stages on every native matrix platform. Windows uses the
official rustup bootstrap through cmd.exe and curl.exe into runner-temporary
Cargo and rustup homes, so it works under a restrictive PowerShell execution
policy and the service account does not depend on another user’s PATH or mutate
host toolchain state. Pin an exact toolchain for release builds. Linux container jobs continue
to obtain Rust from their digest-pinned image contract; Buildchain does not
mutate that container surface.
The rustup server inputs are optional and default to Rust’s official servers. Consumers behind a slow cross-border link may select a trusted transport mirror; rustup still verifies the selected toolchain’s distribution metadata and component checksums.
cargo-registry-index is also optional. When set, Buildchain exposes it to
Cargo as CARGO_REGISTRIES_CRATES_IO_INDEX for every native lifecycle stage,
so a self-hosted runner can use a repository or organization variable without
committing private LAN topology to public workflow YAML. The endpoint must be a
crates.io-compatible index whose config.json download contract serves the
matching checksum-verified crate archives. An empty value preserves Cargo’s
normal crates.io behavior.
The container image provides fnm but does not preinstall Node. Buildchain uses
fnm inside the container to install the requested node-version before it
runs Buildchain runtime scripts or lifecycle actions.
Do not use kungfu-verify for stages that need CMake, Ninja, ccache, Conan, or
Docker image publishing. Those should use a heavier native-build image or remain
on a host runner until their image contract is explicit.
Buildchain Runtime Override
Stable consumers should keep the reusable workflow pinned to stable refs such as
@v3. The optional buildchain-ref input is empty by default; empty means
Buildchain resolves and executes the stable runtime selected by the workflow
shell. The full train validation protocol is documented in
runtime-train-validation.md.
For one-off manual validation, a trusted maintainer can run the caller workflow with a temporary runtime override:
on:
workflow_dispatch:
inputs:
buildchain-ref:
description: "Temporary Buildchain runtime ref for trusted manual validation"
required: false
default: ""
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v3
with:
buildchain-ref: ${{ inputs.buildchain-ref || '' }}
Allowed override refs are deliberately narrow:
| Ref form | Meaning |
|---|---|
train/v3/v3.0/<capability> |
Temporary capability train under the active minor line |
refs/heads/train/v3/v3.0/<capability> |
Explicit branch ref for the same train |
<40-character SHA> |
Exact immutable Buildchain runtime commit |
Override requests fail closed unless the event is workflow_dispatch and the
actor has write, maintain, or admin permission on the caller repository. One
non-override exact-pin case is also admitted: when the reusable workflow itself
is invoked from an exact Buildchain SHA and buildchain-ref names that identical
SHA, the run records pinned-self. The input cannot select code other than the
already-running workflow shell, so protected push and pull-request publication
jobs can retain one immutable runtime root. Different SHA and train requests
still fail closed outside trusted manual dispatch.
Pull requests, including same-repository pull requests and fork-originated pull
requests, cannot select an independent buildchain-ref override. This keeps
automated PR builds on the stable or exact pinned-self runtime surface.
Every run resolves the runtime ref to an immutable SHA before checkout. The job
summary and aggregate build summary record the workflow shell ref, requested
runtime ref, resolved runtime ref, runtime SHA, stability class, trust decision,
and rollback ref. Train refs are development validation refs: they do not move
v3, vX.Y, vX.Y-alpha, npm dist-tags, or production release refs, and they
must not be pinned as long-term production dependencies.
Runtime override validates Buildchain runtime scripts, CLI code, local actions, config parsing, and lifecycle behavior. It cannot validate changes that require the outer reusable workflow YAML itself to change, such as new jobs, permissions, workflow outputs, or matrix topology. Those changes need a canary workflow path or a temporary explicit workflow ref.
Floating Ref Contract Lock
Stable consumers should use floating major refs such as @v3, but a floating
ref is not blind trust. Each released Buildchain ref carries a package-owned
runtime contract world in dist/site/buildchain-contract.json. Consumers may
keep a small lock file, .buildchain/contract-lock.json, recording the
Buildchain ref, resolved SHA, contract digest, compatibility digest, accepted
major line, and compatibility policy they reviewed.
The reusable build trust gate checks this lock before any heavy matrix job:
- resolve the Buildchain runtime ref, for example
v3, to an immutable SHA; - read
dist/site/buildchain-contract.jsonfrom that checked-out Buildchain ref; - read the consumer’s
.buildchain/contract-lock.json; - compare the accepted contract with the current contract.
SHA drift alone is not a failure. v3 is expected to advance. Buildchain only
fails fast when the accepted contract is no longer compatible, for example a
required input is removed, a required output disappears, a protected behavior
promise changes, or the major line changes. Additive changes such as optional
inputs, optional outputs, diagnostics, or documentation updates continue under
the default major-compatible policy.
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v3
permissions:
contents: read
issues: write
id-token: write
with:
buildchain-contract-lock-path: .buildchain/contract-lock.json
buildchain-contract-compatibility-policy: major-compatible
buildchain-contract-drift-issue-mode: compatible-and-breaking
When compatible drift is detected, the build continues and Buildchain opens or updates a low-priority issue in the consumer repository. The issue records the old SHA/digest, new SHA/digest, compatibility result, workflow run, and the next action: review the Buildchain release notes and update the lock. When breaking drift is detected, the same issue path is used, but the trust gate fails before matrix build or publish work starts. If the workflow token cannot write issues, Buildchain writes a copyable issue body into the job summary.
The lock is intentionally small. It does not copy the full contract. The full contract remains in the Buildchain ref and package; the consumer records only what it accepted and the policy used to compare future floating-ref movement.
Advanced alpha-channel consumers select the matching workflow shell:
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v3-alpha
with:
buildchain-contract-lock-path: .buildchain/contract-lock.json
The runtime follows the called workflow through job.workflow_ref. Callers may
also pass buildchain-ref: v3-alpha explicitly; official floating refs are
ordinary channel selections and are allowed on pull requests and pushes. Train
refs and exact SHAs remain trusted manual overrides.
Shifu Cache Profile Passthrough
Buildchain can carry one trusted Shifu cache-profile reference and its exact digest into lifecycle execution. Its contract is an opaque reference and digest only. This surface is deliberately opaque: Buildchain does not fetch the profile, parse JSON, select cache services, rewrite bindings, decide fallback, or emit Shifu resolution evidence. Those semantics remain owned by the consumer’s pinned Shifu implementation.
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/build.yml@v3
with:
shifu-cache-profile-ref: ${{ vars.SHIFU_CACHE_PROFILE_REF }}
shifu-cache-profile-digest: ${{ vars.SHIFU_CACHE_PROFILE_DIGEST }}
The reusable workflow passes the pair as SHIFU_CACHE_PROFILE_REF and
SHIFU_CACHE_PROFILE_DIGEST to install, build, and verify lifecycle commands.
The consumer must invoke its Shifu cache-aware execution surface. An empty pair
preserves existing behavior; a consumer Shifu should fail closed when exactly
one value is present or the resolved bytes do not match the expected digest.
Use trusted repository or organization variables rather than PR-controlled files for private/LAN references. The variables must remain secret-free; any credentials use a separate provider-approved secret surface and must not be embedded in the profile reference. This passthrough is separate from Buildchain’s locked source checkout cache below: Buildchain owns checkout transport and source identity, while Shifu owns post-checkout execution cache bindings and receipts.
Locked Source Checkout Cache
Self-hosted runners that build large repositories can opt into a locked checkout
cache for both the consumer source and the Buildchain runtime. This changes only
the Git object transport. Buildchain still resolves publish-source-sha and the
runtime SHA before any build runner starts, checks out those exact commits, and
verifies each final HEAD plus the resolved consumer source tree SHA before
lifecycle commands run.
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v3
with:
runner-preset: kungfu-v4-self-hosted
checkout-cache-mode: auto
checkout-cache-mirror-url-template: ${{ vars.BUILDCHAIN_CHECKOUT_CACHE_MIRROR_URL_TEMPLATE }}
checkout-cache-reference-repository-template: ${{ vars.BUILDCHAIN_CHECKOUT_CACHE_REFERENCE_REPOSITORY_TEMPLATE }}
checkout-cache-fallback: github
checkout-cache-timeout-seconds: 60
checkout-cache-github-timeout-seconds: 600
checkout-cache-fetch-attempts: 3
checkout-history-mode: shallow
checkout-cache-mode accepts:
| Mode | Behavior |
|---|---|
off |
Default. Buildchain fetches the locked commit from GitHub. |
auto |
Try the trusted cache first; on miss, record the miss and fall back according to checkout-cache-fallback. |
require |
Require the cache to provide the locked commit and fail before lifecycle work if unavailable. |
checkout-history-mode defaults to shallow, preserving the bounded single-
commit transport used by ordinary builds. Set it to full only when a
consumer gate must inspect source ancestry, for example when an Alpha pull
request qualifies GitHub’s synthetic merge ref while retained evidence is
bound to an ancestor of the source-lock head. Full mode still verifies the
resolved immutable HEAD and tree; it changes only whether the advertised
source ref is fetched with depth one or with its reachable history.
The cache can be a local/LAN mirror URL template or a runner-local bare
reference repository template. Templates support {owner}, {repo},
{repository}, {repositorySlug}, and {sha}. The workflow also reads
repository or organization variables named
BUILDCHAIN_CHECKOUT_CACHE_MIRROR_URL_TEMPLATE and
BUILDCHAIN_CHECKOUT_CACHE_REFERENCE_REPOSITORY_TEMPLATE, so consumers can keep
private LAN topology out of repository YAML.
The GitHub-hosted trust gate resolves the reusable workflow shell to an exact
commit and uploads that shell’s small checkout bootstrap script. Native and
Linux-container build jobs download the bootstrap, then use the same cache
policy to obtain both the selected Buildchain runtime and consumer source at
their already resolved immutable SHAs. Keeping the bootstrap owned by the
workflow shell is important when @vN-alpha routes a stable release to an older
vN runtime: the stable runtime does not need to already contain the newest
checkout transport implementation. This also prevents a large direct
actions/checkout runtime clone from becoming a separate timeout path on
constrained self-hosted uplinks. The bootstrap artifact does not contain the
runtime repository and cannot move either selected ref.
Do not read cache URLs or reference paths from PR-controlled files such as
.buildchain/buildchain.toml. These values are trusted workflow inputs or repo/org
variables. Buildchain does not pass GitHub credentials to cache mirrors or
reference repositories. If it must fall back to GitHub, the workflow token is
used only for the GitHub fetch path. Cache attempts use
checkout-cache-timeout-seconds; the potentially larger GitHub fallback uses
the independent checkout-cache-github-timeout-seconds budget (600 seconds by
default). Buildchain fetches the advertised source ref before trying an exact
SHA, so a cache hit or stale-cache seed can contribute objects and the fallback
does not first waste a full timeout on an unadvertised SHA. Retryable timeout
and transient network failures use the bounded checkout-cache-fetch-attempts
budget; permanent failures stop immediately. Diagnostics record both timeout
budgets and the actual GitHub fetch attempts before exact HEAD/tree
verification.
Each platform diagnostics artifact includes source-checkout.json and embeds a
compact sourceCheckout summary in diagnostics.json: mode, transport,
hit/miss, fallback reason, duration, final HEAD verification, and tree
verification. Remote URLs are sanitized and local reference paths are represented
by a short display name plus fingerprint, not by secret-bearing credentials.
Runtime checkout evidence is uploaded separately as runtime-checkout.json,
including cache transport, fallback attempts, and exact runtime HEAD
verification, even when a later lifecycle step fails.
Auditable Compiler Cache
Consumers can prepare sccache on selected platforms after the install
lifecycle and before compilation:
with:
compiler-cache-provider: sccache
compiler-cache-platforms-json: '["windows-x64"]'
compiler-cache-required: true
The consumer remains responsible for installing and pinning the tool before
the preparation step. Buildchain probes its version, runs sccache --zero-stats, and writes
compiler-cache-preparation.json. The receipt binds the source commit/tree,
Buildchain runtime, platform, cache profile, and any declared dependency,
toolchain, or policy roots. It resets counters only; it does not delete cached
compiler outputs. The same preparation exports RUSTC_WRAPPER,
CMAKE_C_COMPILER_LAUNCHER, and CMAKE_CXX_COMPILER_LAUNCHER so Cargo and
CMake/Ninja compilation actually passes through the audited tool.
After the build lifecycle, Buildchain probes the reset counter set again. When
compiler-cache-required is true, the build fails closed if sccache observed
zero compiler requests or zero cacheable requests. This prevents an installed
but unbound sccache binary from being reported as an active compiler cache.
Final diagnostics admit sccache hit/miss outcomes as current-run evidence only
when that preparation receipt is present and valid. A bare sccache --show-stats result without the reset receipt remains cumulative and is
reported as unavailable for the current run. The preparation receipt is copied
into the small diagnostics artifact and sealed by
diagnostics-manifest.json.
When a Buildchain maintainer asks for downstream validation, the expected request is:
Buildchain train ready: buildchain-ref=train/v3/v3.0/<capability>.
Keep uses: ...@v3; run workflow_dispatch with that buildchain-ref and report the runtime evidence summary.
After validation succeeds, the Buildchain change should continue through the normal mainline and release path. Do not treat the train as a pending merge item; it is only a temporary fast-use, diagnostic, and rollback channel. It may remain for a retention window after release, with old trains handled by a separate periodic cleanup task.
Workflow Outputs
The reusable workflow exposes the resolved contract:
| Output | Meaning |
|---|---|
runner-preset |
Resolved preset, or custom when platforms-json was provided |
platforms-json |
Exact matrix JSON used by the build job |
platform-count |
Number of matrix platforms |
linux-container-enabled |
true when Linux platforms are routed through a job container |
linux-container-image |
Resolved digest-pinned Linux job container image |
build-summary-artifact |
Uploaded aggregate summary artifact name |
build-diagnostics-summary-artifact |
Uploaded aggregate diagnostics summary artifact name |
release-candidate-passport-artifact |
Uploaded PR-stage release-candidate passport artifact name when release-candidate is enabled |
release-candidate-passport-json |
Compact release-candidate passport JSON when release-candidate is enabled |
build-summary-json |
Compact aggregate JSON with platform count, file count, and byte total |
build-diagnostics-summary-json |
Compact aggregate diagnostics JSON with platform, lifecycle warning/error, diagnostics contract warning, and sidecar manifest warning totals |
trusted-event |
true when the event is trusted enough to reach build runners |
buildchain-runtime-ref |
Runtime ref selected after applying the empty-default or override policy |
buildchain-runtime-sha |
Immutable Buildchain runtime commit used by all runtime checkouts |
buildchain-runtime-class |
stable, alpha, authority, train, exact-sha, or development |
buildchain-runtime-override |
true when an authority, train, or exact-SHA buildchain-ref override was accepted |
buildchain-runtime-trust-decision |
Runtime override trust decision |
buildchain-contract-lock-status |
unchanged, compatible-drift, breaking-drift, missing-lock, non-floating-runtime, or first-release runtime-contract-unavailable |
buildchain-contract-lock-drift |
true when the floating runtime SHA or contract digest changed |
buildchain-contract-digest |
Current Buildchain runtime contract digest |
publish-channel |
Resolved publish channel requested by the caller |
publish-allowed |
true only when this event/ref may publish after verification |
publish-reason |
Human-readable reason for the publish gate decision |
publish-source-ref |
Gate source ref that was resolved before checkout |
publish-source-sha |
Exact source commit used by checkout, build, verify, and artifacts |
publish-source-locked |
true when a publish-gate/* source ref was explicitly locked |
publish-source-channel |
alpha, release, anchor, or major parsed from the source ref |
publish-source-line |
Product line parsed from source refs such as v22/v22.22 |
publish-source-consumer-version |
Consumer package version parsed from source refs |
release-manifest-json |
Resolved release manifest including source lock, version state, and anchor data |
The aggregate summaries are intentionally artifacts as well as outputs. GitHub
Actions matrix outputs are not a reliable place to carry every platform’s full
manifest, so Buildchain uploads each platform manifest and then emits one
aggregate build summary artifact after the matrix completes. Buildchain uploads
diagnostics-summary.json as a separate aggregate diagnostics summary artifact,
a compact rollup of each platform’s small diagnostics upload. The rollup keeps
per-platform runner facts, checked tool versions/missing tools, package
manager/cache directory details, compiler-cache availability, lifecycle timing,
process sampler context, and links back to the exact platform artifacts. Each
platform diagnostics upload includes diagnostics.json,
diagnostics-manifest.json, the lifecycle events.jsonl, and process sampler
sidecars when enabled, so slow-build diagnosis does not require downloading the
binary platform artifact or the aggregate build summary. The sidecar manifest
records the uploaded diagnostics files with bytes and sha256 hashes. Each
diagnostics.json also records the related binary artifact name, manifest
artifact name, diagnostics artifact name, diagnostics sidecar manifest path, and
platform id in links, so a reviewer can navigate from the small diagnostics
artifact back to the exact platform outputs when deeper inspection is needed.
The workflow output build-diagnostics-summary-json includes
diagnosticsContractWarningCount and diagnosticsManifestWarningCount so
release jobs can detect drifting diagnostics JSON contracts and missing or
drifting diagnostics sidecar manifests without downloading the per-platform
diagnostics artifacts first.
Artifact Signing Authority
Artifact signing is a Buildchain capability, not a macOS application workflow. Consumers declare desired signature state next to their artifact facts; they do not configure certificates, Team IDs, notary credentials, protected environments, authority roles, or signing jobs:
[[signing.artifacts]]
id = "native-engine"
path = "dist/kungfu-engine"
profile = "auto"
kind = "mach-o"
platforms = ["macos-arm64", "macos-x64"]
Every native and container build lane reads this declaration after the build
lifecycle and before verification. Buildchain binds the exact artifact bytes or directory tree to
the caller repository, source commit, source tree, immutable runtime, platform,
and requested signature semantics, then publishes a deterministic
<artifact>-signing-request-<platform>-<source-sha>-<run-id>-<run-attempt>
request. No consumer
workflow step is required. The lifecycle runner automatically adds declarations
selected for the current platform to the build manifest scan, including
subjects outside the caller’s ordinary artifact-paths; this extends the
evidence preimage without silently adding those subjects to the ordinary
artifact upload.
The request root is a Buildchain-owned generated output. After the declaration, lifecycle manifest, and source paths pass validation, sealing replaces that root before materializing the current request set. This keeps repeated jobs on a self-hosted runner idempotent and prevents stale requests from an earlier run from entering the uploaded request artifact. An output root that contains the workspace, working directory, lifecycle manifest, or any declared subject is rejected before cleanup.
Self-hosted runners whose network requires different routes for Artifact upload and download can scope an upload-only proxy bypass to the sealed signing request:
with:
artifact-signing-request-upload-no-proxy: ".blob.core.windows.net"
The caller repository variable
BUILDCHAIN_ARTIFACT_SIGNING_REQUEST_UPLOAD_NO_PROXY provides the same value
without changing a consumer workflow; an explicit workflow input takes
precedence. When neither is set, Buildchain preserves the runner’s existing
NO_PROXY and no_proxy values. The resolved value applies only to the
Buildchain-owned signing-request upload. Authority dispatch and immutable
signed-result download keep the runner’s original proxy route. This is a
transport control only: it does not change request bytes, signing authority,
artifact identity, or verification policy.
profile = "auto" resolves signable Apple artifacts such as Mach-O files,
.dylib, .framework, .app, .xpc, .plugin, .pkg, .dmg, and macOS
archives containing native code to the native apple-developer-id provider.
For a declared macOS archive, the authority safely extracts the sealed
container, signs and verifies every Mach-O payload, signs Mach-O payloads inside
embedded Python wheels, rebuilds each affected wheel’s PEP 427 RECORD, and
recreates the original zip or tar.gz before returning the exact final bytes.
Windows pe and binary artifacts
resolve to timestamped native windows-authenticode; Windows PE never falls
back to a detached signature. Linux and other non-native binary files,
archives, blobs, and directories resolve to detached-signature-v1. Buildchain records that as a
detached cryptographic signature and never misrepresents it as an operating
system code signature. Explicit incompatible provider/kind/platform
combinations fail closed.
The request schema rejects credential and authority-infrastructure fields. The
Buildchain-owned signing authority is responsible for credential selection,
native signing, notarization where applicable, immutable result delivery, and a
receipt bound to the request digest, runtime SHA, output digest, and signature
evidence. Consumer repositories neither receive nor duplicate credential-island
material. Each platform lane seals and uploads the unsigned request plus a
run-attempt-bound control request, completes functional verification, and exits.
It does not dispatch or poll the authority. A separate ubuntu-24.04 controller
starts only after the build matrices complete, validates the exact source,
tree, runtime, request-set root, platform, run attempt, and correlation, then
dispatches and awaits the protected authority workflow. Its retained receipt
records two independent immutable identities: the consumer Buildchain runtime
SHA carried by the control request and the exact authority-ref commit resolved
immediately before dispatch. The former validates the request-producing
runtime; the latter must equal the authority workflow run’s head_sha and is
retained with the exact authority run and result artifact. If the protected ref
moves between resolution and dispatch, settlement fails closed. Failure, timeout, or
cancellation produces a non-qualifying receipt and no finalization delegation.
A second GitHub-hosted finalization lane downloads the original control request, controller receipt, and delegation, verifies their roots and coordinates agree, then verifies the authority result against the sealed request, imports the exact signed bytes, and recomputes the final manifest before replacing the deterministic artifact. The signing result is never downloaded back to a self-hosted native runner, so a macOS caller is released before credential-island signing and notarization complete. Platform manifests, KFD evidence, checksums, and Release Passport inputs therefore observe the final signed artifact rather than the pre-signing build output.
For a standalone Mach-O request, the authority requires strict Developer ID
verification, the declared Team ID, hardened runtime, and an Accepted
notarytool result for the exact submission. Apple creates the notarization
ticket for that binary and publishes it online, but
standalone binaries do not support stapling.
Buildchain therefore records
standalone-notary-ticket-online and does not misapply app-bundle
spctl --assess --type execute semantics to the raw executable.
For a compound archive request, the authority notarizes the complete extracted
signed product tree and records compound-notary-ticket-online. A generic
archive container cannot carry a stapled ticket and is not itself a Gatekeeper
execution target; Gatekeeper evaluates the extracted signed code. Archive path
and symlink validation fail closed before any payload is signed.
For a declared app-bundle, the same protected authority extracts the sealed
application, derives and verifies its bundle identity, signs nested native code,
submits both the application and disk image for notarization, staples and
Gatekeeper-assesses both deliverables, and returns a ZIP, DMG, evidence document,
and source-bound manifest. The reusable workflow verifies those returned bytes
on GitHub-hosted infrastructure, adds them to the normal macOS platform payload,
and publishes a separate <artifact>-macos-credential-<source-sha> projection
for release pipelines that consume the credential-island evidence contract.
Consumers declare the .app under [[signing.artifacts]]; they do not configure
an environment, certificate, notary credential, or authority workflow.
The durable v3 authority runtime is
authority/v3/v3.0/artifact-signing. It is channel-neutral: alpha and stable
release work use the same protected buildchain-artifact-signing environment
and provider identities. The authority ref is protected independently from
release channels and can advance only through reviewed, checked changes; the
temporary train/v3/v3.0/artifact-signing-authority ref is retained only as a
bounded migration rollback.
The older credential-island-macos-* reusable-workflow inputs remain a
compatibility surface while existing callers migrate. They are not the target
consumer contract and must not be used to design new integrations.
Artifact Transfer Relay
By default, platform jobs upload payloads, manifests, and diagnostics directly to GitHub artifacts:
with:
artifact-transfer-mode: github-artifacts
artifact-compression-level: 0
Direct GitHub Artifact payloads default to compression level 0. Buildchain
artifacts are commonly already-compressed archives; storing them without a
second compression pass shortens the upload window while preserving the same
artifact name, run/id/digest binding, retention, and no-overwrite behavior.
Callers may select 1 through 9 for payloads that materially benefit from
compression. Manifests and diagnostics retain their existing small-artifact
behavior.
Large self-hosted native builds can opt into the first-class S3 relay path:
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v3
with:
runner-preset: kungfu-v4-self-hosted
artifact-transfer-mode: s3-to-github-artifacts
artifact-relay-s3-bucket: ${{ vars.BUILDCHAIN_ARTIFACT_RELAY_S3_BUCKET }}
artifact-relay-s3-region: ${{ vars.BUILDCHAIN_ARTIFACT_RELAY_S3_REGION }}
artifact-relay-s3-prefix: ${{ vars.BUILDCHAIN_ARTIFACT_RELAY_S3_PREFIX }}
In relay mode, each self-hosted platform job uploads the heavy payload files to
S3 and uploads only a small relay-manifest.json to GitHub. A GitHub-hosted
relay-artifacts job then assumes the configured download role, downloads the
payloads from S3, verifies every file by SHA256, and re-uploads the normal
GitHub artifacts under the same artifact names that direct mode uses. Downstream
summary, release-candidate, and promote-only workflows therefore continue to
consume GitHub artifacts and do not need custom S3 logic.
The relay implementation uses Node.js plus the standard AWS environment
credentials from GitHub OIDC; runner images and build containers do not need the
AWS CLI installed.
After the GitHub artifact uploads succeed, Buildchain deletes the S3 objects listed in the relay manifest for that platform. If any download, verification, or GitHub artifact upload fails, cleanup is skipped so maintainers can inspect the retained S3 payload. Configure a short bucket lifecycle expiration as a cost and cleanup backstop.
The relay configuration is intentionally generic. Buildchain does not hard-code organization buckets, regions, or role ARNs. Callers may pass explicit inputs, or set repository/organization variables and secrets using these names:
| Variable or secret | Meaning |
|---|---|
BUILDCHAIN_ARTIFACT_RELAY_S3_BUCKET |
Relay bucket name |
BUILDCHAIN_ARTIFACT_RELAY_S3_REGION |
Relay bucket region |
BUILDCHAIN_ARTIFACT_RELAY_S3_PREFIX |
Relay object prefix; defaults to buildchain-artifacts |
BUILDCHAIN_ARTIFACT_RELAY_S3_ROLE_ARN |
Shared OIDC role ARN for upload and download |
BUILDCHAIN_ARTIFACT_RELAY_S3_UPLOAD_ROLE_ARN |
Upload OIDC role ARN for self-hosted build jobs |
BUILDCHAIN_ARTIFACT_RELAY_S3_DOWNLOAD_ROLE_ARN |
Download OIDC role ARN for the GitHub-hosted relay job |
BUILDCHAIN_ARTIFACT_RELAY_S3_OIDC_AUDIENCE |
Optional OIDC audience override |
For AWS China regions, Buildchain defaults the OIDC audience to
sts.amazonaws.com.cn; other regions default to sts.amazonaws.com. The caller
workflow must allow id-token: write, and the target role trust policy should
restrict GitHub OIDC claims to the expected organization, repository, workflow,
and branch/ref. The S3 permissions should be scoped to the relay bucket/prefix
used by the repository.
Upload roles need write/delete access under the relay prefix; download roles
need read access plus delete access for successful cleanup.
Relay mode is opt-in and does not affect forks or open-source users that do not configure S3. Missing bucket, region, upload role, or download role values fail before the heavy build matrix is scheduled. Buildchain treats S3 as a transport cache, not as the final release evidence store; the final audit entry remains the GitHub artifact set plus the Buildchain build summary and release-candidate passport.
Set release-candidate: true when the successful reusable build is meant to be
the artifact source promoted later. Buildchain then uploads
release-candidate-passport.json under the
<artifact-name>-release-candidate-<publish-source-sha> artifact name. Promotion
jobs can pass that passport to promote-buildchain-ref with
promote-only-release-candidate: "true" so source, channel, platforms, and the
aggregate build-summary hash are checked before publish-gate side effects. The
passport records the locked commit’s Git tree SHA, so a post-merge channel HEAD
can be accepted only when it is tree-equivalent to the PR-stage build evidence.
Publish Gate
Buildchain separates “may build/verify” from “may publish.” A same-repository pull request may be trusted enough to run the build matrix, but it still must not publish packages, S3 objects, release pages, or preview aliases. Publishing is allowed only when the caller explicitly requests a channel and the current event/ref matches that channel.
Use publish-channel to request a channel:
jobs:
build:
uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v3
with:
publish-channel: release
publish:
needs: build
if: ${{ needs.build.outputs.publish-allowed == 'true' }}
runs-on: ubuntu-24.04
steps:
- run: ./scripts/publish.sh
Default channels are:
| Channel | Allowed refs |
|---|---|
none |
Never publishes; this is the default |
alpha |
alpha/vN/vN.M branches or exact vN.M.P-alpha.K tags |
release |
release/vN/vN.M branches or release tags such as vN.M.P, vN.M, vN |
major |
publish-gate/major, legacy major-gate, or next-major release tags such as vN.0.0, vN.0, vN |
Pull request events always produce publish-allowed=false, even when the PR is
from the same repository. Untrusted fork events also produce
publish-allowed=false; with the default untrusted-policy: fail, the workflow
then fails before any build runner starts.
Projects with their own channel names can pass publish-refs-json:
with:
publish-channel: nightly
publish-refs-json: >-
{"nightly":["^refs/heads/nightly/v\\d+$"]}
The aggregate build summary includes the same publish gate decision under
publishGate, so a downloaded artifact summary explains both what was built and
why it was or was not eligible to publish.
Publish Source Lock
publish-channel answers “may this event publish?” Source lock answers “which
source tree is the publish decision about?” A caller can pass publish-source-ref
to bind a publish run to a reviewed gate branch before any checkout happens:
| Ref | Meaning |
|---|---|
publish-gate/alpha/<line>/<consumer-version> |
Build and publish an alpha candidate for a consumer line |
publish-gate/release/<line>/<consumer-version> |
Build and publish a production candidate for a consumer line |
publish-gate/anchor |
Resolve an explicit anchor request; it does not publish artifacts by itself |
publish-gate/major |
Gate the next major source state |
major-gate |
Legacy compatibility alias for the major gate |
For alpha and release refs, <line> is intentionally allowed to contain /, so
Kungfu-style lines such as v22/v22.22 stay readable. The final path segment is
the consumer-visible version, for example 22.22.3-kf.0.
The reusable workflow resolves the branch tip to publish-source-sha, checks out
that SHA in every build job, and uses the same SHA in artifact names, manifests,
and aggregate summaries. Reruns therefore rebuild the same source tree even if a
gate branch moves later.
Before any heavy build matrix is scheduled, the workflow also verifies that the
target channel ref implied by the source lock already points at
publish-source-sha and that the target channel HEAD came from the required
merged same-repository channel PR. publish-gate/alpha/<line>/<version> must
match alpha/<line> and have PR lineage dev/<line> -> alpha/<line>;
publish-gate/release/<line>/<version> must match release/<line> and have PR
lineage alpha/<line> -> release/<line>. If either check fails, the run fails
fast with a diagnostic telling maintainers to merge the source commit through
the channel PR first. This keeps verify from spending runner time on a source
tree that cannot legally enter the requested publish channel.
The resolved release manifest is uploaded as an artifact and emitted as
release-manifest-json. It records:
- source ref, source SHA, channel, line, and consumer version;
- configured version strategy and configured version-state files;
- each version file’s value, with release gates failing closed if the configured files do not equal the consumer version;
- anchor manifest summary for anchored/manual projects;
- explicit anchor request JSON for
publish-gate/anchor; - publish registry, dist-tag, and gate visibility metadata.
Publish side-effect jobs should verify the lock immediately before publishing:
- name: Verify publish gate did not move
run: node .buildchain/runtime/scripts/verify-publish-source-lock.mjs
env:
BUILDCHAIN_PUBLISH_SOURCE_REF: ${{ needs.build.outputs.publish-source-ref }}
BUILDCHAIN_PUBLISH_SOURCE_SHA: ${{ needs.build.outputs.publish-source-sha }}
BUILDCHAIN_SOURCE_REPOSITORY: ${{ github.repository }}
GITHUB_TOKEN: ${{ github.token }}
If the branch tip no longer matches the manifest SHA, the publish job must fail closed. Moving a gate branch creates a new publish decision and should produce a new build run.
Release Candidate Promote-Only
For native package sets, the PR build is the only heavy build. When a PR targets
alpha/<line> or release/<line>, .build.yml uploads a release-candidate
bundle next to the platform artifacts. The bundle contains:
release-candidate.passport.json;- the aggregate
build-summary.json; - copied platform manifest evidence for the built platforms.
The passport records two separate source identities:
builtSourceSha/builtSourceTreeSha: the PR-stage source that produced the artifacts, usually the PR merge ref;promotionChannelSha/promotionChannelTreeSha: the post-merge channel commit used for publish authority.
The reusable promote wrapper resolves the merged PR, finds exactly one matching
PR-stage release-candidate artifact, downloads it with the build summary and
payload artifacts from the same PR-stage run, validates the payload count,
compares the built tree with the promotion channel tree, locks
publish-gate/{alpha,release,major} to the promotion channel commit, and then
calls actions/promote-buildchain-ref with
promote-only-release-candidate: "true" and
require-publish-source-lock: "true". The wrapper passes the created
publish-gate/* ref, target SHA, and locked=true into the promote action, so
floating @v3 consumers receive publish-side source-lock drift protection by
default. It also defaults branch-protection-bypass-apps to github-actions
so the workflow automation can apply generated version-state and channel
bookkeeping on protected dev/alpha/release branches after the reviewed
channel PR has merged. Other App slugs and all user or team bypass actors are
rejected. The wrapper uses the run-scoped github.token as the generated ref
update token for protected bookkeeping PATCH calls, so the exact GitHub Actions
App authority can sync dev immediately after alpha/release publish without a
post-publish PR.
It does not call .build.yml, does not create a matrix, and must fail before
publish if the RC evidence, payload set, or source-lock ref is missing or
ambiguous.
The public release-candidate-promote.yml is a generated channel router. It
derives the publication lane from target-ref, then selects the matching
advanced workflow shell, runtime, and consumer lock before the advanced
promotion starts:
- alpha targets use
.release-candidate-promote.yml@vN-alpha, runtimevN-alpha, andbuildchain-alpha-contract-lock-path; - release and major targets use
.release-candidate-promote.yml@vN, runtimevN, andbuildchain-stable-contract-lock-path.
The generated router also owns the stable-shell layout transition through
.buildchain/promotion-shell-routing.json. The v3 stable and alpha lanes call
the hidden advanced workflow at the exact immutable SHA behind their selected
v3 channel state and forward the complete internal promotion identity surface.
The logical shell identity remains vN, and the router retains it in the public
audit outputs. The internal advanced-shell call receives the exact call ref
selected by the routing configuration, so its called-workflow ref check and
checkout SHA both bind to the same immutable identity. Updating a routing pin
after a release does not require any consumer declaration change.
The router resolves immutable SHAs and the selected lock digest before candidate
download. The advanced shell verifies the same router, shell, runtime, lock,
channel, and target binding again. Train and exact-SHA runtime overrides remain
restricted to trusted workflow_dispatch actors with write, maintain, or admin
permission. Promotion controller evidence, the promotion copy of the release
candidate passport, and the final release passport record these identities.
jobs:
promote:
uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v3
secrets:
buildchain-issue-app-id: ${{ secrets.BUILDCHAIN_ISSUE_APP_ID }}
buildchain-issue-app-private-key: ${{ secrets.BUILDCHAIN_ISSUE_APP_PRIVATE_KEY }}
with:
buildchain-channel: auto
buildchain-alpha-contract-lock-path: .buildchain/alpha-contract-lock.json
buildchain-stable-contract-lock-path: .buildchain/contract-lock.json
channel: alpha
target-ref: alpha/v22/v22.22
artifact-name: libnode
# Defaults to build.yml / Build. Override only when the PR-stage build
# workflow uses a different file or display name.
release-candidate-workflow-file: build.yml
release-candidate-workflow-name: Build
package-manager: npm
publish-target: npm
runner-preset: github-hosted
trusted-publishing: true
github-release: true
required-status-check: check / check
required-artifact-count: 3
publish-dist-tag: alpha
publish-package-set-order: platforms-first-main-last
publish-package-main: "@kungfu-tech/libnode"
release-passport-product-name: Libnode
buildchain-contract-drift-issue-mode: compatible-and-breaking
Existing callers may keep buildchain-contract-lock-path; a non-empty explicit
path overrides channel-specific selection for compatibility. Migration only
requires adding the two channel lock inputs and may retain the remaining common
promotion declaration unchanged. Consumers must not call the dot-prefixed
advanced workflow directly.
buildchain-issue-app-id and buildchain-issue-app-private-key are optional
but recommended for cross-repository consumers. They should identify a GitHub
App installation with issues: write on kungfu-systems/buildchain; the
wrapper mints the installation token before calling
actions/report-buildchain-issue. Consumers can also pass a pre-minted
buildchain-issue-token. If both are omitted, the wrapper falls back to
BUILDCHAIN_ISSUE_TOKEN, BUILDCHAIN_PROMOTION_TOKEN, and then the consumer
workflow’s github.token; the last fallback can only report issues when it has
write access to the target Buildchain repository.
publish-required-artifacts-json can still be passed explicitly for custom
publish targets. Custom OCI requirements may omit pre-publish refs and digests;
the action resolves the exact version ref and validates final digests and any
built/reused provenance after lifecycle.publish. For the default
publish-artifact-kind: npm path, consumers do
not download artifacts or run repository scripts to build publish evidence. The
wrapper downloads the PR-stage payload artifacts, finds the downloaded .tgz
packages, reads each tarball’s package/package.json for the real scoped
package name and version, computes the npm sha512-... integrity from the
tarball bytes, marks the package matching publish-package-main as role: main, marks the rest as role: platform, and passes the generated
publish-required-artifacts-json to promote-buildchain-ref before any publish
side effect. Downloaded platform manifests are still passed into the release
passport unless release-passport-platform-manifest-paths is set explicitly.
The same Buildchain contract lock check runs before release-candidate
resolution and before publish. A compatible v3 drift leaves an issue in the
consumer repository but does not trigger a second heavy build; an incompatible
drift fails before publish side effects.
The wrapper publishes the public release tag as a GitHub Release by default.
After promote-buildchain-ref reports a complete release transaction, the
wrapper creates or updates the public release, marks semver prerelease tags
such as v1.2.3-alpha.0, v1.2.3-rc.1, or v22.22.3-kf.3-alpha.7 as
prerelease=true and make_latest=false, marks stable semver tags as latest,
and uploads the publish evidence file plus every file in the generated release
passport directory, including buildchain.release.json and check-report.json.
For anchored/manual package releases, the public release tag is derived from the
published package version and the internal exact transaction tag remains
available in the release passport.
Consumers do not need to hand-write gh release logic to trigger
release.published propagation. Set github-release: false only for
repositories that intentionally do not maintain GitHub Releases.
If the transaction still needs protected-ref finalization, the wrapper defers
GitHub Release creation until the later run that reaches state=complete.
Custom publish jobs can also repeat the channel-ref preflight:
- name: Verify publish channel ref still matches
run: node .buildchain/runtime/scripts/verify-publish-channel-ref.mjs
env:
BUILDCHAIN_PUBLISH_SOURCE_REF: ${{ needs.build.outputs.publish-source-ref }}
BUILDCHAIN_PUBLISH_SOURCE_SHA: ${{ needs.build.outputs.publish-source-sha }}
BUILDCHAIN_SOURCE_REPOSITORY: ${{ github.repository }}
GITHUB_TOKEN: ${{ github.token }}
Anchored/manual package release jobs should also make the Buildchain promotion
action validate that publication is entering through the same
publish-gate/{alpha,release,major} source-lock contract before any package
publish side effect:
- name: Promote release ref and publish npm package set
uses: kungfu-systems/buildchain/actions/promote-buildchain-ref@v3
with:
sha: ${{ needs.build.outputs.publish-source-sha }}
target-ref: release/v22/v22.22
require-publish-source-lock: "true"
publish-source-ref: ${{ needs.build.outputs.publish-source-ref }}
publish-source-sha: ${{ needs.build.outputs.publish-source-sha }}
publish-source-locked: ${{ needs.build.outputs.publish-source-locked }}
target-ref stays the Buildchain channel promotion target, such as
alpha/v22/v22.22, release/v22/v22.22, or publish-gate/major.
publish-source-ref is the reviewed source-lock branch that authorized this
specific package publication. For alpha and release package publications, the
source-lock branch must point at the exact channel-line commit that promotion is
validating; it is not a replacement for target-ref.
This keeps the version bump commit, publish authorization, and auditable publish
entrypoint on the Buildchain source-lock protocol. The CLI form
buildchain publish-source validate-anchored-release --json is still useful for
custom publish scripts, but the preferred GitHub Actions gate is the promotion
action input above. A publish job that still runs directly from alpha/* or
release/* channel branches fails this check because those refs are channel
state, not publish-gate decisions.
Package-Set Publish Plan
Projects that publish multiple packages should treat package publication as a package-set operation. Buildchain’s package-set planner uses these rules:
- platform packages publish first;
- the main package publishes last;
- the dist-tag move happens only after the full package set is present;
- reruns accept already-published packages only when package name, version, and integrity match;
- an existing package with different integrity is a hard failure.
This keeps a consumer from observing a floating dist-tag that points to a main package before all platform artifacts for the same source SHA are available.
Command Sources
The workflow runs .buildchain/buildchain.toml lifecycle stages by default:
[lifecycle.install]
command = "corepack yarn install --immutable"
[lifecycle.build]
commands = [
"corepack yarn make",
"corepack yarn build",
]
[lifecycle.verify]
command = "corepack yarn test"
Callers can override any stage for one invocation:
with:
build-command: cmake --build build --config Release
verify-command: ctest --test-dir build --output-on-failure
Every native and container matrix job is bounded by
lifecycle-timeout-minutes, which defaults to 120 minutes. The same input is
the fallback deadline for each install, build, and verify action, so a hung
command fails with the lifecycle name and matrix platform before it can occupy
a self-hosted runner indefinitely. A stage-level timeout_minutes in
buildchain.toml remains the more specific override for that stage.
with:
lifecycle-timeout-minutes: 90
The reusable build workflow samples the build lifecycle by default and carries the generated summary into the final verify diagnostics. Callers can override the sidecar path or disable sampling:
with:
sample-process-tree: true
process-summary-path: .buildchain/diagnostics/process-summary.json
process-sample-interval-ms: 15000
requested-parallelism: 20
When sample-process-tree is true, Buildchain wraps either build-command or
the configured lifecycle.build stage with buildchain sample process-tree.
The path is relative to the checked-out workspace and is read again during the
final verify lifecycle. Custom workflows can still write their own sampler
summary and pass process-summary-path; Buildchain reads the file after the
lifecycle command finishes, so it may be produced during the same invocation.
When the build stage is optional, the reusable workflow treats the default
sampler path as optional during verify; an explicitly supplied
process-summary-path remains required.
For custom workflows, use the action directly:
- uses: kungfu-systems/buildchain/actions/run-lifecycle@v3
with:
stage: build
required: "true"
timeout-minutes: "90"
artifact-name: libnode-linux-x64-${{ github.sha }}
artifact-paths: |
dist
build/stage
Artifact Contract
Each platform upload uses artifact-name-template. The default is:
{artifact}-{platform}-{sha}
Supported placeholders are {artifact}, {artifactName}, {platform},
{platformId}, {platformName}, {sha}, {shortSha}, {ref}, {runId},
and {runAttempt}. Invalid GitHub artifact name characters are normalized to
-, so {ref} remains deterministic even for refs such as
refs/heads/dev/v3/v3.0.
Each platform also writes and uploads:
.buildchain/artifacts/<platform-id>/manifest.json
.buildchain/artifacts/<platform-id>/summary.json
The manifest schema is:
{
"schemaVersion": 1,
"contract": "kungfu-buildchain-artifact",
"artifactName": "libnode-linux-x64-<sha>",
"platform": {
"id": "linux-x64",
"name": "Linux x64",
"os": "Linux",
"arch": "X64"
},
"git": {
"repository": "kungfu-systems/libnode",
"sha": "<sha>",
"ref": "<ref>",
"runId": "<run id>",
"runAttempt": "<attempt>"
},
"lifecycle": {
"stage": "verify",
"commandSource": "buildchain.toml",
"executed": true
},
"summary": {
"contract": "kungfu-buildchain-artifact-summary",
"artifactName": "libnode-linux-x64-<sha>",
"fileCount": 1,
"totalBytes": 1234,
"digest": "<hex>"
},
"expectedArtifacts": {
"ok": true,
"source": "expected-artifacts-json",
"checks": []
},
"files": [
{
"path": "dist/example.zip",
"size": 1234,
"sha256": "<hex>"
}
]
}
Artifact names do not include actor names, timestamps, or retry counters. Reruns produce a new GitHub Actions run but keep the same source SHA/platform contract.
expected-artifacts-json fails the build before upload when the artifact does
not match the caller’s declared contract. Supported checks are:
| Field | Meaning |
|---|---|
minFiles |
Minimum number of manifest files |
maxFiles |
Maximum number of manifest files |
minTotalBytes |
Minimum total byte count |
requiredPaths |
Exact manifest paths that must exist |
Trusted Event Gate
The workflow has an explicit trust-gate job. By default, pull requests from
forks fail before any build job can reach self-hosted runners, secrets,
publishing credentials, or heavyweight build commands. Same-repository PRs,
workflow dispatches, and protected branch events can proceed.
If a repository wants fork PRs to skip rather than fail, it can set:
with:
untrusted-policy: skip
Do not set require-trusted-event: false for workflows that use self-hosted
runners or secrets.
The build matrix and the workflow control plane are routed independently. The
matrix continues to use runner-preset and platforms-json. Consumers with a
governed runner may also move channel resolution, trust evaluation, contract
resolution, controller evidence, artifact transfer, and aggregation off the
default GitHub-hosted runner:
with:
control-runner-json: '["self-hosted","agent-120"]'
runner-preset: custom
platforms-json: '[{"id":"linux-x64","name":"Linux x64","runner":"[\"self-hosted\",\"agent-120\"]"}]'
control-runner-json is additive and defaults to ["ubuntu-24.04"]. Keep
require-trusted-event: true whenever either runner input selects
self-hosted; a self-hosted control plane must not be exposed to untrusted fork
events or arbitrary caller-controlled workflow code.
require-trusted-event controls access to build runners. It does not override
the publish gate: pull requests remain non-publishing events.
Fixture
fixtures/libnode-shaped is the contract fixture. It has:
package.jsonversion state;.buildchain/buildchain.tomlwithinstall,build,verify, andpublish;- cross-platform Node scripts that create small
dist/outputs; Build Surface Fixtureworkflow coverage.
The fixture proves the reusable surface without running the real libnode native build.