Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
789d9d7
feat(ceremony): rebuild browser ceremony package around supplied popu…
Wondertan Sep 7, 2026
0464089
refactor(ceremony): group CCDP document entrypoints
Wondertan Sep 7, 2026
456aa98
docs(ceremony): organize contracts beside their owning modules
Wondertan Sep 7, 2026
036561f
docs(ceremony): keep distribution contract in package docs
Wondertan Sep 7, 2026
d5d9588
refactor(ceremony): type-check native Node build tooling
Wondertan Sep 7, 2026
5be1bb6
fix(ceremony): bundle Callback startup and version dispatch
Wondertan Sep 7, 2026
f4bf354
fix(ceremony): require explicit Callback version inputs
Wondertan Sep 7, 2026
eec8065
feat(ledger): share encoded identities with ceremony
Wondertan Sep 7, 2026
a4abb99
fix(ceremony): share unversioned Callback deployment inputs
Wondertan Sep 8, 2026
f04dff6
docs(ceremony): consolidate package documentation
Wondertan Sep 8, 2026
3243683
refactor(ceremony): declare assets once and serve with native SWS v3
Wondertan Sep 8, 2026
d7b089b
feat(ceremony): add isolated local development frontend
Wondertan Sep 8, 2026
252a91d
dev(ceremony): reuse trusted mkcert certificates across sessions
Wondertan Sep 8, 2026
977fd82
feat(ceremony): align identity delivery and creation with current con…
Wondertan Sep 8, 2026
cd895c1
fix(ceremony): accept GitHub issuer returns and preserve safe failure…
Wondertan Sep 9, 2026
676522b
fix(ceremony): align notarization disclosures with native range layouts
Wondertan Sep 10, 2026
8d8d0d1
fix(ceremony): forward the browser User-Agent to GitHub
Wondertan Sep 10, 2026
bc49862
dev(ceremony): prepare real local bridge and matched rc.2 assets
Wondertan Sep 10, 2026
8013dfc
dev(ceremony): configure shared public OAuth development clients
Wondertan Sep 10, 2026
0c78642
dev(ceremony): run Bridge and notary with Docker Compose
Wondertan Sep 10, 2026
862f18d
dev: move shared services and frontend into workspace app
Wondertan Sep 10, 2026
bd8220d
dev: close popup after ceremony finishes
Wondertan Sep 10, 2026
45f6dcd
dev: wait for Bridge before starting frontend
Wondertan Sep 10, 2026
4a03355
dev: launch ceremonies from individual platform buttons
Wondertan Sep 10, 2026
576413d
dev: show ceremony run history and local notary
Wondertan Sep 10, 2026
1465174
dev: keep failed ceremony popups open for inspection
Wondertan Sep 10, 2026
5d621ca
docs(ceremony): record updated notary and identity header contracts
Wondertan Sep 10, 2026
1d3fe35
feat(ceremony): use ledger-owned notary routing and allow identity he…
Wondertan Sep 10, 2026
64d07e1
dev: use fixed service configuration without environment overrides
Wondertan Sep 10, 2026
f91aef9
dev: run localhost ceremony services over HTTP and WebSocket
Wondertan Sep 10, 2026
31a5f42
ceremony: qualify matched notary RC3 runtime
Wondertan Sep 10, 2026
2fcdfd8
ceremony: explicitly allow same-origin asset fetches
Wondertan Sep 10, 2026
cf5ee0d
dev: remove redundant service checker
Wondertan Sep 10, 2026
0b707d2
docs(dev): keep README focused on using the app
Wondertan Sep 10, 2026
438902b
ceremony: accept JSON whitespace in transcript fields
Wondertan Sep 10, 2026
7e0bd1a
ceremony: reuse validated asset responses and browser HTTP cache
Wondertan Sep 10, 2026
dcc230c
dev: refresh CCDP without rebuilding service images
Wondertan Sep 10, 2026
ba70eef
ceremony: report sequential stages and terminal outcomes
Wondertan Sep 10, 2026
b2414a9
ceremony: split startup and OAuth return stages
Wondertan Sep 10, 2026
9d6728d
dev: show post-consent ceremony duration
Wondertan Sep 10, 2026
1985a66
ceremony: standardize active and completed stage messages
Wondertan Sep 10, 2026
59fb320
ceremony: finish proof stage when the complete result is delivered
Wondertan Sep 10, 2026
95c82b5
ceremony: clarify notary fetching stage labels
Wondertan Sep 10, 2026
7cfeeaa
ceremony: overlap GitHub identity setup with token exchange
Wondertan Sep 11, 2026
a7da69e
ceremony: tolerate additional OAuth return metadata
Wondertan Sep 11, 2026
a49991c
ceremony: reuse released verification keys for browser proving
Wondertan Sep 11, 2026
d9d8742
ceremony: initialize proof dependencies concurrently
Wondertan Sep 11, 2026
9f76405
ceremony: overlap witness execution with backend initialization
Wondertan Sep 11, 2026
42ea4b6
ceremony: open notary sockets during runtime initialization
Wondertan Sep 11, 2026
f2ae8f0
ceremony: normalize WASM assets for native HTTP compression
Wondertan Sep 11, 2026
ad2d100
ceremony: deliver assets before cache writes finish
Wondertan Sep 11, 2026
560822c
test(ceremony): trust the HTTPS cache harness certificate
Wondertan Sep 11, 2026
1d3b2bb
ceremony: adapt to the updated Bridge configuration
Wondertan Sep 11, 2026
199ab1e
dev: pin Bridge using released libid-rs v0.4.0
Wondertan Sep 11, 2026
efe36c4
specs: leave transcript amendments to PR31
Wondertan Sep 11, 2026
de925e3
ceremony: align transcript header admission with current profiles
Wondertan Sep 11, 2026
8805737
dev: extract Bridge configuration into TOML file
Wondertan Sep 11, 2026
ccb8fed
docs(ceremony): move documentation index to package root
Wondertan Sep 11, 2026
3adbd20
refactor(ceremony): separate proving and notarization modules
Wondertan Sep 11, 2026
8c194ff
refactor(ceremony): flatten runtime modules and name the CCDP client
Wondertan Sep 11, 2026
bd1618f
style(ceremony): document API contracts and standardize layout
Wondertan Sep 11, 2026
f56722e
fix(ceremony): remove obsolete compression sidecars on rebuild
Wondertan Sep 11, 2026
cc34683
refactor(ceremony): align module ownership and catalog imports
Wondertan Sep 11, 2026
1cc79b0
refactor(ceremony): narrow public API and expose version selection
Wondertan Sep 11, 2026
1e8a514
refactor(ceremony): unify protocol operation events
Wondertan Sep 12, 2026
50fe805
docs(ceremony): separate implementation guides from normative contracts
Wondertan Sep 12, 2026
b50a881
fix(ceremony): bind Prover to Callback's authenticated origin
Wondertan Sep 13, 2026
3da209e
refactor(ceremony): distinguish cancellation with CancelError
Wondertan Sep 13, 2026
c16b3bf
feat(ceremony): render popup progress from operation events
Wondertan Sep 13, 2026
310ea31
feat(dev): support concurrent ceremony runs
Wondertan Sep 13, 2026
a56e294
docs(ceremony): align local cancellation and event outcomes
Wondertan Sep 13, 2026
bad86ef
docs(ceremony): align CCDP outcome message names
Wondertan Sep 13, 2026
d1f5dfd
docs(ceremony): generalize notary address forwarding
Wondertan Sep 13, 2026
522373c
refactor(ceremony): align outcomes and popup cancellation
Wondertan Sep 13, 2026
a9e55d9
fix(dev): display prover fallback timing
Wondertan Sep 13, 2026
175898d
refactor(ceremony): keep platform policy in platform modules
Wondertan Sep 13, 2026
655c3a5
test(ceremony): automate browser proof and notary checks in CI
Wondertan Sep 13, 2026
7176eaf
docs(ceremony): consolidate implementation and deployment guides
Wondertan Sep 13, 2026
05a4b99
ci(ceremony): use the shared workspace browser job
Wondertan Sep 14, 2026
bd187b5
fix(ceremony): emit exact SWS header rules and enable the health probe
xgreenx Sep 14, 2026
11e0b28
feat(ceremony): verify release downloads against GitHub asset digests
xgreenx Sep 14, 2026
e7f4e2a
build(ceremony): keep distribution-graph.json in the CCDP image
xgreenx Sep 14, 2026
65d6242
ci(ceremony): build, test and publish the CCDP image
xgreenx Sep 14, 2026
0572726
docs(ceremony): document publication, native SWS behavior and digests
xgreenx Sep 14, 2026
e6fc11e
fix(ceremony): replace the base image's served tree in the CCDP image
xgreenx Sep 14, 2026
51bce90
ci(ceremony): move the CCDP image job into a reusable workflow
xgreenx Sep 14, 2026
67b8f90
ci(ceremony): publish the CCDP image under the release version
xgreenx Sep 14, 2026
55b4795
docs(ceremony): document release publication of the CCDP image
xgreenx Sep 14, 2026
b3971e4
ci(ceremony): keep pull-request CCDP builds read-only
xgreenx Sep 14, 2026
6930fed
ci(ceremony): fail the retention seed closed on unexpected pull errors
xgreenx Sep 14, 2026
0b83e05
ci(ceremony): tag and promote the CCDP image by full commit sha
xgreenx Sep 14, 2026
181b4a8
build(ceremony): drop the release-asset digest lookup
xgreenx Sep 14, 2026
2466700
ci(ceremony): pin the SWS test binary's sha256 in the workflow
xgreenx Sep 14, 2026
49ad478
ci(release): promote only the tested main image, never rebuild
xgreenx Sep 14, 2026
3984b36
fix(ceremony): serve every 404 with an explicit no-store error policy
xgreenx Sep 14, 2026
a610d54
docs(ceremony): align directory redirect qualification
Wondertan Sep 14, 2026
6c8f20e
fix(ceremony): publish only declared archive members
Wondertan Sep 14, 2026
e509b33
fix(ceremony): distinguish interruption and preserve connection errors
Wondertan Sep 15, 2026
ce55950
feat(ceremony): attach diagnostics to operation events
Wondertan Sep 15, 2026
afd1891
feat(ceremony): perform GitHub token exchange in the browser
Wondertan Sep 15, 2026
9737b8b
feat(dev): clarify operation timing history
Wondertan Sep 15, 2026
c17be4e
test(popup): await completed connection handoffs
Wondertan Sep 15, 2026
8334ea5
test(popup): control external-page return navigation
Wondertan Sep 15, 2026
e88ee18
test(ceremony): release held assets during fixture teardown
Wondertan Sep 15, 2026
5d03a9b
test(ceremony): report stalled runtime operations and connections
Wondertan Sep 15, 2026
d3a6d9c
test(ceremony): restore all asset controls after timeouts
Wondertan Sep 15, 2026
eb6f20b
test(popup): prepare workers before isolation handoffs
Wondertan Sep 15, 2026
f78b251
test(ceremony): retain stalled TLS runtime diagnostics
Wondertan Sep 15, 2026
5505c77
fix(ceremony): propagate synchronous notary write failures
Wondertan Sep 15, 2026
3131f6e
fix(harness): backport TLS session driver completion handling
Wondertan Sep 15, 2026
0a3b14d
test(popup): wait for document loading after navigation
Wondertan Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions .github/workflows/ccdp-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
name: CCDP image

