CI/CD — anchor build artifacts as a workflow step

If the artifact you care about lives in a CI pipeline — an eval report, a release manifest, an AI-generated code diff, a security-scan output, a container image, a package — anchor it as a workflow step. One composite GitHub Action covers the common case; thin bash adapters wrap GitLab, Bitbucket, Docker BuildKit, npm provenance, and PyPI PEP 740. All bash-only; no setup-python, no setup-node, no SDK install.

Companion docs: API reference · OpenAPI spec · What to hash · provenance-v1 spec · Production checklist · Compatibility map

1. The 60-second framing

You have a build step that produces an artifact (eval-results.json, build-manifest.json, a .whl, a Docker image manifest). You add one step after that step which anchors it. The next step in your pipeline can read the resulting proof_id / txid / proof_url as step outputs — usually to attach the proof URL to a release note, a Slack message, or a commit-status check.

The artifact is the canonical bytes — the bytes whose hash gets anchored. The provenance metadata (source, subject, identity, attestations) is shaped into a satsignal.provenance.v1 manifest and is itself one of the anchored artifacts. A verifier with the bundle can confirm "the artifact this proof_id covers is bit-identical to the artifact your release shipped" without trusting anyone in the build chain.

2. Use this when

Don't use this when:

3. What you send

The GitHub Action shape

Steleet/satsignal-action@v0 is a composite action — bash only, runs on ubuntu-latest, no language toolchain setup. Inputs:

inputrequiredmeaning
pathyespath to the artifact file (relative to repo root).
folderyesSatsignal folder slug. Must exist.
api-keyyesbearer key (typically ${{ secrets.SATSIGNAL_API_KEY }}).
labelnofree-text tag (e.g. ${{ github.sha }}).
categorynodefaults to commitment.

Outputs:

outputmeaning
proof_idthe canonical proof id
txidthe BSV transaction id
proof_urldashboard URL for humans
bundle_urldownload URL for the .mbnt (bearer-auth required)

The @v0 Action also mirrors its outputs under the legacy names — see the Compatibility map.

The bash adapter shape (every other CI system)

Each adapter is one bash script. The header of each file documents its own usage; they share a common shape:

ARTIFACT_PATH=...
FOLDER=...
SATSIGNAL_API_KEY=...

HASH=$(sha256sum "$ARTIFACT_PATH" | awk '{print $1}')
SIZE=$(wc -c < "$ARTIFACT_PATH" | tr -d ' ')

curl -X POST https://app.satsignal.cloud/api/v1/anchors \
  -H "Authorization: Bearer $SATSIGNAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"folder_slug\":\"$FOLDER\",
    \"sha256_hex\":\"$HASH\",
    \"file_size\":$SIZE,
    \"category\":\"commitment\",
    \"label\":\"$CI_LABEL\",
    \"filename\":\"$(basename "$ARTIFACT_PATH")\"
  }" | jq .

The adapters per CI system wire CI-native env vars ($GITHUB_SHA, $CI_COMMIT_SHA, $BITBUCKET_COMMIT, $DOCKER_METADATA_OUTPUT_JSON) into the label and apply the right artifact-path discovery for that ecosystem.

4. What you store

CI runs are usually transient — the runner disappears after the job. Persist these outside the runner:

For releases specifically: ship the .mbnt as a release asset alongside the artifact. A consumer fetching the release gets both the binary and the chain-anchored proof, and can verify without a Satsignal account.

5. What verification needs later

Three things, every time:

  1. The .mbnt bundle — released as an artifact alongside the binary, or fetched from bundle_url with the bearer key.
  2. The original artifact — the binary, the eval report, the .whl. Bit-identical to what was anchored.
  3. A BSV node — any public one.

Verification re-hashes the artifact, confirms it matches the canonical doc's byte_exact.hash, and confirms the bundle binds to the txid. Today that runs from the web verifier at proof.satsignal.cloud/verify (drag-drop the .mbnt, account-free) or, scriptably in CI, from the unzip + sha256sum recipe (hash the bundle's canonical.json, compare to doc_hash_expected, resolve the txid on any public node). (A standalone satsignal verify CLI, with --spv / --min-confirmations, that folds this into one command is planned but not yet shipped.)

