Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: renovatebot/renovate
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 39.206.0
Choose a base ref
...
head repository: renovatebot/renovate
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 39.207.0
Choose a head ref
  • 2 commits
  • 10 files changed
  • 1 contributor

Commits on Mar 17, 2025

  1. feat(github-actions): Use schema for dependency extraction (#33584)

    zharinov authored Mar 17, 2025
    Copy the full SHA
    19b04ca View commit details
  2. feat(gitlabci): Use schema for dep extraction (#33586)

    zharinov authored Mar 17, 2025
    Copy the full SHA
    6dc2b37 View commit details
87 changes: 27 additions & 60 deletions lib/modules/manager/github-actions/extract.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import is from '@sindresorhus/is';
import { GlobalConfig } from '../../../config/global';
import { logger } from '../../../logger';
import { coerceArray, isNotNullOrUndefined } from '../../../util/array';
import { logger, withMeta } from '../../../logger';
import { detectPlatform } from '../../../util/common';
import { newlineRegex, regEx } from '../../../util/regex';
import { parseSingleYaml } from '../../../util/yaml';
import { GiteaTagsDatasource } from '../../datasource/gitea-tags';
import { GithubReleasesDatasource } from '../../datasource/github-releases';
import { GithubRunnersDatasource } from '../../datasource/github-runners';
@@ -18,7 +15,7 @@ import type {
PackageDependency,
PackageFileContent,
} from '../types';
import type { Workflow } from './types';
import { WorkflowJobsSchema } from './schema';

const dockerActionRe = regEx(/^\s+uses\s*: ['"]?docker:\/\/([^'"]+)\s*$/);
const actionRe = regEx(
@@ -138,18 +135,6 @@ function detectDatasource(registryUrl: string): PackageDependency {
};
}

function extractContainer(
container: unknown,
registryAliases: Record<string, string> | undefined,
): PackageDependency | undefined {
if (is.string(container)) {
return getDep(container, true, registryAliases);
} else if (is.plainObject(container) && is.string(container.image)) {
return getDep(container.image, true, registryAliases);
}
return undefined;
}

const runnerVersionRegex = regEx(
/^\s*(?<depName>[a-zA-Z]+)-(?<currentValue>[^\s]+)/,
);
@@ -182,17 +167,6 @@ function extractRunner(runner: string): PackageDependency | null {
return dependency;
}

function extractRunners(runner: unknown): PackageDependency[] {
const runners: string[] = [];
if (is.string(runner)) {
runners.push(runner);
} else if (is.array(runner, is.string)) {
runners.push(...runner);
}

return runners.map(extractRunner).filter(isNotNullOrUndefined);
}

function extractWithYAMLParser(
content: string,
packageFile: string,
@@ -201,52 +175,45 @@ function extractWithYAMLParser(
logger.trace('github-actions.extractWithYAMLParser()');
const deps: PackageDependency[] = [];

let pkg: Workflow;
try {
// TODO: use schema (#9610)
pkg = parseSingleYaml(content);
} catch (err) {
logger.debug(
{ packageFile, err },
'Failed to parse GitHub Actions Workflow YAML',
);
return [];
}
const jobs = withMeta({ packageFile }, () =>
WorkflowJobsSchema.parse(content),
);

for (const job of Object.values(pkg?.jobs ?? {})) {
const dep = extractContainer(job?.container, config.registryAliases);
if (dep) {
dep.depType = 'container';
deps.push(dep);
for (const job of jobs) {
if (job.container) {
const dep = getDep(job.container, true, config.registryAliases);
if (dep) {
dep.depType = 'container';
deps.push(dep);
}
}

for (const service of Object.values(job?.services ?? {})) {
const dep = extractContainer(service, config.registryAliases);
for (const service of job.services) {
const dep = getDep(service, true, config.registryAliases);
if (dep) {
dep.depType = 'service';
deps.push(dep);
}
}

deps.push(...extractRunners(job?.['runs-on']));
for (const runner of job['runs-on']) {
const dep = extractRunner(runner);
if (dep) {
deps.push(dep);
}
}

const actionsWithVersions: Record<string, Partial<PackageDependency>> = {
go: {
versioning: npmVersioning.id,
},
node: {
versioning: nodeVersioning.id,
},
python: {
versioning: npmVersioning.id,
},
const versionedActions: Record<string, string> = {
go: npmVersioning.id,
node: nodeVersioning.id,
python: npmVersioning.id,
// Not covered yet because they use different datasources/packageNames:
// - dotnet
// - java
};

for (const step of coerceArray(job?.steps)) {
for (const [action, actionData] of Object.entries(actionsWithVersions)) {
for (const step of job.steps) {
for (const [action, versioning] of Object.entries(versionedActions)) {
const actionName = `actions/setup-${action}`;
if (
step.uses === actionName ||
@@ -259,7 +226,7 @@ function extractWithYAMLParser(
datasource: GithubReleasesDatasource.id,
depName: action,
packageName: `actions/${action}-versions`,
...actionData,
versioning,
extractVersion: '^(?<version>\\d+\\.\\d+\\.\\d+)(-\\d+)?$', // Actions release tags are like 1.24.1-13667719799
currentValue,
depType: 'uses-with',
42 changes: 42 additions & 0 deletions lib/modules/manager/github-actions/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { z } from 'zod';
import {
LooseArray,
LooseRecord,
Yaml,
withDebugMessage,
} from '../../../util/schema-utils';

export const WorkflowJobsSchema = Yaml.pipe(
z.object({
jobs: LooseRecord(
z.object({
container: z
.union([
z.string(),
z.object({ image: z.string() }).transform((v) => v.image),
])
.optional()
.catch(undefined),
services: LooseRecord(
z.union([
z.object({ image: z.string() }).transform((v) => v.image),
z.string(),
]),
)
.catch({})
.transform((services) => Object.values(services)),
'runs-on': z
.union([z.string().transform((v) => [v]), z.array(z.string())])
.catch([]),
steps: LooseArray(
z.object({
uses: z.string(),
with: LooseRecord(z.string()),
}),
).catch([]),
}),
),
}),
)
.transform((v) => Object.values(v.jobs))
.catch(withDebugMessage([], 'Error parsing workflow'));
33 changes: 0 additions & 33 deletions lib/modules/manager/github-actions/types.ts

This file was deleted.

81 changes: 9 additions & 72 deletions lib/modules/manager/gitlabci-include/extract.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,9 @@
import is from '@sindresorhus/is';
import { GlobalConfig } from '../../../config/global';
import { logger } from '../../../logger';
import { regEx } from '../../../util/regex';
import { parseYaml } from '../../../util/yaml';
import { GitlabTagsDatasource } from '../../datasource/gitlab-tags';
import {
filterIncludeFromGitlabPipeline,
isGitlabIncludeProject,
isNonEmptyObject,
} from '../gitlabci/common';
import type {
GitlabInclude,
GitlabIncludeProject,
GitlabPipeline,
} from '../gitlabci/types';
import type { PackageDependency, PackageFileContent } from '../types';

function extractDepFromIncludeFile(
includeObj: GitlabIncludeProject,
): PackageDependency {
const dep: PackageDependency = {
datasource: GitlabTagsDatasource.id,
depName: includeObj.project,
depType: 'repository',
};
if (!includeObj.ref) {
dep.skipReason = 'unspecified-version';
return dep;
}
dep.currentValue = includeObj.ref;
return dep;
}

function getIncludeProjectsFromInclude(
includeValue: GitlabInclude[] | GitlabInclude,
): GitlabIncludeProject[] {
const includes = is.array(includeValue) ? includeValue : [includeValue];

// Filter out includes that dont have a file & project.
return includes.filter(isGitlabIncludeProject);
}

function getAllIncludeProjects(data: GitlabPipeline): GitlabIncludeProject[] {
// If Array, search each element.
if (is.array(data)) {
return (data as GitlabPipeline[])
.filter(isNonEmptyObject)
.map(getAllIncludeProjects)
.flat();
}

const childrenData = Object.values(filterIncludeFromGitlabPipeline(data))
.filter(isNonEmptyObject)
.map(getAllIncludeProjects)
.flat();

// Process include key.
if (data.include) {
childrenData.push(...getIncludeProjectsFromInclude(data.include));
}
return childrenData;
}
import { GitlabDocumentArray } from './schema';

export function extractPackageFile(
content: string,
@@ -73,22 +16,14 @@ export function extractPackageFile(
platform === 'gitlab' && endpoint
? [endpoint.replace(regEx(/\/api\/v4\/?/), '')]
: null;

try {
// TODO: use schema (#9610)
const docs = parseYaml<GitlabPipeline>(content, {
uniqueKeys: false,
});
for (const doc of docs) {
if (is.object(doc)) {
const includes = getAllIncludeProjects(doc);
for (const includeObj of includes) {
const dep = extractDepFromIncludeFile(includeObj);
if (registryUrls) {
dep.registryUrls = registryUrls;
}
deps.push(dep);
}
const docs = parseYaml(content, { uniqueKeys: false });
for (const dep of GitlabDocumentArray.parse(docs)) {
if (registryUrls) {
dep.registryUrls = registryUrls;
}
deps.push(dep);
}
} catch (err) /* istanbul ignore next */ {
if (err.stack?.startsWith('YAMLException:')) {
@@ -100,8 +35,10 @@ export function extractPackageFile(
logger.debug({ err, packageFile }, 'Error extracting GitLab CI includes');
}
}

if (!deps.length) {
return null;
}

return { deps };
}
52 changes: 52 additions & 0 deletions lib/modules/manager/gitlabci-include/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { z } from 'zod';
import { toArray } from '../../../util/array';
import { LooseArray } from '../../../util/schema-utils';
import { GitlabTagsDatasource } from '../../datasource/gitlab-tags';
import type { PackageDependency } from '../types';

const GitlabInclude = z
.object({
project: z.string(),
ref: z.string().optional().catch(undefined),
})
.transform(({ project, ref }) => {
const dep: PackageDependency = {
datasource: GitlabTagsDatasource.id,
depName: project,
depType: 'repository',
};

if (!ref) {
dep.skipReason = 'unspecified-version';
return dep;
}

dep.currentValue = ref;
return dep;
});

const GitlabIncludes = z
.union([GitlabInclude.transform(toArray), LooseArray(GitlabInclude)])
.catch([]);

const GitlabRecord = z.record(z.unknown()).transform((obj) => {
const { include, ...rest } = obj;
const children = Object.values(rest);
return { include, children };
});

export const GitlabDocument = z
.union([GitlabRecord.transform(toArray), LooseArray(GitlabRecord)])
.transform((docs): PackageDependency[] =>
docs
.map(({ include, children }) => {
const deps = GitlabIncludes.parse(include);
const childrenDeps = GitlabDocumentArray.parse(children);
return [...childrenDeps, ...deps];
})
.flat(),
);

export const GitlabDocumentArray = LooseArray(GitlabDocument)
.transform((x) => x.flat())
.catch([]);
Loading