.update-deps #5
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: .update-deps | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: "0 9 * * *" | |
| permissions: | |
| contents: read | |
| jobs: | |
| update: | |
| runs-on: ubuntu-24.04 | |
| environment: update-deps # secrets are gated by this environment | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| dep: | |
| - buildx | |
| - buildkit | |
| - sbom | |
| - binfmt | |
| - cosign | |
| - toolkit | |
| steps: | |
| - | |
| name: GitHub auth token from GitHub App | |
| id: write-app | |
| uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| with: | |
| client-id: ${{ vars.DOCKER_GITHUB_BUILDER_WRITE_CLIENT_ID }} | |
| private-key: ${{ secrets.DOCKER_GITHUB_BUILDER_WRITE_PRIVATE_KEY }} | |
| owner: docker | |
| repositories: github-builder | |
| - | |
| name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| token: ${{ steps.write-app.outputs.token }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - | |
| name: Update dependency | |
| id: update | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| INPUT_DEP: ${{ matrix.dep }} | |
| with: | |
| github-token: ${{ steps.write-app.outputs.token }} | |
| script: | | |
| const dep = core.getInput('dep'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const dependencyConfigs = { | |
| buildx: { | |
| key: 'BUILDX_VERSION', | |
| name: 'Buildx version', | |
| branch: 'deps/buildx-version', | |
| files: [ | |
| '.github/workflows/build.yml', | |
| '.github/workflows/bake.yml' | |
| ], | |
| sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/buildx-releases.json', | |
| async resolve({github}) { | |
| const response = await github.rest.repos.getContent({ | |
| owner: 'docker', | |
| repo: 'actions-toolkit', | |
| path: '.github/buildx-releases.json', | |
| ref: 'main' | |
| }); | |
| const content = decodeContent(response.data); | |
| const payload = JSON.parse(content); | |
| const tag = payload?.latest?.tag_name; | |
| if (!tag) { | |
| throw new Error('Unable to resolve latest buildx tag from docker/actions-toolkit/.github/buildx-releases.json'); | |
| } | |
| return { | |
| value: tag, | |
| from: tag, | |
| to: tag | |
| }; | |
| } | |
| }, | |
| buildkit: { | |
| key: 'BUILDKIT_IMAGE', | |
| name: 'BuildKit image', | |
| branch: 'deps/buildkit-image', | |
| files: [ | |
| '.github/workflows/build.yml', | |
| '.github/workflows/bake.yml' | |
| ], | |
| sourceUrl: 'https://github.com/moby/buildkit/releases/latest', | |
| async resolve({github}) { | |
| const release = await github.rest.repos.getLatestRelease({ | |
| owner: 'moby', | |
| repo: 'buildkit' | |
| }); | |
| return { | |
| value: `moby/buildkit:${release.data.tag_name}`, | |
| from: release.data.tag_name, | |
| to: release.data.tag_name | |
| }; | |
| } | |
| }, | |
| sbom: { | |
| key: 'SBOM_IMAGE', | |
| name: 'SBOM image', | |
| branch: 'deps/sbom-image', | |
| files: [ | |
| '.github/workflows/build.yml', | |
| '.github/workflows/bake.yml' | |
| ], | |
| sourceUrl: 'https://github.com/docker/buildkit-syft-scanner/releases/latest', | |
| async resolve({github}) { | |
| const release = await github.rest.repos.getLatestRelease({ | |
| owner: 'docker', | |
| repo: 'buildkit-syft-scanner' | |
| }); | |
| const tag = release.data.tag_name; | |
| return { | |
| value: `docker/buildkit-syft-scanner:${stripLeadingV(tag)}`, | |
| from: tag, | |
| to: stripLeadingV(tag) | |
| }; | |
| } | |
| }, | |
| binfmt: { | |
| key: 'BINFMT_IMAGE', | |
| name: 'Binfmt image', | |
| branch: 'deps/binfmt-image', | |
| files: [ | |
| '.github/workflows/build.yml', | |
| '.github/workflows/bake.yml' | |
| ], | |
| sourceUrl: 'https://github.com/tonistiigi/binfmt/releases/latest', | |
| async resolve({github}) { | |
| const release = await github.rest.repos.getLatestRelease({ | |
| owner: 'tonistiigi', | |
| repo: 'binfmt' | |
| }); | |
| const tag = release.data.tag_name; | |
| if (!tag.startsWith('deploy/')) { | |
| throw new Error(`Expected deploy/ release tag for tonistiigi/binfmt, got ${tag}`); | |
| } | |
| const imageTag = `qemu-${tag.slice('deploy/'.length)}`; | |
| return { | |
| value: `tonistiigi/binfmt:${imageTag}`, | |
| from: tag, | |
| to: imageTag | |
| }; | |
| } | |
| }, | |
| toolkit: { | |
| key: 'DOCKER_ACTIONS_TOOLKIT_MODULE', | |
| name: 'docker/actions-toolkit module', | |
| branch: 'deps/docker-actions-toolkit-module', | |
| files: [ | |
| '.github/workflows/build.yml', | |
| '.github/workflows/bake.yml', | |
| '.github/workflows/verify.yml' | |
| ], | |
| sourceUrl: 'https://github.com/docker/actions-toolkit/releases/latest', | |
| async resolve({github}) { | |
| const release = await github.rest.repos.getLatestRelease({ | |
| owner: 'docker', | |
| repo: 'actions-toolkit' | |
| }); | |
| const tag = release.data.tag_name; | |
| const version = stripLeadingV(tag); | |
| return { | |
| value: `@docker/actions-toolkit@${version}`, | |
| from: tag, | |
| to: version | |
| }; | |
| } | |
| }, | |
| cosign: { | |
| key: 'COSIGN_VERSION', | |
| name: 'Cosign version', | |
| branch: 'deps/cosign-version', | |
| files: [ | |
| '.github/workflows/build.yml', | |
| '.github/workflows/bake.yml' | |
| ], | |
| sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/cosign-releases.json', | |
| async resolve({github}) { | |
| const response = await github.rest.repos.getContent({ | |
| owner: 'docker', | |
| repo: 'actions-toolkit', | |
| path: '.github/cosign-releases.json', | |
| ref: 'main' | |
| }); | |
| const content = decodeContent(response.data); | |
| const payload = JSON.parse(content); | |
| const tag = payload?.latest?.tag_name; | |
| if (!tag) { | |
| throw new Error('Unable to resolve latest cosign tag from docker/actions-toolkit/.github/cosign-releases.json'); | |
| } | |
| return { | |
| value: tag, | |
| from: tag, | |
| to: tag | |
| }; | |
| } | |
| } | |
| }; | |
| function stripLeadingV(value) { | |
| return value.startsWith('v') ? value.slice(1) : value; | |
| } | |
| function escapeRegExp(value) { | |
| return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| } | |
| function decodeContent(data) { | |
| if (Array.isArray(data) || data.type !== 'file' || !data.content) { | |
| throw new Error('Expected a file content response from the GitHub API'); | |
| } | |
| return Buffer.from(data.content, data.encoding).toString('utf8'); | |
| } | |
| function readEnvValue(content, key) { | |
| const pattern = new RegExp(`^ ${escapeRegExp(key)}: "([^"]*)"$`, 'm'); | |
| const match = content.match(pattern); | |
| if (!match) { | |
| throw new Error(`Missing ${key}`); | |
| } | |
| return match[1]; | |
| } | |
| function replaceEnvValue(content, key, value) { | |
| const pattern = new RegExp(`^( ${escapeRegExp(key)}: ")([^"]*)(")$`, 'm'); | |
| const match = content.match(pattern); | |
| if (!match) { | |
| throw new Error(`Missing ${key}`); | |
| } | |
| return { | |
| changed: match[2] !== value, | |
| before: match[2], | |
| content: content.replace(pattern, `$1${value}$3`) | |
| }; | |
| } | |
| function unique(values) { | |
| return [...new Set(values)]; | |
| } | |
| function formatList(values) { | |
| if (values.length === 1) { | |
| return `\`${values[0]}\``; | |
| } | |
| if (values.length === 2) { | |
| return `\`${values[0]}\` and \`${values[1]}\``; | |
| } | |
| const quoted = values.map((value) => `\`${value}\``); | |
| return `${quoted.slice(0, -1).join(', ')}, and ${quoted.at(-1)}`; | |
| } | |
| const config = dependencyConfigs[dep]; | |
| if (!config) { | |
| core.setFailed(`Unknown dependency ${dep}`); | |
| return; | |
| } | |
| const target = await config.resolve({github}); | |
| core.info(`Resolved ${config.key} to ${target.value} from ${config.sourceUrl}`); | |
| const workingFiles = config.files.map((filePath) => { | |
| const absolutePath = path.join(process.env.GITHUB_WORKSPACE, filePath); | |
| const content = fs.readFileSync(absolutePath, 'utf8'); | |
| return { | |
| path: filePath, | |
| absolutePath, | |
| content | |
| }; | |
| }); | |
| const baseValues = unique(workingFiles.map((file) => readEnvValue(file.content, config.key))); | |
| const changes = []; | |
| for (const file of workingFiles) { | |
| const replacement = replaceEnvValue(file.content, config.key, target.value); | |
| if (!replacement.changed) { | |
| continue; | |
| } | |
| changes.push({ | |
| path: file.path, | |
| before: replacement.before, | |
| after: target.value, | |
| content: replacement.content, | |
| absolutePath: file.absolutePath | |
| }); | |
| } | |
| if (changes.length > 0) { | |
| for (const change of changes) { | |
| fs.writeFileSync(change.absolutePath, change.content, 'utf8'); | |
| } | |
| } else { | |
| core.info(`No workspace changes needed for ${config.key}`); | |
| } | |
| const beforeValue = formatList(baseValues); | |
| const commitMessage = `chore(deps): bump ${config.key} to ${target.to}`; | |
| core.setOutput('branch', config.branch); | |
| core.setOutput('commit-message', commitMessage); | |
| core.setOutput('key', config.key); | |
| core.setOutput('before-value', beforeValue); | |
| core.setOutput('target-value', target.value); | |
| core.setOutput('source-url', config.sourceUrl); | |
| - | |
| name: Create pull request | |
| uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 | |
| with: | |
| base: main | |
| branch: ${{ steps.update.outputs.branch }} | |
| token: ${{ steps.write-app.outputs.token }} | |
| commit-message: ${{ steps.update.outputs.commit-message }} | |
| title: ${{ steps.update.outputs.commit-message }} | |
| signoff: true | |
| delete-branch: true | |
| body: | | |
| This updates ${{ steps.update.outputs.key }} from ${{ steps.update.outputs.before-value }} to `${{ steps.update.outputs.target-value }}`. | |
| The source of truth for this update is ${{ steps.update.outputs.source-url }}. |