# Builds the static ceremony artifact and the pinned SWS image for linux/amd64,
# proves the image against the running container and the pinned native
# binary, and — when `push` is true — publishes it to ghcr. Called by ci.yml
# (build and test on pull requests; publish `:sha-<commit sha>` and `:main`
# from main). A manual dispatch is a dry run: it builds and tests and never
# pushes. release.yml never builds; it promotes the `:sha-<commit sha>` image
# published from main.
#
# No `permissions` here: a called workflow runs with its caller's token and
# can only narrow it, so each caller declares what its run needs —
# `packages: read` for pull requests, which execute PR-controlled build code,
# and `packages: write` only for the runs that push. A dispatch runs with the
# repository's default token, which is read-only.
#
# The image is always tagged `ghcr.io/libid-org/ccdp:sha-<commit sha>` (the
# full 40-hex sha, so two commits can never share a tag) — the reference the
# checks run against and the one release.yml promotes after checking that
# the image's `org.opencontainers.image.revision` label names the released
# commit; `tags` adds further references (any whitespace separates them).
# The build is seeded from the previously published `:main` image so immutable
# assets stay available through the compatibility window (see "Publication and
# upgrades" in ts/packages/ceremony/docs/distribution.md).
#
# Every third-party action is pinned by commit SHA, with the tag in a
# comment, so a moved tag cannot change what executes.

