This document describes the supply-chain guarantees the connector project makes about its published artifacts (npm package and GHCR container image), and the recommended pinning strategy for downstream consumers.
Each release publishes two artifacts:
| Artifact | Location | Architectures |
|---|---|---|
| npm package | @toon-protocol/connector on npmjs.com |
n/a (pure JS) |
| Container image | ghcr.io/toon-protocol/connector on GitHub Container Registry |
linux/amd64, linux/arm64 (from the first release after PR #63) |
Releases are cut by semantic-release
on every push to main, when the conventional-commit history warrants a version
bump. The release pipeline is defined in .github/workflows/release.yml.
Multi-arch images (linux/amd64 + linux/arm64) ship from the first release after PR #63; adding architectures is a build-only change (no semver bump). Removing an architecture is a breaking change requiring a MAJOR bump.
For releases cut after PR #60
merged (i.e. the first release cut from a connector main containing the
docker-release ref: main checkout fix):
- Container image, semver tag → digest stability: the digest a semver tag
resolves to (e.g.
ghcr.io/toon-protocol/connector:3.5.1) is stable for the lifetime of that tag. Releases never reuse a previously-published version number, so a givenvX.Y.Ztag points to a single digest forever. - Container image, label correctness: the
org.opencontainers.image.versionlabel on the manifest equals the semver tag the image was published under. This is enforced by a post-publish assertion in the release workflow that fails the run on mismatch. - npm package, version stability:
@toon-protocol/[email protected]is immutable on npmjs.com per npm's package-management rules.
The connector's HTTP admin API surface (everything under /admin/*) follows
strict semver discipline. The rules below tell consumers what kind of
version bump to expect for any change.
| Change | Bump | Example |
|---|---|---|
/admin/* field addition |
MINOR | v3.3.x → v3.4.0 |
/admin/* field rename or removal |
MAJOR | v3.x → v4.0 |
/admin/* endpoint addition |
MINOR | v3.3.x → v3.4.0 |
/admin/* endpoint rename or removal |
MAJOR | v3.x → v4.0 |
| ILP packet wire-format change | MAJOR | v3.x → v4.0 |
Image architecture addition (e.g. arm64) |
none | build-only change |
| Image architecture removal | MAJOR | breaks pinning consumers |
Townhouse pins the connector image by digest in
packages/townhouse/dist/image-manifest.json (built by the publish
workflow — Story 45.1). Each MINOR connector release triggers a manual
digest-pin bump in townhouse, gated on the contract canary
(pnpm --filter @toon-protocol/sdk test:integration -- tests/integration/connector-contract.test.ts)
passing at the new digest. Patch releases (vX.Y.z → vX.Y.z+1) do not
require a townhouse bump unless the patch fixes a behavior townhouse
actively relied on being broken. Major bumps require a deliberate
townhouse migration cycle and a CONNECTOR_MIGRATION.md row.
Starting from v3.6.0 (cut after PR #66 merged), every connector and ATOR sidecar image is cosign-signed via keyless OIDC — no static keys, no secrets beyond the default GITHUB_TOKEN.
# Connector
DIGEST=$(docker buildx imagetools inspect ghcr.io/toon-protocol/connector:<tag> \
--format '{{ json .Manifest }}' | jq -r '.digest')
cosign verify "ghcr.io/toon-protocol/connector@${DIGEST}" \
--certificate-identity-regexp \
'https://github\.com/toon-protocol/connector/\.github/workflows/(build-and-publish|release)\.yml@.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
# ATOR sidecar (narrower regex — only build-and-publish.yml signs the sidecar)
SIDECAR_DIGEST=$(docker buildx imagetools inspect ghcr.io/toon-protocol/ator-sidecar:<tag> \
--format '{{ json .Manifest }}' | jq -r '.digest')
cosign verify "ghcr.io/toon-protocol/ator-sidecar@${SIDECAR_DIGEST}" \
--certificate-identity-regexp \
'https://github\.com/toon-protocol/connector/\.github/workflows/build-and-publish\.yml@.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'Expected output: Verification for ... -- The following checks were performed: ... certificate identity is ... and exit 0.
- Signatures cover the multi-arch manifest index digest (not per-platform sub-manifests). Verifying against the index digest is sufficient;
docker pull <image>@<index-digest>resolves to the correct per-platform sub-manifest at pull time. - Each signature is automatically published to the Sigstore Rekor transparency log.
cosign verifyconsults the log automatically — no separate flag is needed. - The signing certificate's SAN encodes the exact workflow path (e.g.
https://github.com/toon-protocol/connector/.github/workflows/build-and-publish.yml@refs/tags/v3.6.0). The--certificate-identity-regexpflag is required; omitting identity flags causes cosign to reject the verification — by design. - The
(build-and-publish|release)\.ymlregex tolerates both signers:release.ymlfires on the merge-commit-to-main andbuild-and-publish.ymlfires on the tag push. Both produce valid signatures on the same digest.
For maximum supply-chain integrity, pin by content digest rather than by semver tag:
ghcr.io/toon-protocol/connector@sha256:<digest>
Digest pinning gives byte-for-byte reproducibility regardless of any future tag-pointer changes (re-tagging, deletion, or registry compromise). Combined with Supply-chain signing, digest pinning is verifiable end-to-end.
For non-production use where tag mutability is acceptable, semver tags
(:3.5.1, :3.5, :3, :latest) are produced by docker/metadata-action
and follow standard semver-tag floating semantics.
Downstream consumers (notably toon-protocol/town's townhouse package)
learn about new connector releases via:
- GitHub UI subscription — preferred:
Watch → Custom → Releasesontoon-protocol/connector. Releases-only is a UI-side filter the REST API does not expose. ghCLI subscription — fallback: subscribes to all repository events (not releases-only):gh api -X PUT /repos/toon-protocol/connector/subscription \ -f subscribed=true -f ignored=false
Automated subscription (e.g. a GitHub Actions cron polling gh release view and opening a digest-bump PR into townhouse) is OUT OF SCOPE for
v1 and tracked as Open Thread #2 in the Townhouse HS-Mode v1 epic.
A bug in docker-release (introduced when the job was added in PR #45,
fixed in PR #60) caused
actions/checkout to resolve the workflow trigger SHA — the parent of the
chore(release): X.Y.Z [skip ci] commit semantic-release creates — instead of
main's tip. As a result, git describe --tags --abbrev=0 returned the
previous release's tag, and docker/build-push-action silently overwrote
that tag's GHCR pointer with the new release's content.
Net effect: every semver-tagged GHCR image published between PR #45 and
PR #60 carries content one release ahead of what its tag and
org.opencontainers.image.version label claim. The corruption is upward
(newer content under older label), so consumers of these tags are not running
stale code — but the tag-to-content mapping is unreliable for audit and
compliance purposes.
The :latest, :<major> (e.g. :3), and commit-SHA (e.g. :f87d7b9) tags
are not affected, because docker/metadata-action derives them from
out-of-band sources rather than the broken git describe value.
| GHCR tag | Label org.opencontainers.image.version |
Manifest revision (commit) | Actual release contents |
|---|---|---|---|
:3.4.2 |
3.4.2 |
f87d7b9b (PR #59 merge) |
v3.5.0 |
:3.4.1 |
3.4.1 |
541a38ee (PR #57 merge) |
v3.4.2 |
:3.4.0 |
3.4.0 |
5059807a (PR #55 merge) |
v3.4.1 |
:3.3.3 |
3.3.3 |
c0068b4a |
v3.4.0 |
:3.3.2 |
3.3.2 |
2bed61c2 |
one release ahead |
:3.3.1 |
3.3.1 |
96be4e78 |
one release ahead |
:3.3.0 |
3.3.0 |
057b332c (PR #45 merge) |
first release with this workflow shape |
No semver tag exists for v3.5.0 — the build that should have been published
under :3.5.0 was published under :3.4.2 instead. Consumers needing v3.5.0
content must pin by digest (the same digest currently at :3.4.2 and
:latest) until the next release after PR #60 fires, which will be the first
cleanly-tagged release.
Historical tags will not be re-tagged or backfilled; doing so would silently swap content under tags some consumers may already be pinning, with unpredictable blast radius. From the first post-#60 release forward, the guarantees in Stability guarantees apply.
Three mechanisms guard against future tag-vs-content drift:
-
Pre-publish (issue #61 / PR #60): the
docker-releasejob checks outmain's tip sogit describeresolves to the just-cut tag, mirroring the same fix applied tonpm-releasein PR #48. -
Post-publish (issue #61): after
docker/build-push-action, the workflow inspects the just-pushed manifest withdocker buildx imagetools inspectand asserts thatorg.opencontainers.image.versionequals the tag. Any mismatch fails the workflow run. -
Town mirror drift detection: The doc body is mirrored at
packages/sdk/CONNECTOR_RELEASE_CONTRACT.mdintoon-protocol/town. The town copy prepends a 3-line comment header; verify body equivalence from thetoon-protocol/connectorrepo root (withtoon-protocol/towncloned alongside as a sibling directory, e.g.../town):diff CONNECTOR_RELEASE_CONTRACT.md \ <(tail -n +4 ../town/packages/sdk/CONNECTOR_RELEASE_CONTRACT.md)Expected output: empty. Any diff is a drift defect — open a follow-up PR in both repos to restore equivalence.
- Issue #61 — historical GHCR tag corruption analysis and remediation options
- PR #60 —
docker-releaseref: mainfix - PR #48 — earlier
npm-releasefix for the same class of bug - PR #66 — cosign keyless OIDC signing (Story 44.3)
- Townhouse Story 44.4 — downstream consumer-facing release contract
- Interledger Protocol V4 (RFC 0027) — defines the ILP packet wire format referenced by the MAJOR-bump rule in API stability