Files
Martin Costello ea1c9a7bf3 feat(datasource/github-runners): add xcode-27 (#47116)
* feat(datasource/github-runners): add xcode-27

Add `xcode-27` and `xcode-27-xlarge` runner labels to the datasource.

* feat(datasource/github-runners): add release date

Add release date for the XCode 27 runners.
2026-10-19 16:59:55 +00:00

5688 lines
154 KiB
TypeScript

import { codeBlock } from 'common-tags';
import { Fixtures } from '~test/fixtures.ts';
import { fs } from '~test/util.ts';
import { GlobalConfig } from '../../../config/global.ts';
import * as memCache from '../../../util/cache/memory/index.ts';
import * as yaml from '../../../util/yaml.ts';
import { extractPackageFile } from './index.ts';
vi.mock('../../../util/fs/index.ts');
describe('modules/manager/github-actions/extract', () => {
beforeEach(() => {
GlobalConfig.reset();
});
describe('extractPackageFile()', () => {
it('returns null for empty', async () => {
await expect(
extractPackageFile('nothing here', 'empty-workflow.yml'),
).resolves.toBeNull();
});
it('returns null for invalid yaml', async () => {
await expect(
extractPackageFile('nothing here: [', 'invalid-workflow.yml'),
).resolves.toBeNull();
});
it('extracts multiple docker image lines from yaml configuration file', async () => {
const res = await extractPackageFile(
Fixtures.get('workflow_1.yml'),
'workflow_1.yml',
);
expect(res?.deps).toMatchObject([
{
depName: 'actions/bin',
currentValue: 'master',
datasource: 'github-digest',
depType: 'action',
},
{
depName: 'replicated/dockerfilelint',
datasource: 'docker',
depType: 'docker',
},
{
depName: 'actions/docker',
currentValue: 'master',
datasource: 'github-digest',
depType: 'action',
},
{
depName: 'node',
currentValue: '6',
currentDigest:
'sha256:7b65413af120ec5328077775022c78101f103258a1876ec2f83890bce416e896',
datasource: 'docker',
depType: 'docker',
},
{
depName: 'ubuntu',
currentValue: 'latest',
datasource: 'github-runners',
depType: 'github-runner',
skipReason: 'invalid-version',
},
{
depName: 'ubuntu',
currentValue: 'latest',
datasource: 'github-runners',
depType: 'github-runner',
skipReason: 'invalid-version',
},
{
depName: 'ubuntu',
currentValue: 'latest',
datasource: 'github-runners',
depType: 'github-runner',
skipReason: 'invalid-version',
},
{
depName: 'ubuntu',
currentValue: 'latest',
datasource: 'github-runners',
depType: 'github-runner',
skipReason: 'invalid-version',
},
{
depName: 'node',
currentValue: '16-bullseye',
datasource: 'docker',
depType: 'container',
},
{
depName: 'redis',
currentValue: '5',
datasource: 'docker',
depType: 'service',
},
{
depName: 'postgres',
currentValue: '10',
datasource: 'docker',
depType: 'service',
},
{
depName: 'ubuntu',
currentValue: 'latest',
datasource: 'github-runners',
depType: 'github-runner',
skipReason: 'invalid-version',
},
{
depName: 'node',
currentValue: '16-bullseye',
datasource: 'docker',
depType: 'container',
},
{
depName: 'ubuntu',
currentValue: 'latest',
datasource: 'github-runners',
depType: 'github-runner',
skipReason: 'invalid-version',
},
]);
});
it('extracts multiple action tag lines from yaml configuration file', async () => {
const res = await extractPackageFile(
Fixtures.get('workflow_2.yml'),
'workflow_2.yml',
);
expect(res?.deps).toMatchObject([
{
depName: 'actions/bin',
currentValue: 'master',
datasource: 'github-digest',
depType: 'action',
},
{
depName: 'docker/setup-qemu-action',
currentValue: 'v1.1.0',
currentDigest: 'c308fdd69d26ed66f4506ebd74b180abe5362145',
datasource: 'github-tags',
depType: 'action',
replaceString:
'docker/setup-qemu-action@c308fdd69d26ed66f4506ebd74b180abe5362145 # renovate: tag=v1.1.0',
},
{
depName: 'actions/checkout',
currentValue: '1.0.0',
datasource: 'github-tags',
depType: 'action',
},
{
depName: 'docker/setup-qemu-action',
currentDigest: 'c308fdd69d26ed66f4506ebd74b180abe5362145',
datasource: 'github-tags',
depType: 'action',
enabled: false,
skipReason: 'unversioned-reference',
replaceString:
'docker/setup-qemu-action@c308fdd69d26ed66f4506ebd74b180abe5362145',
},
{
depName: 'docker/build-push-action',
currentValue: 'v2',
datasource: 'github-tags',
depType: 'action',
},
{
depName: 'actions/checkout',
currentValue: 'v2.4.0',
currentDigest: 'ec3a7ce113134d7a93b817d10a8272cb61118579',
datasource: 'github-tags',
depType: 'action',
replaceString:
'actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # tag=v2.4.0',
},
{
depName: 'actions-rs/toolchain',
currentValue: 'v1.0.7',
currentDigest: '16499b5e05bf2e26879000db0c1d13f7e13fa3af',
datasource: 'github-tags',
depType: 'action',
replaceString:
'actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # renovate: tag=v1.0.7',
},
{
depName: 'actions-rs/cargo',
currentValue: 'v1.0.3',
datasource: 'github-tags',
depType: 'action',
},
{
depName: 'ubuntu',
currentValue: 'latest',
datasource: 'github-runners',
depType: 'github-runner',
skipReason: 'invalid-version',
},
{
depName: 'ubuntu',
currentValue: 'latest',
datasource: 'github-runners',
depType: 'github-runner',
skipReason: 'invalid-version',
},
{
depName: 'ubuntu',
currentValue: 'latest',
datasource: 'github-runners',
depType: 'github-runner',
skipReason: 'invalid-version',
},
]);
});
it('use github.com as registry when no settings provided', async () => {
const res = await 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', async () => {
GlobalConfig.set({
platform: 'github',
endpoint: 'https://github.enterprise.com',
});
const res = await 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', async () => {
GlobalConfig.set({
platform: 'github',
endpoint: 'https://github.enterprise.com/api/v3',
});
const res = await 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', async () => {
GlobalConfig.set({
platform: 'bitbucket',
endpoint: 'https://bitbucket.enterprise.com',
});
const res = await 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', async () => {
GlobalConfig.set({
platform: 'github',
endpoint: 'https://github.com',
});
const res = await 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', async () => {
GlobalConfig.set({
platform: 'github',
endpoint: 'https://api.github.com',
});
const res = await extractPackageFile(
Fixtures.get('workflow_2.yml'),
'workflow_2.yml',
);
expect(res?.deps[0].registryUrls).toBeUndefined();
});
it('returns undefined registryUrls when endpoint is invalid URL', async () => {
GlobalConfig.set({
platform: 'github',
endpoint: 'not-a-valid-url',
});
const res = await 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', async () => {
const res = await 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', async () => {
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 = await 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', async () => {
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 = await 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', async () => {
const res = await 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', async () => {
const res = await 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', async () => {
const res = await 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('extracts ratchet pinned action in subdirectory', async () => {
const res = await extractPackageFile(
`
jobs:
build:
steps:
- uses: actions/cache/restore@b7e8d49f17405cc70c1c120101943203c98d3a4b # ratchet:actions/cache/restore@v4
`,
'workflow.yml',
);
expect(res?.deps[0]).toMatchObject({
depName: 'actions/cache',
currentValue: 'v4',
currentDigest: 'b7e8d49f17405cc70c1c120101943203c98d3a4b',
datasource: 'github-tags',
versioning: 'github-actions',
replaceString:
'actions/cache/restore@b7e8d49f17405cc70c1c120101943203c98d3a4b # ratchet:actions/cache/restore@v4',
});
});
it('extracts a reusable workflow call as a workflow', async () => {
const res = await extractPackageFile(
codeBlock`
jobs:
release:
uses: some-org/some-repo/.github/workflows/release.yml@v1.2.3
`,
'.github/workflows/ci.yml',
);
expect(res?.deps).toMatchObject([
{
depName: 'some-org/some-repo',
currentValue: 'v1.2.3',
datasource: 'github-tags',
versioning: 'github-actions',
depType: 'workflow',
replaceString:
'some-org/some-repo/.github/workflows/release.yml@v1.2.3',
autoReplaceStringTemplate:
'{{depName}}/.github/workflows/release.yml@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
},
]);
});
it.each`
uses | depType
${'some-org/some-repo/.github/workflows/release.yml@v1'} | ${'workflow'}
${'some-org/some-repo/.github/workflows/release.yaml@v1'} | ${'workflow'}
${'actions/checkout@v4'} | ${'action'}
${'github/codeql-action/init@v3'} | ${'action'}
${'some-org/some-repo/.github/workflows/sub/release.yml@v1'} | ${'action'}
${'some-org/some-repo/.github/workflows@v1'} | ${'action'}
${'some-org/some-repo/.github/workflows/release.json@v1'} | ${'action'}
${'some-org/some-repo/nested/.github/workflows/release.yml@v1'} | ${'action'}
`(
'gives $uses the depType $depType',
async ({ uses, depType }: { uses: string; depType: string }) => {
const res = await extractPackageFile(
codeBlock`
jobs:
build:
steps:
- uses: ${uses}
`,
'.github/workflows/ci.yml',
);
expect(res?.deps[0]).toMatchObject({ depType });
},
);
it('disables naked SHA pins without version comment', async () => {
const res = await 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', async () => {
const res = await 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', async () => {
const res = await 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', async () => {
const res = await 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', async () => {
const res = await 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 macos action runners from yaml configuration file', async () => {
const runnerTestWorkflowMacos = codeBlock`
jobs:
test1:
runs-on: \${{ env.RUNNER }}
test2:
runs-on: abc-123
test3:
runs-on: "macos-12-large"
test4:
runs-on: 'macos-latest'
test5:
runs-on: macos-15-intel
test6:
runs-on: macos-26-intel
`;
const res = await extractPackageFile(
runnerTestWorkflowMacos,
'workflow.yml',
);
expect(res?.deps).toMatchObject([
{
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: 'macos',
currentValue: '15-intel',
replaceString: 'macos-15-intel',
depType: 'github-runner',
datasource: 'github-runners',
autoReplaceStringTemplate: '{{depName}}-{{newValue}}',
},
{
depName: 'macos',
currentValue: '26-intel',
replaceString: 'macos-26-intel',
depType: 'github-runner',
datasource: 'github-runners',
autoReplaceStringTemplate: '{{depName}}-{{newValue}}',
},
]);
expect(
res?.deps.filter((d) => d.datasource === 'github-runners'),
).toHaveLength(4);
});
it('extracts multiple xcode action runners from yaml configuration file', async () => {
const runnerTestWorkflowXcode = codeBlock`
jobs:
test1:
runs-on: xcode-27
test2:
runs-on: xcode-27-xlarge
test3:
runs-on: xcode-latest
`;
const res = await extractPackageFile(
runnerTestWorkflowXcode,
'workflow.yml',
);
expect(res?.deps).toMatchObject([
{
depName: 'xcode',
currentValue: '27',
replaceString: 'xcode-27',
depType: 'github-runner',
datasource: 'github-runners',
autoReplaceStringTemplate: '{{depName}}-{{newValue}}',
},
{
depName: 'xcode',
currentValue: '27-xlarge',
replaceString: 'xcode-27-xlarge',
depType: 'github-runner',
datasource: 'github-runners',
autoReplaceStringTemplate: '{{depName}}-{{newValue}}',
},
{
depName: 'xcode',
currentValue: 'latest',
replaceString: 'xcode-latest',
depType: 'github-runner',
datasource: 'github-runners',
autoReplaceStringTemplate: '{{depName}}-{{newValue}}',
skipReason: 'invalid-version',
},
]);
expect(
res?.deps.filter((d) => d.datasource === 'github-runners'),
).toHaveLength(3);
});
it('extracts multiple ubuntu action runners from yaml configuration file', async () => {
const runnerTestWorkflowUbuntu = codeBlock`
jobs:
test1:
runs-on: ubuntu-latest
test2:
runs-on:
ubuntu-22.04
test3:
runs-on:
group: ubuntu-runners
labels: ubuntu-20.04-16core
test4:
runs-on: ubuntu-22.04-arm
`;
const res = await extractPackageFile(
runnerTestWorkflowUbuntu,
'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: 'ubuntu',
currentValue: '22.04-arm',
replaceString: 'ubuntu-22.04-arm',
depType: 'github-runner',
datasource: 'github-runners',
autoReplaceStringTemplate: '{{depName}}-{{newValue}}',
},
]);
expect(
res?.deps.filter((d) => d.datasource === 'github-runners'),
).toHaveLength(3);
});
it('extracts multiple windows action runners from yaml configuration file', async () => {
const runnerTestWorkflowWindows = codeBlock`
jobs:
test1:
runs-on: |
windows-2019
test2:
runs-on: >
windows-2022
test3:
runs-on: [windows-2022, selfhosted]
test4:
runs-on: windows-11-arm
test5:
runs-on: windows-2025
`;
const res = await extractPackageFile(
runnerTestWorkflowWindows,
'workflow.yml',
);
expect(res?.deps).toMatchObject([
{
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}}',
},
{
depName: 'windows',
currentValue: '11-arm',
replaceString: 'windows-11-arm',
depType: 'github-runner',
datasource: 'github-runners',
autoReplaceStringTemplate: '{{depName}}-{{newValue}}',
},
{
depName: 'windows',
currentValue: '2025',
replaceString: 'windows-2025',
depType: 'github-runner',
datasource: 'github-runners',
autoReplaceStringTemplate: '{{depName}}-{{newValue}}',
},
]);
expect(
res?.deps.filter((d) => d.datasource === 'github-runners'),
).toHaveLength(5);
});
it('extracts x-version from actions/setup-x', async () => {
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 = await 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('extracts x-version from actions/setup-x referenced by an https:// URL', async () => {
const yamlContent = codeBlock`
jobs:
build:
steps:
- uses: https://github.com/actions/setup-node@v4.4.0
with:
node-version: '23.7.0'
`;
const res = await extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps).toMatchObject([
{
depName: 'https://github.com/actions/setup-node',
packageName: 'actions/setup-node',
currentValue: 'v4.4.0',
datasource: 'github-tags',
versioning: 'github-actions',
depType: 'action',
},
{
depName: 'node',
packageName: 'actions/node-versions',
currentValue: '23.7.0',
datasource: 'github-releases',
versioning: 'node',
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$',
depType: 'uses-with',
},
]);
});
it('handles actions/setup-x without x-version field', async () => {
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 = await extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps).toMatchObject([
{
depName: 'actions/setup-node',
depType: 'action',
},
{
depName: 'node',
depType: 'uses-with',
skipStage: 'extract',
skipReason: 'unspecified-version',
},
]);
});
it('extracts x-version from actions/setup-x in composite action', async () => {
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 = await 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', async () => {
const yamlContent = codeBlock`
runs:
using: 'node20'
main: 'index.js'
`;
await expect(
extractPackageFile(yamlContent, 'action.yml'),
).resolves.toBeNull();
});
it('extracts actions and with-version inputs nested in a parallel block', async () => {
const yamlContent = codeBlock`
jobs:
build:
steps:
- uses: actions/checkout@v4
- parallel:
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '20.0.0'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- run: npm test
`;
const res = await extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps).toMatchObject([
{ depName: 'actions/checkout', depType: 'action' },
{ depName: 'actions/setup-node', depType: 'action' },
{ depName: 'actions/setup-go', depType: 'action' },
{ depName: 'node', depType: 'uses-with', currentValue: '20.0.0' },
{ depName: 'go', depType: 'uses-with', currentValue: '1.23' },
]);
});
it('extracts community action with-inputs nested in a parallel block', async () => {
const yamlContent = codeBlock`
jobs:
build:
steps:
- parallel:
- uses: astral-sh/setup-uv@v8.2.0
with:
version: '0.4.x'
`;
const res = await extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps).toMatchObject([
{ depName: 'astral-sh/setup-uv', depType: 'action' },
{
depName: 'astral-sh/uv',
depType: 'uses-with',
currentValue: '0.4.x',
datasource: 'github-releases',
},
]);
});
it.each`
sha256 | currentDigest
${'dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180'} | ${'dad54e0b843908324282b8673f9c0ebc3a4da0c49ad2da309a49bfbc918ba180'}
${'DAD54E0B843908324282B8673F9C0EBC3A4DA0C49AD2DA309A49BFBC918BA180'} | ${undefined}
${'${{ inputs.mise-sha256 }}'} | ${undefined}
${undefined} | ${undefined}
`(
'extracts the mise version and checksum from jdx/mise-action',
async ({ sha256, currentDigest }) => {
const yamlContent = codeBlock`
jobs:
build:
steps:
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
version: 2026.7.10
${sha256 ? `sha256: ${sha256}` : ''}
install_args: cargo:cargo-deny
`;
const res = await extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps).toMatchObject([
{
currentDigest: 'e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d',
currentValue: 'v4.2.0',
datasource: 'github-tags',
depName: 'jdx/mise-action',
depType: 'action',
},
{
...(currentDigest ? { currentDigest } : {}),
currentValue: '2026.7.10',
datasource: 'github-release-attachments',
depName: 'jdx/mise',
depType: 'uses-with',
packageName: 'jdx/mise',
},
]);
},
);
it('extracts steps nested in nested parallel blocks', async () => {
const yamlContent = codeBlock`
jobs:
build:
steps:
- parallel:
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '18.0.0'
- parallel:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
`;
const res = await extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps).toMatchObject([
{ depName: 'actions/setup-node', depType: 'action' },
{ depName: 'actions/setup-go', depType: 'action' },
{ depName: 'node', depType: 'uses-with', currentValue: '18.0.0' },
{ depName: 'go', depType: 'uses-with', currentValue: '1.22' },
]);
});
});
it.each([
{
step: {
uses: 'actions/setup-dotnet@v4',
with: { 'dotnet-version': '8.0.404' },
},
expected: [
{
currentValue: '8.0.404',
datasource: 'dotnet-version',
depName: 'dotnet-sdk',
depType: 'uses-with',
packageName: 'dotnet-sdk',
versioning: 'npm',
},
],
},
{
// a channel installs the latest patch release on it, rather than
// pinning a version
step: {
uses: 'actions/setup-dotnet@v4',
with: { 'dotnet-version': '8.0' },
},
expected: [
{
currentValue: '8.0',
datasource: 'dotnet-version',
depName: 'dotnet-sdk',
depType: 'uses-with',
packageName: 'dotnet-sdk',
versioning: 'npm',
},
],
},
{
// the idiomatic x-range form is a range too
step: {
uses: 'actions/setup-dotnet@v4',
with: { 'dotnet-version': '8.0.x' },
},
expected: [
{
currentValue: '8.0.x',
datasource: 'dotnet-version',
depName: 'dotnet-sdk',
depType: 'uses-with',
packageName: 'dotnet-sdk',
versioning: 'npm',
},
],
},
{
step: {
uses: 'actions/setup-dotnet@v4',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'dotnet-version',
depName: 'dotnet-sdk',
depType: 'uses-with',
packageName: 'dotnet-sdk',
versioning: 'npm',
},
],
},
{
// multiple SDKs, one per line, aren't a single version we can update
step: {
uses: 'actions/setup-dotnet@v4',
with: { 'dotnet-version': '3.1.x\n5.0.x' },
},
expected: [
{
skipStage: 'extract',
skipReason: 'invalid-version',
currentValue: '3.1.x\n5.0.x',
datasource: 'dotnet-version',
depName: 'dotnet-sdk',
depType: 'uses-with',
packageName: 'dotnet-sdk',
versioning: 'npm',
},
],
},
{
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin', 'java-version': '21' },
},
expected: [
{
currentValue: '21',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
versioning: 'semver-partial',
},
],
},
{
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'adopt', 'java-version': '11' },
},
expected: [
{
currentValue: '11',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
versioning: 'semver-partial',
},
],
},
{
step: {
uses: 'actions/setup-java@v4',
with: {
distribution: 'temurin',
'java-version': '21',
'java-package': 'jre',
},
},
expected: [
{
currentValue: '21',
datasource: 'java-version',
depName: 'java-jre',
depType: 'uses-with',
packageName: 'java-jre',
versioning: 'semver-partial',
},
],
},
{
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin' },
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
},
],
},
{
// we can't reliably track version updates for distributions other
// than Temurin/Adopt
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'zulu', 'java-version': '21' },
},
expected: [
{
skipStage: 'extract',
skipReason: 'unsupported',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
},
],
},
{
step: {
uses: 'actions/setup-java@v4',
with: { 'java-version': '21' },
},
expected: [
{
skipStage: 'extract',
skipReason: 'unsupported',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
},
],
},
{
// a major.minor `java-version` is a range too, so it must keep its
// precision rather than gain a patch/build component
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin', 'java-version': '21.0' },
},
expected: [
{
currentValue: '21.0',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
versioning: 'semver-partial',
},
],
},
{
// a fully-qualified `java-version` is pinned, not a range, so it's
// looked up with the default Java versioning
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin', 'java-version': '21.0.9+11.0.LTS' },
},
expected: [
{
currentValue: '21.0.9+11.0.LTS',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
},
],
},
{
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin', 'java-version': '21.0.9' },
},
expected: [
{
currentValue: '21.0.9',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
},
],
},
{
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin', 'java-version': '21.0.9-ea' },
},
expected: [
{
currentValue: '21.0.9-ea',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
},
],
},
{
// `latest` isn't a version we can pin or bump
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin', 'java-version': 'latest' },
},
expected: [
{
currentValue: 'latest',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
skipStage: 'extract',
skipReason: 'unsupported-version',
},
],
},
{
// an `x`-range has no single version to bump it to
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin', 'java-version': '21.x' },
},
expected: [
{
currentValue: '21.x',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
skipStage: 'extract',
skipReason: 'unsupported-version',
},
],
},
{
// a comparator range has no single version to bump it to
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin', 'java-version': '>=21' },
},
expected: [
{
currentValue: '>=21',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
skipStage: 'extract',
skipReason: 'unsupported-version',
},
],
},
{
// early-access releases aren't published by the java-version datasource
step: {
uses: 'actions/setup-java@v4',
with: { distribution: 'temurin', 'java-version': '21-ea' },
},
expected: [
{
currentValue: '21-ea',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
skipStage: 'extract',
skipReason: 'unsupported-version',
},
],
},
{
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: 'aws-actions/amazon-ecs-render-task-definition@v1',
with: {
'container-name': 'web',
image: 'amazon/amazon-ecs-sample:latest',
},
},
expected: [
{
currentValue: 'latest',
datasource: 'docker',
depName: 'amazon/amazon-ecs-sample',
depType: 'uses-with',
packageName: 'amazon/amazon-ecs-sample',
},
],
},
{
step: {
uses: 'aws-actions/amazon-ecs-render-task-definition@v1',
with: {
'container-name': 'web',
image: '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-repo:v1.2.3',
},
},
expected: [
{
currentValue: 'v1.2.3',
datasource: 'docker',
depName: '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-repo',
depType: 'uses-with',
packageName: '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-repo',
},
],
},
{
step: {
uses: 'aws-actions/amazon-ecs-render-task-definition@v1',
with: {
'container-name': 'web',
image:
'amazon/amazon-ecs-sample@sha256:0f7ba2b70c5d1a7e2d95b0f6c3d5b4a1e2f6b6a1e2f6b6a1e2f6b6a1e2f6b6a1',
},
},
expected: [
{
currentDigest:
'sha256:0f7ba2b70c5d1a7e2d95b0f6c3d5b4a1e2f6b6a1e2f6b6a1e2f6b6a1e2f6b6a1',
datasource: 'docker',
depName: 'amazon/amazon-ecs-sample',
depType: 'uses-with',
packageName: 'amazon/amazon-ecs-sample',
},
],
},
{
step: {
uses: 'aws-actions/amazon-ecs-render-task-definition@v1',
with: {
'container-name': 'web',
},
},
expected: [
{
depType: 'uses-with',
skipStage: 'extract',
skipReason: 'unspecified-version',
},
],
},
{
// the image is templated, so we can't reliably determine what to update
step: {
uses: 'aws-actions/amazon-ecs-render-task-definition@v1',
with: {
'container-name': 'web',
image: '${{ steps.build-image.outputs.image }}',
},
},
expected: [
{
depType: 'uses-with',
skipStage: 'extract',
skipReason: 'contains-variable',
},
],
},
{
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: {
uses: 'pnpm/setup@v1',
with: {
version: '12.0.0',
runtime: 'node@24.1.0',
},
},
expected: [
{
currentValue: '12.0.0',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
{
currentValue: '24.1.0',
datasource: 'node-version',
depName: 'node',
depType: 'uses-with',
packageName: 'node',
},
],
},
{
step: {
uses: 'pnpm/setup@v1',
with: {
install: false,
runtime: 'bun@1.2.x',
},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
{
currentValue: '1.2.x',
datasource: 'npm',
depName: 'bun',
depType: 'uses-with',
packageName: 'bun',
},
],
},
{
step: {
uses: 'pnpm/setup@v1',
with: {
runtime: 'deno@2',
},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
{
currentValue: '2',
datasource: 'npm',
depName: 'deno',
depType: 'uses-with',
packageName: 'deno',
},
],
},
{
// no version pinned: the runtime version comes from `devEngines.runtime`
step: {
uses: 'pnpm/setup@v1',
with: {
runtime: 'node',
},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'node-version',
depName: 'node',
depType: 'uses-with',
packageName: 'node',
},
],
},
{
step: {
uses: 'pnpm/setup@v1',
with: {
version: '12.0.0',
runtime: 'python@3.13',
},
},
expected: [
{
currentValue: '12.0.0',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
{
skipStage: 'extract',
skipReason: 'invalid-name',
depName: 'python',
depType: 'uses-with',
packageName: 'python',
},
],
},
{
// missing name: reported under the raw input
step: {
uses: 'pnpm/setup@v1',
with: {
runtime: '@24',
},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
{
skipStage: 'extract',
skipReason: 'invalid-name',
depName: '@24',
depType: 'uses-with',
packageName: '@24',
},
],
},
{
// the runtime is not a literal, so there's no name to resolve
step: {
uses: 'pnpm/setup@v1',
with: {
runtime: '${{ env.RUNTIME }}',
},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
},
{
skipStage: 'extract',
skipReason: 'invalid-name',
depName: '${{ env.RUNTIME }}',
depType: 'uses-with',
packageName: '${{ env.RUNTIME }}',
},
],
},
{
step: {
uses: 'pnpm/setup@v1',
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: 'semver',
},
],
},
{
// `'latest'` is the default `pixi-version`, which is not a version that can be bumped
step: {
uses: 'prefix-dev/setup-pixi@v0.8.3',
with: {
'pixi-version': 'latest',
},
},
expected: [
{
currentValue: 'latest',
datasource: 'github-releases',
depName: 'prefix-dev/pixi',
depType: 'uses-with',
packageName: 'prefix-dev/pixi',
versioning: 'semver',
},
],
},
{
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: 'github-releases',
depName: 'deno',
depType: 'uses-with',
packageName: 'denoland/deno',
versioning: 'npm',
},
],
},
{
step: {
uses: 'denoland/setup-deno@v2',
with: { 'deno-version': '2.4.0' },
},
expected: [
{
currentValue: '2.4.0',
datasource: 'github-releases',
depName: 'deno',
depType: 'uses-with',
packageName: 'denoland/deno',
versioning: 'npm',
},
],
},
{
step: {
uses: 'expo/expo-github-action@v8',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'eas-cli',
depType: 'uses-with',
packageName: 'eas-cli',
},
],
},
{
step: {
uses: 'expo/expo-github-action@v8',
with: { 'eas-version': '23.2.0' },
},
expected: [
{
currentValue: '23.2.0',
datasource: 'npm',
depName: 'eas-cli',
depType: 'uses-with',
packageName: 'eas-cli',
},
],
},
{
step: {
uses: 'ruby/setup-ruby@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'ruby-version',
depName: 'ruby',
depType: 'uses-with',
packageName: 'ruby',
},
],
},
{
// a short `ruby-version` is the latest release matching it, so it must
// keep its precision rather than gain a patch component
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',
versioning: 'semver-partial',
},
],
},
{
step: {
uses: 'ruby/setup-ruby@v1',
with: { 'ruby-version': '3.4.7' },
},
expected: [
{
currentValue: '3.4.7',
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',
},
],
},
{
// a partial `version` resolves to the latest matching release, so it
// must keep both its precision and its `v` prefix
step: {
uses: 'golangci/golangci-lint-action@v9',
with: { version: 'v2.5' },
},
expected: [
{
currentValue: 'v2.5',
datasource: 'github-releases',
depName: 'golangci/golangci-lint',
depType: 'uses-with',
packageName: 'golangci/golangci-lint',
versioning: 'semver-partial',
},
],
},
{
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',
},
],
},
{
step: {
uses: 'docker/setup-docker-action@v4',
with: { version: 'v27.1.0' },
},
expected: [
{
currentValue: 'v27.1.0',
datasource: 'github-releases',
depName: 'docker',
depType: 'uses-with',
packageName: 'moby/moby',
extractVersion: '^docker-(?<version>.+)$',
},
],
},
{
step: {
uses: 'dtolnay/rust-toolchain@master',
with: { toolchain: '1.89.0' },
},
expected: [
{
currentValue: '1.89.0',
datasource: 'rust-version',
depName: 'rust',
depType: 'uses-with',
packageName: 'rust',
},
],
},
{
step: {
uses: 'dtolnay/rust-toolchain@master',
with: { toolchain: 'nightly-2025-01-01' },
},
expected: [
{
currentValue: 'nightly-2025-01-01',
datasource: 'rust-version',
depName: 'rust',
depType: 'uses-with',
packageName: 'rust',
},
],
},
{
step: {
uses: 'dtolnay/rust-toolchain@stable',
with: { toolchain: 'stable' },
},
expected: [
{
currentValue: 'stable',
datasource: 'rust-version',
depName: 'rust',
depType: 'uses-with',
packageName: 'rust',
},
],
},
{
step: {
uses: 'dtolnay/rust-toolchain@master',
with: {},
},
expected: [
{
datasource: 'rust-version',
depName: 'rust',
depType: 'uses-with',
packageName: 'rust',
skipStage: 'extract',
skipReason: 'unspecified-version',
},
],
},
{
step: {
uses: 'azure/setup-helm@v4',
with: { version: 'v3.17.0' },
},
expected: [
{
currentValue: 'v3.17.0',
datasource: 'github-releases',
depName: 'helm',
depType: 'uses-with',
packageName: 'helm/helm',
},
],
},
{
step: {
uses: 'azure/setup-helm@v4',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'helm',
depType: 'uses-with',
packageName: 'helm/helm',
},
],
},
{
step: {
uses: 'docker/setup-buildx-action@v3',
with: { version: 'v0.19.3' },
},
expected: [
{
currentValue: 'v0.19.3',
datasource: 'github-releases',
depName: 'buildx',
depType: 'uses-with',
packageName: 'docker/buildx',
},
],
},
{
step: {
uses: 'docker/setup-buildx-action@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'buildx',
depType: 'uses-with',
packageName: 'docker/buildx',
},
],
},
{
step: {
uses: 'docker/setup-compose-action@v1',
with: { version: 'v2.36.1' },
},
expected: [
{
currentValue: 'v2.36.1',
datasource: 'github-releases',
depName: 'docker/compose',
depType: 'uses-with',
packageName: 'docker/compose',
},
],
},
{
step: {
uses: 'docker/setup-compose-action@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'docker/compose',
depType: 'uses-with',
packageName: 'docker/compose',
},
],
},
{
step: {
uses: 'helm/chart-testing-action@v2',
with: { version: 'v3.12.0' },
},
expected: [
{
currentValue: 'v3.12.0',
datasource: 'github-releases',
depName: 'chart-testing',
depType: 'uses-with',
packageName: 'helm/chart-testing',
},
],
},
{
step: {
uses: 'helm/chart-testing-action@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'chart-testing',
depType: 'uses-with',
packageName: 'helm/chart-testing',
},
],
},
{
step: {
uses: 'sigoden/install-binary@v1',
with: {
repo: 'sigoden/argc',
tag: 'v1.22.0',
},
},
expected: [
{
currentValue: 'v1.22.0',
datasource: 'github-releases',
depName: 'sigoden/argc',
depType: 'uses-with',
packageName: 'sigoden/argc',
},
],
},
{
step: {
uses: 'sigstore/cosign-installer@v4.1.2',
with: { 'cosign-release': 'v3.1.2' },
},
expected: [
{
currentValue: 'v3.1.2',
datasource: 'github-releases',
depName: 'sigstore/cosign',
depType: 'uses-with',
packageName: 'sigstore/cosign',
},
],
},
{
step: {
uses: 'slsa-framework/actions/attest/actions@v0.1.0',
with: { version: 'v0.1.0' },
},
expected: [
{
currentValue: 'v0.1.0',
datasource: 'github-releases',
depName: 'slsa-framework/attester',
depType: 'uses-with',
packageName: 'slsa-framework/attester',
},
],
},
{
step: {
uses: 'slsa-framework/actions/install/verifier@v0.1.0',
with: { version: 'v0.1.0' },
},
expected: [
{
currentValue: 'v0.1.0',
datasource: 'github-releases',
depName: 'slsa-framework/verifier',
depType: 'uses-with',
packageName: 'slsa-framework/verifier',
},
],
},
{
// the `repo` input overrides which repository's releases are tracked
step: {
uses: 'slsa-framework/actions/install/verifier@v0.1.0',
with: { version: 'v1.2.3', repo: 'other-org/other-verifier' },
},
expected: [
{
currentValue: 'v1.2.3',
datasource: 'github-releases',
depName: 'other-org/other-verifier',
depType: 'uses-with',
packageName: 'other-org/other-verifier',
},
],
},
{
step: {
uses: 'slsa-framework/actions/install/verifier@v0.1.0',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'slsa-framework/verifier',
depType: 'uses-with',
packageName: 'slsa-framework/verifier',
},
],
},
{
step: {
uses: 'slsa-framework/actions/slsa_with_provenance@v0.1.0',
with: { version: 'v0.7.1' },
},
expected: [
{
currentValue: 'v0.7.1',
datasource: 'github-releases',
depName: 'slsa-framework/source-tool',
depType: 'uses-with',
packageName: 'slsa-framework/source-tool',
},
],
},
{
step: {
uses: 'slsa-framework/actions/verify/build@v0.1.0',
with: { version: 'v0.1.0' },
},
expected: [
{
currentValue: 'v0.1.0',
datasource: 'github-releases',
depName: 'slsa-framework/verifier',
depType: 'uses-with',
packageName: 'slsa-framework/verifier',
},
],
},
{
step: {
uses: 'slsa-framework/actions/verify/source@v0.1.0',
with: { version: 'v0.1.0' },
},
expected: [
{
currentValue: 'v0.1.0',
datasource: 'github-releases',
depName: 'slsa-framework/verifier',
depType: 'uses-with',
packageName: 'slsa-framework/verifier',
},
],
},
{
step: {
uses: 'slsa-framework/actions/verify/vsa@v0.1.0',
with: { version: 'v0.1.0' },
},
expected: [
{
currentValue: 'v0.1.0',
datasource: 'github-releases',
depName: 'slsa-framework/verifier',
depType: 'uses-with',
packageName: 'slsa-framework/verifier',
},
],
},
{
step: {
uses: 'renovatebot/github-action@v43.0.0',
with: { 'renovate-version': '43.100.0' },
},
expected: [
{
currentValue: '43.100.0',
datasource: 'docker',
depName: 'ghcr.io/renovatebot/renovate',
depType: 'uses-with',
packageName: 'ghcr.io/renovatebot/renovate',
},
],
},
{
// `renovate-version: '43'`, quoted, matching the action's own default
step: {
uses: 'renovatebot/github-action@v43.0.0',
with: { 'renovate-version': '43' },
},
expected: [
{
currentValue: '43',
datasource: 'docker',
depName: 'ghcr.io/renovatebot/renovate',
depType: 'uses-with',
packageName: 'ghcr.io/renovatebot/renovate',
},
],
},
{
// `renovate-version: 43`, unquoted, parses as a YAML number rather than a string
step: {
uses: 'renovatebot/github-action@v43.0.0',
with: { 'renovate-version': 43 },
},
expected: [
{
currentValue: '43',
datasource: 'docker',
depName: 'ghcr.io/renovatebot/renovate',
depType: 'uses-with',
packageName: 'ghcr.io/renovatebot/renovate',
},
],
},
{
step: {
uses: 'renovatebot/github-action@v43.0.0',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'docker',
depName: 'ghcr.io/renovatebot/renovate',
depType: 'uses-with',
packageName: 'ghcr.io/renovatebot/renovate',
},
],
},
{
step: {
uses: 'renovatebot/github-action@v43.0.0',
with: {
'renovate-version': '43.100.0',
'renovate-image': 'ghcr.io/my-org/renovate',
},
},
expected: [
{
currentValue: '43.100.0',
datasource: 'docker',
depName: 'ghcr.io/my-org/renovate',
depType: 'uses-with',
packageName: 'ghcr.io/my-org/renovate',
},
],
},
{
step: {
uses: 'renovatebot/github-action@v43.0.0',
with: {
'renovate-image':
'ghcr.io/renovatebot/renovate@sha256:0f7ba2b70c5d1a7e2d95b0f6c3d5b4a1e2f6b6a1e2f6b6a1e2f6b6a1e2f6b6a1',
},
},
expected: [
{
currentDigest:
'sha256:0f7ba2b70c5d1a7e2d95b0f6c3d5b4a1e2f6b6a1e2f6b6a1e2f6b6a1e2f6b6a1',
datasource: 'docker',
depName: 'ghcr.io/renovatebot/renovate',
depType: 'uses-with',
packageName: 'ghcr.io/renovatebot/renovate',
skipStage: 'extract',
skipReason: 'unspecified-version',
},
],
},
{
// `renovate-image` tag- and digest-pinned, with no `renovate-version`
// set: https://github.com/renovatebot/renovate/discussions/46969
step: {
uses: 'renovatebot/github-action@v43.0.0',
with: {
'renovate-image':
'ghcr.io/renovatebot/renovate:44.104.2@sha256:0f7ba2b70c5d1a7e2d95b0f6c3d5b4a1e2f6b6a1e2f6b6a1e2f6b6a1e2f6b6a1',
},
},
expected: [
{
currentValue: '44.104.2',
currentDigest:
'sha256:0f7ba2b70c5d1a7e2d95b0f6c3d5b4a1e2f6b6a1e2f6b6a1e2f6b6a1e2f6b6a1',
datasource: 'docker',
depName: 'ghcr.io/renovatebot/renovate',
depType: 'uses-with',
packageName: 'ghcr.io/renovatebot/renovate',
},
],
},
{
step: {
uses: 'erlef/setup-beam@v1',
with: {
'otp-version': '27.1.2',
'elixir-version': '1.17.3',
'gleam-version': '1.5.1',
'rebar3-version': '3.24.0',
},
},
expected: [
{
currentValue: '27.1.2',
datasource: 'github-releases',
depName: 'erlang/otp',
depType: 'uses-with',
packageName: 'erlang/otp',
},
{
currentValue: '1.17.3',
datasource: 'github-releases',
depName: 'elixir-lang/elixir',
depType: 'uses-with',
packageName: 'elixir-lang/elixir',
versioning: 'npm',
},
{
currentValue: '1.5.1',
datasource: 'github-releases',
depName: 'gleam-lang/gleam',
depType: 'uses-with',
packageName: 'gleam-lang/gleam',
versioning: 'npm',
},
{
currentValue: '3.24.0',
datasource: 'github-releases',
depName: 'erlang/rebar3',
depType: 'uses-with',
packageName: 'erlang/rebar3',
versioning: 'npm',
},
],
},
{
// most workflows only set a subset of the 4 possible inputs
step: {
uses: 'erlef/setup-beam@v1',
with: { 'otp-version': '27.1.2' },
},
expected: [
{
currentValue: '27.1.2',
datasource: 'github-releases',
depName: 'erlang/otp',
depType: 'uses-with',
packageName: 'erlang/otp',
},
],
},
{
// the documented version specification form is a range, not a pinned
// version
step: {
uses: 'erlef/setup-beam@v1',
with: { 'elixir-version': '1.17.x' },
},
expected: [
{
currentValue: '1.17.x',
datasource: 'github-releases',
depName: 'elixir-lang/elixir',
depType: 'uses-with',
packageName: 'elixir-lang/elixir',
versioning: 'npm',
},
],
},
{
// none of the inputs are set, so no deps should be extracted at all
// (rather than emitting skipped deps for inputs no one set)
step: {
uses: 'erlef/setup-beam@v1',
with: {},
},
expected: [],
},
{
// `'nightly'` is a valid, documented value for rebar3-version, but not
// one we can pin/bump — the value is passed through as-is, and the
// versioning layer skips proposing an update since it isn't a real
// version
step: {
uses: 'erlef/setup-beam@v1',
with: { 'rebar3-version': 'nightly' },
},
expected: [
{
currentValue: 'nightly',
datasource: 'github-releases',
depName: 'erlang/rebar3',
depType: 'uses-with',
packageName: 'erlang/rebar3',
versioning: 'npm',
},
],
},
{
// `false` is a valid, documented value for otp-version (skip
// installing OTP for Gleam-only workflows), but not one we can
// pin/bump — same pass-through treatment as rebar3-version's
// `nightly` above
step: {
uses: 'erlef/setup-beam@v1',
with: { 'otp-version': 'false', 'gleam-version': '1.5.0' },
},
expected: [
{
currentValue: 'false',
datasource: 'github-releases',
depName: 'erlang/otp',
depType: 'uses-with',
packageName: 'erlang/otp',
},
{
currentValue: '1.5.0',
datasource: 'github-releases',
depName: 'gleam-lang/gleam',
depType: 'uses-with',
packageName: 'gleam-lang/gleam',
versioning: 'npm',
},
],
},
{
step: {
uses: 'helm/kind-action@v1',
with: {
version: 'v0.33.0',
node_image: 'kindest/node:v1.31.0',
kubectl_version: 'v1.31.0',
},
},
expected: [
{
currentValue: 'v0.33.0',
datasource: 'github-releases',
depName: 'kubernetes-sigs/kind',
depType: 'uses-with',
packageName: 'kubernetes-sigs/kind',
},
{
currentValue: 'v1.31.0',
datasource: 'docker',
depName: 'kindest/node',
depType: 'uses-with',
packageName: 'kindest/node',
},
{
currentValue: 'v1.31.0',
datasource: 'github-releases',
depName: 'kubernetes/kubernetes',
depType: 'uses-with',
packageName: 'kubernetes/kubernetes',
},
],
},
{
// most workflows only pin one of the 3 possible inputs
step: {
uses: 'helm/kind-action@v1',
with: { version: 'v0.33.0' },
},
expected: [
{
currentValue: 'v0.33.0',
datasource: 'github-releases',
depName: 'kubernetes-sigs/kind',
depType: 'uses-with',
packageName: 'kubernetes-sigs/kind',
},
],
},
{
// none of the inputs are set, so no deps should be extracted at all
// (rather than emitting skipped deps for inputs no one set)
step: {
uses: 'helm/kind-action@v1',
with: {},
},
expected: [],
},
{
// `node_image` may be pinned by digest instead of (or alongside) a tag
step: {
uses: 'helm/kind-action@v1',
with: {
node_image:
'kindest/node:v1.31.0@sha256:0f7ba2b70c5d1a7e2d95b0f6c3d5b4a1e2f6b6a1e2f6b6a1e2f6b6a1e2f6b6a1',
},
},
expected: [
{
currentValue: 'v1.31.0',
currentDigest:
'sha256:0f7ba2b70c5d1a7e2d95b0f6c3d5b4a1e2f6b6a1e2f6b6a1e2f6b6a1e2f6b6a1',
datasource: 'docker',
depName: 'kindest/node',
depType: 'uses-with',
packageName: 'kindest/node',
},
],
},
{
step: {
uses: 'github/gh-aw-actions/setup-cli@v1',
with: { version: 'v0.37.18' },
},
expected: [
{
currentValue: 'v0.37.18',
datasource: 'github-releases',
depName: 'github/gh-aw',
depType: 'uses-with',
packageName: 'github/gh-aw',
},
],
},
{
step: {
uses: 'github/gh-aw-actions/setup-cli@v1',
with: { version: 'v1' },
},
expected: [
{
currentValue: 'v1',
datasource: 'github-releases',
depName: 'github/gh-aw',
depType: 'uses-with',
packageName: 'github/gh-aw',
versioning: 'semver-partial',
},
],
},
{
// `latest` is already a rolling channel, not a version to update.
step: {
uses: 'github/gh-aw-actions/setup-cli@v1',
with: { version: 'latest' },
},
expected: [
{
currentValue: 'latest',
datasource: 'github-releases',
depName: 'github/gh-aw',
depType: 'uses-with',
packageName: 'github/gh-aw',
},
],
},
{
step: {
uses: 'github/gh-aw-actions/setup-cli@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'github/gh-aw',
depType: 'uses-with',
packageName: 'github/gh-aw',
},
],
},
{
step: {
uses: 'GitTools/actions/gitversion/setup@v3',
with: { versionSpec: '6.8.2' },
},
expected: [
{
currentValue: '6.8.2',
datasource: 'github-releases',
depName: 'gitversion',
depType: 'uses-with',
packageName: 'GitTools/GitVersion',
versioning: 'npm',
},
],
},
{
// the documented `versionSpec` form is a range, not a pinned version
step: {
uses: 'GitTools/actions/gitversion/setup@v3',
with: { versionSpec: '6.8.x' },
},
expected: [
{
currentValue: '6.8.x',
datasource: 'github-releases',
depName: 'gitversion',
depType: 'uses-with',
packageName: 'GitTools/GitVersion',
versioning: 'npm',
},
],
},
{
step: {
uses: 'GitTools/actions/gitversion/setup@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'gitversion',
depType: 'uses-with',
packageName: 'GitTools/GitVersion',
versioning: 'npm',
},
],
},
{
step: {
uses: 'subosito/flutter-action@v2',
with: { 'flutter-version': '3.24.3' },
},
expected: [
{
currentValue: '3.24.3',
datasource: 'github-releases',
depName: 'flutter',
depType: 'uses-with',
packageName: 'flutter/flutter',
versioning: 'npm',
},
],
},
{
// an x-range is a range, not a pinned version
step: {
uses: 'subosito/flutter-action@v2',
with: { 'flutter-version': '3.24.x' },
},
expected: [
{
currentValue: '3.24.x',
datasource: 'github-releases',
depName: 'flutter',
depType: 'uses-with',
packageName: 'flutter/flutter',
versioning: 'npm',
},
],
},
{
step: {
uses: 'subosito/flutter-action@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'flutter',
depType: 'uses-with',
packageName: 'flutter/flutter',
versioning: 'npm',
},
],
},
{
step: {
uses: 'aquaproj/aqua-installer@v3',
with: { aqua_version: 'v2.62.3' },
},
expected: [
{
currentValue: 'v2.62.3',
datasource: 'github-releases',
depName: 'aqua',
depType: 'uses-with',
packageName: 'aquaproj/aqua',
},
],
},
{
step: {
uses: 'aquaproj/aqua-installer@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'aqua',
depType: 'uses-with',
packageName: 'aquaproj/aqua',
},
],
},
{
step: {
uses: 'cycjimmy/semantic-release-action@v4',
with: { semantic_version: '^24.0.0' },
},
expected: [
{
currentValue: '^24.0.0',
datasource: 'npm',
depName: 'semantic-release',
depType: 'uses-with',
packageName: 'semantic-release',
versioning: 'npm',
},
],
},
{
step: {
uses: 'cycjimmy/semantic-release-action@v4',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'semantic-release',
depType: 'uses-with',
packageName: 'semantic-release',
versioning: 'npm',
},
],
},
{
step: {
uses: 'cloudflare/wrangler-action@v3',
with: { wranglerVersion: '3.78.0' },
},
expected: [
{
currentValue: '3.78.0',
datasource: 'npm',
depName: 'wrangler',
depType: 'uses-with',
packageName: 'wrangler',
},
],
},
{
step: {
uses: 'cloudflare/wrangler-action@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'wrangler',
depType: 'uses-with',
packageName: 'wrangler',
},
],
},
{
step: {
uses: 'peaceiris/actions-hugo@v3',
with: { 'hugo-version': '0.166.0' },
},
expected: [
{
currentValue: '0.166.0',
datasource: 'github-releases',
depName: 'hugo',
depType: 'uses-with',
packageName: 'gohugoio/hugo',
},
],
},
{
step: {
uses: 'peaceiris/actions-hugo@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'hugo',
depType: 'uses-with',
packageName: 'gohugoio/hugo',
},
],
},
{
step: {
uses: 'jfrog/setup-jfrog-cli@v4',
with: { version: '2.91.0' },
},
expected: [
{
currentValue: '2.91.0',
datasource: 'github-releases',
depName: 'jfrog-cli',
depType: 'uses-with',
packageName: 'jfrog/jfrog-cli',
},
],
},
{
step: {
uses: 'jfrog/setup-jfrog-cli@v4',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'jfrog-cli',
depType: 'uses-with',
packageName: 'jfrog/jfrog-cli',
},
],
},
{
step: {
uses: 'helm/chart-releaser-action@v1',
with: { version: 'v1.7.0' },
},
expected: [
{
currentValue: 'v1.7.0',
datasource: 'github-releases',
depName: 'chart-releaser',
depType: 'uses-with',
packageName: 'helm/chart-releaser',
},
],
},
{
step: {
uses: 'helm/chart-releaser-action@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'chart-releaser',
depType: 'uses-with',
packageName: 'helm/chart-releaser',
},
],
},
{
step: {
uses: 'azure/setup-kubectl@v4',
with: { version: 'v1.31.0' },
},
expected: [
{
currentValue: 'v1.31.0',
datasource: 'github-releases',
depName: 'kubectl',
depType: 'uses-with',
packageName: 'kubernetes/kubernetes',
},
],
},
{
step: {
uses: 'azure/setup-kubectl@v4',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'kubectl',
depType: 'uses-with',
packageName: 'kubernetes/kubernetes',
},
],
},
{
// `'latest'` is a valid, documented value (and the action's own
// default), but not one we can pin/bump — the value is passed
// through as-is, and the versioning layer skips proposing an update
// since it isn't a real version
step: {
uses: 'azure/setup-kubectl@v4',
with: { version: 'latest' },
},
expected: [
{
currentValue: 'latest',
datasource: 'github-releases',
depName: 'kubectl',
depType: 'uses-with',
packageName: 'kubernetes/kubernetes',
},
],
},
{
step: {
uses: 'snok/install-poetry@v1',
with: { version: '1.8.3' },
},
expected: [
{
currentValue: '1.8.3',
datasource: 'pypi',
depName: 'poetry',
depType: 'uses-with',
packageName: 'poetry',
},
],
},
{
step: {
uses: 'snok/install-poetry@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'pypi',
depName: 'poetry',
depType: 'uses-with',
packageName: 'poetry',
},
],
},
{
step: {
uses: 'terraform-linters/setup-tflint@v4',
with: { tflint_version: 'v0.64.0' },
},
expected: [
{
currentValue: 'v0.64.0',
datasource: 'github-releases',
depName: 'tflint',
depType: 'uses-with',
packageName: 'terraform-linters/tflint',
},
],
},
{
step: {
uses: 'terraform-linters/setup-tflint@v4',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'tflint',
depType: 'uses-with',
packageName: 'terraform-linters/tflint',
},
],
},
{
// `'latest'` is a valid, documented value, but not one we can pin/bump
step: {
uses: 'terraform-linters/setup-tflint@v4',
with: { tflint_version: 'latest' },
},
expected: [
{
currentValue: 'latest',
skipStage: 'extract',
skipReason: 'unsupported-version',
datasource: 'github-releases',
depName: 'tflint',
depType: 'uses-with',
packageName: 'terraform-linters/tflint',
},
],
},
{
step: {
uses: 'UpCloudLtd/upcloud-cli-action@main',
with: { version: 'v3.35.0' },
},
expected: [
{
currentValue: 'v3.35.0',
datasource: 'github-releases',
depName: 'UpCloudLtd/upcloud-cli',
depType: 'uses-with',
packageName: 'UpCloudLtd/upcloud-cli',
},
],
},
{
step: {
uses: 'extractions/setup-just@v3',
with: { 'just-version': '1.58.0' },
},
expected: [
{
currentValue: '1.58.0',
datasource: 'github-releases',
depName: 'just',
depType: 'uses-with',
packageName: 'casey/just',
},
],
},
{
step: {
uses: 'extractions/setup-just@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'just',
depType: 'uses-with',
packageName: 'casey/just',
},
],
},
{
step: {
uses: 'gradle/actions/setup-gradle@v4',
with: { 'gradle-version': '8.10' },
},
expected: [
{
currentValue: '8.10',
datasource: 'gradle-version',
depName: 'gradle',
depType: 'uses-with',
packageName: 'gradle/gradle',
versioning: 'gradle',
},
],
},
{
step: {
uses: 'gradle/actions/setup-gradle@v4',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'gradle-version',
depName: 'gradle',
depType: 'uses-with',
packageName: 'gradle/gradle',
versioning: 'gradle',
},
],
},
{
step: {
uses: 'gradle/actions/setup-gradle@v4',
with: { 'gradle-version': 'wrapper' },
},
expected: [
{
currentValue: 'wrapper',
skipStage: 'extract',
skipReason: 'invalid-version',
datasource: 'gradle-version',
depName: 'gradle',
depType: 'uses-with',
packageName: 'gradle/gradle',
versioning: 'gradle',
},
],
},
{
step: {
uses: 'gradle/actions/setup-gradle@v4',
with: { 'gradle-version': 'release-candidate' },
},
expected: [
{
currentValue: 'release-candidate',
skipStage: 'extract',
skipReason: 'invalid-version',
datasource: 'gradle-version',
depName: 'gradle',
depType: 'uses-with',
packageName: 'gradle/gradle',
versioning: 'gradle',
},
],
},
{
step: {
uses: 'hashicorp/setup-terraform@v3',
with: { terraform_version: '1.13.0' },
},
expected: [
{
currentValue: '1.13.0',
datasource: 'github-releases',
depName: 'terraform',
depType: 'uses-with',
packageName: 'hashicorp/terraform',
versioning: 'npm',
},
],
},
{
// a constraint string is a range, not a pinned version
step: {
uses: 'hashicorp/setup-terraform@v3',
with: { terraform_version: '~1.13.0' },
},
expected: [
{
currentValue: '~1.13.0',
datasource: 'github-releases',
depName: 'terraform',
depType: 'uses-with',
packageName: 'hashicorp/terraform',
versioning: 'npm',
},
],
},
{
step: {
uses: 'hashicorp/setup-terraform@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'terraform',
depType: 'uses-with',
packageName: 'hashicorp/terraform',
versioning: 'npm',
},
],
},
{
step: {
uses: 'goreleaser/goreleaser-action@v6',
with: { version: 'v2.4.4' },
},
expected: [
{
currentValue: 'v2.4.4',
datasource: 'github-releases',
depName: 'goreleaser/goreleaser',
depType: 'uses-with',
packageName: 'goreleaser/goreleaser',
versioning: 'npm',
},
],
},
{
step: {
uses: 'goreleaser/goreleaser-action@v6',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'goreleaser/goreleaser',
depType: 'uses-with',
packageName: 'goreleaser/goreleaser',
versioning: 'npm',
},
],
},
{
step: {
uses: 'opentofu/setup-opentofu@v1',
with: { tofu_version: '1.8.0' },
},
expected: [
{
currentValue: '1.8.0',
datasource: 'github-releases',
depName: 'opentofu',
depType: 'uses-with',
packageName: 'opentofu/opentofu',
versioning: 'npm',
},
],
},
{
// a constraint string is a range, not a pinned version
step: {
uses: 'opentofu/setup-opentofu@v1',
with: { tofu_version: '<1.9.0' },
},
expected: [
{
currentValue: '<1.9.0',
datasource: 'github-releases',
depName: 'opentofu',
depType: 'uses-with',
packageName: 'opentofu/opentofu',
versioning: 'npm',
},
],
},
{
step: {
uses: 'opentofu/setup-opentofu@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'opentofu',
depType: 'uses-with',
packageName: 'opentofu/opentofu',
versioning: 'npm',
},
],
},
{
step: {
uses: 'abatilo/actions-poetry@v3',
with: { 'poetry-version': '1.8.3' },
},
expected: [
{
currentValue: '1.8.3',
datasource: 'pypi',
depName: 'poetry',
depType: 'uses-with',
packageName: 'poetry',
},
],
},
{
step: {
uses: 'abatilo/actions-poetry@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'pypi',
depName: 'poetry',
depType: 'uses-with',
packageName: 'poetry',
},
],
},
{
step: {
uses: 'biomejs/setup-biome@v2',
with: { version: '1.9.4' },
},
expected: [
{
currentValue: '1.9.4',
datasource: 'npm',
depName: '@biomejs/biome',
depType: 'uses-with',
packageName: '@biomejs/biome',
},
],
},
{
step: {
uses: 'biomejs/setup-biome@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: '@biomejs/biome',
depType: 'uses-with',
packageName: '@biomejs/biome',
},
],
},
{
step: {
uses: 'astral-sh/ruff-action@v3',
with: { version: '0.16.7' },
},
expected: [
{
currentValue: '0.16.7',
datasource: 'github-releases',
depName: 'ruff',
depType: 'uses-with',
packageName: 'astral-sh/ruff',
},
],
},
{
step: {
uses: 'astral-sh/ruff-action@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'ruff',
depType: 'uses-with',
packageName: 'astral-sh/ruff',
},
],
},
{
step: {
uses: 'reviewdog/action-setup@v1',
with: { reviewdog_version: 'v0.21.1' },
},
expected: [
{
currentValue: 'v0.21.1',
datasource: 'github-releases',
depName: 'reviewdog',
depType: 'uses-with',
packageName: 'reviewdog/reviewdog',
},
],
},
{
step: {
uses: 'reviewdog/action-setup@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'reviewdog',
depType: 'uses-with',
packageName: 'reviewdog/reviewdog',
},
],
},
{
step: {
uses: 'j178/prek-action@v1',
with: { 'prek-version': '0.3.x' },
},
expected: [
{
currentValue: '0.3.x',
datasource: 'github-releases',
depName: 'prek',
depType: 'uses-with',
packageName: 'j178/prek',
versioning: 'npm',
},
],
},
{
step: {
uses: 'j178/prek-action@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'prek',
depType: 'uses-with',
packageName: 'j178/prek',
versioning: 'npm',
},
],
},
{
step: {
uses: 'actions-rust-lang/setup-rust-toolchain@v1',
with: { toolchain: 'stable, nightly' },
},
expected: [
{
currentValue: 'nightly',
datasource: 'rust-version',
depName: 'rust',
depType: 'uses-with',
packageName: 'rust',
},
],
},
{
step: {
uses: 'actions-rust-lang/setup-rust-toolchain@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'rust-version',
depName: 'rust',
depType: 'uses-with',
packageName: 'rust',
},
],
},
{
step: {
uses: 'raven-actions/actionlint@v2',
with: { version: 'v1.7.12' },
},
expected: [
{
currentValue: 'v1.7.12',
datasource: 'github-releases',
depName: 'actionlint',
depType: 'uses-with',
packageName: 'rhysd/actionlint',
},
],
},
{
step: {
uses: 'raven-actions/actionlint@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'actionlint',
depType: 'uses-with',
packageName: 'rhysd/actionlint',
},
],
},
{
step: {
uses: 'bufbuild/buf-setup-action@v1',
with: { version: '1.50.0' },
},
expected: [
{
currentValue: '1.50.0',
datasource: 'github-releases',
depName: 'buf',
depType: 'uses-with',
packageName: 'bufbuild/buf',
},
],
},
{
step: {
uses: 'bufbuild/buf-setup-action@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'buf',
depType: 'uses-with',
packageName: 'bufbuild/buf',
},
],
},
{
step: {
uses: 'stCarolas/setup-maven@v4.5',
with: { 'maven-version': '3.9.9' },
},
expected: [
{
currentValue: '3.9.9',
datasource: 'github-releases',
depName: 'maven',
depType: 'uses-with',
packageName: 'apache/maven',
versioning: 'npm',
},
],
},
{
step: {
uses: 'stCarolas/setup-maven@v4.5',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'maven',
depType: 'uses-with',
packageName: 'apache/maven',
versioning: 'npm',
},
],
},
{
step: {
uses: 'pulumi/actions@v6',
with: { 'pulumi-version': '3.262.0' },
},
expected: [
{
currentValue: '3.262.0',
datasource: 'github-releases',
depName: 'pulumi',
depType: 'uses-with',
packageName: 'pulumi/pulumi',
},
],
},
{
step: {
uses: 'pulumi/actions@v6',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'pulumi',
depType: 'uses-with',
packageName: 'pulumi/pulumi',
},
],
},
{
step: {
uses: 'supabase/setup-cli@v1',
with: { version: '1.200.3' },
},
expected: [
{
currentValue: '1.200.3',
datasource: 'npm',
depName: 'supabase',
depType: 'uses-with',
packageName: 'supabase',
},
],
},
{
step: {
uses: 'supabase/setup-cli@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'supabase',
depType: 'uses-with',
packageName: 'supabase',
},
],
},
{
// `'latest'`/`'beta'` are valid, documented values, but not ones we
// can pin/bump — the value is passed through as-is, and the
// versioning layer skips proposing an update since it isn't a real
// version
step: {
uses: 'supabase/setup-cli@v1',
with: { version: 'beta' },
},
expected: [
{
currentValue: 'beta',
datasource: 'npm',
depName: 'supabase',
depType: 'uses-with',
packageName: 'supabase',
},
],
},
{
step: {
uses: 'mozilla-actions/sccache-action@v0.0.9',
with: { version: 'v0.18.0' },
},
expected: [
{
currentValue: 'v0.18.0',
datasource: 'github-releases',
depName: 'sccache',
depType: 'uses-with',
packageName: 'mozilla/sccache',
},
],
},
{
step: {
uses: 'mozilla-actions/sccache-action@v0.0.9',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'sccache',
depType: 'uses-with',
packageName: 'mozilla/sccache',
},
],
},
{
step: {
uses: 'hashicorp/setup-packer@v3',
with: { version: 'v1.16.0' },
},
expected: [
{
currentValue: 'v1.16.0',
datasource: 'github-releases',
depName: 'packer',
depType: 'uses-with',
packageName: 'hashicorp/packer',
},
],
},
{
step: {
uses: 'hashicorp/setup-packer@v3',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'packer',
depType: 'uses-with',
packageName: 'hashicorp/packer',
},
],
},
{
step: {
uses: 'dagger/dagger-for-github@v8',
with: { version: 'v0.21.9' },
},
expected: [
{
currentValue: 'v0.21.9',
datasource: 'github-releases',
depName: 'dagger',
depType: 'uses-with',
packageName: 'dagger/dagger',
},
],
},
{
step: {
uses: 'dagger/dagger-for-github@v8',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'dagger',
depType: 'uses-with',
packageName: 'dagger/dagger',
},
],
},
{
step: {
uses: 'foundry-rs/foundry-toolchain@v1',
with: { version: 'v1.8.1' },
},
expected: [
{
currentValue: 'v1.8.1',
datasource: 'github-releases',
depName: 'foundry',
depType: 'uses-with',
packageName: 'foundry-rs/foundry',
},
],
},
{
step: {
uses: 'foundry-rs/foundry-toolchain@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'foundry',
depType: 'uses-with',
packageName: 'foundry-rs/foundry',
},
],
},
{
step: {
uses: 'dart-lang/setup-dart@v1',
with: { sdk: '3.5.3' },
},
expected: [
{
currentValue: '3.5.3',
datasource: 'dart-version',
depName: 'dart',
depType: 'uses-with',
packageName: 'dart-lang/sdk',
},
],
},
{
// an SDK release version installs the latest patch release of that
// version, so it must keep its precision
step: {
uses: 'dart-lang/setup-dart@v1',
with: { sdk: '3.5' },
},
expected: [
{
currentValue: '3.5',
datasource: 'dart-version',
depName: 'dart',
depType: 'uses-with',
packageName: 'dart-lang/sdk',
versioning: 'semver-partial',
},
],
},
{
step: {
uses: 'dart-lang/setup-dart@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'dart-version',
depName: 'dart',
depType: 'uses-with',
packageName: 'dart-lang/sdk',
},
],
},
{
// `'stable'` is a valid, documented channel name (and the action's
// own default), but not one we can pin/bump — the value is passed
// through as-is, and the versioning layer skips proposing an update
// since it isn't a real version
step: {
uses: 'dart-lang/setup-dart@v1',
with: { sdk: 'stable' },
},
expected: [
{
currentValue: 'stable',
datasource: 'dart-version',
depName: 'dart',
depType: 'uses-with',
packageName: 'dart-lang/sdk',
},
],
},
{
// the action resolves `version` with node's semver package, so a
// partial version is a range rather than a pinned version
step: {
uses: 'julia-actions/setup-julia@v2',
with: { version: '1.10' },
},
expected: [
{
currentValue: '1.10',
datasource: 'github-releases',
depName: 'julia',
depType: 'uses-with',
packageName: 'JuliaLang/julia',
versioning: 'npm',
},
],
},
{
step: {
uses: 'julia-actions/setup-julia@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'julia',
depType: 'uses-with',
packageName: 'JuliaLang/julia',
},
],
},
{
// `'lts'`/`'pre'` are valid, documented values, but not ones we can
// pin/bump — the value is passed through as-is, and the versioning
// layer skips proposing an update since it isn't a real version
step: {
uses: 'julia-actions/setup-julia@v2',
with: { version: 'lts' },
},
expected: [
{
currentValue: 'lts',
datasource: 'github-releases',
depName: 'julia',
depType: 'uses-with',
packageName: 'JuliaLang/julia',
},
],
},
{
step: {
uses: 'cargo-bins/cargo-binstall@v1',
with: { version: 'v1.23.0' },
},
expected: [
{
currentValue: 'v1.23.0',
datasource: 'github-releases',
depName: 'cargo-bins/cargo-binstall',
depType: 'uses-with',
packageName: 'cargo-bins/cargo-binstall',
},
],
},
{
step: {
uses: 'cargo-bins/cargo-binstall@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'cargo-bins/cargo-binstall',
depType: 'uses-with',
packageName: 'cargo-bins/cargo-binstall',
},
],
},
{
step: {
uses: 'PyO3/maturin-action@v1',
with: { 'maturin-version': 'v1.7.4' },
},
expected: [
{
currentValue: 'v1.7.4',
datasource: 'pypi',
depName: 'maturin',
depType: 'uses-with',
packageName: 'maturin',
},
],
},
{
step: {
uses: 'PyO3/maturin-action@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'pypi',
depName: 'maturin',
depType: 'uses-with',
packageName: 'maturin',
},
],
},
{
step: {
uses: 'cue-lang/setup-cue@v1',
with: { version: 'v0.11.0' },
},
expected: [
{
currentValue: 'v0.11.0',
datasource: 'github-releases',
depName: 'cue',
depType: 'uses-with',
packageName: 'cue-lang/cue',
},
],
},
{
step: {
uses: 'cue-lang/setup-cue@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'cue',
depType: 'uses-with',
packageName: 'cue-lang/cue',
},
],
},
{
step: {
uses: 'superfly/flyctl-actions/setup-flyctl@master',
with: { version: '0.4.102' },
},
expected: [
{
currentValue: '0.4.102',
datasource: 'github-releases',
depName: 'flyctl',
depType: 'uses-with',
packageName: 'superfly/flyctl',
},
],
},
{
step: {
uses: 'superfly/flyctl-actions/setup-flyctl@master',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'flyctl',
depType: 'uses-with',
packageName: 'superfly/flyctl',
},
],
},
{
step: {
uses: 'graalvm/setup-graalvm@v1',
with: { 'java-version': '21', version: '21.0.1' },
},
expected: [
{
currentValue: '21',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
versioning: 'semver-partial',
},
{
currentValue: '21.0.1',
datasource: 'github-releases',
depName: 'graalvm/graalvm-ce-builds',
depType: 'uses-with',
extractVersion: '^(?:jdk|graal)-(?<version>.+)$',
packageName: 'graalvm/graalvm-ce-builds',
},
],
},
{
// the repo alternates between `jdk-` and `graal-` tag prefixes for
// current-era releases, so both must be matched
step: {
uses: 'graalvm/setup-graalvm@v1',
with: { version: '25.3.4.1' },
},
expected: [
{
currentValue: '25.3.4.1',
datasource: 'github-releases',
depName: 'graalvm/graalvm-ce-builds',
depType: 'uses-with',
extractVersion: '^(?:jdk|graal)-(?<version>.+)$',
packageName: 'graalvm/graalvm-ce-builds',
},
],
},
{
// most workflows only pin one of the 2 possible inputs
step: {
uses: 'graalvm/setup-graalvm@v1',
with: { 'java-version': '21' },
},
expected: [
{
currentValue: '21',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
versioning: 'semver-partial',
},
],
},
{
// neither input is set, so no deps should be extracted at all (rather
// than emitting skipped deps for inputs no one set)
step: {
uses: 'graalvm/setup-graalvm@v1',
with: {},
},
expected: [],
},
{
// `java-version` is a range here too, so it must keep its precision
step: {
uses: 'graalvm/setup-graalvm@v1',
with: { 'java-version': '21.0.2' },
},
expected: [
{
currentValue: '21.0.2',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
},
],
},
{
// `dev` and `latest-ea` are documented `java-version`s that we have no
// version to bump them to
step: {
uses: 'graalvm/setup-graalvm@v1',
with: { 'java-version': 'latest-ea' },
},
expected: [
{
currentValue: 'latest-ea',
datasource: 'java-version',
depName: 'java-jdk',
depType: 'uses-with',
packageName: 'java-jdk',
skipStage: 'extract',
skipReason: 'unsupported-version',
},
],
},
{
step: {
uses: 'moonrepo/setup-toolchain@v1',
with: { 'moon-version': '1.35.0', 'proto-version': '0.51.4' },
},
expected: [
{
currentValue: '1.35.0',
datasource: 'github-releases',
depName: 'moonrepo/moon',
depType: 'uses-with',
packageName: 'moonrepo/moon',
},
{
currentValue: '0.51.4',
datasource: 'github-releases',
depName: 'moonrepo/proto',
depType: 'uses-with',
packageName: 'moonrepo/proto',
},
],
},
{
// most workflows only pin one of the 2 possible inputs
step: {
uses: 'moonrepo/setup-toolchain@v1',
with: { 'moon-version': '1.35.0' },
},
expected: [
{
currentValue: '1.35.0',
datasource: 'github-releases',
depName: 'moonrepo/moon',
depType: 'uses-with',
packageName: 'moonrepo/moon',
},
],
},
{
// neither input is set, so no deps should be extracted at all (rather
// than emitting skipped deps for inputs no one set)
step: {
uses: 'moonrepo/setup-toolchain@v1',
with: {},
},
expected: [],
},
{
step: {
uses: 'moonrepo/setup-rust@v1',
with: { channel: 'stable' },
},
expected: [
{
currentValue: 'stable',
datasource: 'rust-version',
depName: 'rust',
depType: 'uses-with',
packageName: 'rust',
},
],
},
{
step: {
uses: 'moonrepo/setup-rust@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'rust-version',
depName: 'rust',
depType: 'uses-with',
packageName: 'rust',
},
],
},
{
step: {
uses: 'crystal-lang/install-crystal@v1',
with: { crystal: '1.21.0', shards: 'v0.20.0' },
},
expected: [
{
currentValue: '1.21.0',
datasource: 'github-releases',
depName: 'crystal-lang/crystal',
depType: 'uses-with',
packageName: 'crystal-lang/crystal',
},
{
currentValue: 'v0.20.0',
datasource: 'github-releases',
depName: 'crystal-lang/shards',
depType: 'uses-with',
packageName: 'crystal-lang/shards',
},
],
},
{
// most workflows only pin one of the 2 possible inputs
step: {
uses: 'crystal-lang/install-crystal@v1',
with: { crystal: '1.21.0' },
},
expected: [
{
currentValue: '1.21.0',
datasource: 'github-releases',
depName: 'crystal-lang/crystal',
depType: 'uses-with',
packageName: 'crystal-lang/crystal',
},
],
},
{
// a partial version installs the latest patch of that release series,
// so it must keep its precision
step: {
uses: 'crystal-lang/install-crystal@v1',
with: { crystal: '1.21', shards: '0.20' },
},
expected: [
{
currentValue: '1.21',
datasource: 'github-releases',
depName: 'crystal-lang/crystal',
depType: 'uses-with',
packageName: 'crystal-lang/crystal',
versioning: 'semver-partial',
},
{
currentValue: '0.20',
datasource: 'github-releases',
depName: 'crystal-lang/shards',
depType: 'uses-with',
packageName: 'crystal-lang/shards',
versioning: 'semver-partial',
},
],
},
{
// neither input is set, so no deps should be extracted at all (rather
// than emitting skipped deps for inputs no one set)
step: {
uses: 'crystal-lang/install-crystal@v1',
with: {},
},
expected: [],
},
{
step: {
uses: 'baptiste0928/cargo-install@v3',
with: { crate: 'cargo-hack', version: '0.6.14' },
},
expected: [
{
currentValue: '0.6.14',
datasource: 'crate',
depName: 'cargo-hack',
depType: 'uses-with',
packageName: 'cargo-hack',
},
],
},
{
step: {
uses: 'baptiste0928/cargo-install@v3',
with: { crate: 'cargo-hack', version: '' },
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'crate',
depName: 'cargo-hack',
depType: 'uses-with',
packageName: 'cargo-hack',
},
],
},
{
// `version` has a default (`'latest'`), so real workflows commonly
// omit it from `with:` entirely rather than passing an empty string
step: {
uses: 'baptiste0928/cargo-install@v3',
with: { crate: 'cargo-hack' },
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'crate',
depName: 'cargo-hack',
depType: 'uses-with',
packageName: 'cargo-hack',
},
],
},
{
step: {
uses: 'jwlawson/actions-setup-cmake@v2',
with: { 'cmake-version': '4.3.5' },
},
expected: [
{
currentValue: '4.3.5',
datasource: 'github-releases',
depName: 'cmake',
depType: 'uses-with',
packageName: 'Kitware/CMake',
versioning: 'npm',
},
],
},
{
// a wildcard version is a range, not a pinned version
step: {
uses: 'jwlawson/actions-setup-cmake@v2',
with: { 'cmake-version': '4.3.x' },
},
expected: [
{
currentValue: '4.3.x',
datasource: 'github-releases',
depName: 'cmake',
depType: 'uses-with',
packageName: 'Kitware/CMake',
versioning: 'npm',
},
],
},
{
step: {
uses: 'jwlawson/actions-setup-cmake@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'cmake',
depType: 'uses-with',
packageName: 'Kitware/CMake',
versioning: 'npm',
},
],
},
{
step: {
uses: 'swift-actions/setup-swift@v2',
with: { 'swift-version': '6.3.3' },
},
expected: [
{
currentValue: '6.3.3',
datasource: 'github-releases',
depName: 'swift',
depType: 'uses-with',
packageName: 'swiftlang/swift',
},
],
},
{
// a partial `swift-version` resolves to the latest matching release,
// so it must keep its precision
step: {
uses: 'swift-actions/setup-swift@v2',
with: { 'swift-version': '6.3' },
},
expected: [
{
currentValue: '6.3',
datasource: 'github-releases',
depName: 'swift',
depType: 'uses-with',
packageName: 'swiftlang/swift',
versioning: 'semver-partial',
},
],
},
{
step: {
uses: 'swift-actions/setup-swift@v2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'swift',
depType: 'uses-with',
packageName: 'swiftlang/swift',
},
],
},
{
step: {
uses: 'WillAbides/setup-go-faster@v1',
with: { 'go-version': '1.23.4' },
},
expected: [
{
currentValue: '1.23.4',
datasource: 'github-releases',
depName: 'go',
depType: 'uses-with',
packageName: 'actions/go-versions',
versioning: 'npm',
},
],
},
{
step: {
uses: 'WillAbides/setup-go-faster@v1',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'github-releases',
depName: 'go',
depType: 'uses-with',
packageName: 'actions/go-versions',
versioning: 'npm',
},
],
},
{
step: {
uses: 'conda-incubator/setup-miniconda@v3',
with: { 'miniforge-version': '26.7.2-0', 'python-version': '3.12' },
},
expected: [
{
currentValue: '26.7.2-0',
datasource: 'github-releases',
depName: 'miniforge',
depType: 'uses-with',
packageName: 'conda-forge/miniforge',
},
{
currentValue: '3.12',
datasource: 'github-releases',
depName: 'python',
depType: 'uses-with',
packageName: 'actions/python-versions',
versioning: 'npm',
},
],
},
{
// most workflows only pin one of the 2 supported inputs (of the 6
// this action exposes overall)
step: {
uses: 'conda-incubator/setup-miniconda@v3',
with: { 'python-version': '3.12' },
},
expected: [
{
currentValue: '3.12',
datasource: 'github-releases',
depName: 'python',
depType: 'uses-with',
packageName: 'actions/python-versions',
versioning: 'npm',
},
],
},
{
// neither supported input is set, so no deps should be extracted at
// all (rather than emitting skipped deps for inputs no one set)
step: {
uses: 'conda-incubator/setup-miniconda@v3',
with: {},
},
expected: [],
},
{
step: {
uses: 'voidzero-dev/setup-vp@v1.17.0',
with: { version: '0.3.3' },
},
expected: [
{
currentValue: '0.3.3',
datasource: 'npm',
depName: 'vite-plus',
depType: 'uses-with',
packageName: 'vite-plus',
},
],
},
{
step: {
uses: 'voidzero-dev/setup-vp@v1.17.0',
with: { 'node-version': '22.15.0' },
},
expected: [
{
currentValue: '22.15.0',
datasource: 'node-version',
depName: 'node',
depType: 'uses-with',
packageName: 'node',
},
],
},
{
step: {
uses: 'voidzero-dev/setup-vp@v1.17.0',
with: { version: '0.3.3', 'node-version': '22.15.0' },
},
expected: [
{
currentValue: '0.3.3',
datasource: 'npm',
depName: 'vite-plus',
depType: 'uses-with',
packageName: 'vite-plus',
},
{
currentValue: '22.15.0',
datasource: 'node-version',
depName: 'node',
depType: 'uses-with',
packageName: 'node',
},
],
},
{
step: {
uses: 'voidzero-dev/setup-vp@v1.17.0',
with: {},
},
expected: [],
},
])('extract from $step.uses', async ({ step, expected }) => {
const yamlContent = yaml.dump({ jobs: { build: { steps: [step] } } });
const res = await extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps.filter((pkg) => pkg.depType !== 'action')).toMatchObject(
expected,
);
});
describe('actions.lock', () => {
beforeEach(() => {
// the lock file is read through the memory cache, which production initialises per repository
memCache.init();
});
const workflow = codeBlock`
jobs:
build:
steps:
- uses: actions/checkout@v4.3.1
- uses: docker://alpine:3.20
`;
const lockfile = codeBlock`
version: 'v0.0.2'
workflows:
'.github/workflows/ci.yml':
- 'actions/checkout@v4.3.1'
`;
it('marks digests as externally managed for an onboarded workflow', async () => {
fs.readLocalFile.mockResolvedValueOnce(lockfile);
const res = await extractPackageFile(
workflow,
'.github/workflows/ci.yml',
);
// `gh actions-lock` strips an inline pin back out, so the lookup must not offer one
expect(res?.deps).toMatchObject([
{ depName: 'actions/checkout', digestManagedExternally: true },
{ depName: 'alpine' },
]);
// the lockfile only records `OWNER/REPO@REF` pins, so a `docker://` image is still ours to pin
expect(res?.deps[1]).not.toHaveProperty('digestManagedExternally');
});
it('marks a reusable workflow call, which the lockfile records too', async () => {
fs.readLocalFile.mockResolvedValueOnce(lockfile);
const res = await extractPackageFile(
codeBlock`
jobs:
release:
uses: some-org/some-repo/.github/workflows/release.yml@v1
`,
'.github/workflows/ci.yml',
);
expect(res?.deps).toMatchObject([
{ depType: 'workflow', digestManagedExternally: true },
]);
});
it('leaves a workflow which is not onboarded alone', async () => {
fs.readLocalFile.mockResolvedValueOnce(lockfile);
// a repository can deliberately keep workflows out of the lockfile
const res = await extractPackageFile(
workflow,
'.github/workflows/not-onboarded.yml',
);
expect(res?.deps[0]).not.toHaveProperty('digestManagedExternally');
});
it.each`
packageFile
${'workflow-templates/ci.yml'}
${'.gitea/workflows/ci.yml'}
${'.forgejo/workflows/ci.yml'}
${'.gitea/actions/build/action.yml'}
`(
'leaves $packageFile alone, as the tool never reads it',
async ({ packageFile }: { packageFile: string }) => {
fs.readLocalFile.mockResolvedValueOnce(lockfile);
const res = await extractPackageFile(workflow, packageFile);
expect(res?.deps[0]).not.toHaveProperty('digestManagedExternally');
},
);
it('marks a local composite action, which the lockfile does not key', async () => {
fs.readLocalFile.mockResolvedValueOnce(lockfile);
// composite actions are transitive dependencies of onboarded workflows, so the tool rewrites them too
const res = await extractPackageFile(
workflow,
'.github/actions/build/action.yml',
);
expect(res?.deps[0].digestManagedExternally).toBe(true);
});
it('marks a local composite action when the lockfile has onboarded nothing', async () => {
fs.readLocalFile.mockResolvedValueOnce(
"version: 'v0.0.2'\nworkflows: {}",
);
// a composite action isn't keyed by `workflows:`, so it's still managed even when that key is missing entirely
const res = await extractPackageFile(
workflow,
'.github/actions/build/action.yml',
);
expect(res?.deps[0].digestManagedExternally).toBe(true);
});
it('leaves digests alone when there is no lockfile', async () => {
fs.readLocalFile.mockResolvedValueOnce(null);
const res = await extractPackageFile(
workflow,
'.github/workflows/ci.yml',
);
expect(res?.deps[0]).not.toHaveProperty('digestManagedExternally');
});
it('leaves the digests to the tool when the lockfile cannot be parsed', async () => {
fs.readLocalFile.mockResolvedValueOnce('workflows: [a, b]');
const res = await extractPackageFile(
workflow,
'.github/workflows/ci.yml',
);
// `updateActionsLockfile` refuses to regenerate a lockfile it cannot parse, so pinning inline would leave the lockfile stale
expect(res?.deps[0].digestManagedExternally).toBe(true);
});
it('does not read the lockfile when nothing was extracted', async () => {
const res = await extractPackageFile('nothing: here', 'workflow.yml');
expect(res).toBeNull();
expect(fs.readLocalFile).not.toHaveBeenCalled();
});
it('reads the lockfile once for all package files', async () => {
fs.readLocalFile.mockResolvedValue(lockfile);
const [onboarded, notOnboarded] = await Promise.all([
extractPackageFile(workflow, '.github/workflows/ci.yml'),
extractPackageFile(workflow, '.github/workflows/not-onboarded.yml'),
]);
expect(onboarded?.deps[0].digestManagedExternally).toBe(true);
expect(notOnboarded?.deps[0]).not.toHaveProperty(
'digestManagedExternally',
);
expect(fs.readLocalFile).toHaveBeenCalledOnce();
});
});
});