For satsignal.provenance.v1 proofs specifically: the bundle carries source / subject / identity / attestations metadata in the canonical doc. A verifier can additionally check that the subject.digest matches the artifact and the identity matches the build's claimed signer. Stdlib-only offline-verify recipe is in the provenance-v1 spec.

Provenance proofs are hash_only — they verify offline from the .mbnt and are intentionally not /lookup_hash-resolvable. The verifier needs the bundle; there's no naked-hash lookup path.

6. Copy-paste example

GitHub Actions (composite, ubuntu-latest)

- name: Anchor eval results
  id: anchor
  uses: Steleet/satsignal-action@v0
  with:
    path: ./eval-results.json
    folder: release-gates
    label: ${{ github.sha }}
    api-key: ${{ secrets.SATSIGNAL_API_KEY }}

- name: Publish proof URL to PR
  run: |
    echo "Proof: ${{ steps.anchor.outputs.proof_url }}"
    echo "TXID:  ${{ steps.anchor.outputs.txid }}"

Pin @v0 for the floating major or @v0.1.0 for the exact release. inbox is the only folder that exists out-of-the-box — create others from your workspace before referencing them.

GitLab CI (include: remote:)

include:
  - remote: 'https://satsignal.cloud/gitlab-ci.satsignal.yml'

anchor-eval-results:
  extends: .satsignal-anchor
  variables:
    SATSIGNAL_PATH: ./eval-results.json
    SATSIGNAL_FOLDER: release-gates
    SATSIGNAL_LABEL: $CI_COMMIT_SHA
  # SATSIGNAL_API_KEY comes from GitLab CI/CD variables (masked).

The full gitlab-ci.satsignal.yml is the documented usage at satsignal.cloud/gitlab-ci.satsignal.yml. Its header carries the up-to-date variable list.

Bitbucket Pipelines

pipelines:
  default:
    - step:
        name: Anchor build manifest
        script:
          - curl -sSL https://satsignal.cloud/bitbucket-pipelines.satsignal.yml -o satsignal.sh
          - chmod +x satsignal.sh
          - SATSIGNAL_PATH=./build-manifest.json \
            SATSIGNAL_FOLDER=release-gates \
            SATSIGNAL_LABEL=$BITBUCKET_COMMIT \
            ./satsignal.sh

The bash wrapper at satsignal.cloud/bitbucket-pipelines.satsignal.yml is a single script — copy it into your repo or curl it inline as above.

Docker BuildKit (anchor the image manifest)

# In your CI step after `docker buildx build --metadata-file md.json`:
curl -sSL https://satsignal.cloud/docker-buildx.satsignal.sh -o satsignal.sh
chmod +x satsignal.sh

SATSIGNAL_METADATA=./md.json \
SATSIGNAL_FOLDER=release-gates \
SATSIGNAL_LABEL=$CI_COMMIT_SHA \
./satsignal.sh

The script reads BuildKit's metadata-file JSON to extract the image digest, then anchors a satsignal.provenance.v1 manifest binding the digest to the source commit. Anchored manifest goes into the named folder with category: "evidence_bundle".

npm provenance (alongside npm publish --provenance)

# After your build step, before `npm publish`:
TARBALL=$(npm pack --json | jq -r '.[0].filename')

curl -sSL https://satsignal.cloud/npm-provenance.satsignal.sh -o sat.sh
chmod +x sat.sh

SATSIGNAL_TARBALL="$TARBALL" \
SATSIGNAL_FOLDER=release-gates \
SATSIGNAL_LABEL=$GITHUB_SHA \
./sat.sh

# Then proceed with npm publish.
npm publish --provenance

The Satsignal anchor is a side proof; npm's own provenance attestation (via Sigstore) runs in parallel. A consumer can verify either or both — they bind to the same tarball digest.

PyPI PEP 740

