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, nosetup-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
- You're already in CI/CD and you want anchoring as one more workflow step rather than as a side-channel job.
- The artifact lives only as a build output (eval report, scan result, release manifest) and you want a chain-anchored audit trail before it's published.
- You're shipping AI-generated code and need a tamper-evident binding between "this codegen run produced this diff" and the commit that lands.
- You need provenance attached to a release: "the binary this release publishes is bit-identical to the binary anchored at release-tag-time".
- You're publishing to a package registry (npm with provenance, PyPI PEP 740, container registries) and want a BSV-anchored side proof alongside the platform's native attestation.
Don't use this when:
- The artifact is just a file on a developer's machine — use Files.
- The pipeline is event-driven (webhook in, anchor out) — see Webhooks.
- The artifact is a multi-item batch with > 100 items — use Manifest directly rather than the CI adapter (which is single-artifact per step).
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:
| input | required | meaning |
|---|---|---|
path | yes | path to the artifact file (relative to repo root). |
folder | yes | Satsignal folder slug. Must exist. |
api-key | yes | bearer key (typically ${{ secrets.SATSIGNAL_API_KEY }}). |
label | no | free-text tag (e.g. ${{ github.sha }}). |
category | no | defaults to commitment. |
Outputs:
| output | meaning |
|---|---|
proof_id | the canonical proof id |
txid | the BSV transaction id |
proof_url | dashboard URL for humans |
bundle_url | download 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:
proof_id+txid— write to a release annotation, a Git commit status check, or a release-note JSON. The most common pattern is a commit-status check on the SHA: green with a link to theproof_url.bundle_url— same place. The.mbntis bearer-auth gated, so consider whether your downstream verifiers have the bearer key. For public-verifier flows, mirror the.mbntto a public CDN at release time.- The original artifact bytes. This is the load-bearing step CI runs frequently miss. The eval report / build manifest / release artifact must be persisted alongside the proof — without the bytes, the chain side verifies but no one can tie the proof back to a specific artifact.
- The handoff metadata for
satsignal.provenance.v1proofs:source,subject,identity,attestations— the JSON adapter writes this as a manifest file the verifier consumes.
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:
- The
.mbntbundle — released as an artifact alongside the binary, or fetched frombundle_urlwith the bearer key. - The original artifact — the binary, the eval report, the
.whl. Bit-identical to what was anchored. - 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
Steleet/satsignal-action@v0— floating major (gets patches + new optional inputs, never breaking changes).Steleet/satsignal-action@v0.1.0— pinned exact release. Use this if your security policy requires reproducible CI.- For bash adapters: the URLs above are stable, but the script contents may update. Vendor a copy into your repo if you need byte-exact reproducibility across runs.
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:
| folder | what flows in |
|---|---|
release-gates | release artifacts (wheels, tarballs, images) at tag time |
eval-runs-prod | eval reports from CI |
security-scans | SAST / dependency-audit outputs |
code-attestations | per-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
| code | name | meaning | what to do |
|---|---|---|---|
400 | bad_request | malformed body (usually a missing field or wrong type) | check the Action / script inputs |
401 | unauthorized | SATSIGNAL_API_KEY missing / wrong | check the secret is set in CI vars |
403 | forbidden | scoped key doesn't have access to the named folder | use an unscoped key or extend the scope |
404 | folder_not_found | folder doesn't exist | create the folder before the CI run |
409 | (handled internally) | same hash already anchored | default-dedup returns original proof_id; not an error in CI |
429 | quota_exceeded | monthly anchor count exhausted | email hello@satsignal.cloud for a cap lift, or wait for the reset |
503 | anchor_pipeline_failed | upstream wallet/broadcast failure | Action retries; rerun the job if persistent |
504 | timeout_no_records | burst-load latency tail | retry 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
- The full
satsignal.provenance.v1object shape + the stdlib-only offline-verify recipe is at proof.satsignal.cloud/spec-provenance. - If the artifact you're anchoring is generated by an agent (e.g. AI-assisted code review producing a diff), see Agents for the policy + decisions + manifest pattern that ties the run together.
- For files outside CI (local builds, ad-hoc artifacts), use Files.
- For batched artifacts (a release that ships N binaries together), use Manifest to anchor all N in one transaction.
- For the byte-selection details for build artifacts (canonicalizing a build manifest, hashing a Docker image, hashing a wheel), see What to hash.
Questions about this specification? Email hello@satsignal.cloud.