From 56996d995ae264ddb9d9a197390117f9296545eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Fei?= Date: Mon, 13 Oct 2025 17:51:09 +0200 Subject: [PATCH] ci: add a composite action for copy-to-production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Niccolò Fei --- .github/actions/copy-images/README.md | 120 +++++++++++++++++++++++++ .github/actions/copy-images/action.yml | 75 ++++++++++++++++ .github/workflows/bake_targets.yml | 58 ++---------- renovate.json | 9 ++ 4 files changed, 212 insertions(+), 50 deletions(-) create mode 100644 .github/actions/copy-images/README.md create mode 100644 .github/actions/copy-images/action.yml diff --git a/.github/actions/copy-images/README.md b/.github/actions/copy-images/README.md new file mode 100644 index 00000000..08e69ac2 --- /dev/null +++ b/.github/actions/copy-images/README.md @@ -0,0 +1,120 @@ +# Copy Images Action + +This composite GitHub Action copies a set of container images from a +`testing registry` to a `production registry`, and signs them using `Cosign`. +It requires as input Bake's build result metadata, which is the output provided +by the [bake-action](https://github.com/docker/bake-action?tab=readme-ov-file#outputs). + +--- + +## How it works + +The action assumes a consistent naming convention between your testing and production registries. + +* A production image is named like `ghcr.io/org/image` +* The corresponding testing image must include a suffix, e.g. `ghcr.io/org/image-testing` + +You can customize this suffix with the `inputs.test_registry_suffix` input. + +The action proceeds as follows: + +1. It retrieves all image references from `inputs.bake_build_metadata` +2. It generates a list of destination images by stripping out the `test_registry_suffix` from each image +3. Each image is copied to the destination registry using `Skopeo copy`. The digest of the image is preserved. +4. Each production image is signed using `Cosign` + +--- + +## Requirements + +This composite action requires the calling workflow’s `GITHUB_TOKEN` +to have the following permissions: + +``` +permissions: + contents: read + packages: write + id-token: write # needed by Cosign for signing the images with GitHub OIDC Token +``` + +--- + +## Inputs + +| Name | Description | Required | Default | +| ---------------------- | -------------------------------------------------- | --------- | -------------- | +| `bake_build_metadata` | The JSON build result metadata generated by Bake | ✅ Yes | — | +| `registry_user` | The user used to authenticate to the registry | ✅ Yes | — | +| `registry_token` | The token used to authenticate to the registry | ✅ Yes | — | +| `test_registry_suffix` | The suffix of the testing images | ❌ No | `-testing` | + +Note: + The JSON build result metadata is provided by [bake-action](https://github.com/docker/bake-action) as an output, see + [bake-action outputs](https://github.com/docker/bake-action?tab=readme-ov-file#outputs). + Alternatively, if you are using `docker buildx bake` via commandline, you can write your build metadata to a file + by using `--metadata-file`, and then provide the content of that file as `input.bake_build_metadata`. + +--- + +## Usage + +Example usage: + +``` +jobs: + copytoproduction: + runs-on: ubuntu-latest + needs: + - testbuild + permissions: + contents: read + packages: write + id-token: write + steps: + - name: Copy to production + uses: cloudnative-pg/postgres-containers/.github/actions/copy-images@main + with: + bake_build_metadata: "${{ needs.testbuild.outputs.metadata }}" + registry_user: ${{ github.actor }} + registry_token: ${{ secrets.GITHUB_TOKEN }} +``` + +Example workflow: + +``` +jobs: + # Building and pushing to a testing registry + testbuild: + runs-on: ubuntu-latest + outputs: + metadata: ${{ steps.build.outputs.metadata }} + steps: + ... + - uses: docker/bake-action@v6 + id: build + with: + push: true + + # Here's when you'd want to have one or + # multiple jobs to scan and test your images + scan-images: + ... + + # If the tests passed, we promote the images to the production repo + copytoproduction: + runs-on: ubuntu-latest + needs: + - testbuild + - scan-images + permissions: + contents: read + packages: write + id-token: write + steps: + - name: Copy to production + uses: cloudnative-pg/postgres-containers/.github/actions/copy-images@main + with: + bake_build_metadata: "${{ needs.testbuild.outputs.metadata }}" + registry_user: ${{ github.actor }} + registry_token: ${{ secrets.GITHUB_TOKEN }} +``` diff --git a/.github/actions/copy-images/action.yml b/.github/actions/copy-images/action.yml new file mode 100644 index 00000000..f45100b8 --- /dev/null +++ b/.github/actions/copy-images/action.yml @@ -0,0 +1,75 @@ +name: Copy and sign images +description: Copy and sign images to the production repository +inputs: + bake_build_metadata: + description: "The JSON build metadata of Bake" + required: true + registry_user: + description: "The user used to authenticate to the registry" + required: true + registry_token: + description: "The token used to authenticate to the registry" + required: true + test_registry_suffix: + description: "The testing registry suffix" + required: false + default: '-testing' + +runs: + using: composite + steps: + - name: Log in to the GitHub Container registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + with: + registry: ghcr.io + username: ${{ inputs.registry_user }} + password: ${{ inputs.registry_token }} + + - name: Copy images + shell: bash + env: + # renovate: datasource=docker depName=quay.io/skopeo/stable versioning=loose + SKOPEO_VERSION: "v1.20.0-immutable" + SUFFIX: ${{ inputs.test_registry_suffix }} + run: | + images=$(echo '${{ inputs.bake_build_metadata }}' | + jq -r ' + .[] as $items | + ( + $items."image.name" | + split(",")[] + + "@" + + $items."containerimage.digest" + ) + ' + ) + for image in $images + do + testimageshaonly="${image%:*@*}@${image#*@}" + testimagenosha="${image%@*}" + prodimage="${testimagenosha/$SUFFIX/}" + echo "Copying ${testimageshaonly} to ${prodimage}" + docker run --quiet quay.io/skopeo/stable:$SKOPEO_VERSION copy -q -a \ + --dest-creds ${{ inputs.registry_user }}:${{ inputs.registry_token }} \ + docker://${testimageshaonly} docker://${prodimage} + done + + - name: Install cosign + uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 + + - name: Sign images + shell: bash + env: + SUFFIX: ${{ inputs.test_registry_suffix }} + run: | + images=$(echo '${{ inputs.bake_build_metadata }}' | + jq -r --arg suffix "$SUFFIX" '.[] | + ( + ."image.name" | + sub(",.*";"") | + sub($suffix + ":[^@]+";"") + ) + "@" + ."containerimage.digest" + ' + ) + echo "Signing ${images}" + cosign sign -t 5m --yes ${images} diff --git a/.github/workflows/bake_targets.yml b/.github/workflows/bake_targets.yml index b501a58b..4568ce7c 100644 --- a/.github/workflows/bake_targets.yml +++ b/.github/workflows/bake_targets.yml @@ -184,9 +184,6 @@ jobs: with: sarif_file: snyk.sarif - # Use the metadata generated in the `testbuild` step to find all the images - # that have been built. We copy them one by one to the production registry - # using skopeo. Then we sign the production images too. copytoproduction: name: Copy images to production if: | @@ -199,54 +196,15 @@ jobs: permissions: contents: read packages: write - security-events: write # Required by the cosign step id-token: write steps: - - name: Log in to the GitHub Container registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + - name: Checkout Code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + + - name: Copy to production + uses: ./.github/actions/copy-images with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Copy images - run: | - images=$(echo '${{ needs.testbuild.outputs.metadata }}' | - jq -r ' - .[] as $items | - ( - $items."image.name" | - split(",")[] + - "@" + - $items."containerimage.digest" - ) - ' - ) - for image in $images - do - testimageshaonly="${image%:*@*}@${image#*@}" - testimagenosha="${image%@*}" - prodimage="${testimagenosha/-testing/}" - echo "Copying ${testimageshaonly} to ${prodimage}" - docker run --quiet quay.io/skopeo/stable:v1.17.0-immutable copy -q -a \ - --dest-creds ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }} \ - docker://${testimageshaonly} docker://${prodimage} - done - - - name: Install cosign - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 - - - name: Sign images - run: | - images=$(echo '${{ needs.testbuild.outputs.metadata }}' | - jq -r '.[] | - ( - ."image.name" | - sub(",.*";"") | - sub("-testing:[^@]+";"") - ) + "@" + ."containerimage.digest" - ' - ) - echo "Signing ${images}" - cosign sign -t 5m --yes ${images} + bake_build_metadata: "${{ needs.testbuild.outputs.metadata }}" + registry_user: ${{ github.actor }} + registry_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/renovate.json b/renovate.json index 22391d4a..1cab084e 100644 --- a/renovate.json +++ b/renovate.json @@ -19,6 +19,15 @@ "\\/\\/\\s*renovate:\\s*datasource=(?.*?)\\s+(versioning=(?.*?))?\\s+depName=(?.*?)\\s*\\n\\s*[A-Za-z0-9_-]+\\s*=\\s*\"(?[^\"]+)\"" ], "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}" + }, + { + "customType": "regex", + "managerFilePatterns": [ + "/\\.ya?ml$/" + ], + "matchStrings": [ + "# renovate: datasource=(?[a-z-.]+?) depName=(?[^\\s]+?)(?: (?:lookupName|packageName)=(?[^\\s]+?))?(?: versioning=(?[^\\s]+?))?(?: extractVersion=(?[^\\s]+?))?\\s+[A-Za-z0-9_]+?_VERSION\\s*:\\s*[\"']?(?.+?)[\"']?\\s" + ] } ], "packageRules": [