diff --git a/lib/modules/manager/github-actions/community.ts b/lib/modules/manager/github-actions/community.ts new file mode 100644 index 0000000000..2559bbc3fd --- /dev/null +++ b/lib/modules/manager/github-actions/community.ts @@ -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, +]); diff --git a/lib/modules/manager/github-actions/extract.spec.ts b/lib/modules/manager/github-actions/extract.spec.ts index 982f5ece56..435ed14ff6 100644 --- a/lib/modules/manager/github-actions/extract.spec.ts +++ b/lib/modules/manager/github-actions/extract.spec.ts @@ -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, + ); + }); }); diff --git a/lib/modules/manager/github-actions/extract.ts b/lib/modules/manager/github-actions/extract.ts index f08493c972..0029283cf1 100644 --- a/lib/modules/manager/github-actions/extract.ts +++ b/lib/modules/manager/github-actions/extract.ts @@ -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>[], ): 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}@`)) { diff --git a/lib/modules/manager/github-actions/readme.md b/lib/modules/manager/github-actions/readme.md index d6de2b09ad..81224806a0 100644 --- a/lib/modules/manager/github-actions/readme.md +++ b/lib/modules/manager/github-actions/readme.md @@ -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` diff --git a/lib/modules/manager/github-actions/schema.ts b/lib/modules/manager/github-actions/schema.ts index 12cbca5d04..fbb33c0d27 100644 --- a/lib/modules/manager/github-actions/schema.ts +++ b/lib/modules/manager/github-actions/schema.ts @@ -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;