diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 52c4015..183d8b9 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 }}-pending,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,72 @@ 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} + + - 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}