Repository navigation
Merge pull request #1403 from docker/deps/compose-version #507
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 | |
| 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 }}. |