Skip to content

ci(ceremony): build, test and publish the CCDP image (#28 follow-ups) - #33

Merged
xgreenx merged 17 commits into
feat/ceremony-rebuild-planfrom
ci/ccdp-image
Sep 14, 2026
Merged

xgreenx merged 17 commits into
feat/ceremony-rebuild-planfrom
ci/ccdp-image

Conversation

@xgreenx

@xgreenx xgreenx commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Follow-ups from #28's review: CI builds, tests and publishes the CCDP image.

Changes

  • ccdp-image job (ccdp-image.yml): builds the artifact and linux/amd64 image, tests it in the container and against the pinned SWS binary; PRs never push.
  • ccdp-publish job (main only): the only packages: write job; pushes :sha-<full sha> and :main with OCI labels, seeded from :main for retention.
  • Releases promote only: :sha-<full sha>:<version> (:latest if stable) by digest, revision label checked; no image, no release ("merge to main first"): every deployable image is in the :main retention history.
  • 404 policy: a /** catch-all, emitted first, gives every 404 Cache-Control: no-store, nosniff, CORP same-origin, CSP default-src 'none'; /404.html declares the same.
  • Header rules: one exact rule per physical path (matched after rewrites, redirect-trailing-slash on); the appended form, the raw path of the 404 under a file (/ccdp/assets/a.js/a.js was immutable), is gone. Binary canary pins it.
  • /health on; SWS test binary sha256 pinned; base image public/ cleared (/ is a 404).

Verified

  • lint, format, typecheck, vitest; distribution tests 20/20, none skipped, on the built container and pinned binary: 33/33 declared responses, no redirects, nine 404s with exactly the error policy.
  • YAML, actionlint clean. CI: CCDP image / Build, test and push green.

Not verified / known issues

  • Artifact checksums unpinned (Pin artifact checksums in ceremony asset declarations #34).
  • Promotion and its failure path run only on release.
  • A 200's undeclared headers take the catch-all's value (CSP on plain assets, inert).
  • /ccdp/assets (a directory) 308s to /ccdp/assets/, then 404.
  • Integration smoke: flaky on every branch (live Google JWKS).

The `/ccdp/assets/**` header rule also matched error responses: SWS matches
`[[advanced.headers]]` sources against the raw request path when no file was
resolved, so every 404 beneath the namespace carried
`Cache-Control: public, max-age=31536000, immutable`. Drop the wildcard; each
file already gets its own exact rule with the immutable headers.

Emit both rule forms per file, the physical path and the path with its file
name appended, with identical headers. The pinned SWS 3.0.0-beta.1 appends the
resolved file name before matching when trailing-slash redirects are off; a
future SWS that stops doing so keeps every declared policy. A binary-gated
canary test pins the current matching (appended name, post-rewrite path, raw
path on errors) so a change fails loudly instead of silently dropping headers.

Enable `health = true`: `GET /health` answers 200 for readiness and liveness
probes. Compose healthchecks use it. The real-server tests assert the probe and
that 404s carry no Cache-Control, Last-Modified or Expires; the static test
asserts exact rule sources in both forms for every emitted file.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
GitHub records a sha256 digest for every release asset and returns it in the
release document. For `https://github.com/<owner>/<repo>/releases/download/...`
sources, `download()` reads that document (authenticated with GH_TOKEN or
GITHUB_TOKEN when set) and requires the bytes, fresh or cached, to hash to the
recorded digest; a mismatch fails the build. A release without a digest for the
asset warns and continues; an unreachable document uses a cached download with
a warning and fails without one.

Unit tests inject fetch and a temporary cache directory; nothing hits the
network.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Copy it to /home/sws/, outside the served root, so the next build can read the
previous publication's retention state back out of the image.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
New `ccdp-image` job: install and build the ceremony workspace, seed the build
output from the previously published ghcr.io/libid-org/ccdp:main image (its
/home/sws/public and /home/sws/distribution-graph.json) so immutable assets are
retained through the compatibility window, build the artifact, assemble the
linux/amd64 image, run it read-only with all capabilities dropped, wait for
/health, and run the distribution tests against the container and against the
pinned native SWS binary (downloaded from the GitHub release and checked
against the asset digest from the release API). Nothing may be skipped.

Pull requests build and test only. Pushes to main also push
`:sha-<short sha>` and `:main` and print the digest in the step summary. A
missing previous image (first publication) is a logged skip, with a warning
when publishing.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Describe the CI image publication and how it seeds retention from the previous
image, the pinned SWS header-matching quirk and the other native behaviors the
configuration accounts for (config.toml precedence, security-headers off with
HSTS at the ingress, HEAD without Content-Length, /health and EXPOSE 8787),
the release-digest verification, and the tests that cover them.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The SWS base image ships a placeholder /home/sws/public/index.html, and
`COPY public/ /home/sws/public/` merged into that directory, so the image
answered `GET /` with the server's default page (200, with validators) instead
of a 404. Remove the base tree before copying so the served tree is exactly
the build output. The real-server test now asserts that `/` is an uncacheable
404 alongside the other unknown routes.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The build, test and publish steps of the `ccdp-image` job now live in
.github/workflows/ccdp-image.yml (`workflow_call`, plus `workflow_dispatch`
for a dry run with `push` left false) so release.yml can run the same path.
Inputs: `push` (boolean) and `tags` (image references, one per line, added
to the always-tagged `ghcr.io/libid-org/ccdp:sha-<short sha>`; workflow
expressions cannot shorten a sha, so the workflow derives that tag itself).
ci.yml calls it with push = pushes to main and the `:main` tag; the check
now reads "CCDP image / Build, test and push".

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
A published release `v<version>` now publishes
ghcr.io/libid-org/ccdp:<version>, plus `:latest` when the version has no
prerelease `-` (the npm dist-tag rule). `publish-ccdp-image` promotes the
image ci.yml pushed as `sha-<short sha>` when the released commit landed on
main: a registry-side retag by digest (`imagetools create
--prefer-index=false`, which copies a single manifest as-is instead of
wrapping it in a new index), verified to resolve to the same digest and
printed in the step summary. When that image does not exist (not found),
`build-ccdp-image` builds, tests and pushes from source through the
reusable workflow under the version tags; any other inspect failure fails
the job instead of silently rebuilding.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
"Publication and upgrades" now describes the reusable workflow, the release
tags (`:<version>`, `:latest` for stable versions), promotion as a
same-digest retag of the tested main image (no retention seed needed), the
from-source fallback seeded from `:main`, and pinning deployments by digest.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx xgreenx self-assigned this Sep 14, 2026
@xgreenx
xgreenx marked this pull request as ready for review September 14, 2026 12:21
@xgreenx
xgreenx requested a lite review from Copilot September 14, 2026 12:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Address the two critical CI permission/retention issues and the short-SHA promotion collision risk.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds CCDP image build, test, retention, publication workflows, SWS hardening, release digest verification, and deployment documentation.

Changes:

  • Adds reusable CI and release image workflows.
  • Fixes served-tree, health-check, header, and immutable-asset behavior.
  • Adds release digest verification, tests, compose probes, and documentation.
File summaries
File Reviewed changes and findings
ts/packages/ceremony/e2e/compose.yaml Uses /health for container health checks.
ts/packages/ceremony/docs/traceability.md Updates CCDP qualification traceability.
ts/packages/ceremony/docs/testing.md Documents expanded distribution testing.
ts/packages/ceremony/docs/distribution.md Documents runtime behavior, retention, publication, and deployment.
ts/packages/ceremony/ccdp.Dockerfile Replaces the base served tree and embeds distribution metadata.
ts/packages/ceremony/build/sws.ts Adds health support and exact header rules.
ts/packages/ceremony/build/sws.test.ts Tests SWS headers, health behavior, and matching quirks.
ts/packages/ceremony/build/release.ts Verifies GitHub release asset digests.
ts/packages/ceremony/build/release.test.ts Tests release digest verification and caching.
ts/packages/ceremony/build/distribution.test.ts Tests generated artifacts and served HTTP behavior.
ts/apps/dev/compose.yaml Uses /health for readiness checks.
.github/workflows/release.yml Adds release image promotion and fallback publishing.
.github/workflows/ci.yml Invokes the reusable CCDP workflow. Critical (1 vote): PR execution grants write-capable package permissions and exposes GH_TOKEN to PR-controlled build code.
.github/workflows/ccdp-image.yml Builds, tests, retains, and publishes CCDP images. Critical (2 votes): Treating all pull failures as missing images can bypass retention safeguards. Moderate (1 vote): Seven-character SHA promotion references can collide and promote the wrong image.
Review details

Suppressed comments (1)

.github/workflows/ccdp-image.yml:80

  • The release promotion identifies the source only by the first seven SHA characters. If two commits eventually share that prefix, the later main publication overwrites this tag and a release can promote a different commit's image while the post-promotion check still passes because it only compares the new tags with that already-wrong source digest. Use a collision-resistant/full commit reference for the promotion lookup, or verify an exact commit identity embedded in the image before promoting.
          sha_ref="$IMAGE:sha-${GITHUB_SHA:0:7}"
  • Files reviewed: 14/14 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/ccdp-image.yml Outdated
Comment on lines +106 to +112
if ! docker pull --platform linux/amd64 "$IMAGE:main"; 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e5388a2: the pull's stderr is captured and the seed is skipped only when the registry says the image does not exist (manifest unknown|not found|name unknown, which is also what the read-only PR token gets before the first publication). Any other pull error (denied, network, registry) fails the run with ::error so the publication can be re-run.

Comment thread .github/workflows/ci.yml
Comment on lines +177 to +179
permissions:
contents: read
packages: write

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5eb0d0b: the reusable workflow declares no permissions and inherits its caller's, and ci.yml now has a read-only ccdp-image job for pull requests (packages: read, never pushes) plus a ccdp-publish job that runs only on pushes to main with packages: write. GH_TOKEN is still exported for the release-asset digest lookups, but on PR runs it is the read-only token.

The reusable workflow declared packages: write and ci.yml granted it on
every event, so PR-controlled build code ran with a write-capable token
although pull requests never push. The called workflow now declares no
permissions and inherits its caller's; ci.yml splits the caller into a
read-only pull-request job and a main-only publish job. A manual
dispatch is a dry run on the repository's read-only default token.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Every failed pull of :main counted as "no previous image", so a registry
outage or an auth regression on a main publication would build without
distribution-graph.json and drop retained immutable assets past the
compatibility-window guard. Only the registry's answer that the image
does not exist (manifest unknown, not found, name unknown) skips the
seed now; any other error fails the run so it can be re-run.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The promotion source was sha-<7 hex>; two commits sharing that prefix
would let a later main publication overwrite the tag and a release
promote another commit's image, unnoticed because the post-promotion
check only compares the new tags with that source. The sha tag is now
the full 40-hex sha, the image carries OCI revision/source/version
labels, and release.yml refuses to promote unless the source image's
revision label equals the released commit.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx

xgreenx commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Short-SHA promotion collision (Copilot's suppressed finding on ccdp-image.yml:80), fixed in 01fea8b: the image tag is sha-<full 40-hex sha> everywhere (workflow, release promotion, docs), the build bakes in org.opencontainers.image.revision (plus source and version), and the release promotion reads that label from the source image and refuses to promote unless it equals github.sha.

@Wondertan Wondertan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wildcard removal is useful. Two remaining findings and one simplification below.

Comment thread .github/workflows/release.yml Outdated
Comment thread ts/packages/ceremony/build/distribution.test.ts Outdated
Comment thread ts/packages/ceremony/build/sws.ts Outdated
Querying GitHub's release API for an asset digest at build time checks
what GitHub currently serves, not a reviewed value, so it adds little
over pinning checksums in the declarations (review on #33). Restore the
plain cached download and drop its tests and docs. Checksum pinning is
tracked in #34.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The CCDP image job checked the pinned static-web-server tarball against
the digest GitHub's release API reports, which is what GitHub serves
today rather than a reviewed value. Pin the sha256 literal next to
SWS_VERSION and check the download against it. The build step no longer
needs GH_TOKEN.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
A fallback build pushed `:<version>` (and `:latest`) but never `:main`,
while every later build seeds retention from `:main` alone, so assets a
fallback release introduced could vanish from the next main deployment.
Without a `sha-<commit sha>` image the release now fails and names the
fix: merge to main, let ccdp-publish run, then re-publish the release.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Absent cache headers leave a 404 heuristically cacheable (RFC 9110
§15.5.5), and the per-file rule keyed on the appended-name form
(`<file>/<name>`) equalled the raw path of the 404 beneath the file, so
`/ccdp/assets/a.js/a.js` answered 404 as immutable. Rules are now keyed
on the plain physical path only, which the pinned SWS matches after
rewrites with redirect-trailing-slash on, behind a `/**` catch-all that
carries the error policy; the canary pins that model on the real binary.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Keep KIT-001A aligned with the permitted same-origin directory redirect
and clarify that its slash destination returns 404. Actual protocol and
asset resources remain redirect-free.

Assisted-by: GPT-6
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
@xgreenx
xgreenx merged commit 71e11dc into feat/ceremony-rebuild-plan Sep 14, 2026
6 checks passed
xgreenx added a commit that referenced this pull request Sep 14, 2026
Querying GitHub's release API for an asset digest at build time checks
what GitHub currently serves, not a reviewed value, so it adds little
over pinning checksums in the declarations (review on #33). Restore the
plain cached download and drop its tests and docs. Checksum pinning is
tracked in #34.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Wondertan pushed a commit that referenced this pull request Sep 15, 2026
Querying GitHub's release API for an asset digest at build time checks
what GitHub currently serves, not a reviewed value, so it adds little
over pinning checksums in the declarations (review on #33). Restore the
plain cached download and drop its tests and docs. Checksum pinning is
tracked in #34.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Wondertan pushed a commit that referenced this pull request Sep 15, 2026
Querying GitHub's release API for an asset digest at build time checks
what GitHub currently serves, not a reviewed value, so it adds little
over pinning checksums in the declarations (review on #33). Restore the
plain cached download and drop its tests and docs. Checksum pinning is
tracked in #34.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Wondertan pushed a commit that referenced this pull request Sep 15, 2026
Querying GitHub's release API for an asset digest at build time checks
what GitHub currently serves, not a reviewed value, so it adds little
over pinning checksums in the declarations (review on #33). Restore the
plain cached download and drop its tests and docs. Checksum pinning is
tracked in #34.

Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants