From 8dd40fe523f60a31511e7dd5a32a153bc49bdb6a Mon Sep 17 00:00:00 2001 From: Maxime Brunet <max@brnt.mx> Date: Thu, 4 Aug 2022 21:57:33 -0700 Subject: [PATCH] feat(github-actions): update job and service containers (#16770) --- .../__fixtures__/workflow_1.yml | 11 +++ .../__snapshots__/extract.spec.ts.snap | 38 ++++++++++- .../manager/github-actions/extract.spec.ts | 29 ++++++-- lib/modules/manager/github-actions/extract.ts | 68 +++++++++++++++++-- lib/modules/manager/github-actions/types.ts | 31 +++++++++ 5 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 lib/modules/manager/github-actions/types.ts diff --git a/lib/modules/manager/github-actions/__fixtures__/workflow_1.yml b/lib/modules/manager/github-actions/__fixtures__/workflow_1.yml index 1513408eee..42fb8da096 100644 --- a/lib/modules/manager/github-actions/__fixtures__/workflow_1.yml +++ b/lib/modules/manager/github-actions/__fixtures__/workflow_1.yml @@ -30,3 +30,14 @@ jobs: steps: - name: Node 6 test uses: docker://node:6@sha256:7b65413af120ec5328077775022c78101f103258a1876ec2f83890bce416e896 + container-job: + runs-on: ubuntu-latest + container: node:16-bullseye + services: + redis: + image: redis:5 + postgres: postgres:10 + container-job-with-image-keyword: + runs-on: ubuntu-latest + container: + image: node:16-bullseye 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 721889f778..8bf855a07b 100644 --- a/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap +++ b/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap @@ -157,7 +157,6 @@ Array [ "depName": "replicated/dockerfilelint", "depType": "docker", "replaceString": "replicated/dockerfilelint", - "versioning": "docker", }, Object { "autoReplaceStringTemplate": "{{depName}}/cli@{{#if newDigest}}{{newDigest}}{{#if newValue}} # tag={{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}", @@ -178,7 +177,42 @@ Array [ "depName": "node", "depType": "docker", "replaceString": "node:6@sha256:7b65413af120ec5328077775022c78101f103258a1876ec2f83890bce416e896", - "versioning": "docker", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": "16-bullseye", + "datasource": "docker", + "depName": "node", + "depType": "container", + "replaceString": "node:16-bullseye", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": "5", + "datasource": "docker", + "depName": "redis", + "depType": "service", + "replaceString": "redis:5", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": "10", + "datasource": "docker", + "depName": "postgres", + "depType": "service", + "replaceString": "postgres:10", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": "16-bullseye", + "datasource": "docker", + "depName": "node", + "depType": "container", + "replaceString": "node:16-bullseye", }, ] `; diff --git a/lib/modules/manager/github-actions/extract.spec.ts b/lib/modules/manager/github-actions/extract.spec.ts index 55ce36032c..6b541887da 100644 --- a/lib/modules/manager/github-actions/extract.spec.ts +++ b/lib/modules/manager/github-actions/extract.spec.ts @@ -4,19 +4,33 @@ import { extractPackageFile } from '.'; describe('modules/manager/github-actions/extract', () => { describe('extractPackageFile()', () => { it('returns null for empty', () => { - expect(extractPackageFile('nothing here')).toBeNull(); + expect( + extractPackageFile('nothing here', 'empty-workflow.yml') + ).toBeNull(); + }); + + it('returns null for invalid yaml', () => { + expect( + extractPackageFile('nothing here: [', 'invalid-workflow.yml') + ).toBeNull(); }); it('extracts multiple docker image lines from yaml configuration file', () => { - const res = extractPackageFile(Fixtures.get('workflow_1.yml')); + const res = extractPackageFile( + Fixtures.get('workflow_1.yml'), + 'workflow_1.yml' + ); expect(res?.deps).toMatchSnapshot(); expect(res?.deps.filter((d) => d.datasource === 'docker')).toHaveLength( - 2 + 6 ); }); it('extracts multiple action tag lines from yaml configuration file', () => { - const res = extractPackageFile(Fixtures.get('workflow_2.yml')); + const res = extractPackageFile( + Fixtures.get('workflow_2.yml'), + 'workflow_2.yml' + ); expect(res?.deps).toMatchSnapshot(); expect( res?.deps.filter((d) => d.datasource === 'github-tags') @@ -24,7 +38,10 @@ describe('modules/manager/github-actions/extract', () => { }); it('extracts multiple action tag lines with double quotes and comments', () => { - const res = extractPackageFile(Fixtures.get('workflow_3.yml')); + const res = extractPackageFile( + Fixtures.get('workflow_3.yml'), + 'workflow_3.yml' + ); expect(res?.deps).toMatchSnapshot([ { currentValue: 'v0.13.1', @@ -78,7 +95,7 @@ describe('modules/manager/github-actions/extract', () => { - name: "quoted, no comment, outdated" uses: "actions/setup-java@v2"`; - const res = extractPackageFile(yamlContent); + const res = extractPackageFile(yamlContent, 'workflow.yml'); expect(res?.deps).toMatchObject([ { depName: 'actions/setup-node', diff --git a/lib/modules/manager/github-actions/extract.ts b/lib/modules/manager/github-actions/extract.ts index c5162e583a..874e8b4aed 100644 --- a/lib/modules/manager/github-actions/extract.ts +++ b/lib/modules/manager/github-actions/extract.ts @@ -1,11 +1,13 @@ +import { load } from 'js-yaml'; import { logger } from '../../../logger'; import { newlineRegex, regEx } from '../../../util/regex'; import { GithubTagsDatasource } from '../../datasource/github-tags'; import * as dockerVersioning from '../../versioning/docker'; import { getDep } from '../dockerfile/extract'; import type { PackageDependency, PackageFile } from '../types'; +import type { Container, Workflow } from './types'; -const dockerRe = regEx(/^\s+uses: docker:\/\/([^"]+)\s*$/); +const dockerActionRe = regEx(/^\s+uses: ['"]?docker:\/\/([^'"]+)\s*$/); const actionRe = regEx( /^\s+-?\s+?uses: (?<replaceString>['"]?(?<depName>[\w-]+\/[\w-]+)(?<path>\/.*)?@(?<currentValue>[^\s'"]+)['"]?(?:\s+#\s+(?:renovate:\s+)?tag=(?<tag>\S+))?)/ ); @@ -13,20 +15,19 @@ const actionRe = regEx( // SHA1 or SHA256, see https://github.blog/2020-10-19-git-2-29-released/ const shaRe = regEx(/^[a-z0-9]{40}|[a-z0-9]{64}$/); -export function extractPackageFile(content: string): PackageFile | null { - logger.trace('github-actions.extractPackageFile()'); +function extractWithRegex(content: string): PackageDependency[] { + logger.trace('github-actions.extractWithRegex()'); const deps: PackageDependency[] = []; for (const line of content.split(newlineRegex)) { if (line.trim().startsWith('#')) { continue; } - const dockerMatch = dockerRe.exec(line); + const dockerMatch = dockerActionRe.exec(line); if (dockerMatch) { const [, currentFrom] = dockerMatch; const dep = getDep(currentFrom); dep.depType = 'docker'; - dep.versioning = dockerVersioning.id; deps.push(dep); continue; } @@ -68,6 +69,63 @@ export function extractPackageFile(content: string): PackageFile | null { deps.push(dep); } } + return deps; +} + +function extractContainer(container: string | Container): PackageDependency { + let dep: PackageDependency; + if (typeof container === 'string') { + dep = getDep(container); + } else { + dep = getDep(container?.image); + } + return dep; +} + +function extractWithYAMLParser( + content: string, + filename: string +): PackageDependency[] { + logger.trace('github-actions.extractWithYAMLParser()'); + const deps: PackageDependency[] = []; + + let pkg: Workflow; + try { + pkg = load(content, { json: true }) as Workflow; + } catch (err) { + logger.debug( + { filename, err }, + 'Failed to parse GitHub Actions Workflow YAML' + ); + return []; + } + + for (const job of Object.values(pkg.jobs ?? {})) { + if (job.container !== undefined) { + const dep = extractContainer(job.container); + dep.depType = 'container'; + deps.push(dep); + } + + for (const service of Object.values(job.services ?? {})) { + const dep = extractContainer(service); + dep.depType = 'service'; + deps.push(dep); + } + } + + return deps; +} + +export function extractPackageFile( + content: string, + filename: string +): PackageFile | null { + logger.trace('github-actions.extractPackageFile()'); + const deps = [ + ...extractWithRegex(content), + ...extractWithYAMLParser(content, filename), + ]; if (!deps.length) { return null; } diff --git a/lib/modules/manager/github-actions/types.ts b/lib/modules/manager/github-actions/types.ts new file mode 100644 index 0000000000..4787ca108a --- /dev/null +++ b/lib/modules/manager/github-actions/types.ts @@ -0,0 +1,31 @@ +/** + * Container describes a Docker container used within a {@link Job}. + * {@link https://docs.github.com/en/actions/using-containerized-services} + * + * @param image - The Docker image to use as the container to run the action. The value can be the Docker Hub image name or a registry name. + */ +export interface Container { + image: string; +} + +/** + * Job describes a single job within a {@link Workflow}. + * {@link https://docs.github.com/en/actions/using-jobs} + * + * @param container - {@link https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer} + * @param services - {@link https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idservices} + */ +export interface Job { + container?: string | Container; + services?: Record<string, string | Container>; +} + +/** + * Workflow describes a GitHub Actions Workflow. + * {@link https://docs.github.com/en/actions/using-workflows} + * + * @param jobs - {@link https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobs} + */ +export interface Workflow { + jobs: Record<string, Job>; +} -- GitLab