Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/__main-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ permissions:
pull-requests: write
security-events: write
statuses: write
# FIXME: This is a workaround for having workflow actions. See https://github.com/orgs/community/discussions/38659
id-token: write

concurrency:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/__pull-request-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ permissions:
pull-requests: write
security-events: write
statuses: write
# FIXME: This is a workaround for having workflow actions. See https://github.com/orgs/community/discussions/38659
id-token: write

concurrency:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/__shared-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ permissions:
pull-requests: read
security-events: write
statuses: write
# yamllint disable-line rule:line-length
# FIXME: This is a workaround for having workflow actions. See https://github.com/orgs/community/discussions/38659
id-token: write

jobs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ permissions:
issues: read
packages: write
pull-requests: read
# FIXME: This is a workaround for having workflow actions. See https://github.com/orgs/community/discussions/38659
id-token: write

env:
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/__test-action-helm-release-chart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ jobs:
tests:
name: Test for "helm/release-chart" action with simple chart
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/__test-action-helm-test-chart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ permissions:
issues: read
packages: write
pull-requests: read
# FIXME: This is a workaround for having workflow actions. See https://github.com/orgs/community/discussions/38659
id-token: write

jobs:
Expand Down
60 changes: 50 additions & 10 deletions .github/workflows/__test-workflow-docker-build-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ permissions:
issues: read
packages: write
pull-requests: read
# FIXME: This is a workaround for having workflow actions. See https://github.com/orgs/community/discussions/38659
id-token: write

# jscpd:ignore-start
jobs:
arrange:
name: Arrange
runs-on: ubuntu-latest
steps:
- run: |
Expand All @@ -24,7 +24,8 @@ jobs:
exit 1
fi

act-build-arch:
act-build-images:
name: Act - Build multi-arch and mono-arch images
needs: arrange
uses: ./.github/workflows/docker-build-images.yml
secrets:
Expand Down Expand Up @@ -55,17 +56,18 @@ jobs:
}
]

assert-build-arch:
needs: act-build-arch
assert-build-arch-mono-arch:
name: Assert - multi-arch and mono-arch builds
needs: act-build-images
runs-on: "ubuntu-latest"
steps:
- name: Check built images ouput
- name: Assert - built images output
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const assert = require("assert");

const builtImagesOutput = `${{ needs.act-build-arch.outputs.built-images }}`;
const builtImagesOutput = `${{ needs.act-build-images.outputs.built-images }}`;
assert(builtImagesOutput.length, `"built-images" output is empty`);

// Check if is valid Json
Expand Down Expand Up @@ -132,13 +134,13 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ github.token }}

- name: Check multi-arch docker image and manifest
- name: Assert - multi-arch docker image and manifest
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const assert = require("assert");

const image = `${{ fromJson(needs.act-build-arch.outputs.built-images).test-multi-arch.images[0] }}`;
const image = `${{ fromJson(needs.act-build-images.outputs.built-images).test-multi-arch.images[0] }}`;

await exec.exec('docker', ['pull', image]);

Expand Down Expand Up @@ -194,13 +196,32 @@ jobs:
assert.equal(annotations[key], value, `Expected annotation not found: ${key}`);
});

- name: Check mono-arch docker image
- uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1

- name: Assert - signed multi-arch docker image
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const images = ${{ toJson(fromJson(needs.act-build-images.outputs.built-images).test-multi-arch.images) }};

for(const image of images) {
await exec.exec(
'cosign',
[
'verify', image,
'--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com',
'--certificate-identity-regexp', 'https://github.com/hoverkraft-tech/ci-github-container',
]
);
}

- name: Assert - mono-arch docker image
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const assert = require("assert");

const image = `${{ fromJson(needs.act-build-arch.outputs.built-images).test-mono-arch.images[0] }}`;
const image = `${{ fromJson(needs.act-build-images.outputs.built-images).test-mono-arch.images[0] }}`;

