Sitelet https://github.com/cuioss/API-Sheriff/pull/145
Skip to content

feat(release): publish hardened, scanned and signed OCI image to GHCR - #145

Merged
cuioss-oliver merged 12 commits into
mainfrom
feature/plan-26-release-artifacts-oci
Aug 3, 2026
Merged

cuioss-oliver merged 12 commits into
mainfrom
feature/plan-26-release-artifacts-oci

Conversation

@cuioss-oliver

@cuioss-oliver cuioss-oliver commented Aug 2, 2026 •

Copy link
Copy Markdown
Collaborator

Intent

API Sheriff had a release procedure but no release product definition: release.yml published jars to Maven Central and nothing else, no container image was published anywhere, and no document stated what a release contains. This PR fixes both halves — it defines the release artifact set and makes the OCI container a first-class, hardened, scanned, signed, published release artifact.

Implements PLAN-26 of the api-sheriff-roadmap epic (workstream WS-04, track 0.1.0 RELEASE).

Deliverables

# Deliverable Footprint
1 OCI hardening: build-context allowlist + build-arg version label Dockerfile.native, api-sheriff/.dockerignore (new), integration-tests/docker-compose.yml
2 Release-artifact policy: the artifact set, the two-tag scheme, version identity doc/development/release-process.adoc
3 GHCR publish lane: a dependent sibling job on the dispatch-only release workflow .github/workflows/release.yml
4 Release-lane SBOM + authoritative gating vulnerability scan .github/workflows/release.yml
5 PR-lane non-gating supply-chain scan .github/workflows/maven.yml
6 Cosign keyless-OIDC signing of the published digest .github/workflows/release.yml
7 Operator guide: pulling, running and verifying the published image doc/user/container-image.adoc (new), doc/user/README.adoc
8 Release-pipeline SVG flow diagram doc/resources/diagrams/release-publish-flow.svg (new)
9 Reference-layer coverage for the container-image topic doc/README.adoc

