Kungfu UNGFU™Developer Platform

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

Reusable Build Surface

Buildchain manual page.


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:

  1. an explicit buildchain-ref protected authority, 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 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-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. 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:

  1. resolve the Buildchain runtime ref, for example v3, 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. 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, 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. 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.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.6-alpha.0
Digest
sha256:fe06637d604101269b013163be558c1ecc842276e27f2b5abfb6eea39e98feb0