# After `python -m build`, before `twine upload`:
WHEEL=$(ls dist/*.whl | head -1)

curl -sSL https://satsignal.cloud/pypi-pep740.satsignal.sh -o sat.sh
chmod +x sat.sh

SATSIGNAL_WHEEL="$WHEEL" \
SATSIGNAL_FOLDER=release-gates \
SATSIGNAL_LABEL=v$(python setup.py --version) \
./sat.sh

twine upload "$WHEEL"

The satsignal.provenance.v1 manifest binds the wheel's digest to the source commit + the build attestations from PEP 740. The .mbnt is uploaded as a release asset alongside the wheel — see the script's header for the recommended publish pattern.

7. Production notes

Pinning

Idempotency in CI

CI re-runs (same SHA, same job) will hit the same sha256_hex — the notary's default-dedup gate returns the original proof_id without burning fresh quota. This is the desired behavior: a re-run of a green build shouldn't double-anchor.

If you want a fresh anchor on re-run (rare — usually for debugging idempotency itself), set the force_new input on the Action or pass SATSIGNAL_FORCE_NEW=true to the bash adapters.

Folders for CI

Create one folder per release line / per repo / per stage. The recommended layout:

folderwhat flows in
release-gatesrelease artifacts (wheels, tarballs, images) at tag time
eval-runs-prodeval reports from CI
security-scansSAST / dependency-audit outputs
code-attestationsper-PR code-generation artifacts

A scoped API key per folder limits blast radius if a CI secret leaks. Out-of-scope reads return 404 — a leaked scoped key can't enumerate the rest of your workspace.

Issuer chain-tag — what a chain observer sees

Each anchor's on-chain payload carries a 4-byte issuer_id TLV (/spec-mbnt §11). On the hosted tier it is one constant shared by all workspaces, so an observer sees "a Satsignal-issued anchor at time T" — not which workspace or repo; the artifact stays behind the hash. A self-run deployment anchoring with its own issuer DID makes its anchors enumerable as a class on-chain — release cadence becomes publicly observable. There is no per-request opt-out on the hosted API today; self-run pipelines can suppress the TLV via the pipeline's publish configuration. Details: /whats-on-chain §4.

Rate limits

CI fan-out can spike fast (parallel matrix builds anchoring at once). There is no per-minute or per-hour burst limiter on the anchor API today — the plan quota window is the only key-level throttle (details in the Files guide), so a parallel matrix drains quota faster, not a burst bucket. Under heavy bursts individual requests can 504; retry with the same Idempotency-Key (see §7 above).

Quota

Each anchored artifact = 1 anchor against the monthly quota. A 50-job matrix that all anchor at release time burns 50 slots. Wire GET /api/v1/usage into a pre-flight check on the release-tag workflow if quota exhaustion would block a release.

Failed-anchor recovery in CI

If the anchor step errors with a 5xx or a 504: the Action retries with exponential backoff (3 attempts default, configurable via retries input). After that it surfaces the error as a job failure with the upstream HTTP body in the log. For the bash adapters, wrap in retry or your CI's max-retries block.

Key rotation

Rotate the CI secret (SATSIGNAL_API_KEY) at the secret-manager level. Past anchors are not invalidated — only future anchor calls and bundle downloads need the new key.

For the full pre-flight checklist (key rotation, broadcast failure recovery, support flow), see Production checklist.

8. Errors you might see

codenamemeaningwhat to do
400bad_requestmalformed body (usually a missing field or wrong type)check the Action / script inputs
401unauthorizedSATSIGNAL_API_KEY missing / wrongcheck the secret is set in CI vars
403forbiddenscoped key doesn't have access to the named folderuse an unscoped key or extend the scope
404folder_not_foundfolder doesn't existcreate the folder before the CI run
409(handled internally)same hash already anchoreddefault-dedup returns original proof_id; not an error in CI
429quota_exceededmonthly anchor count exhaustedemail hello@satsignal.cloud for a cap lift, or wait for the reset
503anchor_pipeline_failedupstream wallet/broadcast failureAction retries; rerun the job if persistent
504timeout_no_recordsburst-load latency tailretry with the same idempotency key

9. Legacy field aliases (if you see them)

New workflow code: read the canonical outputs (proof_id, proof_url, folder_slug). The @v0 Action keeps mirroring the legacy output names for pre-vocabulary-rename workflows.

Full canonical/legacy mapping across endpoints, fields, scopes, CLI flags, and error codes: Compatibility map.

10. Where this fits

Questions about this specification? Email hello@satsignal.cloud.