Files
renovate/lib/modules/manager/github-actions/extract.spec.ts
T
Jamie Tanna 57d5fb3142 feat(manager/github-actions): use github-actions versioning by default (#42713)
As a re-attempt for #42331, and to support the migration for Actions
publishing Immutable Releases.
2026-04-21 11:45:52 +00:00

1591 lines
48 KiB
TypeScript

import { codeBlock } from 'common-tags';
import { Fixtures } from '~test/fixtures.ts';
import { GlobalConfig } from '../../../config/global.ts';
import * as yaml from '../../../util/yaml.ts';
import { extractPackageFile } from './index.ts';
const runnerTestWorkflow = codeBlock`
jobs:
test1:
runs-on: ubuntu-latest
test2:
runs-on:
ubuntu-22.04
test3:
runs-on: "macos-12-large"
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();
});
describe('extractPackageFile()', () => {
it('returns null for empty', () => {
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'),
'workflow_1.yml',
);
expect(res?.deps).toMatchSnapshot();
expect(res?.deps.filter((d) => d.datasource === 'docker')).toHaveLength(
6,
);
});
it('extracts multiple action tag lines from yaml configuration file', () => {
const res = extractPackageFile(
Fixtures.get('workflow_2.yml'),
'workflow_2.yml',
);
expect(res?.deps).toMatchSnapshot();
expect(
res?.deps.filter((d) => d.datasource === 'github-tags'),
).toHaveLength(7);
expect(
res?.deps.filter((d) => d.datasource === 'github-digest'),
).toHaveLength(1);
});
it('use github.com as registry when no settings provided', () => {
const res = extractPackageFile(
Fixtures.get('workflow_2.yml'),
'workflow_2.yml',
);
expect(res?.deps[0].registryUrls).toBeUndefined();
});
it('use github.enterprise.com first and then github.com as registry running against github.enterprise.com', () => {
GlobalConfig.set({
platform: 'github',
endpoint: 'https://github.enterprise.com',
});
const res = extractPackageFile(
Fixtures.get('workflow_2.yml'),
'workflow_2.yml',
);
expect(res?.deps[0].registryUrls).toEqual([
'https://github.enterprise.com',
'https://github.com',
]);
});
it('use github.enterprise.com first and then github.com as registry running against github.enterprise.com/api/v3', () => {
GlobalConfig.set({
platform: 'github',
endpoint: 'https://github.enterprise.com/api/v3',
});
const res = extractPackageFile(
Fixtures.get('workflow_2.yml'),
'workflow_2.yml',
);
expect(res?.deps[0].registryUrls).toEqual([
'https://github.enterprise.com',
'https://github.com',
]);
});
it('use github.com only as registry when running against non-GitHub', () => {
GlobalConfig.set({
platform: 'bitbucket',
endpoint: 'https://bitbucket.enterprise.com',
});
const res = extractPackageFile(
Fixtures.get('workflow_2.yml'),
'workflow_2.yml',
);
expect(res?.deps[0].registryUrls).toBeUndefined();
});
it('use github.com only as registry when running against github.com', () => {
GlobalConfig.set({
platform: 'github',
endpoint: 'https://github.com',
});
const res = extractPackageFile(
Fixtures.get('workflow_2.yml'),
'workflow_2.yml',
);
expect(res?.deps[0].registryUrls).toBeUndefined();
});
it('use github.com only as registry when running against api.github.com', () => {
GlobalConfig.set({
platform: 'github',
endpoint: 'https://api.github.com',
});
const res = extractPackageFile(
Fixtures.get('workflow_2.yml'),
'workflow_2.yml',
);
expect(res?.deps[0].registryUrls).toBeUndefined();
});
it('extracts multiple action tag lines with double quotes and comments', () => {
const res = extractPackageFile(
Fixtures.get('workflow_3.yml'),
'workflow_3.yml',
);
expect(res?.deps).toMatchObject([
{
currentValue: 'v0.13.1',
datasource: 'github-tags',
depName: 'pascalgn/automerge-action',
depType: 'action',
replaceString: '"pascalgn/automerge-action@v0.13.1"',
versioning: 'github-actions',
},
{
currentValue: 'v2.3.5',
datasource: 'github-tags',
depName: 'actions/checkout',
depType: 'action',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # renovate: tag=v2.3.5',
versioning: 'github-actions',
},
{
currentValue: 'v1',
datasource: 'github-tags',
depName: 'actions/checkout',
depType: 'action',
replaceString: 'actions/checkout@v1',
versioning: 'github-actions',
},
{
currentValue: 'v1.1.2',
datasource: 'github-tags',
depName: 'actions/checkout',
depType: 'action',
replaceString: '"actions/checkout@v1.1.2"',
versioning: 'github-actions',
},
{
currentValue: '1.37.0-glibc',
datasource: 'docker',
depName: 'busybox',
depType: 'docker',
replaceString: 'busybox:1.37.0-glibc',
},
{
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',
},
]);
});
it('maintains quotes', () => {
const yamlContent = codeBlock`
jobs:
build:
steps:
- name: "test1"
uses: actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # tag=v3.1.1
- name: "test2"
uses: 'actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561' # tag=v3.1.1
- name: "test3"
uses: "actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561" # tag=v2.5.1
- name: "checkout repository"
uses: "actions/checkout@v2" # comment after
- name: "quoted, no comment, outdated"
uses: "actions/setup-java@v2"`;
const res = extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps).toMatchObject([
{
depName: 'actions/setup-node',
commitMessageTopic: '{{{depName}}} action',
datasource: 'github-tags',
versioning: 'github-actions',
depType: 'action',
replaceString:
'actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # tag=v3.1.1',
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
currentValue: 'v3.1.1',
currentDigest: '56337c425554a6be30cdef71bf441f15be286854',
},
{
depName: 'actions/setup-node',
commitMessageTopic: '{{{depName}}} action',
datasource: 'github-tags',
versioning: 'github-actions',
depType: 'action',
replaceString:
"'actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561' # tag=v3.1.1",
autoReplaceStringTemplate:
"'{{depName}}@{{#if newDigest}}{{newDigest}}'{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}'{{/unless}}",
currentValue: 'v3.1.1',
currentDigest: '1f8c6b94b26d0feae1e387ca63ccbdc44d27b561',
},
{
depName: 'actions/setup-node',
commitMessageTopic: '{{{depName}}} action',
datasource: 'github-tags',
versioning: 'github-actions',
depType: 'action',
replaceString:
'"actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561" # tag=v2.5.1',
autoReplaceStringTemplate:
'"{{depName}}@{{#if newDigest}}{{newDigest}}"{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}"{{/unless}}',
currentValue: 'v2.5.1',
currentDigest: '1f8c6b94b26d0feae1e387ca63ccbdc44d27b561',
},
{
depName: 'actions/checkout',
commitMessageTopic: '{{{depName}}} action',
datasource: 'github-tags',
versioning: 'github-actions',
depType: 'action',
replaceString: '"actions/checkout@v2"',
autoReplaceStringTemplate:
'"{{depName}}@{{#if newDigest}}{{newDigest}}"{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}"{{/unless}}',
currentValue: 'v2',
},
{
depName: 'actions/setup-java',
commitMessageTopic: '{{{depName}}} action',
datasource: 'github-tags',
versioning: 'github-actions',
depType: 'action',
replaceString: '"actions/setup-java@v2"',
autoReplaceStringTemplate:
'"{{depName}}@{{#if newDigest}}{{newDigest}}"{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}"{{/unless}}',
currentValue: 'v2',
},
]);
});
it('maintains spaces between hash and comment', () => {
const yamlContent = codeBlock`
jobs:
build:
steps:
# One space
- name: "test1"
uses: actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # tag=v3.1.1
- name: "test2"
uses: 'actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561' # tag=v3.1.1
- name: "test3"
uses: "actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561" # tag=v2.5.1
# Two space
- name: "test1"
uses: actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # tag=v3.1.1
- name: "test2"
uses: 'actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561' # tag=v3.1.1
- name: "test3"
uses: "actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561" # tag=v2.5.1
"`;
const res = extractPackageFile(yamlContent, 'workflow.yml');
expect(res).toMatchObject({
deps: [
{
replaceString:
'actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # tag=v3.1.1',
},
{
replaceString:
"'actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561' # tag=v3.1.1",
},
{
replaceString:
'"actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561" # tag=v2.5.1',
},
{
replaceString:
'actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # tag=v3.1.1',
},
{
replaceString:
"'actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561' # tag=v3.1.1",
},
{
replaceString:
'"actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561" # tag=v2.5.1',
},
],
});
});
it('extracts tags in different formats', () => {
const res = extractPackageFile(
Fixtures.get('workflow_4.yml'),
'workflow_4.yml',
);
expect(res?.deps).toMatchObject([
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: '1.2.3',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # 1.2.3',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: '1.2',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # 1.2',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: '1',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # 1',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: 'v1.2.3',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # v1.2.3',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: 'v1.2',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # v1.2',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: 'v1',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # v1',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: 'v2.1.0',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # @v2.1.0',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: 'v2.1.0',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # pin @v2.1.0',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: 'v2.1.0',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # tag=v2.1.0',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: 'v2.1.0',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # v2.1.0',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: 'v2.1.0',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 #v2.1.0',
},
{
currentDigest: '1e204e9a9253d643386038d443f96446fa156a97',
currentValue: 'v2.1.0',
replaceString:
'actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 #v2.1.0',
},
{
currentDigestShort: '1e204e',
currentValue: 'v2.1.0',
replaceString: 'actions/checkout@1e204e # v2.1.0',
},
{
currentDigestShort: '1e204e',
currentValue: 'some-ref-name',
replaceString: 'actions/checkout@1e204e # some-ref-name',
},
{
currentValue: '01aecc#v2.1.0',
replaceString: 'actions/checkout@01aecc#v2.1.0',
},
{
currentDigest: '689fcce700ae7ffc576f2b029b51b2ffb66d3abd',
currentValue: undefined,
replaceString:
'actions/checkout@689fcce700ae7ffc576f2b029b51b2ffb66d3abd',
},
{
currentDigest: '689fcce700ae7ffc576f2b029b51b2ffb66d3abd',
currentValue: 'v2.1.0',
replaceString:
'actions/checkout@689fcce700ae7ffc576f2b029b51b2ffb66d3abd # v2.1.0',
},
{
currentDigest: '689fcce700ae7ffc576f2b029b51b2ffb66d3abd',
currentValue: 'v2.1.0',
replaceString:
'actions/checkout@689fcce700ae7ffc576f2b029b51b2ffb66d3abd # ratchet:actions/checkout@v2.1.0',
},
{
currentDigest: '689fcce700ae7ffc576f2b029b51b2ffb66d3abd',
currentValue: undefined,
replaceString:
'actions/checkout@689fcce700ae7ffc576f2b029b51b2ffb66d3abd # ratchet:exclude',
},
{
currentDigest: 'f1d7c52253b89f0beae60141f8465d9495cdc2cf',
currentValue: 'actions-runner-controller-0.23.5',
replaceString:
'actions-runner-controller/execute-assert-arc-e2e@f1d7c52253b89f0beae60141f8465d9495cdc2cf # actions-runner-controller-0.23.5',
},
{
currentDigest: '4b1248585248751e3b12fd020cf7ac91540ca09c',
currentValue: 'prettier/v2.0.1',
replaceString:
'grafana/writers-toolkit@4b1248585248751e3b12fd020cf7ac91540ca09c # prettier/v2.0.1',
},
]);
expect(res!.deps[14]).not.toHaveProperty('skipReason');
});
it('extracts non-semver ref automatically', () => {
const res = extractPackageFile(
`
jobs:
build:
steps:
- uses: taiki-e/install-action@cargo-llvm-cov
`,
'workflow.yml',
);
expect(res?.deps[0]).toMatchObject({
depName: 'taiki-e/install-action',
currentValue: 'cargo-llvm-cov',
datasource: 'github-digest',
versioning: 'exact',
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
});
});
it('extracts pinned non-semver ref with digest', () => {
const res = extractPackageFile(
`
jobs:
build:
steps:
- uses: taiki-e/install-action@4b1248585248751e3b12fd020cf7ac91540ca09c # cargo-llvm-cov
`,
'workflow.yml',
);
expect(res?.deps[0]).toMatchObject({
depName: 'taiki-e/install-action',
currentValue: 'cargo-llvm-cov',
currentDigest: '4b1248585248751e3b12fd020cf7ac91540ca09c',
datasource: 'github-digest',
versioning: 'exact',
replaceString:
'taiki-e/install-action@4b1248585248751e3b12fd020cf7ac91540ca09c # cargo-llvm-cov',
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
});
});
it('disables naked SHA pins without version comment', () => {
const res = extractPackageFile(
codeBlock`
jobs:
build:
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
`,
'workflow.yml',
);
expect(res?.deps[0]).toMatchObject({
depName: 'actions/checkout',
currentDigest: 'c85c95e3d7251135ab7dc9ce3241c5835cc595a9',
currentValue: undefined,
enabled: false,
skipReason: 'unversioned-reference',
});
});
it('disables naked short SHA pins without version comment', () => {
const res = extractPackageFile(
codeBlock`
jobs:
build:
steps:
- uses: actions/checkout@c85c95e
`,
'workflow.yml',
);
expect(res?.deps[0]).toMatchObject({
depName: 'actions/checkout',
currentDigestShort: 'c85c95e',
currentValue: undefined,
enabled: false,
skipReason: 'unversioned-reference',
});
});
it('does not disable SHA pins with version comment', () => {
const res = extractPackageFile(
codeBlock`
jobs:
build:
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v4
`,
'workflow.yml',
);
expect(res?.deps[0]).toEqual({
depName: 'actions/checkout',
commitMessageTopic: '{{{depName}}} action',
versioning: 'github-actions',
depType: 'action',
replaceString:
'actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v4',
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
currentValue: 'v4',
currentDigest: 'c85c95e3d7251135ab7dc9ce3241c5835cc595a9',
datasource: 'github-tags',
});
});
it('does not disable short SHA pins with version comment', () => {
const res = extractPackageFile(
codeBlock`
jobs:
build:
steps:
- uses: actions/checkout@c85c95e # v4
`,
'workflow.yml',
);
expect(res?.deps[0]).toEqual({
depName: 'actions/checkout',
commitMessageTopic: '{{{depName}}} action',
versioning: 'github-actions',
depType: 'action',
replaceString: 'actions/checkout@c85c95e # v4',
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
currentValue: 'v4',
currentDigestShort: 'c85c95e',
datasource: 'github-tags',
});
});
it('extracts actions with fqdn', () => {
const res = extractPackageFile(
codeBlock`
jobs:
build:
steps:
- name: "test1"
uses: https://github.com/actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # tag=v4.2.0
- name: "test2"
uses: https://code.forgejo.org/actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # v3.1.1
- name: "test3"
uses: https://gitea.com/actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # v3.1.1
- name: "test4"
uses: https://code.domain.test/actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # v3.1.1
`,
'sample.yml',
);
expect(res).toMatchObject({
deps: [
{
depName: 'https://github.com/actions/cache',
packageName: 'actions/cache',
currentDigest: '1bd1e32a3bdc45362d1e726936510720a7c30a57',
currentValue: 'v4.2.0',
replaceString:
'https://github.com/actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # tag=v4.2.0',
datasource: 'github-tags',
registryUrls: ['https://github.com/'],
},
{
depName: 'https://code.forgejo.org/actions/setup-node',
packageName: 'actions/setup-node',
currentDigest: '56337c425554a6be30cdef71bf441f15be286854',
currentValue: 'v3.1.1',
replaceString:
'https://code.forgejo.org/actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # v3.1.1',
datasource: 'forgejo-tags',
registryUrls: ['https://code.forgejo.org/'],
},
{
depName: 'https://gitea.com/actions/setup-node',
packageName: 'actions/setup-node',
currentDigest: '56337c425554a6be30cdef71bf441f15be286854',
currentValue: 'v3.1.1',
replaceString:
'https://gitea.com/actions/setup-node@56337c425554a6be30cdef71bf441f15be286854 # v3.1.1',
datasource: 'gitea-tags',
registryUrls: ['https://gitea.com/'],
},
{
skipReason: 'unsupported-url',
},
],
});
expect(res!.deps[3]).not.toHaveProperty('registryUrls');
});
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-large',
replaceString: 'macos-12-large',
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);
});
it('extracts x-version from actions/setup-x', () => {
const yamlContent = codeBlock`
jobs:
build:
steps:
- name: "Setup Node.js"
uses: actions/setup-node@v3
with:
node-version: '16.x'
- name: "Setup Node.js with exact version"
uses: actions/setup-node@v3
with:
node-version: '20.0.0'
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: "Setup Python with range"
uses: actions/setup-python@v3
with:
python-version: '>=3.8.0 <3.10.0'
- name: "Setup Node.js with latest"
uses: actions/setup-node@v3
with:
node-version: 'latest'
`;
const res = extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps).toMatchObject([
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v3',
datasource: 'github-tags',
depName: 'actions/setup-node',
depType: 'action',
replaceString: 'actions/setup-node@v3',
versioning: 'github-actions',
},
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v3',
datasource: 'github-tags',
depName: 'actions/setup-node',
depType: 'action',
replaceString: 'actions/setup-node@v3',
versioning: 'github-actions',
},
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v5',
datasource: 'github-tags',
depName: 'actions/setup-go',
depType: 'action',
replaceString: 'actions/setup-go@v5',
versioning: 'github-actions',
},
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v3',
datasource: 'github-tags',
depName: 'actions/setup-python',
depType: 'action',
replaceString: 'actions/setup-python@v3',
versioning: 'github-actions',
},
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v3',
datasource: 'github-tags',
depName: 'actions/setup-node',
depType: 'action',
replaceString: 'actions/setup-node@v3',
versioning: 'github-actions',
},
{
depName: 'node',
packageName: 'actions/node-versions',
currentValue: '16.x',
datasource: 'github-releases',
versioning: 'node',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
{
depName: 'node',
packageName: 'actions/node-versions',
currentValue: '20.0.0',
datasource: 'github-releases',
versioning: 'node',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
{
depName: 'go',
packageName: 'actions/go-versions',
currentValue: '1.23',
datasource: 'github-releases',
versioning: 'npm',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
{
depName: 'python',
packageName: 'actions/python-versions',
currentValue: '>=3.8.0 <3.10.0',
datasource: 'github-releases',
versioning: 'npm',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
{
depName: 'node',
packageName: 'actions/node-versions',
currentValue: 'latest',
datasource: 'github-releases',
versioning: 'node',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
]);
});
it('handles actions/setup-x without x-version field', () => {
const yamlContent = codeBlock`
jobs:
build:
steps:
- name: "Setup Node.js without version"
uses: actions/setup-node@v3
with:
registry-url: 'https://npm.pkg.github.com'
`;
const res = extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps).toHaveLength(1);
expect(res?.deps[0]).toMatchObject({
depName: 'actions/setup-node',
depType: 'action',
});
});
it('extracts x-version from actions/setup-x in composite action', () => {
const yamlContent = codeBlock`
runs:
using: 'composite'
steps:
- name: "Setup Node.js"
uses: actions/setup-node@v3
with:
node-version: '16.x'
- name: "Setup Node.js with exact version"
uses: actions/setup-node@v3
with:
node-version: '20.0.0'
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: "Setup Python with range"
uses: actions/setup-python@v3
with:
python-version: '>=3.8.0 <3.10.0'
- name: "Setup Node.js with latest"
uses: actions/setup-node@v3
with:
node-version: 'latest'
`;
const res = extractPackageFile(yamlContent, 'action.yml');
expect(res?.deps).toMatchObject([
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v3',
datasource: 'github-tags',
depName: 'actions/setup-node',
depType: 'action',
replaceString: 'actions/setup-node@v3',
versioning: 'github-actions',
},
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v3',
datasource: 'github-tags',
depName: 'actions/setup-node',
depType: 'action',
replaceString: 'actions/setup-node@v3',
versioning: 'github-actions',
},
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v5',
datasource: 'github-tags',
depName: 'actions/setup-go',
depType: 'action',
replaceString: 'actions/setup-go@v5',
versioning: 'github-actions',
},
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v3',
datasource: 'github-tags',
depName: 'actions/setup-python',
depType: 'action',
replaceString: 'actions/setup-python@v3',
versioning: 'github-actions',
},
{
autoReplaceStringTemplate:
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
commitMessageTopic: '{{{depName}}} action',
currentValue: 'v3',
datasource: 'github-tags',
depName: 'actions/setup-node',
depType: 'action',
replaceString: 'actions/setup-node@v3',
versioning: 'github-actions',
},
{
depName: 'node',
packageName: 'actions/node-versions',
currentValue: '16.x',
datasource: 'github-releases',
versioning: 'node',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
{
depName: 'node',
packageName: 'actions/node-versions',
currentValue: '20.0.0',
datasource: 'github-releases',
versioning: 'node',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
{
depName: 'go',
packageName: 'actions/go-versions',
currentValue: '1.23',
datasource: 'github-releases',
versioning: 'npm',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
{
depName: 'python',
packageName: 'actions/python-versions',
currentValue: '>=3.8.0 <3.10.0',
datasource: 'github-releases',
versioning: 'npm',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
{
depName: 'node',
packageName: 'actions/node-versions',
currentValue: 'latest',
datasource: 'github-releases',
versioning: 'node',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
]);
});
it('logs unknown schema', () => {
const yamlContent = codeBlock`
runs:
using: 'node20'
main: 'index.js'
`;
expect(extractPackageFile(yamlContent, 'action.yml')).toBeNull();
});
});
it.each([
{
step: {
uses: 'aquasecurity/setup-trivy@v0.2.6',
with: {},
},
expected: [
{
datasource: 'github-releases',
depName: 'aquasecurity/trivy',
depType: 'uses-with',
packageName: 'aquasecurity/trivy',
skipStage: 'extract',
skipReason: 'unspecified-version',
},
],
},
{
step: {
uses: 'aquasecurity/setup-trivy@v0.2.6',
with: {
version: 'latest',
},
},
expected: [
{
currentValue: 'latest',
datasource: 'github-releases',
depName: 'aquasecurity/trivy',
depType: 'uses-with',
packageName: 'aquasecurity/trivy',
},
],
},
{
step: {
uses: 'aquasecurity/setup-trivy@v0.2.6',
with: {
version: 'v0.70.0',
},
},
expected: [
{
currentValue: 'v0.70.0',
datasource: 'github-releases',
depName: 'aquasecurity/trivy',
depType: 'uses-with',
packageName: 'aquasecurity/trivy',
},
],
},
{
step: {
uses: 'aquasecurity/trivy-action@v0.35.0',
with: {
version: 'latest',
},
},
expected: [
{
currentValue: 'latest',
datasource: 'github-releases',
depName: 'aquasecurity/trivy',
depType: 'uses-with',
packageName: 'aquasecurity/trivy',
},
],
},
{
step: {
uses: 'aquasecurity/trivy-action@v0.35.0',
with: {
version: 'v0.70.0',
},
},
expected: [
{
currentValue: 'v0.70.0',
datasource: 'github-releases',
depName: 'aquasecurity/trivy',
depType: 'uses-with',
packageName: 'aquasecurity/trivy',
},
],
},
{
step: {
uses: 'astral-sh/setup-uv@v5',
with: {
version: 'latest',
},
},
expected: [
{
currentValue: 'latest',
datasource: 'github-releases',
depName: 'astral-sh/uv',
depType: 'uses-with',
packageName: 'astral-sh/uv',
},
],
},
{
step: {
name: 'Pinning a minor version of uv',
uses: 'astral-sh/setup-uv@v5',
with: {
version: '0.4.x',
},
},
expected: [
{
currentValue: '0.4.x',
datasource: 'github-releases',
depName: 'astral-sh/uv',
depType: 'uses-with',
packageName: 'astral-sh/uv',
},
],
},
{
step: {
name: 'Pinning a minor version of uv',
uses: 'https://github.com/astral-sh/setup-uv@v5',
with: {
version: '0.4.x',
},
},
expected: [
{
currentValue: '0.4.x',
datasource: 'github-releases',
depName: 'astral-sh/uv',
depType: 'uses-with',
packageName: 'astral-sh/uv',
versioning: 'npm',
},
],
},
{
step: {
uses: 'https://github.com/astral-sh/setup-uv@v5',
with: {},
},
expected: [
{
datasource: 'github-releases',
depName: 'astral-sh/uv',
depType: 'uses-with',
skipStage: 'extract',
skipReason: 'unspecified-version',
packageName: 'astral-sh/uv',
versioning: 'npm',
},
],
},
{
step: {
uses: 'pnpm/action-setup@v4',
with: {
version: 'latest',
},
},
expected: [
{
currentValue: 'latest',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
],
},
{
step: {
uses: 'pnpm/action-setup@v4',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
],
},
{
step: {
name: 'Install gotestsum',
uses: 'jaxxstorm/action-install-gh-release@v1.10.0',
with: {
repo: 'gotestyourself/gotestsum',
tag: 'v1.12.1',
platform: 'linux',
arch: 'amd64',
},
},
expected: [
{
currentValue: 'v1.12.1',
datasource: 'github-releases',
depName: 'gotestyourself/gotestsum',
depType: 'uses-with',
packageName: 'gotestyourself/gotestsum',
},
],
},
{
step: {
uses: 'pnpm/action-setup@v4',
with: {
version: 10,
},
},
expected: [
{
currentValue: '10',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
],
},
{
step: {
uses: 'pnpm/action-setup@v4',
with: {
version: '10.x',
},
},
expected: [
{
currentValue: '10.x',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
],
},
{
step: {
uses: 'pdm-project/setup-pdm@v4.2',
with: {
version: '1.2.3',
},
},
expected: [
{
currentValue: '1.2.3',
datasource: 'pypi',
depName: 'pdm',
depType: 'uses-with',
packageName: 'pdm',
},
],
},
{
step: {
uses: 'pdm-project/setup-pdm@v4.2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'pypi',
depName: 'pdm',
depType: 'uses-with',
packageName: 'pdm',
},
],
},
{
step: {
uses: 'prefix-dev/setup-pixi@v0.8.3',
with: {
'pixi-version': 'v0.41.4',
},
},
expected: [
{
currentValue: 'v0.41.4',
datasource: 'github-releases',
depName: 'prefix-dev/pixi',
depType: 'uses-with',
packageName: 'prefix-dev/pixi',
versioning: 'conda',
},
],
},
{
step: {
uses: 'oven-sh/setup-bun@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'bun',
depType: 'uses-with',
packageName: 'bun',
},
],
},
{
step: {
uses: 'oven-sh/setup-bun@v2',
with: { 'bun-version': '1.2.0' },
},
expected: [
{
currentValue: '1.2.0',
datasource: 'npm',
depName: 'bun',
depType: 'uses-with',
packageName: 'bun',
},
],
},
{
step: {
uses: 'denoland/setup-deno@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'deno',
depType: 'uses-with',
packageName: 'deno',
},
],
},
{
step: {
uses: 'denoland/setup-deno@v2',
with: { 'deno-version': '2.4.0' },
},
expected: [
{
currentValue: '2.4.0',
datasource: 'npm',
depName: 'deno',
depType: 'uses-with',
packageName: 'deno',
},
],
},
{
step: {
uses: 'ruby/setup-ruby@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'ruby-version',
depName: 'ruby',
depType: 'uses-with',
packageName: 'ruby',
},
],
},
{
step: {
uses: 'ruby/setup-ruby@v1',
with: { 'ruby-version': '3.4' },
},
expected: [
{
currentValue: '3.4',
datasource: 'ruby-version',
depName: 'ruby',
depType: 'uses-with',
packageName: 'ruby',
},
],
},
{
step: {
uses: 'pypa/hatch@install',
with: { version: '1.14.2' },
},
expected: [
{
currentValue: '1.14.2',
datasource: 'github-releases',
depName: 'pypa/hatch',
depType: 'uses-with',
packageName: 'pypa/hatch',
},
],
},
{
step: {
uses: 'pypa/hatch@install',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'pypa/hatch',
depType: 'uses-with',
packageName: 'pypa/hatch',
},
],
},
{
step: {
uses: 'jakebailey/pyright-action@v2',
with: { version: '1.1.100' },
},
expected: [
{
currentValue: '1.1.100',
datasource: 'npm',
depName: 'pyright',
depType: 'uses-with',
packageName: 'pyright',
},
],
},
{
step: {
uses: 'jakebailey/pyright-action@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'pyright',
depType: 'uses-with',
packageName: 'pyright',
},
],
},
{
step: {
uses: 'jakebailey/pyright-action@v2',
with: { version: 'PATH' },
},
expected: [
{
currentValue: 'PATH',
skipStage: 'extract',
skipReason: 'invalid-version',
datasource: 'npm',
depName: 'pyright',
depType: 'uses-with',
packageName: 'pyright',
},
],
},
{
step: {
uses: 'golangci/golangci-lint-action@v9',
with: { version: 'v2.5.0' },
},
expected: [
{
currentValue: 'v2.5.0',
datasource: 'github-releases',
depName: 'golangci/golangci-lint',
depType: 'uses-with',
packageName: 'golangci/golangci-lint',
},
],
},
{
step: {
uses: 'golangci/golangci-lint-action@v9',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'golangci/golangci-lint',
depType: 'uses-with',
packageName: 'golangci/golangci-lint',
},
],
},
{
step: {
uses: 'zizmorcore/zizmor-action@v0.5.2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'docker',
depName: 'ghcr.io/zizmorcore/zizmor',
depType: 'uses-with',
packageName: 'ghcr.io/zizmorcore/zizmor',
},
],
},
{
step: {
uses: 'zizmorcore/zizmor-action@v0.5.2',
with: {
version: 'v1.23.1',
},
},
expected: [
{
currentValue: 'v1.23.1',
datasource: 'docker',
depName: 'ghcr.io/zizmorcore/zizmor',
depType: 'uses-with',
packageName: 'ghcr.io/zizmorcore/zizmor',
},
],
},
{
step: {
uses: 'zizmorcore/zizmor-action@v0.5.2',
with: {
version: '1.23.1',
},
},
expected: [
{
currentValue: '1.23.1',
datasource: 'docker',
depName: 'ghcr.io/zizmorcore/zizmor',
depType: 'uses-with',
packageName: 'ghcr.io/zizmorcore/zizmor',
},
],
},
])('extract from $step.uses', ({ step, expected }) => {
const yamlContent = yaml.dump({ jobs: { build: { steps: [step] } } });
const res = extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps.filter((pkg) => pkg.depType !== 'action')).toMatchObject(
expected,
);
});
});