await exec.exec('docker', ['pull', image]);

Expand Down Expand Up @@ -239,7 +260,25 @@ jobs:
);
});

- name: Assert - signed mono-arch docker image
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const images = ${{ toJson(fromJson(needs.act-build-images.outputs.built-images).test-mono-arch.images) }};

for(const image of images) {
await exec.exec(
'cosign',
[
'verify', image,
'--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com',
'--certificate-identity-regexp', 'https://github.com/hoverkraft-tech/ci-github-container',
]
);
}

act-build-args-secrets-and-registry-caching:
name: Act - Build with args, secrets and registry caching
needs: arrange
uses: ./.github/workflows/docker-build-images.yml
secrets:
Expand Down Expand Up @@ -275,6 +314,7 @@ jobs:
SECRET_ENV_GITHUB_APP_TOKEN_2

assert-build-args-secrets-and-registry-caching:
name: Assert - Build with args, secrets and registry caching
needs: act-build-args-secrets-and-registry-caching
runs-on: "ubuntu-latest"
steps:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/docker-build-images.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Needs the following permissions:
- `issues`: `read`
- `packages`: `write`
- `pull-requests`: `read`
- `id-token`: `write` <!-- FIXME: This is a workaround for having workflow actions. See https://github.com/orgs/community/discussions/38659 -->
- `id-token`: `write`

<!-- end description -->
<!-- start contents -->
Expand All @@ -39,7 +39,6 @@ permissions:
issues: read
packages: write
pull-requests: read
# FIXME: This is a workaround for having workflow actions. See https://github.com/orgs/community/discussions/38659
id-token: write

jobs:
Expand Down
21 changes: 20 additions & 1 deletion .github/workflows/docker-build-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ permissions:
issues: read
packages: write
pull-requests: read
# FIXME: This is a workaround for having workflow actions. See https://github.com/orgs/community/discussions/38659
id-token: write

jobs:
Expand Down Expand Up @@ -500,3 +499,23 @@ jobs:
oci-registry-username: ${{ inputs.oci-registry-username }}
oci-registry-password: ${{ secrets.oci-registry-password }}
built-images: ${{ steps.built-images.outputs.built-images }}

- id: get-images-to-sign
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const builtImagesInput = `${{ steps.built-images.outputs.built-images }}`;
let builtImages = null;
try {
builtImages = JSON.parse(builtImagesInput);
} catch (error) {
throw new Error(`"built-images" input is not a valid JSON: ${error}`);
}

// Get images to sign
const imagesToSign = Object.values(builtImages).map(image => image.images).flat();
core.setOutput('images-to-sign', JSON.stringify(imagesToSign));
- uses: ./self-workflow/actions/docker/sign-images
with:
images: ${{ steps.get-images-to-sign.outputs.images-to-sign }}
github-token: ${{ secrets.GITHUB_TOKEN }}
3 changes: 2 additions & 1 deletion actions/docker/create-images-manifests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,13 @@ runs:
)
.flat().join(" ");

createManifestCommand = `docker buildx imagetools create ${annotations} ${tags} ${digests}`;
const createManifestCommand = `docker buildx imagetools create ${annotations} ${tags} ${digests}`;

