Kungfu UNGFU™Developer Platform

Back to Buildchain homemanual / manual:reusable-build-surface

Reusable Build Surface

Buildchain manual page.

Reusable Build Surface

Buildchain v2 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.

Automatic Channel Router

The preferred consumer surface is one reusable workflow call. After v2.12 reaches the stable major ref, 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:

  1. an explicit buildchain-ref train, SHA, or official channel;
  2. an explicit buildchain-channel: alpha|stable;
  3. publish-channel: alpha|release|major;
  4. GitHub release prerelease metadata;
  5. a canonical semver tag;
  6. 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 the v2.12 prerelease evaluation window, canaries use build.yml@v3-alpha. The same router then selects v3-alpha or stable v2 as the runtime. Production consumers should adopt build.yml@v3 after the router has reached stable; 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
      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

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
custom Requires platforms-json

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-container when 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. Pull requests, including same-repository pull requests and fork-originated pull requests, cannot use buildchain-ref override. This keeps automated PR builds on the stable 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 v2, 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:

  1. resolve the Buildchain runtime ref, for example v2, to an immutable SHA;
  2. read dist/site/buildchain-contract.json from that checked-out Buildchain ref;
  3. read the consumer’s .buildchain/contract-lock.json;
  4. compare the accepted contract with the current contract.

SHA drift alone is not a failure. v2 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-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.

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.

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, train, exact-sha, or development
buildchain-runtime-override true when a 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.

macOS Credential Island

Alpha consumers can ask the build controller to seal one exact macOS app and hand it to a separate protected signing job. The built-in mode is suitable when the protected environment belongs to the repository that defines the reusable workflow:

with:
  credential-island-macos-app-path: product/dist/desktop/mac-arm64/Kungfu Episodes.app
  credential-island-environment: alpha-macos-signing
  credential-island-macos-platform-id: macos-arm64

For a cross-repository reusable workflow, keep the credentials in the consumer repository and select caller-owned mode:

with:
  credential-island-macos-app-path: product/dist/desktop/mac-arm64/Kungfu Episodes.app
  credential-island-caller-owned: true
  credential-island-macos-platform-id: macos-arm64

The caller then runs its own no-checkout macOS job after the reusable build, binds the consumer-owned protected environment on that job, and downloads the source-bound input plus immutable action runtime from the same workflow run. This is required for cross-repository callers because environment credentials must never be widened into repository secrets merely to cross a reusable workflow boundary.

The ordinary matrix uploads credential-island-input-<platform>-<source-sha>, containing a ditto archive and a manifest bound to the caller repository, source commit, source tree, bundle identity, version, size, and digest. The pinned reusable workflow either starts its built-in GitHub-hosted macOS job or leaves that job to the caller. In both modes the credential-bearing job has no source checkout or package-manager step: it downloads the immutable Buildchain action runtime plus the sealed app and never invokes consumer code.

The protected environment supplies these non-secret variables:

  • BUILDCHAIN_MACOS_EXPECTED_BUNDLE_ID
  • BUILDCHAIN_MACOS_EXPECTED_TEAM_ID
  • BUILDCHAIN_MACOS_CERTIFICATE_SHA1

and these secrets:

  • BUILDCHAIN_MACOS_CERTIFICATE_P12_BASE64
  • BUILDCHAIN_MACOS_CERTIFICATE_PASSWORD
  • BUILDCHAIN_MACOS_NOTARY_API_KEY_P8_BASE64
  • BUILDCHAIN_MACOS_NOTARY_API_KEY_ID
  • BUILDCHAIN_MACOS_NOTARY_API_ISSUER

The signed DMG, signed app ZIP, credential evidence, and their source-bound platform manifest form one additional release-candidate platform. Reviewers can therefore verify the exact caller source and immutable Buildchain runtime that entered the credential island before admitting or publishing the signed artifacts.

Artifact Transfer Relay

By default, platform jobs upload payloads, manifests, and diagnostics directly to GitHub artifacts:

with:
  artifact-transfer-mode: github-artifacts

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, runtime vN-alpha, and buildchain-alpha-contract-lock-path;
  • release and major targets use .release-candidate-promote.yml@vN, runtime vN, and buildchain-stable-contract-lock-path.

The generated router also owns the stable-shell layout transition through .buildchain/promotion-shell-routing.json. Stable v2.14.13 contains the hidden advanced workflow, so the stable lane calls that workflow at the exact immutable SHA behind the released v2 state and forwards 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 the routing pin after a stable 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 v2 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/v2/v2.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.json version state;
  • .buildchain/buildchain.toml with install, build, verify, and publish;
  • cross-platform Node scripts that create small dist/ outputs;
  • Build Surface Fixture workflow coverage.

The fixture proves the reusable surface without running the real libnode native build.

Page metadata

Route
/docs/reusable-build-surface/
Category
manual
Source path
docs/reusable-build-surface.md
Package
@kungfu-tech/buildchain@3.0.2-alpha.2
Digest
sha256:6a739755239e5592299f6f8789cf6f9091350bb945b5db5df11edff3496df026