Skip to content

Merge pull request #1402 from docker/bot/compose-releases-json #505

Merge pull request #1402 from docker/bot/compose-releases-json

Merge pull request #1402 from docker/bot/compose-releases-json #505

Workflow file for this run

name: update-deps
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
on:
workflow_dispatch:
schedule:
- cron: '0 9 * * *'
push:
branches:
- 'main'
jobs:
update:
runs-on: ubuntu-26.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:
- docker
- buildx
- buildkit
- compose
- cosign
- regctl
- undock
- lima
steps:
-
name: GitHub auth token from GitHub App
id: write-app
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.GHACTIONS_REPO_WRITE_CLIENT_ID }}
private-key: ${{ secrets.GHACTIONS_REPO_WRITE_PRIVATE_KEY }}
owner: docker
repositories: actions-toolkit
permission-contents: write
permission-pull-requests: write
permission-workflows: write
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
token: ${{ steps.write-app.outputs.token }}
fetch-depth: 0
persist-credentials: false
-
name: Install npm deps
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const path = require('path');
await core.group(`Install npm deps`, async () => {
await exec.exec('npm', [
'install',
'--prefix', path.join(process.env.RUNNER_TEMP, 'update-deps'),
'--loglevel=error',
'--no-save',
'--package-lock=false',
'--ignore-scripts',
'--omit=dev',
'--prefer-offline',
'--fund=false',
'--audit=false',
'semver@7.8.3'
]);
});
-
name: Update dependency
id: update
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
INPUT_DEP: ${{ matrix.dep }}
with:
script: |
const fs = require('fs');
const path = require('path');
const semver = require(path.join(process.env.RUNNER_TEMP, 'update-deps', 'node_modules', 'semver'));
const dep = core.getInput('dep');
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function formatList(values) {
const quoted = values.map(value => `\`${value}\``);
if (quoted.length === 1) {
return quoted[0];
}
if (quoted.length === 2) {
return `${quoted[0]} and ${quoted[1]}`;
}
return `${quoted.slice(0, -1).join(', ')}, and ${quoted.at(-1)}`;
}
function unique(values) {
return [...new Set(values)];
}
function stripLeadingV(value) {
return value.startsWith('v') ? value.slice(1) : value;
}
function stripDockerTag(value) {
return value.replace(/^docker-v/, '').replace(/^v/, '');
}
function majorMinor(value) {
const match = value.match(/^(\d+\.\d+)/);
if (!match) {
throw new Error(`Unable to derive major.minor version from ${value}`);
}
return match[1];
}
function readJson(relativePath) {
const absolutePath = path.join(process.env.GITHUB_WORKSPACE, relativePath);
return JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
}
function readLatestTag(relativePath) {
return readReleaseTag(relativePath, 'latest');
}
function readReleaseTag(relativePath, alias) {
const tag = readJson(relativePath)?.[alias]?.tag_name;
if (!tag) {
throw new Error(`Unable to resolve ${alias} tag from ${relativePath}`);
}
return tag;
}
async function latestSemverReleaseOrPrereleaseTag({github, owner, repo}) {
const releases = await github.rest.repos.listReleases({
owner,
repo,
per_page: 100
});
const release = releases.data.find(release => !release.draft && semver.valid(release.tag_name));
if (!release) {
throw new Error(`Unable to resolve latest semver release or pre-release for ${owner}/${repo}`);
}
return release.tag_name;
}
function dockerfileArgPattern(key) {
return new RegExp(`^(ARG ${escapeRegExp(key)}=)(.+)$`, 'm');
}
function workflowEnvPattern(key) {
return new RegExp(`^( ${escapeRegExp(key)}: ")([^"]*)(")$`, 'm');
}
function tsConstStringPattern(key) {
return new RegExp(`^((?:export )?const ${escapeRegExp(key)} = ')([^']*)(';)$`, 'm');
}
const dependencyConfigs = {
docker: {
name: 'Docker version',
branch: 'deps/docker-version',
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/docker-releases.json',
async resolve() {
const tag = readLatestTag('.github/docker-releases.json');
const version = majorMinor(stripDockerTag(tag));
return {
titleValue: version,
targets: [
{
path: 'dev.Dockerfile',
key: 'DOCKER_VERSION',
value: version,
pattern: dockerfileArgPattern('DOCKER_VERSION')
}
]
};
}
},
buildx: {
name: 'Buildx version',
branch: 'deps/buildx-version',
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/buildx-releases.json',
async resolve() {
const tag = readReleaseTag('.github/buildx-releases.json', 'edge');
return {
titleValue: tag,
targets: [
{
path: 'dev.Dockerfile',
key: 'BUILDX_VERSION',
value: stripLeadingV(tag),
pattern: dockerfileArgPattern('BUILDX_VERSION')
},
{
path: '.github/workflows/test.yml',
key: 'BUILDX_VERSION',
value: tag,
pattern: workflowEnvPattern('BUILDX_VERSION')
}
]
};
}
},
buildkit: {
name: 'BuildKit image',
branch: 'deps/buildkit-image',
sourceUrl: 'https://github.com/moby/buildkit/releases',
async resolve({github}) {
const tag = await latestSemverReleaseOrPrereleaseTag({
github,
owner: 'moby',
repo: 'buildkit'
});
const image = `moby/buildkit:${tag}`;
return {
titleValue: image,
targets: [
{
path: '.github/workflows/test.yml',
key: 'BUILDKIT_IMAGE',
value: image,
pattern: workflowEnvPattern('BUILDKIT_IMAGE')
}
]
};
}
},
compose: {
name: 'Compose version',
branch: 'deps/compose-version',
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/compose-releases.json',
async resolve() {
const tag = readLatestTag('.github/compose-releases.json');
return {
titleValue: tag,
targets: [
{
path: 'dev.Dockerfile',
key: 'COMPOSE_VERSION',
value: stripLeadingV(tag),
pattern: dockerfileArgPattern('COMPOSE_VERSION')
}
]
};
}
},
undock: {
name: 'Undock version',
branch: 'deps/undock-version',
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/undock-releases.json',
async resolve() {
const tag = readLatestTag('.github/undock-releases.json');
const version = stripLeadingV(tag);
return {
titleValue: tag,
targets: [
{
path: 'dev.Dockerfile',
key: 'UNDOCK_VERSION',
value: version,
pattern: dockerfileArgPattern('UNDOCK_VERSION')
},
{
path: 'src/docker/assets.ts',
key: 'limaUndockVersion',
value: version,
pattern: tsConstStringPattern('limaUndockVersion')
}
]
};
}
},
lima: {
name: 'Lima images',
branch: 'deps/lima-images',
sourceUrl: 'https://github.com/lima-vm/lima/blob/master/templates/_images/ubuntu-24.04.yaml',
async resolve({github}) {
const {data} = await github.rest.repos.getContent({
owner: 'lima-vm',
repo: 'lima',
path: 'templates/_images/ubuntu-24.04.yaml',
ref: 'master'
});
const template = Buffer.from(data.content, 'base64').toString('utf8');
const baseUrl = 'https://cloud-images.ubuntu.com/releases/noble/';
const release = template.match(new RegExp(`${escapeRegExp(baseUrl)}release-(\\d{8})/ubuntu-24\\.04-server-cloudimg-amd64\\.img`))?.[1];
if (!release) {
throw new Error('Unable to resolve the pinned Ubuntu 24.04 release from Lima');
}
const targets = [];
for (const [directory, filenames] of [
['', ['ubuntu-24.04-server-cloudimg-amd64.img', 'ubuntu-24.04-server-cloudimg-arm64.img']],
['unpacked/', ['ubuntu-24.04-server-cloudimg-amd64-vmlinuz-generic', 'ubuntu-24.04-server-cloudimg-amd64-initrd-generic']]
]) {
const response = await fetch(`${baseUrl}release-${release}/${directory}SHA256SUMS`);
if (!response.ok) {
throw new Error(`Unable to fetch Ubuntu checksums: ${response.status} ${response.statusText}`);
}
const checksums = await response.text();
for (const filename of filenames) {
const digest = checksums.match(new RegExp(`^([a-f0-9]{64}) [ *]${escapeRegExp(filename)}\\r?$`, 'm'))?.[1];
if (!digest) {
throw new Error(`Missing Ubuntu checksum for ${filename}`);
}
const locationPattern = `${escapeRegExp(baseUrl)}release-\\d{8}/${escapeRegExp(directory + filename)}`;
targets.push(
{
path: 'src/docker/assets.ts',
key: `${filename} location`,
value: `${baseUrl}release-${release}/${directory}${filename}`,
pattern: new RegExp(`(location: ")(${locationPattern})(")`)
},
{
path: 'src/docker/assets.ts',
key: `${filename} digest`,
value: `sha256:${digest}`,
pattern: new RegExp(`(location: "${locationPattern}"\\r?\\n(?: arch: "[^"]+"\\r?\\n)? +digest: ")([^"]+)(")`)
}
);
}
}
return {titleValue: release, targets};
}
},
regctl: {
name: 'Regctl version',
branch: 'deps/regctl-version',
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/regclient-releases.json',
async resolve() {
const tag = readLatestTag('.github/regclient-releases.json');
return {
titleValue: tag,
targets: [
{
path: 'dev.Dockerfile',
key: 'REGCTL_VERSION',
value: tag,
pattern: dockerfileArgPattern('REGCTL_VERSION')
}
]
};
}
},
cosign: {
name: 'Cosign version',
branch: 'deps/cosign-version',
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/cosign-releases.json',
async resolve() {
const tag = readLatestTag('.github/cosign-releases.json');
return {
titleValue: tag,
targets: [
{
path: 'dev.Dockerfile',
key: 'COSIGN_VERSION',
value: tag,
pattern: dockerfileArgPattern('COSIGN_VERSION')
},
{
path: '__tests__/sigstore/sigstore.test.itg.ts',
key: 'currentCosignVersion',
value: tag,
pattern: tsConstStringPattern('currentCosignVersion')
}
]
};
}
}
};
const config = dependencyConfigs[dep];
if (!config) {
core.setFailed(`Unknown dependency ${dep}`);
return;
}
const resolved = await config.resolve({github});
const currentValues = [];
const changedFiles = [];
for (const target of resolved.targets) {
const absolutePath = path.join(process.env.GITHUB_WORKSPACE, target.path);
const content = fs.readFileSync(absolutePath, 'utf8');
const match = content.match(target.pattern);
if (!match) {
throw new Error(`Missing ${target.key} in ${target.path}`);
}
currentValues.push(match[2]);
if (match[2] === target.value) {
continue;
}
const updatedContent = content.replace(target.pattern, (...args) => {
const groups = args.slice(1, -2);
const prefix = groups[0];
const suffix = groups[2] || '';
return `${prefix}${target.value}${suffix}`;
});
fs.writeFileSync(absolutePath, updatedContent, 'utf8');
changedFiles.push(target.path);
}
core.info(`Resolved ${config.name} from ${config.sourceUrl}`);
if (changedFiles.length === 0) {
core.info(`No workspace changes needed for ${config.name}`);
} else {
core.info(`New ${config.name} ${resolved.titleValue} found`);
}
core.setOutput('branch', config.branch);
core.setOutput('title', `chore(deps): update ${config.name} to ${resolved.titleValue}`);
core.setOutput('before', formatList(unique(currentValues)));
core.setOutput('files', formatList(unique(changedFiles)));
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.title }}
title: ${{ steps.update.outputs.title }}
signoff: true
sign-commits: true
delete-branch: true
body: |
This updates the pinned value from ${{ steps.update.outputs.before }} in ${{ steps.update.outputs.files }}.
The source of truth for this update is ${{ steps.update.outputs.source-url }}.