return new Promise(async (resolve, reject) => {
try {
await exec.exec(createManifestCommand);
core.debug(`Create manifest for "${builtImage.name}" ("${createManifestCommand}") executed`);

resolve();
} catch(error){
reject(error);
Expand Down
37 changes: 37 additions & 0 deletions actions/docker/sign-images/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!-- start title -->

# <img src=".github/ghadocs/branding.svg" width="60px" align="center" alt="branding<icon:package color:gray-dark>" /> GitHub Action: Docker - Sign Images

<!-- end title -->
<!--
// jscpd:ignore-start
-->
<!-- markdownlint-disable MD013 -->
<!-- start badges -->
<!-- end badges -->
<!-- markdownlint-enable MD013 -->
<!--
// jscpd:ignore-end
-->
<!-- start description -->
<!-- end description -->
<!-- start contents -->
<!-- end contents -->

If default GitHub token is used, the following permissions are required:

```yml
permissions:
id-token: write
```

<!-- start usage -->
<!-- end usage -->
<!-- start inputs -->
<!-- end inputs -->
<!-- markdownlint-disable MD013 -->
<!-- start outputs -->
<!-- end outputs -->
<!-- markdownlint-enable MD013 -->
<!-- start [.github/ghadocs/examples/] -->
<!-- end [.github/ghadocs/examples/] -->
102 changes: 102 additions & 0 deletions actions/docker/sign-images/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
name: "Docker - Sign images"
description: |
Action to sign OCI images.
It is based on [cosign](https://github.com/sigstore/cosign).
It signs the images with the GitHub Actions OIDC token.
If the provided image does not have a digest, it will retrieve the digest using `docker buildx imagetools inspect`.

author: hoverkraft
branding:
icon: award
color: blue

inputs:
images:
description: |
Images to sign.
Can be a single image or a list of images separated by commas or newlines or spaces.
The images should be in the format `ghcr.io/my-org/my-repo/application:pr-63-5222075`.
It can also be a list of images in JSON format.
Example:
```
[
"ghcr.io/my-org/my-repo/application:pr-63-5222075",
"ghcr.io/my-org/my-repo/application:pr-63"
]
```
required: true
github-token:
description: |
GitHub Token to sign the image.
Permissions:
- id-token: write
default: ${{ github.token }}

runs:
using: "composite"
steps:
- uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1

- name: Sign the images with GitHub OIDC Token
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ inputs.github-token }}
script: |
const images = `${{ inputs.images }}`;
let imagesList = null;
try {
// Try to parse the input as JSON
imagesList = JSON.parse(images);
} catch (error) {
// If it fails, split the input by commas, newlines or spaces
imagesList = images.split(/[\s,]+/).filter(image => image.trim() !== "");
}

if (!Array.isArray(imagesList) || imagesList.length === 0) {
throw new Error(`"images" input is not a valid JSON array or a non-empty string: ${images}`);
}

const getImageDigest = async function(image) {
// Check if the image already has a digest
if (image.match(/@/)) {
core.debug(`Image "${image}" already has a digest, skipping inspection.`);
return image;
}

const inspectImageCommand = `docker buildx imagetools inspect ${image}`;
core.debug(`Inspecting image "${image}" with command: "${inspectImageCommand}"`);

const { stdout } = await exec.getExecOutput(inspectImageCommand);

core.debug(`Inspect image "${image}" ("${inspectImageCommand}") executed: ${stdout}`);

if (!stdout) {
throw new Error(`Failed to retrieve manifest for image "${image}": "${inspectImageCommand}" returned empty output`);
}

// Retrieve digest from the manifest
const digestRegex = /Digest:\s+([a-z0-9]+:[a-z0-9]{64})/;
const digestMatch = stdout.match(digestRegex);
if (!digestMatch || digestMatch.length < 2) {
throw new Error(`Failed to retrieve digest for image "${image}": "${inspectImageCommand}" returned unexpected output: ${stdout}`);
}

const digest = digestMatch[1];
if (!digest) {
throw new Error(`Failed to retrieve digest for image "${image}": "${inspectImageCommand}" returned empty digest`);
}

core.debug(`Digest for image "${image}" is "${digest}"`);
return `${image}@${digest}`;
}

// Wait for all images to be inspected and digests retrieved
const imagesWithDigests = await Promise.all(imagesList.map(image => getImageDigest(image)));

// Create manifest for each image
const signImageCommand = `cosign sign --recursive --yes ${imagesWithDigests.map(image => `"${image}"`).join(" ")}`;

core.debug(`Signing images with command: "${signImageCommand}"`);
await exec.exec(signImageCommand);
core.debug("All images signed successfully");
Loading
Loading