From 1d3dff8e3de2e667316929fd1070259ac13b98bd Mon Sep 17 00:00:00 2001 From: Hidetake Iwata Date: Sat, 10 Feb 2024 16:48:08 +0900 Subject: [PATCH] Support annotations (#327) * Support annotations * Fix * Install the latest buildx * Rename to annotations * Rename to index-annotations * Fix test --- .github/workflows/e2e-docker.yaml | 11 +++++--- .github/workflows/e2e-kaniko.yaml | 13 +++++---- README.md | 16 +++++++---- action.yaml | 3 +++ src/main.ts | 1 + src/run.ts | 35 +++++++++++++++++++----- tests/run.test.ts | 44 +++++++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 20 deletions(-) diff --git a/.github/workflows/e2e-docker.yaml b/.github/workflows/e2e-docker.yaml index 1dcce5b..cfe856e 100644 --- a/.github/workflows/e2e-docker.yaml +++ b/.github/workflows/e2e-docker.yaml @@ -57,10 +57,16 @@ jobs: - run: yarn package # run the action + - uses: docker/setup-buildx-action@v3.0.0 + - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + id: metadata + with: + images: ghcr.io/${{ github.repository }}/e2e - name: docker-manifest-create-action (dry-run) uses: ./ with: push: false + index-annotations: ${{ steps.metadata.outputs.labels }} sources: | ${{ needs.build-linux-amd64.outputs.image-uri }} ${{ needs.build-linux-arm64.outputs.image-uri }} @@ -70,14 +76,11 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 - id: metadata - with: - images: ghcr.io/${{ github.repository }}/e2e - name: docker-manifest-create-action id: build uses: ./ with: + index-annotations: ${{ steps.metadata.outputs.labels }} tags: ${{ steps.metadata.outputs.tags }} sources: | ${{ needs.build-linux-amd64.outputs.image-uri }} diff --git a/.github/workflows/e2e-kaniko.yaml b/.github/workflows/e2e-kaniko.yaml index 856b9bf..8f17ec6 100644 --- a/.github/workflows/e2e-kaniko.yaml +++ b/.github/workflows/e2e-kaniko.yaml @@ -66,10 +66,17 @@ jobs: - run: yarn package # run the action + - uses: docker/setup-buildx-action@v3.0.0 + - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + id: metadata + with: + images: ghcr.io/${{ github.repository }}/e2e + flavor: latest=false,suffix=-kaniko - name: docker-manifest-create-action (dry-run) uses: ./ with: push: false + index-annotations: ${{ steps.metadata.outputs.labels }} sources: | ${{ needs.build-linux-amd64.outputs.image-uri }} @@ -78,15 +85,11 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 - id: metadata - with: - images: ghcr.io/${{ github.repository }}/e2e - flavor: latest=false,suffix=-kaniko - name: docker-manifest-create-action@v2 id: build uses: ./ with: + index-annotations: ${{ steps.metadata.outputs.labels }} tags: ${{ steps.metadata.outputs.tags }} sources: | ${{ needs.build-linux-amd64.outputs.image-uri }} diff --git a/README.md b/README.md index 719772b..42801a1 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ jobs: - uses: int128/docker-manifest-create-action@v2 id: build with: + index-annotations: ${{ steps.metadata.outputs.labels }} tags: ${{ steps.metadata.outputs.tags }} sources: | ghcr.io/${{ github.repository }}@${{ needs.build-linux-amd64.outputs.digest }} @@ -176,6 +177,7 @@ jobs: - uses: int128/docker-manifest-create-action@v2 id: build with: + index-annotations: ${{ steps.metadata.outputs.labels }} tags: ${{ steps.metadata.outputs.tags }} sources: | ${{ steps.ecr.outputs.registry }}/${{ github.repository }}@${{ needs.build-linux-amd64.outputs.digest }} @@ -188,14 +190,18 @@ This action requires Docker Buildx. ### Inputs -| Name | Default | Description | -| --------- | ---------------------------- | -------------------------------------------------- | -| `push` | `true` | Push the manifest to the registry | -| `tags` | (required if `push` is true) | Tags of the destination images (multi-line string) | -| `sources` | (required) | Image URIs of the sources (multi-line string) | +| Name | Default | Description | +| ------------------- | ---------------------------- | ------------------------------------------------------ | +| `push` | `true` | Push the manifest to the registry | +| `index-annotations` | - | Add annotations to the image index (multi-line string) | +| `tags` | (required if `push` is true) | Tags of the destination images (multi-line string) | +| `sources` | (required) | Image URIs of the sources (multi-line string) | If `push` is false, this action runs `docker buildx imagetools create --dry-run`. +If `index-annotations` is set, this action adds `--annotation`. +See https://docs.docker.com/engine/reference/commandline/buildx_imagetools_create/#annotation for details. + ### Outputs | Name | Description | diff --git a/action.yaml b/action.yaml index 392a75a..9cfd41c 100644 --- a/action.yaml +++ b/action.yaml @@ -6,6 +6,9 @@ inputs: description: Push the manifest to the registry required: false default: 'true' + index-annotations: + description: Add annotations to the image index (multi-line string) + required: false tags: description: Tags of the destination images (multi-line string) required: false diff --git a/src/main.ts b/src/main.ts index 4aab229..ac47d6b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { run } from './run' const main = async (): Promise => { const outputs = await run({ push: core.getBooleanInput('push', { required: true }), + indexAnnotations: core.getMultilineInput('index-annotations'), tags: core.getMultilineInput('tags'), sources: core.getMultilineInput('sources', { required: true }), }) diff --git a/src/run.ts b/src/run.ts index 55c7a54..75b9598 100644 --- a/src/run.ts +++ b/src/run.ts @@ -4,6 +4,7 @@ import * as exec from '@actions/exec' type Inputs = { push: boolean + indexAnnotations: string[] tags: string[] sources: string[] } @@ -18,27 +19,49 @@ export const run = async (inputs: Inputs): Promise => { core.endGroup() if (!inputs.push) { - await dryRunCreateManifest(inputs.sources) + await dryRunCreateManifest(inputs.sources, inputs.indexAnnotations) return { digest: undefined } } assert(inputs.tags.length > 0, 'tags must be set') for (const tag of inputs.tags) { - await createManifest(tag, inputs.sources) + await createManifest(tag, inputs.sources, inputs.indexAnnotations) } const digest = await getDigest(inputs.tags[0]) return { digest } } -const dryRunCreateManifest = async (sources: string[]) => { - await exec.exec('docker', ['buildx', 'imagetools', 'create', '--dry-run', ...sources]) +const dryRunCreateManifest = async (sources: string[], indexAnnotations: string[]) => { + await exec.exec('docker', [ + 'buildx', + 'imagetools', + 'create', + '--dry-run', + ...toAnnotationFlags(indexAnnotations), + ...sources, + ]) } -const createManifest = async (destination: string, sources: string[]) => { - await exec.exec('docker', ['buildx', 'imagetools', 'create', '-t', destination, ...sources]) +const createManifest = async (destination: string, sources: string[], indexAnnotations: string[]) => { + await exec.exec('docker', [ + 'buildx', + 'imagetools', + 'create', + ...toAnnotationFlags(indexAnnotations), + '-t', + destination, + ...sources, + ]) await exec.exec('docker', ['buildx', 'imagetools', 'inspect', destination]) } +const toAnnotationFlags = (indexAnnotations: string[]): string[] => + indexAnnotations.flatMap((a) => [ + '--annotation', + // https://docs.docker.com/engine/reference/commandline/buildx_imagetools_create/#annotation + `index:${a}`, + ]) + const getDigest = async (tag: string): Promise => { const { stdout } = await exec.getExecOutput('docker', [ 'buildx', diff --git a/tests/run.test.ts b/tests/run.test.ts index c687e3b..a410256 100644 --- a/tests/run.test.ts +++ b/tests/run.test.ts @@ -14,6 +14,7 @@ it('should run docker buildx imagetools', async () => { const outputs = await run({ push: true, + indexAnnotations: [], tags: ['ghcr.io/int128/docker-manifest-create-action:main'], sources: [ 'ghcr.io/int128/docker-manifest-create-action@sha256:0000000000000000000000000000000000000000000000000000000000000000', @@ -44,6 +45,7 @@ it('should run docker buildx imagetools', async () => { it('should run docker buildx imagetools --dry-run if push is false', async () => { const outputs = await run({ push: false, + indexAnnotations: [], tags: [], sources: [ 'ghcr.io/int128/docker-manifest-create-action@sha256:0000000000000000000000000000000000000000000000000000000000000000', @@ -61,3 +63,45 @@ it('should run docker buildx imagetools --dry-run if push is false', async () => 'ghcr.io/int128/docker-manifest-create-action@sha256:0000000000000000000000000000000000000000000000000000000000000001', ]) }) + +it('should add annotations', async () => { + jest.mocked(exec).getExecOutput.mockResolvedValue({ + exitCode: 0, + stdout: '"sha256:f000000000000000000000000000000000000000000000000000000000000000"', + stderr: '', + }) + + const outputs = await run({ + push: true, + indexAnnotations: [ + 'org.opencontainers.image.revision=0123456789012345678901234567890123456789', + 'org.opencontainers.image.created=2021-01-01T00:00:00Z', + ], + tags: ['ghcr.io/int128/docker-manifest-create-action:main'], + sources: [ + 'ghcr.io/int128/docker-manifest-create-action@sha256:0000000000000000000000000000000000000000000000000000000000000000', + ], + }) + expect(outputs).toStrictEqual({ + digest: 'sha256:f000000000000000000000000000000000000000000000000000000000000000', + }) + + expect(exec.exec).toHaveBeenCalledWith('docker', [ + 'buildx', + 'imagetools', + 'create', + '--annotation', + 'index:org.opencontainers.image.revision=0123456789012345678901234567890123456789', + '--annotation', + 'index:org.opencontainers.image.created=2021-01-01T00:00:00Z', + '-t', + 'ghcr.io/int128/docker-manifest-create-action:main', + 'ghcr.io/int128/docker-manifest-create-action@sha256:0000000000000000000000000000000000000000000000000000000000000000', + ]) + expect(exec.exec).toHaveBeenCalledWith('docker', [ + 'buildx', + 'imagetools', + 'inspect', + 'ghcr.io/int128/docker-manifest-create-action:main', + ]) +})