on:
workflow_call:
inputs:
push:
description: Push the image to ghcr once the checks pass.
type: boolean
default: false
tags:
description: Image references to tag besides ghcr.io/libid-org/ccdp:sha-<commit sha>, one per line.
type: string
default: ""
workflow_dispatch:

jobs:
image:
name: Build, test and push
runs-on: ubuntu-latest
timeout-minutes: 20
env:
IMAGE: ghcr.io/libid-org/ccdp
ARTIFACTS: ts/packages/ceremony/dist-artifacts
SWS_VERSION: v3.0.0-beta.1
# sha256 of static-web-server-$SWS_VERSION-x86_64-unknown-linux-musl.tar.gz,
# computed from the downloaded release asset; recompute when bumping SWS_VERSION.
SWS_SHA256: c4b043f61eb63ea0fb6b13d42cc29d5732e893ab17fcee320982dcf57574378b
# A dispatch has no inputs, so this is "false" unless a caller asked.
PUSH: ${{ inputs.push == true }}
EXTRA_TAGS: ${{ inputs.tags }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
version: 10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: ts/pnpm-lock.yaml

- name: Install dependencies
run: pnpm -C ts install --frozen-lockfile

- name: Build
run: pnpm -C ts --filter '@libid/ceremony...' build

# SHA_REF is the commit's own tag; REFS is every reference the image gets
# (SHA_REF first). Each extra reference must be a tag of this image.
# IMAGE_VERSION, the `org.opencontainers.image.version` label, is the
# first extra tag (`main`) or else the sha tag.
- name: Resolve the image references
run: |
set -euo pipefail
sha_ref="$IMAGE:sha-$GITHUB_SHA"
refs="$sha_ref"
version=""
for ref in $EXTRA_TAGS; do
case "$ref" in
"$IMAGE":*) ;;
*) echo "::error::'$ref' is not a tag of $IMAGE"; exit 1 ;;
esac
case " $refs " in
*" $ref "*) ;;
*) refs="$refs $ref" ;;
esac
: "${version:=${ref#"$IMAGE":}}"
done
: "${version:=${sha_ref#"$IMAGE":}}"
echo "Image references: $refs (push: $PUSH, version label: $version)"
{
echo "SHA_REF=$sha_ref"
echo "REFS=$refs"
echo "IMAGE_VERSION=$version"
} >> "$GITHUB_ENV"

