Reproducible CI
Pin the toolchain version, let it pin the image digest, and gate builds on manifest validation, drift checks, and an artifact hash.
A reproducible custom build means: same inputs, same wasm bytes, every time. The toolchain closes most of that loop for you — what is left is choosing where to run it and what to assert afterwards.
1. Pin the toolchain, not a tag
{
"devDependencies": {
"@libcascade/toolchain": "3.0.0",
"libcascade": "3.0.0"
}
}Commit your lockfile. The toolchain package ships images.json, in which each
image tag was resolved to an immutable ghcr.io/taucad/opencascade.js@sha256:…
digest at publish time. @libcascade/toolchain@3.0.0 therefore names one
reproducible build environment forever, and npm ls answers "which toolchain
built this artifact".
The driver runs repository@digest — never a tag — and verifies the local repo
digest after pulling. A mismatch is an error naming both digests, not a silent
roll-forward.
This replaces the pattern it supersedes: an image tag pasted into a shell
script that had to be kept in sync by hand with the libcascade version the
application depended on.
Because the toolchain and libcascade are lockstep-versioned, keeping the two
dependencies on the same version is the whole compatibility story.
2. Run it where a container engine exists
jobs:
build-wasm:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx libcascade check src
- run: npx libcascade build
- run: npx libcascade assemble
- name: Assert wasm hash
run: |
EXPECTED=$(cat .wasm-hash)
ACTUAL=$(sha256sum dist/myapp_single.wasm | cut -d' ' -f1)
[ "$EXPECTED" = "$ACTUAL" ] || { echo "wasm hash drift"; exit 1; }GitHub-hosted macOS runners ship no container engine at all — run toolchain
builds on a Linux runner. The driver's engine probe order is
$LIBCASCADE_CONTAINER_CMD, then docker, then podman; if none responds it
fails with install options rather than a cryptic ENOENT.
Three gates are doing work in that job:
checkfails when your source references a symbol the config does not bind — the failure that otherwise reaches production as a runtimeBindingError.buildfails when the container'sbuild-manifest.jsonreportsvalidation_passed: false, printing the unsatisfied symbols.- The hash assertion catches everything else. If
EXPECTEDandACTUALdiverge, something changed — either intentionally (bump.wasm-hash) or by accident (investigate).
3. Keep the provenance sidecars
Every variant build emits <outputName>.provenance.json next to the binary,
recording the active compile preset, flags, and the source commit the wasm was
built from. Diff those across builds to detect surprise cache turnover, and
publish them with your package.
4. Verify the image supply chain (optional)
The driver already proves the image is the digest the toolchain pinned. If your threat model wants the signature too, verify it before the build step. Every published image is signed with cosign via OIDC keyless signing, with the signature on the manifest-list digest — one signature verifies regardless of which architecture pulls it.
IMAGES=node_modules/@libcascade/toolchain/generated/images.json
REF="$(jq -r '.repository + "@" + .singleThreaded.digest' "$IMAGES")"
cosign verify "$REF" \
--certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comA successful verification confirms the image was built by the
taucad/opencascade.js GitHub Actions docker.yml workflow and has not been
tampered with since publication.
The image also ships a SLSA provenance attestation and an SBOM:
cosign verify-attestation --type slsaprovenance \
--certificate-identity-regexp 'https://github\.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$REF"
docker buildx imagetools inspect --format '{{ json .SBOM }}' "$REF"The SBOM lists every apt package and pinned commit (OCCT, freetype, rapidjson) in the image. Diff it against the previous digest's SBOM to flag unexpected dependency bumps.
5. Never opt out in CI
$LIBCASCADE_IMAGE points the driver at any locally reachable image and skips
digest verification, printing a provenance warning when it does. That is a dev
loop, not a CI setting. Leave it unset in every automated build.
Upstream reproducibility
The repository runs .github/workflows/reproducibility.yml weekly and on
demand: two isolated Linux/amd64 cold builds in parallel, runtime smoke for
each, and an exact artifact-ledger comparison. Stable npm publication invokes
that same exact-commit gate; canary and beta builds keep the single-candidate
path and rely on scheduled cold coverage.
Related
- Driver environment — engine probing, overrides, and what each one forfeits.
- CLI reference — the commands this workflow runs.
- Docker image — tags, labels, and the stage layout behind the digests.