feat(release): publish hardened, scanned and signed OCI image to GHCR - #145
Conversation
…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
There was a problem hiding this comment.
Sorry @cuioss-oliver, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
Warning Review limit reached
Next review available in: 56 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Repository: cuioss/coderabbit/.coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (10)
📝 WalkthroughWalkthroughThe 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. ChangesContainer image lifecycle
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
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. Comment |
PR Reviewer Guide 🔍(Review updated until commit b7240b2)
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
.github/workflows/release.yml (1)
186-195: 🔒 Security & Privacy | 🔵 TrivialNo documented exception path for unfixed CVEs in the pinned base image.
ignore-unfixed: 'false'combined withseverity: HIGH,CRITICALandexit-code: '1'means an unfixed HIGH or CRITICAL CVE inquay.io/quarkus/quarkus-distroless-image:2.0blocks every release, with no.trivyignoreor 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
.trivyignorewith 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
⛔ Files ignored due to path filters (1)
doc/resources/diagrams/release-publish-flow.svgis excluded by!**/*.svg
📒 Files selected for processing (10)
.github/workflows/maven.yml.github/workflows/release.ymlapi-sheriff/.dockerignoreapi-sheriff/src/main/docker/Dockerfile.nativedoc/README.adocdoc/development/README.adocdoc/development/release-process.adocdoc/user/README.adocdoc/user/container-image.adocintegration-tests/docker-compose.yml
…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
Review triage complete — all 5 actionable comments accepted and fixed in b7240b2Every inline thread has an individual reply and is resolved. Summary of dispositions:
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 CVEsAgreed, and this is a real operational trap —
No Other bots
Two limitations that remain, by designBoth are recorded in the PR description rather than silently carried, and both need a real dispatched release to resolve safely:
|
|
/review |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai full review The previous |
|
✅ Action performedFull 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. |
Intent
API Sheriff had a release procedure but no release product definition:
release.ymlpublished 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-26of theapi-sheriff-roadmapepic (workstream WS-04, track 0.1.0 RELEASE).Deliverables
Dockerfile.native,api-sheriff/.dockerignore(new),integration-tests/docker-compose.ymldoc/development/release-process.adoc.github/workflows/release.yml.github/workflows/release.yml.github/workflows/maven.yml.github/workflows/release.ymldoc/user/container-image.adoc(new),doc/user/README.adocdoc/resources/diagrams/release-publish-flow.svg(new)doc/README.adocNo
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 buildonce → run the ITs against that image → push it. That sequence is not implementable as written:integration-tests/pom.xml:313-332bindsdocker compose build api-sherifftopre-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 tointegration-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 withexit-code: '0'on every Trivy step —continue-on-erroris 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. Butreusable-maven-release.yml@15376a19declares noon.workflow_call.outputsblock, soneeds.release.outputs.released-versionevaluates to the empty string and would pushghcr.io/cuioss/api-sheriff:— a malformed reference, or worse a silently mislabelled image. The version is instead re-read from.github/project.ymlusing 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.ymlremainsworkflow_dispatch:-only. The diff is strictly additive (@@ -24,3 +24,317 @@); theon:block and the comment recording the 2026-07-12 accidental Maven Central release are byte-identical tomain. Both "Why…" sections ofrelease-process.adocwere read in full before the file was touched.Decisions taken by the operator
id-token: writeis the whole credential (already precedent atscorecards.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.<version>plus an immutablesha-<40-char release-tag commit>. No floating:latest— a floating tag is exactly what makes a deployment non-reproducible.Three-layer documentation
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.adocindex entry updated.doc/user/container-image.adoc) — how to pull, run and verify the published image, including the exactcosign verifyrecipe with--certificate-identity/--certificate-oidc-issuer.doc/README.adocgains the container-image topic.doc/configuration.adocanddoc/architecture.adocare genuinely unchanged, not silently skipped: this plan introduces no configuration key and no architectural component. Verified —git diff --statagainst 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=muslis configured), which is why the base is the glibc-bearingquarkus-distroless-imagerather thanscratch. 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-lanefsscan, 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
docker rmiremoves only the two tags; the image stays resident under itsghcr.io/cuioss/api-sheriff@sha256:…RepoDigest, so the pull is a no-op and thePULLED_ID != IMAGE_IDassertion 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 underset -euo pipefail, so a wrong guess breaks the release lane.publish-imageruns harden-runner inauditmode while holdingpackages: write+id-token: writesimultaneously.egress-policy: blockis 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.release-process.adoc.verify -Ppre-commitapplies OpenRewrite import-reordering to 8 files underapi-sheriff/src/{main,test}/javathat 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)release.ymltriggers confirmed{workflow_dispatch: None}at each step.uses:pins resolved against upstream refs. One was wrong and is fixed:trivy-actionwas 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.cosign verifystrings duplicated betweenrelease.ymlandcontainer-image.adocconfirmed character-identical by exact literal match.#ffffffand#0d1117via containerisedrsvg-convert; the first pass surfaced three real layout defects which were fixed and re-rendered clean..dockerignoreallowlist was traced rule-by-rule under Docker's last-match-wins semantics against the Dockerfile's singleCOPY --chmod=0755 target/*-runner /app/application.The deliverable-1 IT verification did not run locally (
architecture resolvereportsexecution_tier: orchestrator, 652s, exceeding the ceiling). It closes on this PR:integration-tests.ymlrunsverify -Pintegration-tests -pl integration-tests -am, which exercisesdocker compose build api-sheriffand therefore empirically proves the reduced build context, Dockerfile readability under the new.dockerignore, and theAPP_VERSIONwiring. Please do not waive that check.🤖 Generated with Claude Code
https://claude.ai/code/session_01RgENQAQcLGAWiLFfM414Yx
Summary by CodeRabbit
New Features
Documentation