- name: Log in to ghcr
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

# The previous publication's served tree and graph let build/distribution.ts
# retain its immutable assets and verify reused URLs are unchanged. Only
# the registry's word that the image does not exist (first publication,
# or a package without a `:main` tag yet; "manifest unknown" for the
# read-only pull-request token too) skips the seed. Any other pull
# failure — network, registry, auth — fails the run, so a publication
# never drops retained assets silently and is simply re-run.
- name: Seed retention from the previously published image
run: |
set -euo pipefail
if docker pull --platform linux/amd64 "$IMAGE:main" 2>"$RUNNER_TEMP/pull.err"; then
previous=$(docker image inspect -f '{{index .RepoDigests 0}}' "$IMAGE:main")
cid=$(docker create --platform linux/amd64 "$IMAGE:main")
mkdir -p "$ARTIFACTS"
docker cp "$cid:/home/sws/public" "$ARTIFACTS/public"
docker cp "$cid:/home/sws/distribution-graph.json" "$ARTIFACTS/distribution-graph.json"
docker rm "$cid" >/dev/null
echo "Seeded $(find "$ARTIFACTS/public" -type f | wc -l) files from $previous"
exit 0
fi
cat "$RUNNER_TEMP/pull.err"
if grep -qiE 'manifest unknown|not found|name unknown' "$RUNNER_TEMP/pull.err"; then
echo "No previous image at $IMAGE:main; building without retained assets."
if [ "$PUSH" = "true" ]; then
echo "::warning::Retention seed unavailable; this publication retains no previous immutable assets."
fi
exit 0
fi
echo "::error::Could not pull $IMAGE:main; fix the cause (or re-run) rather than building without retained assets."
exit 1

