Skip to content

Commit a436e4d

Browse files
authored
Merge pull request #195 from coopernetes/fix/bump-base-image
fix: bump base images, overhaul container scan and release workflow
2 parents 6c193b4 + 78758f6 commit a436e4d

9 files changed

Lines changed: 157 additions & 70 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Fetch Latest Grype Container Scan Report
2+
3+
Fetch the latest grype container scan report from GitHub Actions and update the Dockerfile base image SHAs to address any CVEs.
4+
5+
## Steps
6+
7+
1. Find the latest completed container scan run and download the report into a temp directory:
8+
```
9+
gh run list --workflow=container-scan.yml --status=completed --limit=5 --json databaseId,displayTitle,createdAt,conclusion
10+
GRYPE_TMP=$(mktemp -d)
11+
gh run download <RUN_ID> -n grype-container-scan -D "$GRYPE_TMP"
12+
cat "$GRYPE_TMP/grype-report.txt"
13+
```
14+
15+
2. Analyze the report:
16+
- All current CVEs are in `openjdk` or Ubuntu base packages — both are fixed by updating the base image
17+
- Check the "FIXED IN" column: if `*21.0.X` is listed, a new temurin release is available
18+
- If no fix is listed, the CVE cannot be addressed by a base image bump
19+
- **If CVEs remain after a digest bump:** the temurin Docker image may not yet include the upstream JDK fix.
20+
OpenJDK releases and the corresponding `eclipse-temurin` Docker image publish are independent — there is
21+
typically a lag of days to weeks. Check https://github.com/adoptium/containers for open issues or pending
22+
PRs tracking the update.
23+
24+
3. Pull the latest temurin images and extract the multi-arch index digests:
25+
```bash
26+
CONTAINER_TOOL=$(command -v docker || command -v podman)
27+
28+
for IMG in eclipse-temurin:21-jre eclipse-temurin:21-jdk; do
29+
$CONTAINER_TOOL pull docker.io/$IMG --quiet
30+
PLATFORM_DIGESTS=$($CONTAINER_TOOL manifest inspect docker.io/$IMG | python3 -c "import json,sys; [print(m['digest']) for m in json.load(sys.stdin)['manifests']]")
31+
$CONTAINER_TOOL image inspect docker.io/$IMG --format '{{.RepoDigests}}' \
32+
| tr ' ' '\n' | sed 's/.*@//' | tr -d '[]' \
33+
| while read d; do
34+
[ -n "$d" ] && ! echo "$PLATFORM_DIGESTS" | grep -qF "$d" && echo "$IMG sha256: $d"
35+
done
36+
done
37+
```
38+
39+
4. Update `Dockerfile` — replace both `@sha256:` pins:
40+
- Build stage: `FROM docker.io/eclipse-temurin:21-jdk@sha256:<new>`
41+
- Runtime stage: `FROM docker.io/eclipse-temurin:21-jre@sha256:<new>`
42+
43+
5. Optionally verify locally that the Dockerfile changes resolve the findings. Build the image first, then scan it (`fail-on-severity`, `sort-by`, and `output-template-file` come from `.grype.yaml`):
44+
```
45+
CONTAINER_TOOL=$(command -v docker || command -v podman)
46+
$CONTAINER_TOOL build -t git-proxy-java:local-verify .
47+
SCAN_TMP=$(mktemp -d)
48+
grype git-proxy-java:local-verify --config .grype.yaml -o "template=$SCAN_TMP/report.txt" -o "json=$SCAN_TMP/report.json"
49+
cat "$SCAN_TMP/report.txt"
50+
```
51+
52+
6. Commit on a new branch:
53+
```
54+
fix: bump temurin base images to resolve CVEs
55+
56+
Addresses: <list CVE IDs from report>
57+
closes #<issue number if one exists>
58+
```

.claude/commands/release-tag.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ allowed-tools:
1212
This is phase 2 of the release process. Phase 1 (`/release`) bumped the version and pushed to main.
1313
This command creates the git tag and pushes it, which triggers the Docker publish workflow.
1414

15+
When the tag is pushed, the publish workflow promotes the already-built `:edge` image to the release
16+
tags (`:v<version>`, `:latest`, etc.) — it does **not** rebuild the image. The image being released is
17+
byte-for-byte identical to what was scanned when the version bump merged to main.
18+
1519
Arguments passed: `$ARGUMENTS`
1620

1721
`$ARGUMENTS` is the version string (without `v` prefix), e.g. `1.0.0-alpha.9`.
@@ -24,15 +28,16 @@ Arguments passed: `$ARGUMENTS`
2428

