diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml index ad21e0f6..256600a3 100644 --- a/.github/workflows/ghcr.yml +++ b/.github/workflows/ghcr.yml @@ -4,22 +4,87 @@ on: # yamllint disable-line rule:truthy branches: - main +env: + REMOTE_IMAGE: ghcr.io/dependabot/proxy + permissions: {} jobs: publish: name: Build and publish Docker images runs-on: ubuntu-latest + if: github.repository == 'dependabot/proxy' permissions: contents: write packages: write + id-token: write + attestations: write + artifact-metadata: write steps: - name: Check out code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + + - name: Log in to GHCR + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set version tag + run: | + VERSION="v2.0.$(date -u +%Y%m%d%H%M%S)" + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + + - name: Build and push image + id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + push: true + tags: | + ${{ env.REMOTE_IMAGE }}:${{ env.VERSION }} + ${{ env.REMOTE_IMAGE }}:latest + platforms: linux/amd64 + build-args: GIT_COMMIT=${{ github.sha }} + cache-to: type=inline + outputs: type=image,oci-mediatypes=false + # Generate signed provenance separately without changing the image manifest. + provenance: false + sbom: false - - name: Publish Docker - run: script/cibuild-publish-ghcr + - name: Require image digest env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IMAGE_DIGEST: ${{ steps.build.outputs.digest }} + run: ': "${IMAGE_DIGEST:?Build returned no image digest}"' + + - name: Generate artifact attestation + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-name: ${{ env.REMOTE_IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + + - name: Create Git tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IMAGE_DIGEST: ${{ steps.build.outputs.digest }} + run: | + set -euo pipefail + gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ + -f "ref=refs/tags/${VERSION}" \ + -f "sha=${GITHUB_SHA}" \ + --silent + + { + printf 'Published `%s:%s` and `%s:latest`\n\n' "$REMOTE_IMAGE" "$VERSION" "$REMOTE_IMAGE" + printf 'Digest: `%s`\n\n' "$IMAGE_DIGEST" + printf 'Source commit: `%s`\n\n' "$GITHUB_SHA" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index a23fd4dd..d432cfb4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,33 @@ To build and run the proxy, you need to have the following installed: - [Go][go] (version 1.26 or later) - [Docker][docker] +## Image provenance + +The `main` publish workflow builds `ghcr.io/dependabot/proxy` for `linux/amd64`. +The build pushes a `v2.0.YYYYMMDDHHMMSS` image tag and updates `latest`. +The workflow then publishes signed build provenance for the image digest to GitHub and GHCR. +After attestation succeeds, it creates the matching Git tag at the source commit. +If attestation fails, the image tags remain published and the workflow fails. + +To verify an image, replace the placeholders with its digest and the expected source commit: + +```bash +IMAGE_DIGEST='sha256:' +SOURCE_SHA='' + +gh attestation verify "oci://ghcr.io/dependabot/proxy@${IMAGE_DIGEST}" \ + --repo dependabot/proxy \ + --signer-workflow dependabot/proxy/.github/workflows/ghcr.yml \ + --source-ref refs/heads/main \ + --source-digest "${SOURCE_SHA}" +``` + +The publication summary includes the digest, source commit and attestation link. +Add `--bundle-from-oci` to retrieve the attestation from GHCR instead of the GitHub API. + +These attestations cover newly built container images. The workflow does not backfill historical images. +Runtime verification in the CLI and Action, and attestations for the native CodeQL archives, are separate work. + ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/script/_common b/script/_common index 6ce1d86b..5b53e0ee 100644 --- a/script/_common +++ b/script/_common @@ -1,7 +1,6 @@ export PROXY_IMAGE=dependabot/proxy export TESTS_IMAGE=dependabot/proxy-builder-tests export REMOTE_IMAGE=ghcr.io/dependabot/proxy -export DEFAULT_BRANCH=refs/heads/main function docker_build() { [[ -n "$SKIP_BUILD" ]] && return diff --git a/script/cibuild-publish-ghcr b/script/cibuild-publish-ghcr deleted file mode 100755 index defff0c6..00000000 --- a/script/cibuild-publish-ghcr +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -set -e -cd "$(dirname "$0")/.." -ROOT="$(pwd)" -source "${ROOT}/script/_common" - -function main() { - docker_build - push_to_ghcr -} - -function push_to_ghcr() { - if [[ -z "$GITHUB_TOKEN" ]]; then - echo "Skipping GHCR publish as this isn't Actions. Set GITHUB_TOKEN= to override." - return - fi - - echo "$GITHUB_TOKEN" | docker login ghcr.io -u x --password-stdin - - if [ "$GITHUB_REF" == "$DEFAULT_BRANCH" ]; then - DATE_BASED_VERSION=v2.0.$(date +%Y%m%d%H%M%S) - docker tag "$PROXY_IMAGE" "$REMOTE_IMAGE:$DATE_BASED_VERSION" - docker tag "$PROXY_IMAGE" "$REMOTE_IMAGE:latest" - docker push "$REMOTE_IMAGE:$DATE_BASED_VERSION" - docker push "$REMOTE_IMAGE:latest" - git tag "$DATE_BASED_VERSION" - git push origin "$DATE_BASED_VERSION" - fi -} - -main