Skip to content

Commit e983daa

Browse files
authored
feat(github-actions): Support digests for non-semver refs (renovatebot#40225)
* feat(github-actions): Support non-semver refs (tags and branches) * refactor(github-actions): drop ref= prefix, use bare token comments Address review feedback: use bare `# cargo-llvm-cov` instead of `# ref=cargo-llvm-cov` for non-semver pin comments, consistent with how version comments work (`# v4.0.0`). - Rename pinnedVersionRe -> pinTokenRe, add bareTokenRe fallback - Add `ref` field to CommentData, separate from pinnedVersion - Guard replaceString extension to only use ref for SHA-pinned actions - Remove redundant autoReplaceStringTemplate override - Deduplicate versionLikeRe (export from parse.ts) - Add datasource assertions to non-semver tests * refactor(github-actions): remove unreachable parseComment branch * test(github-actions): add coverage for non-version ref fallback
1 parent 264f813 commit e983daa

File tree

8 files changed

+143
-22
lines changed

8 files changed

+143
-22
lines changed

lib/modules/manager/github-actions/__fixtures__/workflow_4.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jobs:
1414
- uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 #v2.1.0
1515
- uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 #v2.1.0
1616
- uses: actions/checkout@1e204e # v2.1.0
17+
- uses: actions/checkout@1e204e # some-ref-name
1718
- uses: actions/checkout@01aecc#v2.1.0
1819
- uses: actions/checkout@689fcce700ae7ffc576f2b029b51b2ffb66d3abd # comment containing 2.1.0
1920
- uses: actions/checkout@689fcce700ae7ffc576f2b029b51b2ffb66d3abd # v2.1.0 additional comment

lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ exports[`modules/manager/github-actions/extract > extractPackageFile() > extract
66
"autoReplaceStringTemplate": "{{depName}}/shellcheck@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}",
77
"commitMessageTopic": "{{{depName}}} action",
88
"currentValue": "master",
9-
"datasource": "github-tags",
9+
"datasource": "github-digest",
1010
"depName": "actions/bin",
1111
"depType": "action",
1212
"replaceString": "actions/bin/shellcheck@master",
13-
"versioning": "docker",
13+
"versioning": "exact",
1414
},
1515
{
1616
"autoReplaceStringTemplate": "{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}",
@@ -122,11 +122,11 @@ exports[`modules/manager/github-actions/extract > extractPackageFile() > extract
122122
"autoReplaceStringTemplate": "{{depName}}/shellcheck@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}",
123123
"commitMessageTopic": "{{{depName}}} action",
124124
"currentValue": "master",
125-
"datasource": "github-tags",
125+
"datasource": "github-digest",
126126
"depName": "actions/bin",
127127
"depType": "action",
128128
"replaceString": "actions/bin/shellcheck@master",
129-
"versioning": "docker",
129+
"versioning": "exact",
130130
},
131131
{
132132
"autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
@@ -142,11 +142,11 @@ exports[`modules/manager/github-actions/extract > extractPackageFile() > extract
142142
"autoReplaceStringTemplate": "{{depName}}/cli@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}",
143143
"commitMessageTopic": "{{{depName}}} action",
144144
"currentValue": "master",
145-
"datasource": "github-tags",
145+
"datasource": "github-digest",
146146
"depName": "actions/docker",
147147
"depType": "action",
148148
"replaceString": "actions/docker/cli@master",
149-
"versioning": "docker",
149+
"versioning": "exact",
150150
},
151151
{
152152
"autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",

lib/modules/manager/github-actions/extract.spec.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ describe('modules/manager/github-actions/extract', () => {
7070
expect(res?.deps).toMatchSnapshot();
7171
expect(
7272
res?.deps.filter((d) => d.datasource === 'github-tags'),
73-
).toHaveLength(8);
73+
).toHaveLength(7);
74+
expect(
75+
res?.deps.filter((d) => d.datasource === 'github-digest'),
76+
).toHaveLength(1);
7477
});
7578

7679
it('use github.com as registry when no settings provided', () => {
@@ -429,6 +432,11 @@ describe('modules/manager/github-actions/extract', () => {
429432
currentValue: 'v2.1.0',
430433
replaceString: 'actions/checkout@1e204e # v2.1.0',
431434
},
435+
{
436+
currentDigestShort: '1e204e',
437+
currentValue: 'some-ref-name',
438+
replaceString: 'actions/checkout@1e204e # some-ref-name',
439+
},
432440
{
433441
currentValue: '01aecc#v2.1.0',
434442
replaceString: 'actions/checkout@01aecc#v2.1.0',
@@ -473,6 +481,49 @@ describe('modules/manager/github-actions/extract', () => {
473481
expect(res!.deps[14]).not.toHaveProperty('skipReason');
474482
});
475483

484+
it('extracts non-semver ref automatically', () => {
485+
const res = extractPackageFile(
486+
`
487+
jobs:
488+
build:
489+
steps:
490+
- uses: taiki-e/install-action@cargo-llvm-cov
491+
`,
492+
'workflow.yml',
493+
);
494+
expect(res?.deps[0]).toMatchObject({
495+
depName: 'taiki-e/install-action',
496+
currentValue: 'cargo-llvm-cov',
497+
datasource: 'github-digest',
498+
versioning: 'exact',
499+
autoReplaceStringTemplate:
500+
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
501+
});
502+
});
503+
504+
it('extracts pinned non-semver ref with digest', () => {
505+
const res = extractPackageFile(
506+
`
507+
jobs:
508+
build:
509+
steps:
510+
- uses: taiki-e/install-action@4b1248585248751e3b12fd020cf7ac91540ca09c # cargo-llvm-cov
511+
`,
512+
'workflow.yml',
513+
);
514+
expect(res?.deps[0]).toMatchObject({
515+
depName: 'taiki-e/install-action',
516+
currentValue: 'cargo-llvm-cov',
517+
currentDigest: '4b1248585248751e3b12fd020cf7ac91540ca09c',
518+
datasource: 'github-digest',
519+
versioning: 'exact',
520+
replaceString:
521+
'taiki-e/install-action@4b1248585248751e3b12fd020cf7ac91540ca09c # cargo-llvm-cov',
522+
autoReplaceStringTemplate:
523+
'{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}',
524+
});
525+
});
526+
476527
it('extracts actions with fqdn', () => {
477528
const res = extractPackageFile(
478529
codeBlock`

lib/modules/manager/github-actions/extract.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { detectPlatform } from '../../../util/common.ts';
55
import { newlineRegex, regEx } from '../../../util/regex.ts';
66
import { ForgejoTagsDatasource } from '../../datasource/forgejo-tags/index.ts';
77
import { GiteaTagsDatasource } from '../../datasource/gitea-tags/index.ts';
8+
import { GithubDigestDatasource } from '../../datasource/github-digest/index.ts';
89
import { GithubReleasesDatasource } from '../../datasource/github-releases/index.ts';
910
import { GithubRunnersDatasource } from '../../datasource/github-runners/index.ts';
1011
import { GithubTagsDatasource } from '../../datasource/github-tags/index.ts';
1112
import * as dockerVersioning from '../../versioning/docker/index.ts';
13+
import * as exactVersioning from '../../versioning/exact/index.ts';
1214
import * as nodeVersioning from '../../versioning/node/index.ts';
1315
import * as npmVersioning from '../../versioning/npm/index.ts';
1416
import { getDep } from '../dockerfile/extract.ts';
@@ -19,7 +21,7 @@ import type {
1921
} from '../types.ts';
2022
import { CommunityActions } from './community.ts';
2123
import type { DockerReference, RepositoryReference } from './parse.ts';
22-
import { isSha, isShortSha, parseUsesLine } from './parse.ts';
24+
import { isSha, isShortSha, parseUsesLine, versionLikeRe } from './parse.ts';
2325
import type { Steps } from './schema.ts';
2426
import { Workflow } from './schema.ts';
2527

@@ -84,7 +86,6 @@ function extractRepositoryAction(
8486
const dep: PackageDependency = {
8587
depName,
8688
commitMessageTopic: '{{{depName}}} action',
87-
datasource: GithubTagsDatasource.id,
8889
versioning: dockerVersioning.id,
8990
depType: 'action',
9091
replaceString: valueString,
@@ -99,10 +100,13 @@ function extractRepositoryAction(
99100
}
100101

101102
// Extend replaceString to include relevant comment portions:
102-
// - Pinned version: include only up to the version (truncate trailing text)
103+
// - Pinned version or ref: include only up to the matched token (truncate trailing text)
103104
// - Ratchet exclude: include the full comment to preserve the marker
105+
const pinComment =
106+
commentData.pinnedVersion ??
107+
(isSha(ref) || isShortSha(ref) ? commentData.ref : undefined);
104108
if (
105-
commentData.pinnedVersion &&
109+
pinComment &&
106110
!is.undefined(commentData.index) &&
107111
!is.undefined(commentData.matchedString)
108112
) {
@@ -117,15 +121,24 @@ function extractRepositoryAction(
117121
}
118122

119123
if (isSha(ref)) {
120-
dep.currentValue = commentData.pinnedVersion;
124+
dep.currentValue = commentData.pinnedVersion ?? commentData.ref;
121125
dep.currentDigest = ref;
122126
} else if (isShortSha(ref)) {
123-
dep.currentValue = commentData.pinnedVersion;
127+
dep.currentValue = commentData.pinnedVersion ?? commentData.ref;
124128
dep.currentDigestShort = ref;
125129
} else {
126130
dep.currentValue = ref;
127131
}
128132

133+
const isVersionLike =
134+
dep.currentValue && versionLikeRe.test(dep.currentValue);
135+
if (!dep.datasource && dep.currentValue && !isVersionLike) {
136+
dep.datasource = GithubDigestDatasource.id;
137+
dep.versioning = exactVersioning.id;
138+
}
139+
140+
dep.datasource ??= GithubTagsDatasource.id;
141+
129142
return dep;
130143
}
131144

lib/modules/manager/github-actions/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Category } from '../../../constants/index.ts';
22
import { GiteaTagsDatasource } from '../../datasource/gitea-tags/index.ts';
3+
import { GithubDigestDatasource } from '../../datasource/github-digest/index.ts';
34
import { GithubRunnersDatasource } from '../../datasource/github-runners/index.ts';
45
import { GithubTagsDatasource } from '../../datasource/github-tags/index.ts';
56

@@ -18,6 +19,7 @@ export const defaultConfig = {
1819

1920
export const supportedDatasources = [
2021
GiteaTagsDatasource.id,
21-
GithubTagsDatasource.id,
22+
GithubDigestDatasource.id,
2223
GithubRunnersDatasource.id,
24+
GithubTagsDatasource.id,
2325
];

lib/modules/manager/github-actions/parse.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,28 @@ describe('modules/manager/github-actions/parse', () => {
245245
pinnedVersion: 'node/v20',
246246
});
247247
});
248+
249+
it('parses bare non-semver ref', () => {
250+
const result = parseComment(' cargo-llvm-cov');
251+
expect(result).toEqual({
252+
index: 0,
253+
matchedString: ' cargo-llvm-cov',
254+
ref: 'cargo-llvm-cov',
255+
});
256+
});
257+
258+
it('parses bare branch name', () => {
259+
const result = parseComment(' main');
260+
expect(result).toEqual({
261+
index: 0,
262+
matchedString: ' main',
263+
ref: 'main',
264+
});
265+
});
266+
267+
it('ignores multi-word comments', () => {
268+
expect(parseComment('do not update')).toEqual({});
269+
});
248270
});
249271

250272
describe('parseQuote', () => {

lib/modules/manager/github-actions/parse.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,28 +194,28 @@ export function parseActionReference(uses: string): ActionReference | null {
194194

195195
export interface CommentData {
196196
pinnedVersion?: string;
197+
ref?: string;
197198
ratchetExclude?: boolean;
198199
matchedString?: string;
199200
index?: number;
200201
}
201202

202-
// Matches version strings with optional prefixes, e.g.:
203-
// - "@v1.2.3", "v1.2.3", "1.2.3"
204-
// - "renovate: pin @v1.2.3", "tag=v1.2.3"
205-
// - "ratchet:owner/repo@v1.2.3"
206-
// - "stable/v1.2.3", "stable-v1.2.3"
207-
const pinnedVersionRe = regEx(
203+
const pinTokenRe = regEx(
208204
/^\s*(?:(?:renovate\s*:\s*)?(?:pin\s+|tag\s*=\s*)?|(?:ratchet:[\w-]+\/[.\w-]+))?@?(?<version>([\w-]*[-/])?v?\d+(?:\.\d+(?:\.\d+)?)?)/,
209205
);
210206

207+
export const versionLikeRe = regEx(/^v?\d+/);
208+
209+
const bareTokenRe = regEx(/^\s*(?<token>\S+)\s*$/);
210+
211211
export function parseComment(commentBody: string): CommentData {
212212
const trimmed = commentBody.trim();
213213
if (trimmed === 'ratchet:exclude') {
214214
return { ratchetExclude: true };
215215
}
216216

217217
// We use commentBody (with leading spaces) to get the correct index relative to the comment start
218-
const match = pinnedVersionRe.exec(commentBody);
218+
const match = pinTokenRe.exec(commentBody);
219219
if (match?.groups?.version) {
220220
return {
221221
pinnedVersion: match.groups.version,
@@ -224,6 +224,15 @@ export function parseComment(commentBody: string): CommentData {
224224
};
225225
}
226226

227+
const bareMatch = bareTokenRe.exec(commentBody);
228+
if (bareMatch?.groups?.token) {
229+
return {
230+
ref: bareMatch.groups.token,
231+
matchedString: bareMatch[0],
232+
index: bareMatch.index,
233+
};
234+
}
235+
227236
return {};
228237
}
229238

@@ -290,7 +299,7 @@ export function parseUsesLine(line: string): ParsedUsesLine | null {
290299
);
291300

292301
const { value, quote } = parseQuote(rawValuePart);
293-
// commentPart always starts with '#' since we found ' #' and sliced after the space
302+
// commentPart always starts with '#' (see commentIndex search above)
294303
const cleanCommentBody = commentPart.slice(1);
295304

296305
return {

lib/modules/manager/github-actions/readme.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,29 @@ If you want to automatically pin action digests add the `helpers:pinGitHubAction
3939
}
4040
```
4141

42+
### Non-semver refs (branches and feature tags)
43+
44+
Renovate supports GitHub Actions that reference non-semver refs like branch names (`main`, `master`) or feature-oriented tags (`cargo-llvm-cov`).
45+
46+
When the action reference doesn't look like a version number (i.e., doesn't match `/^v?\d+/`), Renovate routes to the `github-digest` datasource which fetches both tags and branches.
47+
Since these refs have no version ordering, only digest pinning updates are supported.
48+
49+
**Routing logic:**
50+
51+
- `actions/checkout@v4.2.0` → `github-tags` datasource (version updates)
52+
- `actions/checkout@v4` → `github-tags` datasource (version updates)
53+
- `taiki-e/install-action@cargo-llvm-cov` → `github-digest` datasource (digest pinning only)
54+
- `actions/checkout@main` → `github-digest` datasource (digest pinning only)
55+
56+
When pinning, Renovate adds a comment to preserve the original ref:
57+
58+
```yaml
59+
- uses: taiki-e/install-action@d8c10dae823f48238abff23fee4146b448aed2f1 # cargo-llvm-cov
60+
```
61+
62+
Non-semver ref support is currently limited to GitHub-hosted actions.
63+
Gitea and Forgejo support the same ref types, but Renovate does not yet handle them for these platforms.
64+
4265
### Non-support of Variables
4366

4467
Renovate ignores any GitHub runners which are configured in variables.

0 commit comments

Comments
 (0)