2529
2. **Verify checks passed.** Run:
2630
```
27-
gh run list --branch main --limit 4 --json name,status,conclusion
31+
gh run list --branch main --limit 6 --json name,status,conclusion
2832
```
2933
Confirm that these checks all show `conclusion: "success"`:
3034
- `CI / Build & Test`
3135
- `CI / E2E Test`
3236
- `CodeQL / java-kotlin`
3337
- `CodeQL / actions`
34-
- `CVE / Dependency Check (Gradle)`
35-
- `CVE / Grype (npm)`
38+
- `CVE / Gradle`
39+
- `CVE / npm`
40+
- `Container Scan`
3641

3742
If any are still in progress or failed, tell the user and stop.
3843

.github/workflows/container-scan.yml

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,16 @@ jobs:
2727
grep "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" "grype_${GRYPE_VERSION}_checksums.txt" | sha256sum --check
2828
tar -xzf "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" -C /usr/local/bin grype
2929
30+
# Single scan — fail on high+, emit template report and JSON in one pass.
31+
# SARIF upload intentionally omitted — OS-layer CVEs are triaged with application
32+
# context; uploading creates misleading noise in the GitHub Security tab.
3033
- name: Scan image
31-
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # ratchet:anchore/scan-action@v7
32-
id: scan
33-
with:
34-
image: ghcr.io/coopernetes/git-proxy-java:latest
35-
fail-build: true
36-
severity-cutoff: high
37-
only-fixed: true
38-
config: .grype.yaml
39-
40-
# SARIF upload intentionally omitted — OS-layer CVEs from the base image are triaged
41-
# by internal scanning with application context. Uploading here creates misleading noise
42-
# in the GitHub Security tab (high CVSS score ≠ high actual risk for this workload).
43-
# The build still fails on high/critical with a fix available via fail-build: true above.
44-
45-
- name: Generate human-readable report
4634
if: always()
4735
run: |
4836
grype ghcr.io/coopernetes/git-proxy-java:latest \
4937
--config .grype.yaml \
50-
--output table > grype-report.txt || true
51-
grype ghcr.io/coopernetes/git-proxy-java:latest \
52-
--config .grype.yaml \
53-
--output json > grype-report.json || true
38+
-o "template=grype-report.txt" \
39+
-o "json=grype-report.json"
5440
5541
- name: Upload scan reports
5642
if: always()

.github/workflows/docker-publish.yml

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ permissions:
1818

1919
jobs:
2020
build-and-push:
21+
# Tags are released by promoting the already-built edge image — no rebuild needed.
22+
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
2123
runs-on: ubuntu-latest
2224
outputs:
2325
digest: ${{ steps.push.outputs.digest }}
2426
permissions:
2527
contents: read
2628
packages: write
27-
id-token: write # required for SLSA provenance signing
28-
attestations: write # required for GitHub artifact attestations
29+
id-token: write
30+
attestations: write
2931

