From 1ecaab241d4ffd769630eb3ac03797465fc2de7e Mon Sep 17 00:00:00 2001 From: PeterNitsche <79380100+PeterNitsche@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:59:41 +0200 Subject: [PATCH] feat(github-actions): support GitHub actions runners (#23633) Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/datasource/api.ts | 2 + .../datasource/github-runners/index.spec.ts | 30 +++++ .../datasource/github-runners/index.ts | 58 +++++++++ .../datasource/github-runners/readme.md | 11 ++ .../__snapshots__/extract.spec.ts.snap | 81 +++++++++++++ .../manager/github-actions/extract.spec.ts | 112 ++++++++++++++++++ lib/modules/manager/github-actions/extract.ts | 47 ++++++++ lib/modules/manager/github-actions/index.ts | 6 +- lib/modules/manager/github-actions/readme.md | 15 +++ lib/modules/manager/github-actions/types.ts | 1 + 10 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 lib/modules/datasource/github-runners/index.spec.ts create mode 100644 lib/modules/datasource/github-runners/index.ts create mode 100644 lib/modules/datasource/github-runners/readme.md diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts index ed22a7daa4..0abd7bfd7e 100644 --- a/lib/modules/datasource/api.ts +++ b/lib/modules/datasource/api.ts @@ -25,6 +25,7 @@ import { GitRefsDatasource } from './git-refs'; import { GitTagsDatasource } from './git-tags'; import { GithubReleaseAttachmentsDatasource } from './github-release-attachments'; import { GithubReleasesDatasource } from './github-releases'; +import { GithubRunnersDatasource } from './github-runners'; import { GithubTagsDatasource } from './github-tags'; import { GitlabPackagesDatasource } from './gitlab-packages'; import { GitlabReleasesDatasource } from './gitlab-releases'; @@ -90,6 +91,7 @@ api.set( new GithubReleaseAttachmentsDatasource() ); api.set(GithubReleasesDatasource.id, new GithubReleasesDatasource()); +api.set(GithubRunnersDatasource.id, new GithubRunnersDatasource()); api.set(GithubTagsDatasource.id, new GithubTagsDatasource()); api.set(GitlabPackagesDatasource.id, new GitlabPackagesDatasource()); api.set(GitlabReleasesDatasource.id, new GitlabReleasesDatasource()); diff --git a/lib/modules/datasource/github-runners/index.spec.ts b/lib/modules/datasource/github-runners/index.spec.ts new file mode 100644 index 0000000000..2461c625f5 --- /dev/null +++ b/lib/modules/datasource/github-runners/index.spec.ts @@ -0,0 +1,30 @@ +import { getPkgReleases } from '..'; +import { GithubRunnersDatasource } from '.'; + +describe('modules/datasource/github-runners/index', () => { + describe('getReleases', () => { + it('returns releases if package is known', async () => { + const res = await getPkgReleases({ + datasource: GithubRunnersDatasource.id, + packageName: 'ubuntu', + }); + + expect(res).toMatchObject({ + releases: [ + { version: '18.04' }, + { version: '20.04' }, + { version: '22.04' }, + ], + }); + }); + + it('returns null if package is unknown', async () => { + const res = await getPkgReleases({ + datasource: GithubRunnersDatasource.id, + packageName: 'unknown', + }); + + expect(res).toBeNull(); + }); + }); +}); diff --git a/lib/modules/datasource/github-runners/index.ts b/lib/modules/datasource/github-runners/index.ts new file mode 100644 index 0000000000..37dda6fdba --- /dev/null +++ b/lib/modules/datasource/github-runners/index.ts @@ -0,0 +1,58 @@ +import { id as dockerVersioningId } from '../../versioning/docker'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; + +export class GithubRunnersDatasource extends Datasource { + static readonly id = 'github-runners'; + + /** + * Only add stable runners to the datasource. See datasource readme for details. + */ + private static readonly releases: Record<string, Release[] | undefined> = { + ubuntu: [{ version: '22.04' }, { version: '20.04' }, { version: '18.04' }], + macos: [ + { version: '13' }, + { version: '13-xl' }, + { version: '12' }, + { version: '12-xl' }, + { version: '11' }, + { version: '10.15' }, + ], + windows: [{ version: '2022' }, { version: '2019' }], + }; + + public static isValidRunner( + runnerName: string, + runnerVersion: string + ): boolean { + const runnerReleases = GithubRunnersDatasource.releases[runnerName]; + if (!runnerReleases) { + return false; + } + + const versionExists = runnerReleases.some( + ({ version }) => version === runnerVersion + ); + + return runnerVersion === 'latest' || versionExists; + } + + override readonly defaultVersioning = dockerVersioningId; + + constructor() { + super(GithubRunnersDatasource.id); + } + + override getReleases({ + packageName, + }: GetReleasesConfig): Promise<ReleaseResult | null> { + const releases = GithubRunnersDatasource.releases[packageName]; + const releaseResult: ReleaseResult | null = releases + ? { + releases, + sourceUrl: 'https://github.com/actions/runner-images', + } + : null; + return Promise.resolve(releaseResult); + } +} diff --git a/lib/modules/datasource/github-runners/readme.md b/lib/modules/datasource/github-runners/readme.md new file mode 100644 index 0000000000..08f336fe3c --- /dev/null +++ b/lib/modules/datasource/github-runners/readme.md @@ -0,0 +1,11 @@ +This datasource returns a list of all _stable_ runners that are hosted by GitHub. +This datasource ignores beta releases. +The datasource is based on [GitHub's `runner-images` repository](https://github.com/actions/runner-images). + +Examples: `windows-2019` / `ubuntu-22.04` / `macos-13` + +## Maintenance + +New _stable_ runner versions must be added to the datasource with a pull request. +Unstable runners are tagged as `[beta]` in the readme of the [`runner-images` repository](https://github.com/actions/runner-images). +Once a runner version becomes stable, the `[beta]` tag is removed and the suffix `latest` is added to its YAML label. diff --git a/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap b/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap index 47d08862bf..c10544c99c 100644 --- a/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap +++ b/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap @@ -87,6 +87,33 @@ exports[`modules/manager/github-actions/extract extractPackageFile() extracts mu "replaceString": "actions-rs/cargo@v1.0.3", "versioning": "docker", }, + { + "autoReplaceStringTemplate": "{{depName}}-{{newValue}}", + "currentValue": "latest", + "datasource": "github-runners", + "depName": "ubuntu", + "depType": "github-runner", + "replaceString": "ubuntu-latest", + "skipReason": "invalid-version", + }, + { + "autoReplaceStringTemplate": "{{depName}}-{{newValue}}", + "currentValue": "latest", + "datasource": "github-runners", + "depName": "ubuntu", + "depType": "github-runner", + "replaceString": "ubuntu-latest", + "skipReason": "invalid-version", + }, + { + "autoReplaceStringTemplate": "{{depName}}-{{newValue}}", + "currentValue": "latest", + "datasource": "github-runners", + "depName": "ubuntu", + "depType": "github-runner", + "replaceString": "ubuntu-latest", + "skipReason": "invalid-version", + }, ] `; @@ -132,6 +159,42 @@ exports[`modules/manager/github-actions/extract extractPackageFile() extracts mu "depType": "docker", "replaceString": "node:6@sha256:7b65413af120ec5328077775022c78101f103258a1876ec2f83890bce416e896", }, + { + "autoReplaceStringTemplate": "{{depName}}-{{newValue}}", + "currentValue": "latest", + "datasource": "github-runners", + "depName": "ubuntu", + "depType": "github-runner", + "replaceString": "ubuntu-latest", + "skipReason": "invalid-version", + }, + { + "autoReplaceStringTemplate": "{{depName}}-{{newValue}}", + "currentValue": "latest", + "datasource": "github-runners", + "depName": "ubuntu", + "depType": "github-runner", + "replaceString": "ubuntu-latest", + "skipReason": "invalid-version", + }, + { + "autoReplaceStringTemplate": "{{depName}}-{{newValue}}", + "currentValue": "latest", + "datasource": "github-runners", + "depName": "ubuntu", + "depType": "github-runner", + "replaceString": "ubuntu-latest", + "skipReason": "invalid-version", + }, + { + "autoReplaceStringTemplate": "{{depName}}-{{newValue}}", + "currentValue": "latest", + "datasource": "github-runners", + "depName": "ubuntu", + "depType": "github-runner", + "replaceString": "ubuntu-latest", + "skipReason": "invalid-version", + }, { "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", "currentDigest": undefined, @@ -159,6 +222,15 @@ exports[`modules/manager/github-actions/extract extractPackageFile() extracts mu "depType": "service", "replaceString": "postgres:10", }, + { + "autoReplaceStringTemplate": "{{depName}}-{{newValue}}", + "currentValue": "latest", + "datasource": "github-runners", + "depName": "ubuntu", + "depType": "github-runner", + "replaceString": "ubuntu-latest", + "skipReason": "invalid-version", + }, { "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", "currentDigest": undefined, @@ -168,5 +240,14 @@ exports[`modules/manager/github-actions/extract extractPackageFile() extracts mu "depType": "container", "replaceString": "node:16-bullseye", }, + { + "autoReplaceStringTemplate": "{{depName}}-{{newValue}}", + "currentValue": "latest", + "datasource": "github-runners", + "depName": "ubuntu", + "depType": "github-runner", + "replaceString": "ubuntu-latest", + "skipReason": "invalid-version", + }, ] `; diff --git a/lib/modules/manager/github-actions/extract.spec.ts b/lib/modules/manager/github-actions/extract.spec.ts index ec9bea9051..7c20146113 100644 --- a/lib/modules/manager/github-actions/extract.spec.ts +++ b/lib/modules/manager/github-actions/extract.spec.ts @@ -2,6 +2,35 @@ import { Fixtures } from '../../../../test/fixtures'; import { GlobalConfig } from '../../../config/global'; import { extractPackageFile } from '.'; +const runnerTestWorkflow = ` +jobs: + test1: + runs-on: ubuntu-latest + test2: + runs-on: + ubuntu-22.04 + test3: + runs-on: "macos-12-xl" + test4: + runs-on: 'macos-latest' + test5: + runs-on: | + windows-2019 + test6: + runs-on: > + windows-2022 + test7: + runs-on: [windows-2022, selfhosted] + test8: + runs-on: \${{ env.RUNNER }} + test9: + runs-on: + group: ubuntu-runners + labels: ubuntu-20.04-16core + test10: + runs-on: abc-123 +`; + describe('modules/manager/github-actions/extract', () => { beforeEach(() => { GlobalConfig.reset(); @@ -121,6 +150,7 @@ describe('modules/manager/github-actions/extract', () => { Fixtures.get('workflow_3.yml'), 'workflow_3.yml' ); + expect(res?.deps).toMatchObject([ { currentValue: 'v0.13.1', @@ -155,6 +185,20 @@ describe('modules/manager/github-actions/extract', () => { replaceString: '"actions/checkout@v1.1.2"', versioning: 'docker', }, + { + currentValue: 'latest', + datasource: 'github-runners', + depName: 'ubuntu', + depType: 'github-runner', + replaceString: 'ubuntu-latest', + }, + { + currentValue: 'latest', + datasource: 'github-runners', + depName: 'ubuntu', + depType: 'github-runner', + replaceString: 'ubuntu-latest', + }, ]); }); @@ -342,5 +386,73 @@ describe('modules/manager/github-actions/extract', () => { }, ]); }); + + it('extracts multiple action runners from yaml configuration file', () => { + const res = extractPackageFile(runnerTestWorkflow, 'workflow.yml'); + + expect(res?.deps).toMatchObject([ + { + depName: 'ubuntu', + currentValue: 'latest', + replaceString: 'ubuntu-latest', + depType: 'github-runner', + datasource: 'github-runners', + autoReplaceStringTemplate: '{{depName}}-{{newValue}}', + skipReason: 'invalid-version', + }, + { + depName: 'ubuntu', + currentValue: '22.04', + replaceString: 'ubuntu-22.04', + depType: 'github-runner', + datasource: 'github-runners', + autoReplaceStringTemplate: '{{depName}}-{{newValue}}', + }, + { + depName: 'macos', + currentValue: '12-xl', + replaceString: 'macos-12-xl', + depType: 'github-runner', + datasource: 'github-runners', + autoReplaceStringTemplate: '{{depName}}-{{newValue}}', + }, + { + depName: 'macos', + currentValue: 'latest', + replaceString: 'macos-latest', + depType: 'github-runner', + datasource: 'github-runners', + autoReplaceStringTemplate: '{{depName}}-{{newValue}}', + skipReason: 'invalid-version', + }, + { + depName: 'windows', + currentValue: '2019', + replaceString: 'windows-2019', + depType: 'github-runner', + datasource: 'github-runners', + autoReplaceStringTemplate: '{{depName}}-{{newValue}}', + }, + { + depName: 'windows', + currentValue: '2022', + replaceString: 'windows-2022', + depType: 'github-runner', + datasource: 'github-runners', + autoReplaceStringTemplate: '{{depName}}-{{newValue}}', + }, + { + depName: 'windows', + currentValue: '2022', + replaceString: 'windows-2022', + depType: 'github-runner', + datasource: 'github-runners', + autoReplaceStringTemplate: '{{depName}}-{{newValue}}', + }, + ]); + expect( + res?.deps.filter((d) => d.datasource === 'github-runners') + ).toHaveLength(7); + }); }); }); diff --git a/lib/modules/manager/github-actions/extract.ts b/lib/modules/manager/github-actions/extract.ts index 0671794f4f..4b2916c9fc 100644 --- a/lib/modules/manager/github-actions/extract.ts +++ b/lib/modules/manager/github-actions/extract.ts @@ -2,7 +2,9 @@ import is from '@sindresorhus/is'; import { load } from 'js-yaml'; import { GlobalConfig } from '../../../config/global'; import { logger } from '../../../logger'; +import { isNotNullOrUndefined } from '../../../util/array'; import { newlineRegex, regEx } from '../../../util/regex'; +import { GithubRunnersDatasource } from '../../datasource/github-runners'; import { GithubTagsDatasource } from '../../datasource/github-tags'; import * as dockerVersioning from '../../versioning/docker'; import { getDep } from '../dockerfile/extract'; @@ -112,6 +114,49 @@ function extractContainer(container: unknown): PackageDependency | undefined { return undefined; } +const runnerVersionRegex = regEx( + /^\s*(?<depName>[a-zA-Z]+)-(?<currentValue>[^\s]+)/ +); + +function extractRunner(runner: string): PackageDependency | null { + const runnerVersionGroups = runnerVersionRegex.exec(runner)?.groups; + if (!runnerVersionGroups) { + return null; + } + + const { depName, currentValue } = runnerVersionGroups; + + if (!GithubRunnersDatasource.isValidRunner(depName, currentValue)) { + return null; + } + + const dependency: PackageDependency = { + depName, + currentValue, + replaceString: `${depName}-${currentValue}`, + depType: 'github-runner', + datasource: GithubRunnersDatasource.id, + autoReplaceStringTemplate: '{{depName}}-{{newValue}}', + }; + + if (!dockerVersioning.api.isValid(currentValue)) { + dependency.skipReason = 'invalid-version'; + } + + return dependency; +} + +function extractRunners(runner: unknown): PackageDependency[] { + const runners: string[] = []; + if (is.string(runner)) { + runners.push(runner); + } else if (is.array(runner, is.string)) { + runners.push(...runner); + } + + return runners.map(extractRunner).filter(isNotNullOrUndefined); +} + function extractWithYAMLParser( content: string, packageFile: string @@ -144,6 +189,8 @@ function extractWithYAMLParser( deps.push(dep); } } + + deps.push(...extractRunners(job?.['runs-on'])); } return deps; diff --git a/lib/modules/manager/github-actions/index.ts b/lib/modules/manager/github-actions/index.ts index 254cc35903..8ed2e8898e 100644 --- a/lib/modules/manager/github-actions/index.ts +++ b/lib/modules/manager/github-actions/index.ts @@ -1,4 +1,5 @@ import type { Category } from '../../../constants'; +import { GithubRunnersDatasource } from '../../datasource/github-runners'; import { GithubTagsDatasource } from '../../datasource/github-tags'; export { extractPackageFile } from './extract'; @@ -11,4 +12,7 @@ export const defaultConfig = { export const categories: Category[] = ['ci']; -export const supportedDatasources = [GithubTagsDatasource.id]; +export const supportedDatasources = [ + GithubTagsDatasource.id, + GithubRunnersDatasource.id, +]; diff --git a/lib/modules/manager/github-actions/readme.md b/lib/modules/manager/github-actions/readme.md index 19603da5e6..891eaf827b 100644 --- a/lib/modules/manager/github-actions/readme.md +++ b/lib/modules/manager/github-actions/readme.md @@ -24,3 +24,18 @@ If you want to automatically pin action digests add the `helpers:pinGitHubAction "extends": ["helpers:pinGitHubActionDigests"] } ``` + +Renovate ignores any GitHub runners which are configured in variables. +For example, Renovate ignores the runner configured in the `RUNNER` variable: + +```yaml +name: build +on: [push] + +env: + RUNNER: ubuntu-20.04 + +jobs: + build: + runs-on: ${{ env.RUNNER }} +``` diff --git a/lib/modules/manager/github-actions/types.ts b/lib/modules/manager/github-actions/types.ts index 4787ca108a..7b81e0fd79 100644 --- a/lib/modules/manager/github-actions/types.ts +++ b/lib/modules/manager/github-actions/types.ts @@ -18,6 +18,7 @@ export interface Container { export interface Job { container?: string | Container; services?: Record<string, string | Container>; + 'runs-on'?: string | string[]; } /** -- GitLab