Skip to content

Commit 52ea39a

Browse files
committed
ci: automated dependency update workflow
Signed-off-by: CrazyMax <[email protected]>
1 parent c9ad217 commit 52ea39a

1 file changed

Lines changed: 329 additions & 0 deletions

File tree

.github/workflows/update-deps.yml

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
name: update-deps
2+
3+
concurrency:
4+
group: ${{ github.workflow }}-${{ github.ref }}
5+
cancel-in-progress: true
6+
7+
permissions:
8+
contents: read
9+
10+
on:
11+
workflow_dispatch:
12+
schedule:
13+
- cron: '0 9 * * *'
14+
push:
15+
branches:
16+
- 'main'
17+
paths:
18+
- '.github/buildx-releases.json'
19+
- '.github/compose-releases.json'
20+
- '.github/cosign-releases.json'
21+
- '.github/docker-releases.json'
22+
- '.github/regclient-releases.json'
23+
- '.github/undock-releases.json'
24+
- '.github/workflows/test.yml'
25+
- '.github/workflows/update-deps.yml'
26+
27+
jobs:
28+
update:
29+
runs-on: ubuntu-24.04
30+
environment: update-deps # secrets are gated by this environment
31+
timeout-minutes: 10
32+
permissions:
33+
contents: write
34+
pull-requests: write
35+
strategy:
36+
fail-fast: false
37+
matrix:
38+
dep:
39+
- docker
40+
- buildx
41+
- buildkit
42+
- compose
43+
- cosign
44+
- regctl
45+
- undock
46+
steps:
47+
-
48+
name: GitHub auth token from GitHub App
49+
id: write-app
50+
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
51+
with:
52+
client-id: ${{ vars.GHACTIONS_REPO_WRITE_CLIENT_ID }}
53+
private-key: ${{ secrets.GHACTIONS_REPO_WRITE_PRIVATE_KEY }}
54+
owner: docker
55+
repositories: actions-toolkit
56+
-
57+
name: Checkout
58+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
59+
with:
60+
token: ${{ steps.write-app.outputs.token }}
61+
fetch-depth: 0
62+
persist-credentials: false
63+
-
64+
name: Update dependency
65+
id: update
66+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
67+
env:
68+
INPUT_DEP: ${{ matrix.dep }}
69+
with:
70+
script: |
71+
const fs = require('fs');
72+
const path = require('path');
73+
74+
const dep = core.getInput('dep');
75+
76+
function escapeRegExp(value) {
77+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
78+
}
79+
80+
function formatList(values) {
81+
const quoted = values.map(value => `\`${value}\``);
82+
if (quoted.length === 1) {
83+
return quoted[0];
84+
}
85+
if (quoted.length === 2) {
86+
return `${quoted[0]} and ${quoted[1]}`;
87+
}
88+
return `${quoted.slice(0, -1).join(', ')}, and ${quoted.at(-1)}`;
89+
}
90+
91+
function unique(values) {
92+
return [...new Set(values)];
93+
}
94+
95+
function stripLeadingV(value) {
96+
return value.startsWith('v') ? value.slice(1) : value;
97+
}
98+
99+
function stripDockerTag(value) {
100+
return value.replace(/^docker-v/, '').replace(/^v/, '');
101+
}
102+
103+
function majorMinor(value) {
104+
const match = value.match(/^(\d+\.\d+)/);
105+
if (!match) {
106+
throw new Error(`Unable to derive major.minor version from ${value}`);
107+
}
108+
return match[1];
109+
}
110+
111+
function readJson(relativePath) {
112+
const absolutePath = path.join(process.env.GITHUB_WORKSPACE, relativePath);
113+
return JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
114+
}
115+
116+
function readLatestTag(relativePath) {
117+
const tag = readJson(relativePath)?.latest?.tag_name;
118+
if (!tag) {
119+
throw new Error(`Unable to resolve latest tag from ${relativePath}`);
120+
}
121+
return tag;
122+
}
123+
124+
function dockerfileArgPattern(key) {
125+
return new RegExp(`^(ARG ${escapeRegExp(key)}=)(.+)$`, 'm');
126+
}
127+
128+
function workflowEnvPattern(key) {
129+
return new RegExp(`^( ${escapeRegExp(key)}: ")([^"]*)(")$`, 'm');
130+
}
131+
132+
const dependencyConfigs = {
133+
docker: {
134+
name: 'Docker version',
135+
branch: 'deps/docker-version',
136+
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/docker-releases.json',
137+
async resolve() {
138+
const tag = readLatestTag('.github/docker-releases.json');
139+
const version = majorMinor(stripDockerTag(tag));
140+
return {
141+
titleValue: version,
142+
targets: [
143+
{
144+
path: 'dev.Dockerfile',
145+
key: 'DOCKER_VERSION',
146+
value: version,
147+
pattern: dockerfileArgPattern('DOCKER_VERSION')
148+
}
149+
]
150+
};
151+
}
152+
},
153+
buildx: {
154+
name: 'Buildx version',
155+
branch: 'deps/buildx-version',
156+
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/buildx-releases.json',
157+
async resolve() {
158+
const tag = readLatestTag('.github/buildx-releases.json');
159+
return {
160+
titleValue: tag,
161+
targets: [
162+
{
163+
path: 'dev.Dockerfile',
164+
key: 'BUILDX_VERSION',
165+
value: stripLeadingV(tag),
166+
pattern: dockerfileArgPattern('BUILDX_VERSION')
167+
},
168+
{
169+
path: '.github/workflows/test.yml',
170+
key: 'BUILDX_VERSION',
171+
value: tag,
172+
pattern: workflowEnvPattern('BUILDX_VERSION')
173+
}
174+
]
175+
};
176+
}
177+
},
178+
buildkit: {
179+
name: 'BuildKit image',
180+
branch: 'deps/buildkit-image',
181+
sourceUrl: 'https://github.com/moby/buildkit/releases/latest',
182+
async resolve({github}) {
183+
const release = await github.rest.repos.getLatestRelease({
184+
owner: 'moby',
185+
repo: 'buildkit'
186+
});
187+
const image = `moby/buildkit:${release.data.tag_name}`;
188+
return {
189+
titleValue: image,
190+
targets: [
191+
{
192+
path: '.github/workflows/test.yml',
193+
key: 'BUILDKIT_IMAGE',
194+
value: image,
195+
pattern: workflowEnvPattern('BUILDKIT_IMAGE')
196+
}
197+
]
198+
};
199+
}
200+
},
201+
compose: {
202+
name: 'Compose version',
203+
branch: 'deps/compose-version',
204+
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/compose-releases.json',
205+
async resolve() {
206+
const tag = readLatestTag('.github/compose-releases.json');
207+
return {
208+
titleValue: tag,
209+
targets: [
210+
{
211+
path: 'dev.Dockerfile',
212+
key: 'COMPOSE_VERSION',
213+
value: stripLeadingV(tag),
214+
pattern: dockerfileArgPattern('COMPOSE_VERSION')
215+
}
216+
]
217+
};
218+
}
219+
},
220+
undock: {
221+
name: 'Undock version',
222+
branch: 'deps/undock-version',
223+
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/undock-releases.json',
224+
async resolve() {
225+
const tag = readLatestTag('.github/undock-releases.json');
226+
return {
227+
titleValue: tag,
228+
targets: [
229+
{
230+
path: 'dev.Dockerfile',
231+
key: 'UNDOCK_VERSION',
232+
value: stripLeadingV(tag),
233+
pattern: dockerfileArgPattern('UNDOCK_VERSION')
234+
}
235+
]
236+
};
237+
}
238+
},
239+
regctl: {
240+
name: 'Regctl version',
241+
branch: 'deps/regctl-version',
242+
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/regclient-releases.json',
243+
async resolve() {
244+
const tag = readLatestTag('.github/regclient-releases.json');
245+
return {
246+
titleValue: tag,
247+
targets: [
248+
{
249+
path: 'dev.Dockerfile',
250+
key: 'REGCTL_VERSION',
251+
value: tag,
252+
pattern: dockerfileArgPattern('REGCTL_VERSION')
253+
}
254+
]
255+
};
256+
}
257+
},
258+
cosign: {
259+
name: 'Cosign version',
260+
branch: 'deps/cosign-version',
261+
sourceUrl: 'https://github.com/docker/actions-toolkit/blob/main/.github/cosign-releases.json',
262+
async resolve() {
263+
const tag = readLatestTag('.github/cosign-releases.json');
264+
return {
265+
titleValue: tag,
266+
targets: [
267+
{
268+
path: 'dev.Dockerfile',
269+
key: 'COSIGN_VERSION',
270+
value: tag,
271+
pattern: dockerfileArgPattern('COSIGN_VERSION')
272+
}
273+
]
274+
};
275+
}
276+
}
277+
};
278+
279+
const config = dependencyConfigs[dep];
280+
if (!config) {
281+
core.setFailed(`Unknown dependency ${dep}`);
282+
return;
283+
}
284+
285+
const resolved = await config.resolve({github});
286+
const currentValues = [];
287+
const changedFiles = [];
288+
289+
for (const target of resolved.targets) {
290+
const absolutePath = path.join(process.env.GITHUB_WORKSPACE, target.path);
291+
const content = fs.readFileSync(absolutePath, 'utf8');
292+
const match = content.match(target.pattern);
293+
if (!match) {
294+
throw new Error(`Missing ${target.key} in ${target.path}`);
295+
}
296+
currentValues.push(match[2]);
297+
if (match[2] === target.value) {
298+
continue;
299+
}
300+
fs.writeFileSync(absolutePath, content.replace(target.pattern, `$1${target.value}$3`), 'utf8');
301+
changedFiles.push(target.path);
302+
}
303+
304+
core.info(`Resolved ${config.name} from ${config.sourceUrl}`);
305+
if (!changed) {
306+
core.info(`No workspace changes needed for ${config.name}`);
307+
}
308+
309+
core.setOutput('changed', changed ? 'true' : 'false');
310+
core.setOutput('branch', config.branch);
311+
core.setOutput('title', `chore(deps): update ${config.name} to ${resolved.titleValue}`);
312+
core.setOutput('before', formatList(unique(currentValues)));
313+
core.setOutput('files', formatList(unique(changedFiles)));
314+
core.setOutput('source-url', config.sourceUrl);
315+
-
316+
name: Create pull request
317+
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
318+
with:
319+
base: main
320+
branch: ${{ steps.update.outputs.branch }}
321+
token: ${{ steps.write-app.outputs.token }}
322+
commit-message: ${{ steps.update.outputs.title }}
323+
title: ${{ steps.update.outputs.title }}
324+
signoff: true
325+
delete-branch: true
326+
body: |
327+
This updates the pinned value from ${{ steps.update.outputs.before }} in ${{ steps.update.outputs.files }}.
328+
329+
The source of truth for this update is ${{ steps.update.outputs.source-url }}.

0 commit comments

Comments
 (0)