feat(github-actions): add some community action support (#34791)

Co-authored-by: Michael Kriese <michael.kriese@gmx.de>
This commit is contained in:
Trim21
2025-08-08 00:17:21 +08:00
committed by GitHub
parent 26e465cccc
commit ff290b2ed3
5 changed files with 395 additions and 1 deletions
@@ -0,0 +1,144 @@
import { z } from 'zod';
import type { SkipReason, StageName } from '../../../types';
import { escapeRegExp, regEx } from '../../../util/regex';
import { GithubReleasesDatasource } from '../../datasource/github-releases';
import { NpmDatasource } from '../../datasource/npm';
import { PypiDatasource } from '../../datasource/pypi';
import * as condaVersioning from '../../versioning/conda';
import * as npmVersioning from '../../versioning/npm';
import * as pep440versioning from '../../versioning/pep440';
import type { PackageDependency } from '../types';
function matchAction(action: string): z.Schema {
return z
.string()
.regex(regEx(`(?:https?://[^/]+/)?${escapeRegExp(action)}(?:@.+)?$`));
}
const SetupUVSchema = z
.object({
// https://github.com/astral-sh/setup-uv
uses: matchAction('astral-sh/setup-uv'),
with: z.object({ version: z.string().optional() }),
})
.transform(({ with: val }): PackageDependency => {
let skipStage: StageName | undefined;
let skipReason: SkipReason | undefined;
if (!val.version) {
skipStage = 'extract';
skipReason = 'unspecified-version';
}
return {
datasource: GithubReleasesDatasource.id,
depName: 'astral-sh/uv',
versioning: npmVersioning.id,
packageName: 'astral-sh/uv',
...(skipStage && { skipStage }),
...(skipReason && { skipReason }),
currentValue: val.version,
depType: 'uses-with',
};
});
const SetupPnpmSchema = z
.object({
uses: matchAction('pnpm/action-setup'),
with: z.object({
version: z.string().optional(),
}),
})
.transform(({ with: val }): PackageDependency => {
let skipStage: StageName | undefined;
let skipReason: SkipReason | undefined;
if (!val.version) {
skipStage = 'extract';
skipReason = 'unspecified-version';
}
return {
datasource: NpmDatasource.id,
depName: 'pnpm',
versioning: npmVersioning.id,
packageName: 'pnpm',
...(skipStage && { skipStage }),
...(skipReason && { skipReason }),
currentValue: val.version,
depType: 'uses-with',
};
});
const SetupPDMSchema = z
.object({
uses: matchAction('pdm-project/setup-pdm'),
with: z.object({ version: z.string().optional() }),
})
.transform(({ with: val }): PackageDependency => {
let skipStage: StageName | undefined;
let skipReason: SkipReason | undefined;
if (!val.version) {
skipStage = 'extract';
skipReason = 'unspecified-version';
}
return {
datasource: PypiDatasource.id,
depName: 'pdm',
versioning: pep440versioning.id,
packageName: 'pdm',
currentValue: val.version,
...(skipStage && { skipStage }),
...(skipReason && { skipReason }),
depType: 'uses-with',
};
});
const InstallBinarySchema = z
.object({
uses: z.union([
matchAction('jaxxstorm/action-install-gh-release'),
matchAction('sigoden/install-binary'),
]),
with: z.object({ repo: z.string(), tag: z.string() }),
})
.transform(({ with: val }): PackageDependency => {
return {
datasource: GithubReleasesDatasource.id,
depName: val.repo,
packageName: val.repo,
currentValue: val.tag,
depType: 'uses-with',
};
});
const SetupPixiSchema = z
.object({
uses: matchAction('prefix-dev/setup-pixi'),
with: z.object({ 'pixi-version': z.string() }),
})
.transform(({ with: val }): PackageDependency => {
return {
datasource: GithubReleasesDatasource.id,
versioning: condaVersioning.id,
depName: 'prefix-dev/pixi',
packageName: 'prefix-dev/pixi',
currentValue: val['pixi-version'],
depType: 'uses-with',
};
});
/**
* schema here should match the whole step,
* there may be some actions use env as arguments version.
*
* each type should return `PackageDependency | undefined`
*/
export const CommunityActions = z.union([
InstallBinarySchema,
SetupPDMSchema,
SetupPixiSchema,
SetupPnpmSchema,
SetupUVSchema,
]);
@@ -1,5 +1,6 @@
import { codeBlock } from 'common-tags';
import { GlobalConfig } from '../../../config/global';
import * as yaml from '../../../util/yaml';
import { extractPackageFile } from '.';
import { Fixtures } from '~test/fixtures';
@@ -852,4 +853,233 @@ describe('modules/manager/github-actions/extract', () => {
expect(extractPackageFile(yamlContent, 'action.yml')).toBeNull();
});
});
it.each([
{
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',
versioning: 'npm',
},
],
},
{
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',
versioning: 'npm',
},
],
},
{
step: {
name: 'Pinning a minor version of uv',
uses: 'https://github.com/astral-sh/setup-uv@v5',
with: {
version: '0.4.x',
},
},
expected: [
{
currentValue: '0.4.x',
datasource: 'github-releases',
depName: 'astral-sh/uv',
depType: 'uses-with',
packageName: 'astral-sh/uv',
versioning: 'npm',
},
],
},
{
step: {
uses: 'https://github.com/astral-sh/setup-uv@v5',
with: {},
},
expected: [
{
datasource: 'github-releases',
depName: 'astral-sh/uv',
depType: 'uses-with',
skipStage: 'extract',
skipReason: 'unspecified-version',
packageName: 'astral-sh/uv',
versioning: 'npm',
},
],
},
{
step: {
uses: 'pnpm/action-setup@v4',
with: {
version: 'latest',
},
},
expected: [
{
currentValue: 'latest',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
versioning: 'npm',
},
],
},
{
step: {
uses: 'pnpm/action-setup@v4',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
versioning: 'npm',
},
],
},
{
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',
versioning: 'npm',
},
],
},
{
step: {
uses: 'pnpm/action-setup@v4',
with: {
version: '10.x',
},
},
expected: [
{
currentValue: '10.x',
datasource: 'npm',
depName: 'pnpm',
depType: 'uses-with',
packageName: 'pnpm',
versioning: 'npm',
},
],
},
{
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',
versioning: 'pep440',
},
],
},
{
step: {
uses: 'pdm-project/setup-pdm@v4.2',
with: {},
},
expected: [
{
skipStage: 'extract',
skipReason: 'unspecified-version',
datasource: 'pypi',
depName: 'pdm',
depType: 'uses-with',
packageName: 'pdm',
versioning: 'pep440',
},
],
},
{
step: {
uses: 'prefix-dev/setup-pixi@v0.8.3',
with: {
'pixi-version': 'v0.41.4',
},
},
expected: [
{
currentValue: 'v0.41.4',
datasource: 'github-releases',
depName: 'prefix-dev/pixi',
depType: 'uses-with',
packageName: 'prefix-dev/pixi',
versioning: 'conda',
},
],
},
])('extract from $step.uses', ({ step, expected }) => {
const yamlContent = yaml.dump({ jobs: { build: { steps: [step] } } });
const res = extractPackageFile(yamlContent, 'workflow.yml');
expect(res?.deps.filter((pkg) => pkg.depType !== 'action')).toMatchObject(
expected,
);
});
});
@@ -16,6 +16,7 @@ import type {
PackageDependency,
PackageFileContent,
} from '../types';
import { CommunityActions } from './community';
import type { Steps } from './schema';
import { WorkflowSchema } from './schema';
@@ -188,6 +189,12 @@ function extractSteps(
deps: PackageDependency<Record<string, any>>[],
): void {
for (const step of steps) {
const res = CommunityActions.safeParse(step);
if (res.success) {
deps.push(res.data);
continue;
}
for (const [action, versioning] of Object.entries(versionedActions)) {
const actionName = `actions/setup-${action}`;
if (step.uses === actionName || step.uses?.startsWith(`${actionName}@`)) {
@@ -75,3 +75,14 @@ For example, normally the `^` syntax is not used in `go` or `python`, but it's s
Depending on your use case, you may need to change `versioning` manually.
If you find a use case which you think Renovate could/should automatically detect and support without manual configuration, please raise a Discussion to suggest it.
### commonly used community actions
Renovate also supports some commonly used community actions:
- `astral-sh/setup-uv`
- `pnpm/action-setup`
- `pdm-project/setup-pdm`
- `jaxxstorm/action-install-gh-release`
- `sigoden/install-binary`
- `prefix-dev/setup-pixi`
+3 -1
View File
@@ -8,7 +8,9 @@ import {
const StepsSchema = z.object({
uses: z.string(),
with: LooseRecord(z.string()),
with: LooseRecord(
z.union([z.string(), z.number().transform((s) => s.toString())]),
),
});
export type Steps = z.infer<typeof StepsSchema>;