No api-sheriff/src/main/java/** change — this is packaging, policy and CI, exactly as the plan predicted.


Read this before "simplifying" the publish lane

1. The build → test → push order is deliberately INVERTED, and the inversion is what makes the guarantee true

The plan specified docker build once → run the ITs against that image → push it. That sequence is not implementable as written: integration-tests/pom.xml:313-332 binds docker compose build api-sheriff to pre-integration-test, so running the IT suite always rebuilds the image. A literal implementation would have tested one image and pushed a different one — precisely the failure the deliverable exists to prevent.

The lane therefore runs the IT suite first and lets its build be the one and only build, then tags and pushes exactly the image it produced. One build total. "Never rebuild between test and push" now holds by construction rather than by discipline — there is only one build, so nothing can drift.

Do not "correct" this back toward the literal spec order, do not add a second docker build, and do not add a skip-guard to integration-tests/pom.xml.

2. Why this satisfies TEST THE DELIVERED ARTIFACT

A push is a transfer, not a rebuild. An image digest is computed from content, so the registry digest equals the local one and the suite ran against the published artifact by digest identity, not by assertion. The image is handed between steps by its content-derived ID and then by registry digest — never by tag, because the local image store is tag-keyed and will silently serve a stale image. (This project has already been bitten by exactly that in the IT fast loop.)

3. The two-lane scan arrangement was chosen over the cheaper single-lane option

The PR lane (maven.yml) is an early-warning, non-gating signal on every change; the release lane is the authoritative gate on the exact tested artifact before it is published. These are two distinct guarantees and neither is a simplification of the other. The cheaper release-lane-only option was explicitly considered and rejected. Non-gating is implemented with exit-code: '0' on every Trivy step — continue-on-error is deliberately not used, because it would also mask a genuine tool or network failure.

4. The composability hypothesis was half false — resolved without forking

A caller can hang a dependent sibling job off a uses: job. But reusable-maven-release.yml@15376a19 declares no on.workflow_call.outputs block, so needs.release.outputs.released-version evaluates to the empty string and would push ghcr.io/cuioss/api-sheriff: — a malformed reference, or worse a silently mislabelled image. The version is instead re-read from .github/project.yml using the same pinned action the org workflow itself uses. The org workflow is not forked and the dispatch-only guarantee is not weakened.

5. The release trigger is untouched

release.yml remains workflow_dispatch:-only. The diff is strictly additive (@@ -24,3 +24,317 @@); the on: block and the comment recording the 2026-07-12 accidental Maven Central release are byte-identical to main. Both "Why…" sections of release-process.adoc were read in full before the file was touched.


Decisions taken by the operator

  • Cosign signing: APPROVED, keyless OIDC only. No managed key, no long-lived secret — id-token: write is the whole credential (already precedent at scorecards.yml:18, pr-agent.yml:26, claude.yml:29). Signing is by digest, after the push and after the smoke. SLSA provenance and further attestation are deliberately out of scope.
  • Trivy threshold: HIGH and CRITICAL both fail the release. The stricter posture, chosen because API Sheriff is a security-focused gateway. Not a tool default.
  • Tags: <version> plus an immutable sha-<40-char release-tag commit>. No floating :latest — a floating tag is exactly what makes a deployment non-reproducible.
  • Scan runs in BOTH lanes (see §3 above).

Three-layer documentation

  • Developer (doc/development/release-process.adoc) — deliverable 1: the artifact set, the two-tag scheme, the version-identity guarantee, the one-build inversion, the one-time GHCR action. doc/development/README.adoc index entry updated.
  • Operator (doc/user/container-image.adoc) — how to pull, run and verify the published image, including the exact cosign verify recipe with --certificate-identity / --certificate-oidc-issuer.
  • Reference — doc/README.adoc gains the container-image topic. doc/configuration.adoc and doc/architecture.adoc are genuinely unchanged, not silently skipped: this plan introduces no configuration key and no architectural component. Verified — git diff --stat against both is empty.

Scope honesty: what the image scan does and does not cover

The image is the pinned distroless base plus exactly one layer: a GraalVM native executable. It is dynamically linked (no --static, no --libc=musl is configured), which is why the base is the glibc-bearing quarkus-distroless-image rather than scratch. There are no jars in the image, so Trivy's Java analyzer finds nothing and the image scan is in substance a scan of the pinned base image's OS packages. Java dependency CVEs are covered by the PR-lane fs scan, Sonar and Dependabot — not by the image scan. A clean image scan is not evidence of a clean dependency tree. This is stated in the workflow and in the docs so it cannot be misread later.

Known limitations, recorded rather than hidden

  1. The smoke step's pull-by-digest is not a real registry round-trip. docker rmi removes only the two tags; the image stays resident under its ghcr.io/cuioss/api-sheriff@sha256:… RepoDigest, so the pull is a no-op and the PULLED_ID != IMAGE_ID assertion cannot fire. The digest-identity guarantee still holds by content addressing — what is unproven is registry retrievability. Not fixed here because evicting the image ID can fail against a stopped IT container and the step runs under set -euo pipefail, so a wrong guess breaks the release lane.
  2. publish-image runs harden-runner in audit mode while holding packages: write + id-token: write simultaneously. egress-policy: block is the stronger posture, but a correct allowlist needs an observed audit run of a real dispatched release. Recommended follow-up after the first 0.1.0 release.
  3. A new GHCR package is private by default and the in-workflow smoke pulls with its own credentials, so it cannot detect this. The first release needs a one-time manual visibility flip — on the release checklist in release-process.adoc.
  4. Unrelated pre-existing drift observed, not carried: running verify -Ppre-commit applies OpenRewrite import-reordering to 8 files under api-sheriff/src/{main,test}/java that this plan does not own. Reverted rather than committed, to honour the plan's explicit no-production-source boundary. Worth its own follow-up.

Verification performed

  • verify -Ppre-commit — green (490s)
  • verify (full) — green (342s)
  • Both workflow files re-parse as valid YAML after every edit; release.yml triggers confirmed {workflow_dispatch: None} at each step.
  • All 8 distinct uses: pins resolved against upstream refs. One was wrong and is fixed: trivy-action was pinned to the annotated-tag object SHA rather than a commit, which Dependabot's github-actions ecosystem does not track — a scanning action would have silently stopped receiving bumps.
  • The two cosign verify strings duplicated between release.yml and container-image.adoc confirmed character-identical by exact literal match.
  • The SVG was rendered and read back at both #ffffff and #0d1117 via containerised rsvg-convert; the first pass surfaced three real layout defects which were fixed and re-rendered clean.
  • The .dockerignore allowlist was traced rule-by-rule under Docker's last-match-wins semantics against the Dockerfile's single COPY --chmod=0755 target/*-runner /app/application.

The deliverable-1 IT verification did not run locally (architecture resolve reports execution_tier: orchestrator, 652s, exceeding the ceiling). It closes on this PR: integration-tests.yml runs verify -Pintegration-tests -pl integration-tests -am, which exercises docker compose build api-sheriff and therefore empirically proves the reduced build context, Dockerfile readability under the new .dockerignore, and the APP_VERSION wiring. Please do not waive that check.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RgENQAQcLGAWiLFfM414Yx

Summary by CodeRabbit

  • New Features

    • Added a published container image with version labels and support for digest-pinned deployment.
    • Added image signing, SBOM generation, vulnerability scanning, and release validation.
    • Management health probes now use HTTPS by default when certificates are configured.
  • Documentation

    • Added guidance for pulling, running, verifying, and securely deploying the container image.
    • Expanded release documentation covering image tags, signatures, SBOMs, and security checks.

cuioss-oliver and others added 11 commits August 2, 2026 22:33
…build context

Replace the stale hardcoded org.opencontainers.image.version="0.1.0-SNAPSHOT" with
${APP_VERSION}, declared as ARG APP_VERSION=dev after FROM so it is visible to the
stage. The `dev` default is deliberate: a locally or PR-built image is never
published, and a version-shaped default would lie the moment the project version
moved.

Add api-sheriff/.dockerignore at the build-context root (not the repo root — the
context is `../api-sheriff` per docker-compose.yml and `api-sheriff/` per the
production docker build). It is an allowlist reducing the context to the single
artifact the Dockerfile consumes, using the stepwise re-inclusion idiom Docker's
directory-exclusion rule requires.

Wire build.args.APP_VERSION into the compose api-sheriff service — the harness
build is the one and only image build, so this is the single supply point for the
version label.

Correct the stale line-4 comment, which asserted a fixed plain-HTTP management
posture. Management has exactly one port and is HTTPS by default per ADR-0025;
plain HTTP is reachable only through the named
quarkus.management.tls-configuration-name=plain-management opt-out.

Co-Authored-By: Claude <noreply@anthropic.com>
Add `What a release contains` and `Container image tags` to the release process
page: the full artifact set (Maven coordinates, the GHCR image, its SBOM and its
Cosign signature), the Helm chart and Compose sample named as explicitly deferred,
and the rule that the image version is the Maven version — enforced by the publish
job's version-identity assertion rather than by convention.

Record the build-authority inversion (test-which-builds, then push-that-digest) and
why it exists, so a later reader does not "correct" it back into a second docker
build or a skip-guard on integration-tests/pom.xml.

State the two-tag scheme: `<version>` for humans and `sha-<commit>` for immutable
provenance, where `<commit>` is the full 40-character SHA of the commit the release
tag points at, not the dispatching main HEAD. No floating `:latest` at pre-1.0, with
the reason.

Add the one-time post-first-release action to the checklist: a GHCR package created
by a GITHUB_TOKEN push is private by default and the in-workflow smoke cannot detect
it.

The two `Why …` sections recording the 2026-07-12 accidental release and the
merge-order rule are untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
…ase workflow

Add one dependent sibling job, publish-image, needs: [release]. The trigger is
untouched: release.yml stays workflow_dispatch-only and the 2026-07-12 comment
recording the accidental Maven Central release stays verbatim. The release job,
its uses: pin and its secrets: block are unchanged.

The job re-reads the released version from .github/project.yml with the same
pinned read-project-config action the release job used, because
reusable-maven-release.yml declares no on.workflow_call.outputs block and
needs.release.outputs.released-version would silently evaluate to the empty
string. It then checks out the release tag — never the dispatching ref — and
asserts the checked-out project.version equals that version before anything is
built or pushed.

The integration-test suite is the one and only image build in the lane: its
pre-integration-test binding already runs docker compose build, so
test-which-builds then push-that-digest makes "push exactly what was tested" true
by construction. The image is addressed by content-derived image ID between build
and push, and by registry manifest digest thereafter.

Both release tags are pushed (<version> and sha-<release-tag-commit>) against a
hardcoded lowercase registry path, the pushed digest is resolved once and asserted
unique, and the published artifact is smoked by digest: pulled image ID equals the
tested ID (the load-bearing assertion), the version label matches, and the
container becomes live over its HTTPS management interface.

Co-Authored-By: Claude <noreply@anthropic.com>
…d emit an SBOM

Insert two steps into publish-image, between the image-ID resolution and the GHCR
login so the gate provably precedes any registry write.

anchore/sbom-action emits SPDX JSON for the tested image and retains it as a
workflow artifact named for the released version. It is pinned by SHA with a
version comment, like every other action in this repository, and
upload-release-assets is off because the job holds only contents: read.

aquasecurity/trivy-action scans the same image ID with severity HIGH,CRITICAL and
exit-code 1. A finding at either severity fails the job before docker login runs.
The threshold is stricter than a CRITICAL-only gate on purpose and the workflow
says why.

Two comments record what the file would otherwise not tell a future reader: the
scan's real coverage — the image is the pinned distroless base plus one static
native executable and carries no jars, so this is in substance a base-OS-package
scan and a clean result is not a clean dependency tree — and the pinned
attestation posture, namely that the lane must not switch to
docker buildx build --push, whose default attestations would wrap the push in a
manifest list and change what the pull-by-digest smoke pulls and what Cosign signs.

Co-Authored-By: Claude <noreply@anthropic.com>
…orkflow

Add a second job, supply-chain-scan, to maven.yml. It declares no needs:, so it
neither delays nor is delayed by the existing build job, and it runs on the
workflow's existing pull_request / push / merge_group triggers with no trigger
change.

It scans the pinned base image — read by digest out of Dockerfile.native rather
than restated, with a guard that fails the step if the base ever stops being
digest-pinned — and the repository filesystem, which is what actually surfaces
Java dependency CVEs. It emits an SPDX SBOM for the base image, a readable report
into the job summary, and SARIF as an artifact.

No pull request gains a native image build: scanning the base plus the filesystem
gets the early-warning signal at roughly a minute and zero builds, and the release
image is that same base plus one static binary carrying no package metadata.

Every Trivy step sets exit-code: 0, so a vulnerability finding never fails the PR.
continue-on-error is deliberately not used — it would also mask a genuine tool or
network failure, whereas exit-code: 0 suppresses only the vulnerability verdict.

A comment names the other lane and the distinct purpose of each, so the two are
not later "simplified" as redundant.

Co-Authored-By: Claude <noreply@anthropic.com>
Append sigstore/cosign-installer and a `cosign sign --yes` over the resolved
registry digest to publish-image, after the push and after the smoke, so the
signed artifact is provably the tested and published one. Signing is by digest,
never by tag: a tag is a mutable pointer and signing one signs nothing durable.

No new job, no new permission — id-token: write is already on the job — no key
material, no COSIGN_PRIVATE_KEY and no new secret. SLSA provenance and any further
attestation stay out of scope.

A comment records the two values a consumer needs to verify the signature. They
are derived from this job's identity and change silently if the workflow file is
renamed or a release is dispatched from a non-main ref, so recording them here is
what keeps them from drifting from the copy the operator guide publishes.

Also route the GHCR credentials through the step environment instead of
interpolating `${{ }}` into the run block, matching the treatment already used for
every other value in this job.

Co-Authored-By: Claude <noreply@anthropic.com>
Add doc/user/container-image.adoc: which tag to use and why no `:latest` exists,
a `docker run` that reflects what the artifact genuinely requires rather than an
idealised one-liner — the mandatory mounted configuration directory (the container
fails fast without one because sheriff.config.dir is relative), the
deployment-supplied certificate material for both listeners, the published ports,
and the QUARKUS_HTTP_SSL_PORT move that tls.passthrough_sni forces.

State honestly what the SBOM and the scan cover: the release fails on a HIGH or
CRITICAL finding against the exact published image, but that image is the pinned
distroless base plus one static executable with no jars, so an operator who needs
a Java dependency inventory reads the SBOM rather than inferring it from a clean
scan.

Give the complete `cosign verify` recipe with both mandatory flags, explain what
each one pins and why omitting either makes the verification meaningless, and
state that a non-zero exit means the artifact must not be deployed. The identity
and issuer strings are character-identical to the comment in release.yml.

Add the corresponding row to the operator-guide index; the layer's scope statement
already named the production container image, so no scope text changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Add doc/resources/diagrams/release-publish-flow.svg, a two-track flow diagram
authored to the existing flow type: the release lane (dispatch, Maven release,
release-tag checkout with the version assertion, the IT suite that is the one and
only image build, the SBOM and gating scan, tag and push, pull-by-digest smoke,
Cosign sign) and the independent non-gating pull-request lane. The two tracks
deliberately do not converge — that is the diagram's load-bearing idea, and the
footer caption says so.

Shape carries the stage role: taller process shapes for the work that takes time,
a diamond for the scan gate, event shapes for the moments. Theme-neutral strategy
A with the single #6e7681 token, matching the five existing diagrams, and
viewBox width 1160 matching request-pipeline.svg so the two flow diagrams render
at the same scale.

Rendered against #ffffff and #0d1117 and both PNGs read back; the Step 4 checklist
passes on both backgrounds. The rendered page carries exactly one reference to the
SVG.

Embed it in the `What a release contains` section with alt text. Every section the
page already had survives unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Add a `Container Image (three-layer topic)` row to the design index, following the
shape the existing TLS Edge row establishes. The design index is where a reader
discovers that a topic exists across all three layers, so this is the reference
layer's real, non-vacuous edit.

The row itself carries the reference-layer statement: the image is a packaging of
the same binary the architecture document already describes, so it adds no runtime
component, no request-lifecycle stage and no gateway.yaml key. Neither
doc/architecture.adoc nor doc/configuration.adoc has a surface for it, and both are
genuinely unchanged by this plan rather than skipped.

Co-Authored-By: Claude <noreply@anthropic.com>
…view

Pre-submission self-review caught claims the artifact does not support:

- The native executable is NOT statically linked. quarkus.native.additional-build-args
  configures no --static and no --libc=musl, which is exactly why the base is the
  glibc-bearing quarkus-distroless-image rather than scratch. Corrected in both
  workflow comments and the three doc occurrences. The load-bearing property is
  unchanged and in fact stronger: the layer carries no package metadata and no jars.
- container-image.adoc told operators to read the SBOM for a Java dependency
  inventory. The SBOM catalogues the same jar-free image the scan covers, so it
  structurally cannot contain one; the very next sentence already routed the reader
  elsewhere. Java dependency CVEs come from the fs-scan, Sonar and Dependabot lanes.
- The SBOM is a workflow-run artifact (upload-release-assets: false), not something
  "published" with the release.
- cosign >= 2.0 refuses keyless verification outright without --certificate-identity
  and --certificate-oidc-issuer; it does not "accept any Sigstore identity".
- doc/development/README.adoc was the one layer index the plan missed while
  release-process.adoc gained 97 lines.

The two cosign verify strings in release.yml and container-image.adoc were confirmed
character-identical by exact literal match, not by eye.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgENQAQcLGAWiLFfM414Yx
Security audit of the plan footprint (persona-security-expert + oci-security):

- trivy-action was pinned to a9c7b0f0, which is the ANNOTATED TAG OBJECT for
  v0.36.0, not a commit. GitHub's ref API peels it so the workflow ran, but
  Dependabot's github-actions ecosystem tracks commit SHAs — a scanning action
  pinned in tag-object form silently stops receiving bumps. Repinned to the
  commit ed142fd0 in all five occurrences.
- The operator cosign recipe verified a TAG. The signature is over the digest,
  so verifying a tag makes cosign resolve it independently of the deployment's
  own resolution, leaving a repoint window between the two — contradicting the
  doc's own "a digest cannot be repointed" note. The recipe now resolves the
  digest and passes that reference to both cosign verify and the manifest.
- Operator docker run recipe gains --cap-drop=ALL --security-opt=no-new-privileges
  (oci-security D01/D04), safe by construction: nonroot, unprivileged ports,
  distroless so no setuid binary exists.

Audit confirmed and left unchanged: release.yml remains workflow_dispatch-only
and its diff is strictly additive; no ${{ }} interpolation survives in any run
body; per-job permissions are least-privilege; Cosign is genuinely keyless with
no key material; the .dockerignore allowlist admits only target/*-runner.

Two findings filed rather than fixed: the smoke step's pull-by-digest is not a
real registry round-trip (the image stays resident under its RepoDigest, so the
identity assertion cannot fail) and publish-image runs harden-runner in audit
mode while holding packages+id-token write. Both need a real dispatched release
to fix safely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgENQAQcLGAWiLFfM414Yx

@sourcery-ai sourcery-ai Bot 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.

Sorry @cuioss-oliver, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cuioss-oliver, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1d21b380-51a7-4fcf-b2be-c3fe167d62e7

📥 Commits

Reviewing files that changed from the base of the PR and between 0e14620 and b7240b2.

⛔ Files ignored due to path filters (1)
  • doc/resources/diagrams/release-publish-flow.svg is excluded by !**/*.svg