- name: Build the CCDP artifact
run: pnpm -C ts --filter @libid/ceremony build:ccdp-artifacts

# The OCI labels carry the built commit inside the image, where a moved
# or reused tag cannot change it; release.yml refuses to promote an
# image whose revision label is not the released commit.
- name: Build the image (linux/amd64)
run: |
set -euo pipefail
tags=()
for ref in $REFS; do tags+=(-t "$ref"); done
docker buildx build --platform linux/amd64 --load "${tags[@]}" \
--label "org.opencontainers.image.revision=$GITHUB_SHA" \
--label "org.opencontainers.image.source=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \
--label "org.opencontainers.image.version=$IMAGE_VERSION" \
-f ts/packages/ceremony/ccdp.Dockerfile "$ARTIFACTS"
docker image inspect "$SHA_REF" \
-f 'built {{.Os}}/{{.Architecture}} {{.Id}} revision {{index .Config.Labels "org.opencontainers.image.revision"}}'

- name: Run the image hardened and wait for /health
run: |
set -euo pipefail
docker run -d --name ccdp --read-only --cap-drop ALL --security-opt no-new-privileges \
-p 127.0.0.1:28787:8787 "$SHA_REF"
for _ in $(seq 1 50); do
if curl -fsS -o /dev/null http://127.0.0.1:28787/health; then
echo "CCDP image answers /health"
exit 0
fi
sleep 0.2
done
docker logs ccdp
echo "::error::CCDP image did not answer /health"
exit 1

# The same-length ETag regression and the header-matching canary need the
# native binary; its bytes must hash to the sha256 pinned in SWS_SHA256.
- name: Fetch the pinned SWS binary and check its sha256
run: |
set -euo pipefail
asset="static-web-server-$SWS_VERSION-x86_64-unknown-linux-musl.tar.gz"
curl -fsSL -o "$RUNNER_TEMP/sws.tar.gz" \
"https://github.com/static-web-server/static-web-server/releases/download/$SWS_VERSION/$asset"
echo "$SWS_SHA256 $RUNNER_TEMP/sws.tar.gz" | sha256sum -c -
mkdir -p "$RUNNER_TEMP/sws"
tar -xzf "$RUNNER_TEMP/sws.tar.gz" -C "$RUNNER_TEMP/sws"
bin=$(find "$RUNNER_TEMP/sws" -type f -name static-web-server)
"$bin" --version
echo "CEREMONY_SWS_BINARY=$bin" >> "$GITHUB_ENV"

- name: Distribution tests against the running image and the native binary
env:
CEREMONY_SWS_URL: http://127.0.0.1:28787
CEREMONY_SWS_TEST_PORT: "28790"
run: |
set -euo pipefail
pnpm -C ts --filter @libid/ceremony test:distribution 2>&1 | tee "$RUNNER_TEMP/distribution-tests.log"
# Both inputs above are set, so nothing may have been skipped.
if grep -Eq 'skipped [1-9]' "$RUNNER_TEMP/distribution-tests.log"; then
echo "::error::distribution tests were skipped; the server/binary inputs did not reach them"
exit 1
fi

