# Build and supply-chain gate ## Local blocking controls - `pnpm install --frozen-lockfile` and a real manifest/lock mismatch fixture - all direct and transitive lockfile rows with package SHA-512 integrity - production/development, direct/transitive and platform-optional classification - package-manifest license allow/deny policy - approved inventory baseline digest and actual add/remove/change/upgrade diff - independent review for new direct production dependencies - CycloneDX 1.6 SBOM and inventory component/edge coherence - source/lock/SBOM/dist-linked local provenance statement - source, opt-in recipes, scripts, tests, tracked config/schema, public, built asset and generated release metadata secret scan - two-build `SOURCE_DATE_EPOCH` reproducibility check The canonical commands are: ```bash corepack pnpm verify:lockfile corepack pnpm verify:reproducible-build corepack pnpm build:release-candidate corepack pnpm verify:local-evidence corepack pnpm check:supply-chain:fixtures ``` `config/security/dependency-baseline.json` is the approved local baseline. Changing it requires `DEPENDENCY_BASELINE_OWNER` and `DEPENDENCY_BASELINE_REASON`; editing the digest or hardcoding an empty diff is rejected. ## External promotion controls Promotion reads the provider files named by `VULNERABILITY_REPORT_PATH` and `PROVENANCE_ATTESTATION_PATH`. The vulnerability report must bind both the exact lockfile digest and candidate `distSha256`; the provenance attestation must name `dist` with that same digest. Both documents use strict schemas and Ed25519 signatures verified with separately configured trusted public keys and key IDs (`VULNERABILITY_PUBLIC_KEY_PATH`, `VULNERABILITY_KEY_ID`, `PROVENANCE_PUBLIC_KEY_PATH`, and `PROVENANCE_KEY_ID`). Keys of another curve, including Ed448, are rejected even if a document labels its algorithm `Ed25519`. `immutable_build` archives the raw `pnpm-lock.yaml`, `dist` (including hidden `.vite` files), the build manifest, module inventory, release verification, secret-scan result, and local supply-chain evidence once. The candidate manifest hashes the raw lockfile bytes and requires that digest to equal the dependency inventory's `lockfileSha256`. Before upload, the producer validates the manifest-bound exact archive member set and every member digest, then publishes the archive SHA-256 as an immutable job output. The two provider jobs download this same archive separately, compare that output digest, validate the exact member set before extracting only into isolated roots, and receive `CANDIDATE_LOCKFILE_PATH` and `CANDIDATE_DIST_SHA256`; configured `VULNERABILITY_PROVIDER_COMMAND` and `PROVENANCE_PROVIDER_COMMAND` must emit the signed reports. After each external command returns, provider upload validation rechecks the unchanged archive and extracted candidate, parses the provider JSON with its strict schema, and binds its dist and lockfile digests before upload. If either provider input is absent, local verification remains meaningful but `artifacts/security/supply-chain-verification.json` records `promotionStatus: FAIL_UNVERIFIED`. `verify:provider-evidence` and `verify:promotion` then exit non-zero. Promotion recomputes the candidate file set and digests, then read-only revalidates the archived executable schemas, raw lockfile, module inventory, build outputs, release coherence, SBOM, provenance, security scan and supply-chain coherence. It never rebuilds or rewrites candidate evidence. Promotion uploads the already verified archive itself with the two provider reports and verification records; it does not create a replacement archive from extracted files. Scanner or signing outages are not converted to an empty PASS. The generated workflow is also a supply-chain control. `config/ci/gates.json` is its sole typed authority. Run `corepack pnpm generate:ci-workflow` after a contract change and `corepack pnpm check:ci-workflow` (or the encompassing `corepack pnpm check:ci`) to reject byte drift in the checked-in Gitea adapter. Action resolution is separately closed over one typed, runtime-frozen registry in `scripts/contracts/ci-gates.ts`. Every generated `uses:` value is an absolute upstream URL pinned to a full commit SHA: - `https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5` (`v4.3.1`) - `https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020` (`v4.4.0`) - `https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7` (patched `v4` branch) - `https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7` (patched `v4` branch) Unknown actions, relative repositories, tags/branches and short SHAs are rejected. Gitea 1.22's official Actions documentation recommends the `ChristopherHX` patched artifact forks for v4 compatibility; the supported deployment baseline is nevertheless Gitea 1.26.4+ with Gitea Runner 1.0.0+. A real end-to-end provider smoke on the staging Gitea instance remains mandatory before any generated job becomes a required check. External provider supervision is fail-closed and requires a Linux runner with an executable `/usr/bin/bwrap`. Bubblewrap mounts the repository workspace read-only, hides `.git`, and read-only binds the trusted `process.execPath` inside the sandbox at `/tmp/node`. It exposes only the sibling `untrusted` raw-evidence directory as writable. Provider commands run non-interactively through `/bin/sh -eu -c`, receive a minimized environment plus only their own provider-prefixed credentials, and have a 30-minute limit. They must consume the supplied candidate paths and digests, write exactly the configured raw report, and must not depend on workspace mutation, host home/toolcache access, or direct access to the sealed evidence path. Missing sandbox support, stale or misplaced outputs, command failure/timeout, and post-command candidate drift all stop publication. The final promotion verification/staging step must be immediately adjacent to the promoted-release upload, and that upload must not use `always()`. This reduces the post-verification mutation window but does not seal a pathname across two action steps. The runner is therefore required to be trusted, exclusive and single-tenant, with no provider command or other same-UID process surviving from staging into the immediately following upload. The artifact service and transfer actions also remain outside the candidate's cryptographic identity: every downstream consumer must revalidate the downloaded archive, manifest member digests and signed provider evidence. Producer-side adjacency does not provide consumer-side digest revalidation. Approved vulnerability exceptions require vulnerability/package identity, owner, a different reviewer, reason and expiry. Expired or self-approved exceptions are blocking.