Reproducible CI
Pin GHCR images by digest, capture provenance and SBOM, lock downstream consumer pins.
A reproducible OCJS build means: same inputs, same wasm bytes, every time. Three controls close the loop.
1. Pin the image by manifest-list digest
:single-threaded, :multi-threaded, and :bindgen-base are moving tags
that roll forward with every release. CI should pin by digest so a new
publish doesn't silently change your wasm output. Pinning the manifest-list
digest (not the per-arch image digest) covers both linux/amd64 and
linux/arm64 sub-images in a single deterministic pin.
# Resolve the manifest-list digest once
docker buildx imagetools inspect ghcr.io/taucad/opencascade.js:single-threaded \
--format '{{json .Manifest.Digest}}'
# → "sha256:abc123…"
# Use the digest in CI (resolves to the matching native arch transparently)
docker pull ghcr.io/taucad/opencascade.js@sha256:abc123...
docker run --rm \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
ghcr.io/taucad/opencascade.js@sha256:abc123... \
link mybuild.yml2. Verify the cosign signature
Every published image is signed via cosign keyless signing (OIDC) of the manifest-list digest — one signature verifies regardless of which arch the consumer pulls.
cosign verify ghcr.io/taucad/opencascade.js@sha256:abc123... \
--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. CI failing this step on a sudden
mismatch is a smoking gun that something tampered with the supply chain.
2b. Verify SLSA provenance attestation
In addition to the cosign signature, the image ships a SLSA provenance attestation:
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 \
ghcr.io/taucad/opencascade.js@sha256:abc123...The attestation records the source commit, the runner, and the workflow that produced the image.
3. Extract SBOM
docker buildx imagetools inspect \
--format '{{ json .SBOM }}' \
ghcr.io/taucad/opencascade.js@sha256:abc123...The SBOM lists every apt package and pinned commit (OCCT, freetype, rapidjson) embedded in the image. Diff it against the prior digest's SBOM in CI to flag unexpected dep bumps.
4. Lock downstream consumers
In your package.json, pin the npm tarball alongside the docker digest:
{
"dependencies": {
"@taucad/opencascade.js": "3.0.0-beta.5"
},
"pnpm": {
"supportedArchitectures": {
"os": ["linux", "darwin"],
"cpu": ["x64", "arm64"]
}
}
}Commit pnpm-lock.yaml. The lockfile records the integrity hash of the
published tarball; if it ever changes, pnpm install --frozen-lockfile fails
in CI.
End-to-end CI snippet
jobs:
build-wasm:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Verify cosign signature
run: |
DIGEST=$(cat .ocjs-digest)
cosign verify "ghcr.io/taucad/opencascade.js@${DIGEST}" \
--certificate-identity-regexp 'https://github.com/taucad/opencascade\.js/\.github/workflows/docker\.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
- name: Verify image provenance
run: |
DIGEST=$(cat .ocjs-digest)
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 \
"ghcr.io/taucad/opencascade.js@${DIGEST}"
- name: Build wasm
run: |
DIGEST=$(cat .ocjs-digest)
docker run --rm \
-v "$(pwd):/src" \
-u "$(id -u):$(id -g)" \
"ghcr.io/taucad/opencascade.js@${DIGEST}" \
link build-configs/my-config.yml
- name: Assert wasm hash
run: |
EXPECTED=$(cat .wasm-hash)
ACTUAL=$(sha256sum my-config.wasm | cut -d' ' -f1)
[ "$EXPECTED" = "$ACTUAL" ] || { echo "wasm hash drift"; exit 1; }The trailing assert wasm hash step is the safety net. If EXPECTED and
ACTUAL ever diverge, something in the published image changed — either
intentionally (bump your .wasm-hash) or by accident (investigate).