OpenCascade.js

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.yml

2. 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.com

A 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

.github/workflows/wasm-build.yml
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).

On this page