📒 Files selected for processing (10)
  • .github/workflows/maven.yml
  • .github/workflows/release.yml
  • api-sheriff/.dockerignore
  • api-sheriff/src/main/docker/Dockerfile.native
  • doc/README.adoc
  • doc/development/README.adoc
  • doc/development/release-process.adoc
  • doc/user/README.adoc
  • doc/user/container-image.adoc
  • integration-tests/docker-compose.yml
📝 Walkthrough

Walkthrough

The change adds versioned container builds, an independent supply-chain scan, a manual GHCR publication workflow, digest-based smoke testing, Cosign signing, and operator and release documentation.

Changes

Container image lifecycle

Layer / File(s) Summary
Versioned image build
api-sheriff/.dockerignore, api-sheriff/src/main/docker/Dockerfile.native, integration-tests/docker-compose.yml
The native image build restricts its context, accepts APP_VERSION, applies it to the OCI label, and receives it from integration tests.
Supply-chain scan workflow
.github/workflows/maven.yml
The workflow validates the pinned base image, generates an SPDX SBOM, runs Trivy scans, publishes summaries, and uploads SARIF artifacts.
Release validation and tested build
.github/workflows/release.yml
The publish-image job loads the release version, checks out the release tag, verifies version identity, and builds the integration-test image once.
Scanned registry publication
.github/workflows/release.yml
The workflow generates an image SBOM, blocks publication on HIGH or CRITICAL findings, pushes two GHCR tags, and validates one registry digest.
Digest verification and signing
.github/workflows/release.yml
The workflow smoke-tests the published digest over HTTPS, removes the smoke container, and signs the verified digest with Cosign.
Container operation and release documentation
doc/README.adoc, doc/development/README.adoc, doc/development/release-process.adoc, doc/user/README.adoc, doc/user/container-image.adoc
The documentation describes image operation, release artifacts, tags, scanning, SBOMs, digest verification, Cosign verification, and deployment procedures.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes publishing a hardened, scanned, and signed OCI image to GHCR.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cuioss-review-bot