- name: Push the image
if: env.PUSH == 'true'
run: |
set -euo pipefail
for ref in $REFS; do docker push "$ref"; done
digest=$(docker image inspect -f '{{index .RepoDigests 0}}' "$SHA_REF")
{
echo "### CCDP image published"
echo
echo "- \`$digest\`"
for ref in $REFS; do echo "- \`$ref\`"; done
} >> "$GITHUB_STEP_SUMMARY"

- name: Summary (not pushed)
if: env.PUSH != 'true'
run: echo "### CCDP image built and tested; not pushed (push is false on ${{ github.event_name }})" >> "$GITHUB_STEP_SUMMARY"

- name: Container logs on failure
if: failure()
run: docker logs ccdp || true
38 changes: 35 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: CI

# TypeScript checks, workspace browser tests, infrastructure smoke and DCO.
# TypeScript checks, workspace browser tests, infrastructure smoke, CCDP image and DCO.
#
# Every third-party action is pinned by commit SHA, with the tag in a
# comment, so a moved tag cannot change what executes.
Expand Down Expand Up @@ -52,7 +52,7 @@ jobs:

# ---------------------------------------------------------------------------
# Integration smoke: boot the compose stack (fresh anvil + released
# libid-deploy + released notary/backend/keeper images) and assert the
# libid-deploy/backend plus backported notary/keeper builds) and assert the
# seams: the declarative deploy converged onto the declared canonical
# addresses, the RPC answers, the notary serves its signer identity, the
# keeper's real MPC-TLS JWKS rotation landed roots on-chain, and the
Expand All @@ -79,7 +79,7 @@ jobs:
env:
GH_OAUTH_CLIENT_ID: dummy
GH_OAUTH_CLIENT_SECRET: dummy
run: docker compose up -d --wait --wait-timeout 300
run: docker compose up -d --wait --build --wait-timeout 300

- name: Deploy service exited 0 (declarative convergence check passed)
working-directory: harness
Expand Down Expand Up @@ -158,6 +158,38 @@ jobs:
working-directory: harness
run: docker compose logs

# ---------------------------------------------------------------------------
# CCDP image: build the static ceremony artifact and the pinned SWS image for
# linux/amd64, prove the image against the running container and the pinned
# native binary, and publish it to ghcr on pushes to main. The steps live in
# the reusable ccdp-image.yml. A release only promotes the `sha-<commit sha>`
# image published here to the release version; it never builds one.
#
# Two callers so the token matches the run. Pull requests execute
# PR-controlled build code, so they get a read-only token (the called
# workflow inherits these permissions and cannot widen them) and never push.
# Only a push to main holds `packages: write`, and only it publishes.
# ---------------------------------------------------------------------------
ccdp-image:
name: CCDP image
if: github.event_name != 'push' || github.ref != 'refs/heads/main'
permissions:
contents: read
packages: read
uses: ./.github/workflows/ccdp-image.yml

ccdp-publish:
name: CCDP image (publish)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
uses: ./.github/workflows/ccdp-image.yml
with:
push: true
# `sha-<commit sha>` is always tagged; `:main` seeds the next build's retention.
tags: ghcr.io/libid-org/ccdp:main

# ---------------------------------------------------------------------------
# DCO: every commit carries a Signed-off-by trailer. Plain git over the
# event's commit range — no marketplace action, so there is nothing to pin
Expand Down
102 changes: 98 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name: Release

# Publishing a GitHub Release tagged `v<version>` publishes @libid/claim
# and @libid/claim-full (whose version tracks claim's) to npm. The tag is a
# pointer, never a source: it must equal both package.json versions. Auth
# is token-first (org NPM_TOKEN secret) with OIDC trusted publishing as the
# fallback — same model as libid-contracts.
# and @libid/claim-full (whose version tracks claim's) to npm, and the CCDP
# image to ghcr as `ghcr.io/libid-org/ccdp:<version>` (plus `:latest` for a
# stable version). The tag is a pointer, never a source: it must equal both
# package.json versions. npm auth is token-first (org NPM_TOKEN secret) with
# OIDC trusted publishing as the fallback — same model as libid-contracts.

