From 11a86e53c71e1de2fcba71b9fa14b6ff3d96f557 Mon Sep 17 00:00:00 2001 From: Van Nguyen Date: Sat, 10 Oct 2026 23:06:35 +0700 Subject: [PATCH] fix(gitlab): keep Renovate off the images of the managed jobs Renovate reads .gitlab-ci.yml, and repokeeper's own jobs there name images with a version: renovate/renovate to the patch, node, the .NET SDK. Its first real run opened a merge request raising the renovate/renovate tag, which is an edit to a managed key: repokeeper check reports it and the next update takes it back, and a new one would follow every week. The generated renovate.json now disables updates of those images in .gitlab-ci.yml. A test builds the CI file of every fixture and fails when a managed job pins an image that is not on the list. On the pilot project Renovate then closed its merge request itself and no longer lists the managed images. --- docs/gitlab.md | 10 +++++- src/platforms/gitlab.ts | 16 ++++++++++ test/platforms-gitlab-stacks.test.ts | 46 ++++++++++++++++++++++++++++ test/platforms-gitlab.test.ts | 6 ++++ 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/docs/gitlab.md b/docs/gitlab.md index b529197..c22103d 100644 --- a/docs/gitlab.md +++ b/docs/gitlab.md @@ -77,7 +77,15 @@ Two jobs stay inactive until you set them up in the project's CI/CD settings: marked Protected is only visible on protected branches, so protect the default branch or leave the variable unprotected; otherwise the job silently stays away. - **`renovate`** needs a CI/CD variable `RENOVATE_TOKEN` (same scopes) and a pipeline schedule, - for example weekly. The schedule alone decides how often Renovate runs. + for example weekly. The schedule alone decides how often Renovate runs. It opens one merge + request for minor and patch updates, one per major, and an issue named Dependency Dashboard. + - Renovate leaves alone the images of repokeeper's own jobs in `.gitlab-ci.yml` (`node`, + `renovate/renovate`, the .NET SDK): the standard names their versions and `repokeeper update` + moves them. A job of your own in that file that uses one of these images is not updated + either. + - Changelogs in its merge requests come from github.com, which answers few requests without a + token. A CI/CD variable `GITHUB_COM_TOKEN` (a GitHub token with no scopes at all) lifts that + limit; without it Renovate works and says in the dashboard that release notes were skipped. ## Project settings: `repokeeper gitlab apply` diff --git a/src/platforms/gitlab.ts b/src/platforms/gitlab.ts index dc820ef..41ede17 100644 --- a/src/platforms/gitlab.ts +++ b/src/platforms/gitlab.ts @@ -37,6 +37,13 @@ const ciKey = (module: string, key: string, value: unknown): Output => ({ /** Image of the jobs that only run tools, whatever Node.js versions the project tests on. */ const TOOL_IMAGE = "node:24"; +/** + * The images repokeeper's own jobs name with a version Renovate would move: the tool image, Renovate itself and + * the .NET SDK of the dotnet and powershell jobs. Images named through a CI variable or by a moving tag + * (`latest`, `stable`) are not in its reach. + */ +export const MANAGED_IMAGES = ["node", "renovate/renovate", "mcr.microsoft.com/dotnet/sdk"] as const; + /** The node stack's CI inputs, as the GitHub reusable workflow receives them, as a GitLab job. */ function nodeJob(input: Record): Record { const pm = input["package-manager"] ?? "npm"; @@ -165,6 +172,15 @@ export const gitlabPlatform: PlatformAdapter = { { matchUpdateTypes: ["minor", "patch"], groupName: "minor and patch updates" }, // @types/node majors track the Node.js line a project runs on, which the project chooses { matchPackageNames: ["@types/node"], matchUpdateTypes: ["major"], enabled: false }, + // The images of repokeeper's own jobs are the standard's to move, with `repokeeper update`. A merge + // request that bumps one edits a managed key: `repokeeper check` reports it, the next update takes it + // back, and Renovate (pinned to the patch) would open one every week. + { + matchManagers: ["gitlabci"], + matchFileNames: [CI_FILE], + matchPackageNames: [...MANAGED_IMAGES], + enabled: false, + }, ], }), }, diff --git a/test/platforms-gitlab-stacks.test.ts b/test/platforms-gitlab-stacks.test.ts index 5460a2d..80e467b 100644 --- a/test/platforms-gitlab-stacks.test.ts +++ b/test/platforms-gitlab-stacks.test.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { parse } from "yaml"; import { run } from "../src/cli.js"; +import { MANAGED_IMAGES } from "../src/platforms/gitlab.js"; import { SCRIPT_ANALYZER_VERSION, SHFMT_VERSION } from "../src/platforms/gitlab-jobs.js"; import { PACKAGE_VERSION, TOOL_VERSIONS } from "../src/version.js"; import { capture, tempDir } from "./helpers.js"; @@ -70,6 +71,23 @@ function expectBump(project: Awaited>, files: string expect(project.releaseScript).toContain(`@semantic-release/exec@${TOOL_VERSIONS.semanticReleaseExec}`); } +/** Every fixture: each stack, and the second package manager or build tool of the ones that have two. */ +const FIXTURES = [ + "node", + "node-pnpm", + "go", + "python", + "rust", + "ruby", + "php", + "dart", + "java-gradle", + "java-maven", + "kotlin", + "dotnet", + "script", +] as const; + describe("gitlab ci for the other stacks", () => { it("python: one job per version, pip or uv", async () => { const pip = await onGitlab("python"); @@ -244,6 +262,34 @@ describe("gitlab ci for the other stacks", () => { expect(github).toContain(`-RequiredVersion ${SCRIPT_ANALYZER_VERSION} `); }); + // Renovate reads .gitlab-ci.yml too. An image one of repokeeper's jobs names with a version is the standard's + // to move, so Renovate is told to leave it: otherwise it opens a merge request against a managed key. + it("names for Renovate every image its own jobs pin to a version", async () => { + const pinned = new Set(); + for (const fixture of FIXTURES) { + const project = await onGitlab(fixture); + for (const job of Object.values(project.ci)) { + const image = (job as Partial).image; + if (!image) continue; + const at = image.lastIndexOf(":"); + const [name, tag] = at < 0 ? [image, "latest"] : [image.slice(0, at), image.slice(at + 1)]; + // a CI variable or a moving tag is nothing Renovate can raise + if (tag.includes("${") || tag === "latest" || tag === "stable") continue; + pinned.add(name); + } + const renovate = JSON.parse(await readFile(join(project.dir, "renovate.json"), "utf8")) as { + packageRules: Array>; + }; + expect(renovate.packageRules.at(-1)).toEqual({ + matchManagers: ["gitlabci"], + matchFileNames: [".gitlab-ci.yml"], + matchPackageNames: [...MANAGED_IMAGES], + enabled: false, + }); + } + expect([...pinned].sort()).toEqual([...MANAGED_IMAGES].sort()); + }); + it("says that os has no effect, whichever stack names one", async () => { const python = await onGitlab("python", {}, " python:\n os: [ubuntu-latest, windows-latest]"); const c = capture(python.dir); diff --git a/test/platforms-gitlab.test.ts b/test/platforms-gitlab.test.ts index bfece51..132b590 100644 --- a/test/platforms-gitlab.test.ts +++ b/test/platforms-gitlab.test.ts @@ -268,6 +268,12 @@ describe("gitlab dependency updates", () => { packageRules: [ { matchUpdateTypes: ["minor", "patch"], groupName: "minor and patch updates" }, { matchPackageNames: ["@types/node"], matchUpdateTypes: ["major"], enabled: false }, + { + matchManagers: ["gitlabci"], + matchFileNames: [".gitlab-ci.yml"], + matchPackageNames: ["node", "renovate/renovate", "mcr.microsoft.com/dotnet/sdk"], + enabled: false, + }, ], }); expect(files(depsModule.outputs(gitlabContext()))[".github/dependabot.yml"]).toBeUndefined();