cuioss-review-bot Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit b7240b2)

🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

🧹 Nitpick comments (1)
.github/workflows/release.yml (1)

186-195: 🔒 Security & Privacy | 🔵 Trivial

No documented exception path for unfixed CVEs in the pinned base image.

ignore-unfixed: 'false' combined with severity: HIGH,CRITICAL and exit-code: '1' means an unfixed HIGH or CRITICAL CVE in quay.io/quarkus/quarkus-distroless-image:2.0 blocks every release, with no .trivyignore or override mechanism visible in this workflow. Since the base image is pinned by digest, a release can become permanently blocked until the base image is manually re-pinned to a patched digest, even for CVEs with no available upstream fix at all.

Consider documenting (or wiring in) an explicit, reviewed suppression path (e.g. a checked-in .trivyignore with recorded justification) for the case where a HIGH/CRITICAL finding has no fix yet, so a release is not indefinitely stuck behind a base-image CVE outside this repository's control.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ab54a59d-294e-4d7c-911a-56f0047cac75

📥 Commits

Reviewing files that changed from the base of the PR and between 0e14620 and ad7026c.

⛔ Files ignored due to path filters (1)
  • doc/resources/diagrams/release-publish-flow.svg is excluded by !**/*.svg
📒 Files selected for processing (10)
  • .github/workflows/maven.yml
  • .github/workflows/release.yml
  • api-sheriff/.dockerignore
  • api-sheriff/src/main/docker/Dockerfile.native
  • doc/README.adoc
  • doc/development/README.adoc
  • doc/development/release-process.adoc
  • doc/user/README.adoc
  • doc/user/container-image.adoc
  • integration-tests/docker-compose.yml

Comment thread .github/workflows/release.yml
Comment thread doc/development/release-process.adoc Outdated
Comment thread doc/user/container-image.adoc
Comment thread doc/user/container-image.adoc Outdated
Comment thread doc/user/container-image.adoc
…c accuracy

Five actionable review comments, all accepted.

1. RELEASE-BREAKING: the release-config checkout used a single-file
   sparse-checkout pattern while sparse-checkout-cone-mode defaulted to true.
   Cone mode matches DIRECTORY entries only, so `.github/project.yml` would have
   matched nothing, `.release-config/.github/project.yml` would not have existed,
   and read-project-config would have failed on the first real dispatch — for a
   reason reading nothing like "sparse checkout mode". Set cone mode false.

2. The release is NOT atomic, and the docs implied it was. publish-image declares
   `needs: [release]`, so Maven Central is published before any image step runs;
   a failure in the image lane leaves an irrevocable jars-only release. It cannot
   be reordered — the released version does not exist until the release job cuts
   it. Added an explicit section documenting the partial-release state and the
   recovery procedure (re-run the failed job alone, never the whole workflow),
   and narrowed every "before anything is pushed" to "pushed to GHCR".

3. `sha-<commit>` was described as immutable. It is an ordinary mutable registry
   tag; only `@sha256:...` is immutable. Reframed as a provenance tag in both
   docs, with deployment pinning pointed at the digest — which is also why Cosign
   signs the digest rather than either tag.

4. Both operator recipes resolved the digest with `{{index .RepoDigests 0}}`.
   RepoDigests is unordered and carries an entry per repository the image was
   pushed to or pulled from, so element 0 can belong to a different repository.
   Both now filter for ghcr.io/cuioss/api-sheriff and assert exactly one match —
   the same rule release.yml already applied.

5. Added a passthrough-deployment run recipe carrying
   QUARKUS_HTTP_SSL_PORT=8444. Deliberately a second recipe rather than adding
   the variable to the default one: on a terminating deployment it would move the
   listener off the port `-p 8443:8443` publishes, and the container would serve
   nothing on 8443.

Also took the nitpick: documented the sanctioned escape hatch for an unfixable
base-image CVE (re-pin the base first; a justified .trivyignore entry only if no
fixed base exists) so the first occurrence does not become an ad-hoc threshold
relaxation under release pressure. No .trivyignore is added now — an empty
suppression file invites undocumented appends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgENQAQcLGAWiLFfM414Yx
@cuioss-oliver

Copy link
Copy Markdown
Collaborator Author

Review triage complete — all 5 actionable comments accepted and fixed in b7240b2

Every inline thread has an individual reply and is resolved. Summary of dispositions:

# Comment Disposition
1 sparse-checkout-cone-mode for a single-file pattern Fixed — would have broken the first real release dispatch
2 Release is not atomic; "before anything is pushed" is misleading Fixed — documented the partial-release state + recovery procedure
3 sha-<commit> described as immutable Fixed — reframed as provenance in both doc layers
4 RepoDigests[0] assumption in the operator recipes Fixed — filter-and-assert-one, matching what release.yml already did
5 QUARKUS_HTTP_SSL_PORT for passthrough deployments Fixed — separate passthrough recipe (adding it to the default one would break terminating deployments)

Comment 1 is the one that mattered most: it was a latent release-breaking defect, not a style issue.

Nitpick also taken: no exception path for unfixed base-image CVEs

Agreed, and this is a real operational trap — ignore-unfixed: 'false' plus a digest-pinned base means an unfixable HIGH/CRITICAL can block every release indefinitely, over a layer this repository does not control.

release-process.adoc now carries a === If the scan blocks on an unfixable base-image CVE section with a preference order: re-pin the base to a patched digest first; only if no fixed base exists, add a .trivyignore entry carrying the CVE id, why it is not exploitable in this image, who accepted it and the date. It states explicitly that relaxing severity or flipping exit-code to '0' is never the answer, because those change the gate for every future release while a .trivyignore entry is scoped and visible in review. The release.yml gate comment points at that section.

No .trivyignore is added now, deliberately: an empty suppression file is an invitation to append to it without the justification.

Other bots

  • PR-Agent reported no security concerns and no major issues. Re-requesting a pass below, since it does not re-review automatically on push.
  • Sourcery hit its weekly rate limit and did not review. Not actionable here.

Two limitations that remain, by design

Both are recorded in the PR description rather than silently carried, and both need a real dispatched release to resolve safely:

  1. The smoke step's pull-by-digest is not a genuine registry round-trip — docker rmi drops only the tags, so the image stays resident under its RepoDigest and the identity assertion cannot fail. The digest-identity guarantee holds regardless by content addressing; registry retrievability is what goes unproven.
  2. publish-image runs harden-runner in audit mode while holding packages: write + id-token: write. block needs an egress allowlist derived from an observed release run.

@cuioss-oliver

Copy link
Copy Markdown
Collaborator Author

/review

@cuioss-oliver

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cuioss-oliver

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

The previous @coderabbitai review returned "Review finished", but the rate-limit banner updated at
the same moment still reports the ad7026c..b7240b2 range as never started, and no comments were
posted for it. Requesting a full review so the fix commit is actually covered — it touches
.github/workflows/release.yml, doc/development/release-process.adoc and
doc/user/container-image.adoc, i.e. the same release-path file where your first pass found the
sparse-checkout-cone-mode defect.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

@cuioss-oliver I will run a full review of pull request #145. The review will include ad7026c..b7240b2 and the release-path files you identified.

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 56 minutes.

@cuioss-oliver
cuioss-oliver added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit 36508b2 Aug 3, 2026
46 checks passed
@cuioss-oliver
cuioss-oliver deleted the feature/plan-26-release-artifacts-oci branch August 3, 2026 07:04
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.

1 participant