on:
release:
Expand Down Expand Up @@ -109,3 +110,96 @@ jobs:
(cd "ts/packages/$dir" \
&& pnpm publish --access public --no-git-checks --tag "${{ steps.dist-tag.outputs.dist-tag }}")
done

# ---------------------------------------------------------------------------
# CCDP image: publish the released commit's image under the version. The
# image that ci.yml built, tested and pushed as `sha-<commit sha>` when the
# commit landed on main is promoted — retagged registry-side by digest, no
# rebuild — so the release image is byte-identical to the tested one and
# needs no retention seed. The tag is the full sha and the image's own
# revision label must name this commit, so a promotion cannot pick up
# another commit's image. A release never builds: without that image the
# job fails, because only images published from main enter the `:main`
# retention history every later build seeds from.
# ---------------------------------------------------------------------------
publish-ccdp-image:
name: Publish the CCDP image
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [verify-tag]
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/libid-org/ccdp
steps:
# `<version>` is the tag without its `v`, as on npm; a `-` marks a
# prerelease (the dist-tag rule above), which never moves `latest`.
- name: Derive the image tags from the release version
id: tags
env:
TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
version="${TAG#v}"
printf '%s' "$version" | grep -qE '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$' || {
echo "::error::version '$version' is not usable as an image tag" >&2
exit 1
}
tags="$IMAGE:$version"
if [ "$version" = "${version#*-}" ]; then
tags="$tags"$'\n'"$IMAGE:latest"
fi
echo "$tags"
{ echo "tags<<EOF"; echo "$tags"; echo "EOF"; } >> "$GITHUB_OUTPUT"

- name: Log in to ghcr
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

# `--prefer-index=false`: with a single source, imagetools would otherwise
# wrap a plain manifest in a new index, which has a different digest.
# "not found" means the commit never published from main (or its run
# failed): the fix is to merge and let ccdp-publish run, never to build
# here. Before retagging, the image's `org.opencontainers.image.revision`
# label (baked in by ccdp-image.yml) must equal the released commit: the
# tag alone is a mutable pointer. `.Image` is one config for a plain
# manifest and a per-platform map for an index.
- name: Promote the tested main image of this commit
env:
TAGS: ${{ steps.tags.outputs.tags }}
run: |
set -euo pipefail
source="$IMAGE:sha-$GITHUB_SHA"
if ! digest=$(docker buildx imagetools inspect "$source" --format '{{json .Manifest.Digest}}' \
2>"$RUNNER_TEMP/inspect.err" | jq -r .); then
cat "$RUNNER_TEMP/inspect.err"
if grep -qiE 'not found|manifest unknown|name unknown' "$RUNNER_TEMP/inspect.err"; then
echo "::error::no tested main image for $GITHUB_SHA: merge to main and let ccdp-publish run, then re-publish the release"
else
echo "::error::could not inspect $source; fix the cause and re-run"
fi
exit 1
fi
echo "$source is $digest"
revision=$(docker buildx imagetools inspect "$IMAGE@$digest" --format '{{json .Image}}' \
| jq -r 'if has("config") then . else .["linux/amd64"] end
| .config.Labels["org.opencontainers.image.revision"] // empty')
if [ "$revision" != "$GITHUB_SHA" ]; then
echo "::error::$source carries revision '${revision:-none}', not $GITHUB_SHA; refusing to promote"
exit 1
fi
echo "$source was built from $revision"
args=()
for ref in $TAGS; do args+=(-t "$ref"); done
docker buildx imagetools create --prefer-index=false "${args[@]}" "$IMAGE@$digest"
{
echo "### CCDP image promoted"
echo
echo "- \`$source\` = \`$digest\`"
} >> "$GITHUB_STEP_SUMMARY"
for ref in $TAGS; do
got=$(docker buildx imagetools inspect "$ref" --format '{{json .Manifest.Digest}}' | jq -r .)
echo "$ref is $got"
[ "$got" = "$digest" ] || { echo "::error::$ref resolved to $got, not $digest"; exit 1; }
echo "- \`$ref\` = \`$got\`" >> "$GITHUB_STEP_SUMMARY"
done
Loading