3032
steps:
3133
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6
@@ -49,7 +51,6 @@ jobs:
4951
tags: |
5052
type=ref,event=pr
5153
type=raw,value=edge,enable={{is_default_branch}}
52-
type=raw,value=${{ github.ref_name }}-pending,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
5354
type=raw,value=${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
5455
5556
- name: Build and push
@@ -98,82 +99,94 @@ jobs:
9899
tar -xzf "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" -C /usr/local/bin grype
99100
100101
- name: Scan image
101-
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # ratchet:anchore/scan-action@v7
102-
id: scan
103-
with:
104-
image: ghcr.io/${{ github.repository }}@${{ needs.build-and-push.outputs.digest }}
105-
fail-build: true
106-
severity-cutoff: high
107-
only-fixed: true
108-
config: .grype.yaml
109-
110-
# SARIF upload intentionally omitted — OS-layer CVEs from the base image are triaged
111-
# by internal scanning with application context. Uploading here creates misleading noise
112-
# in the GitHub Security tab (high CVSS score ≠ high actual risk for this workload).
113-
# The build still fails on high/critical with a fix available via fail-build: true above.
114-
115-
- name: Generate scan report
116-
if: always()
117102
run: |
118103
grype ghcr.io/${{ github.repository }}@${{ needs.build-and-push.outputs.digest }} \
119104
--config .grype.yaml \
120-
--only-fixed \
121-
--output table | tee grype-report.txt || true
105+
-o "template=grype-report.txt" \
106+
-o "json=grype-report.json"
122107
123-
- name: Upload scan report
108+
- name: Upload scan reports
124109
if: always()
125110
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # ratchet:actions/upload-artifact@v7
126111
with:
127112
name: grype-container-scan
128-
path: grype-report.txt
113+
path: |
114+
grype-report.txt
115+
grype-report.json
129116
retention-days: 30
130117

131118
publish-release:
132-
name: Publish Release Tags
133-
needs: [build-and-push, scan]
119+
name: Publish Release
134120
runs-on: ubuntu-latest
135121
if: startsWith(github.ref, 'refs/tags/v')
136122
permissions:
137123
contents: read
138124
packages: write
139125

140126
steps:
127+
- name: Checkout
128+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6
129+
130+
- name: Set up Docker Buildx
131+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # ratchet:docker/setup-buildx-action@v4
132+
141133
- name: Log in to GHCR
142134
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # ratchet:docker/login-action@v4
143135
with:
144136
registry: ghcr.io
145137
username: ${{ github.actor }}
146138
password: ${{ secrets.GITHUB_TOKEN }}
147139

148-
- name: Set up Docker Buildx
149-
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # ratchet:docker/setup-buildx-action@v4
140+
- name: Install grype
141+
env:
142+
GRYPE_VERSION: "0.112.0"
143+
run: |
144+
cd /tmp
145+
curl -sSfL -o "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" \
146+
"https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}/grype_${GRYPE_VERSION}_linux_amd64.tar.gz"
147+
curl -sSfL -o "grype_${GRYPE_VERSION}_checksums.txt" \
148+
"https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}/grype_${GRYPE_VERSION}_checksums.txt"
149+
grep "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" "grype_${GRYPE_VERSION}_checksums.txt" | sha256sum --check
150+
tar -xzf "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" -C /usr/local/bin grype
151+
152+
# Resolve the digest of the already-built, already-scanned edge image.
153+
# The tag ruleset enforces Container Scan passed on this commit before the tag
154+
# could be pushed, so edge is guaranteed clean at this point.
155+
- name: Resolve edge digest
156+
id: edge
157+
run: |
158+
DIGEST=$(docker buildx imagetools inspect \
159+
ghcr.io/${{ github.repository }}:edge \
160+
--format '{{.Manifest.Digest}}')
161+
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
162+
echo "Promoting edge digest: ${DIGEST}"
163+
164+
- name: Scan edge image
165+
run: |
166+
grype ghcr.io/${{ github.repository }}@${{ steps.edge.outputs.digest }} \
167+
--config .grype.yaml \
168+
-o "template=grype-report.txt" \
169+
-o "json=grype-report.json"
170+
171+
- name: Upload scan reports
172+
if: always()
173+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # ratchet:actions/upload-artifact@v7
174+
with:
175+
name: grype-release-scan
176+
path: |
177+
grype-report.txt
178+
grype-report.json
179+
retention-days: 90
150180

151-
# Retag the scanned ephemeral image to final release tags without rebuilding.
152-
# Only runs after scan passes — latest always points to a clean image.
153-
- name: Retag to release version and latest
181+
- name: Promote edge to release tags
154182
run: |
155183
VERSION=${GITHUB_REF_NAME}
156184
MAJOR=$(echo ${VERSION} | cut -d. -f1)
157185
MINOR=$(echo ${VERSION} | cut -d. -f1-2)
158-
SOURCE="ghcr.io/${{ github.repository }}@${{ needs.build-and-push.outputs.digest }}"
186+
SOURCE="ghcr.io/${{ github.repository }}@${{ steps.edge.outputs.digest }}"
159187
docker buildx imagetools create \
160188
--tag "ghcr.io/${{ github.repository }}:${VERSION}" \
161189
--tag "ghcr.io/${{ github.repository }}:${MINOR}" \
162190
--tag "ghcr.io/${{ github.repository }}:${MAJOR}" \
163191
--tag "ghcr.io/${{ github.repository }}:latest" \
164192
${SOURCE}
165-
166-
- name: Delete ephemeral pending tag
167-
env:
168-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
169-
run: |
170-
PACKAGE_NAME=$(basename ${{ github.repository }})
171-
TAG="${GITHUB_REF_NAME}-pending"
172-
VERSION_ID=$(gh api "/user/packages/container/${PACKAGE_NAME}/versions" \
173-
--paginate --jq ".[] | select(.metadata.container.tags[] == \"${TAG}\") | .id")
174-
if [ -n "${VERSION_ID}" ]; then
175-
gh api --method DELETE "/user/packages/container/${PACKAGE_NAME}/versions/${VERSION_ID}"
176-
echo "Deleted ephemeral tag ${TAG}"
177-
else
178-
echo "No ephemeral tag ${TAG} found — nothing to clean up"
179-
fi

.grype-report.tmpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{- range .Matches -}}
2+
{{.Vulnerability.ID}} {{.Vulnerability.Severity}} {{.Artifact.Name}} {{.Artifact.Version}} ({{.Artifact.Type}})
3+
Fix state: {{.Vulnerability.Fix.State}}
4+
Fixed in: {{range $i, $v := .Vulnerability.Fix.Versions}}{{if $i}}, {{end}}{{$v}}{{end}}
5+
6+
{{ end -}}

