From 1b4c973a30c8b9cd07d92312039a5f468476d701 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Fri, 1 May 2026 10:22:00 -0400 Subject: [PATCH 1/2] fix: gate release tags behind container scan, bump grype to 0.111.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On tag pushes, build to an ephemeral :vX.Y.Z-rc tag first, scan it, then retag to :vX.Y.Z / :X.Y / :X / :latest only when the scan passes. This ensures :latest always points to a clean image. Edge continues to track every main commit without scanning — it's a mutable dev target and scan failures there are acceptable. Also bumps GRYPE_VERSION from 0.111.0 (non-existent release) to 0.111.1, applies the same PATH fix and tee-to-stdout report approach from the CVE jobs. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docker-publish.yml | 72 ++++++++++++++++++---------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 52c4015..4936738 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -47,13 +47,9 @@ jobs: with: images: ghcr.io/${{ github.repository }} tags: | - type=ref,event=branch type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=raw,value=edge,enable={{is_default_branch}} + type=raw,value=${{ github.ref_name }}-rc,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=raw,value=${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }} - name: Build and push @@ -84,10 +80,9 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - security-events: write - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' env: - GRYPE_VERSION: "0.111.0" + GRYPE_VERSION: "0.111.1" steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 @@ -108,32 +103,57 @@ jobs: # in the GitHub Security tab (high CVSS score ≠ high actual risk for this workload). # The build still fails on high/critical with a fix available via fail-build: true above. - - name: Install grype + - name: Add grype to PATH if: always() - run: | - curl -sSfL -o /tmp/grype.tar.gz \ - https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}/grype_${GRYPE_VERSION}_linux_amd64.tar.gz - curl -sSfL -o /tmp/grype_checksums.txt \ - https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}/grype_${GRYPE_VERSION}_checksums.txt - grep "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" /tmp/grype_checksums.txt | sha256sum --check --ignore-missing - tar -xzf /tmp/grype.tar.gz -C /usr/local/bin grype - - - name: Generate human-readable report + run: echo "$(dirname $(find /opt/hostedtoolcache/grype -name grype -type f | head -1))" >> $GITHUB_PATH + + - name: Generate scan report if: always() run: | grype ghcr.io/${{ github.repository }}@${{ needs.build-and-push.outputs.digest }} \ --config .grype.yaml \ - --output table > grype-report.txt || true - grype ghcr.io/${{ github.repository }}@${{ needs.build-and-push.outputs.digest }} \ - --config .grype.yaml \ - --output json > grype-report.json || true + --only-fixed \ + --output table | tee grype-report.txt || true - - name: Upload scan reports + - name: Upload scan report if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # ratchet:actions/upload-artifact@v7 with: name: grype-container-scan - path: | - grype-report.txt - grype-report.json + path: grype-report.txt retention-days: 30 + + publish-release: + name: Publish Release Tags + needs: [build-and-push, scan] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: read + packages: write + + steps: + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # ratchet:docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # ratchet:docker/setup-buildx-action@v4 + + # Retag the scanned ephemeral image to final release tags without rebuilding. + # Only runs after scan passes — latest always points to a clean image. + - name: Retag to release version and latest + run: | + VERSION=${GITHUB_REF_NAME} + MAJOR=$(echo ${VERSION} | cut -d. -f1) + MINOR=$(echo ${VERSION} | cut -d. -f1-2) + SOURCE="ghcr.io/${{ github.repository }}@${{ needs.build-and-push.outputs.digest }}" + docker buildx imagetools create \ + --tag "ghcr.io/${{ github.repository }}:${VERSION}" \ + --tag "ghcr.io/${{ github.repository }}:${MINOR}" \ + --tag "ghcr.io/${{ github.repository }}:${MAJOR}" \ + --tag "ghcr.io/${{ github.repository }}:latest" \ + ${SOURCE} From 70bb4911209e27f7fe1a1d9c1ed5e77327872f28 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Fri, 1 May 2026 10:28:18 -0400 Subject: [PATCH 2/2] fix: gate release tags behind container scan, bump grype to 0.111.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release flow for tag pushes: - Build to ephemeral :vX.Y.Z-pending tag - Scan by digest (fails build on high CVEs with a fix) - On scan pass: retag to :vX.Y.Z / :X.Y / :X / :latest, delete -pending :edge continues to track every main commit without scanning — mutable dev target, scan failures acceptable there. :latest now always points to a scan-clean release image. Also adds retag.yml — workflow_dispatch to retag any digest to arbitrary tags without rebuilding, for emergency promotions and re-pointing :latest. Bumps GRYPE_VERSION 0.111.0 → 0.111.1. The root cause of the original scan-action failure was a bug in that pinned commit of anchore/scan-action which constructs the download URL as /releases/0.111.0 instead of /releases/tag/v0.111.0, causing a 404. The fix bypasses the install script entirely and downloads directly from the GitHub releases URL. Applies PATH fix and tee-to-stdout report from the CVE jobs. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docker-publish.yml | 17 +++++++++- .github/workflows/retag.yml | 49 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/retag.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4936738..183d8b9 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -49,7 +49,7 @@ jobs: tags: | type=ref,event=pr type=raw,value=edge,enable={{is_default_branch}} - type=raw,value=${{ github.ref_name }}-rc,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=raw,value=${{ github.ref_name }}-pending,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=raw,value=${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }} - name: Build and push @@ -157,3 +157,18 @@ jobs: --tag "ghcr.io/${{ github.repository }}:${MAJOR}" \ --tag "ghcr.io/${{ github.repository }}:latest" \ ${SOURCE} + + - name: Delete ephemeral pending tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PACKAGE_NAME=$(basename ${{ github.repository }}) + TAG="${GITHUB_REF_NAME}-pending" + VERSION_ID=$(gh api "/user/packages/container/${PACKAGE_NAME}/versions" \ + --paginate --jq ".[] | select(.metadata.container.tags[] == \"${TAG}\") | .id") + if [ -n "${VERSION_ID}" ]; then + gh api --method DELETE "/user/packages/container/${PACKAGE_NAME}/versions/${VERSION_ID}" + echo "Deleted ephemeral tag ${TAG}" + else + echo "No ephemeral tag ${TAG} found — nothing to clean up" + fi diff --git a/.github/workflows/retag.yml b/.github/workflows/retag.yml new file mode 100644 index 0000000..965b11d --- /dev/null +++ b/.github/workflows/retag.yml @@ -0,0 +1,49 @@ +name: Retag Image + +# Manually retag an existing image digest to one or more target tags without +# rebuilding. Use cases: +# - Re-point :latest to a previous release after a bad release +# - Emergency: promote a known-good digest to a specific tag + +on: + workflow_dispatch: + inputs: + source-digest: + description: "Source image digest (e.g. sha256:abc123...)" + required: true + type: string + target-tags: + description: "Space-separated target tags (e.g. 'latest v1.0.0 1.0')" + required: true + type: string + +permissions: + contents: read + +jobs: + retag: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # ratchet:docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # ratchet:docker/setup-buildx-action@v4 + + - name: Retag + run: | + SOURCE="ghcr.io/${{ github.repository }}@${{ inputs.source-digest }}" + TAG_ARGS="" + for tag in ${{ inputs.target-tags }}; do + TAG_ARGS="$TAG_ARGS --tag ghcr.io/${{ github.repository }}:${tag}" + done + echo "Retagging ${SOURCE} -> ${{ inputs.target-tags }}" + docker buildx imagetools create ${TAG_ARGS} ${SOURCE}