Skip to content

.update-deps

.update-deps #3

Workflow file for this run

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 }}.