.grype.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
fail-on-severity: high
2+
sort-by: severity
3+
output-template-file: .grype-report.tmpl
24

35
ignore:
46
# OS packages with no fix available — upstream patch not yet released or won't be released.

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ Config override file mounted at `/app/conf/git-proxy-local.yml` inside the conta
9797
Refer to [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for detailed docs on YAML config structure, environment variable
9898
overrides, and provider-specific settings.
9999

100+
## Commit conventions
101+
102+
- Always squash related commits into one before pushing — use `git reset --soft` to squash, not `git rebase -i` (requires TTY).
103+
- Always include a `Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>` trailer in every commit. This is a project transparency requirement.
104+
- Always include `closes #N` / `resolves #N` in commit messages when addressing a GitHub issue.
105+
- Never add `[ci skip]` to commits unless explicitly asked.
106+
100107
## Testing conventions
101108

102109
- Always use JUnit assertions (`org.junit.jupiter.api.Assertions.*`) — not manual `if`/`throw` checks.

CONTRIBUTING.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,16 @@ This sets `core.hooksPath` to `.githooks/`. The hook runs on every `git commit`:
429429
See [docs/internals/JGIT_INFRASTRUCTURE.md](docs/internals/JGIT_INFRASTRUCTURE.md) for the store-and-forward
430430
architecture and [docs/internals/GIT_INTERNALS.md](docs/internals/GIT_INTERNALS.md) for wire-protocol details.
431431

432+
## Releases
433+
434+
Releases follow a two-phase process to ensure every published image is identical to what was already scanned and running as `:edge`.
435+
436+
**Phase 1 — version bump.** Create a `release/<version>` branch, update `version` in `build.gradle`, open a PR, and enable auto-merge. The PR must pass all CI, CodeQL, CVE, and container scan checks before it can merge. Use the `/release` Claude command to automate this.
437+
438+
**Phase 2 — tag.** Once the version bump lands on `main`, push an annotated tag (`v<version>`). The tag ruleset enforces the same checks must have passed on that commit. The publish workflow then promotes the already-built `:edge` image directly to the release tags (`:v1.0.0`, `:latest`, etc.) — no rebuild occurs. Use the `/release-tag` Claude command for this step.
439+
440+
This means every release image is byte-for-byte identical to the `:edge` image that was scanned when the version bump merged.
441+
432442
## Issues and pull requests
433443

434444
The issue tracker is at [coopernetes/git-proxy-java](https://github.com/coopernetes/git-proxy-java/issues). Reference

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# syntax=docker/dockerfile:1
22

33
# ── Build stage ──────────────────────────────────────────────────────────────
4-
FROM docker.io/eclipse-temurin:21-jdk@sha256:06a4f4be86d459307036eb97c55a24686bd1312fe88c152723c915b7b2e6a8b4 AS builder
4+
FROM docker.io/eclipse-temurin:21-jdk@sha256:e58e492628c1428ceb838afc1a1b8762673d5eaa09296f560c363daea0fdcf3b AS builder
55

66
# Install Node.js directly from the official distribution with SHA256 verification.
77
# To update: download the new tarball, verify against nodejs.org/dist/vX.Y.Z/SHASUMS256.txt,
@@ -47,7 +47,7 @@ RUN sed -i \
4747
git-proxy-java-dashboard/build/install/git-proxy-java-dashboard/bin/git-proxy-java-dashboard
4848

4949
# ── Runtime stage ─────────────────────────────────────────────────────────────
50-
FROM docker.io/eclipse-temurin:21-jre@sha256:137163a1850fd2088d94ecd8420358d83086bd287d3c4f6f14b7d09786490c4d
50+
FROM docker.io/eclipse-temurin:21-jre@sha256:ff65ff0d43c73d2b675eb4b758665a5cb487e7df127436a9979f8172c144c819
5151

5252
WORKDIR /app
5353

0 commit comments

Comments
 (0)