diff --git a/docs/usage/opentelemetry.md b/docs/usage/opentelemetry.md index 38ada0d4ccc..1242583f0d6 100644 --- a/docs/usage/opentelemetry.md +++ b/docs/usage/opentelemetry.md @@ -77,6 +77,7 @@ Renovate provides instrumentation through traces for (non-exhaustively): - Any Git operations - Per-manager traces when performing the `lookup` and `extract` splits - Per-branch traces when performing the `update` split +- Cache lifecycle operations (repository cache load/save, package cache init/destroy) - Important functions (more instrumentation be added) As well as following [OpenTelemetry's semantic conventions](https://opentelemetry.io/docs/specs/semconv/) where possible, Renovate defines several Custom Attributes, which can be found in [`lib/instrumentation/types.ts`](https://github.com/renovatebot/renovate/blob/main/lib/instrumentation/types.ts). diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 5d444221feb..46879cb4eaf 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -649,7 +649,7 @@ const options: Readonly[] = [ description: 'Change this value to override the default Renovate sidecar image.', type: 'string', - default: 'ghcr.io/renovatebot/base-image:13.27.4', + default: 'ghcr.io/renovatebot/base-image:13.27.5', globalOnly: true, deprecationMsg: 'The usage of `binarySource=docker` is deprecated, and will be removed in the future', diff --git a/lib/modules/manager/github-actions/__fixtures__/workflow_4.yml b/lib/modules/manager/github-actions/__fixtures__/workflow_4.yml index efc9ab54211..3af0d386c1a 100644 --- a/lib/modules/manager/github-actions/__fixtures__/workflow_4.yml +++ b/lib/modules/manager/github-actions/__fixtures__/workflow_4.yml @@ -14,6 +14,7 @@ jobs: - uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 #v2.1.0 - uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 #v2.1.0 - uses: actions/checkout@1e204e # v2.1.0 + - uses: actions/checkout@1e204e # some-ref-name - uses: actions/checkout@01aecc#v2.1.0 - uses: actions/checkout@689fcce700ae7ffc576f2b029b51b2ffb66d3abd # comment containing 2.1.0 - uses: actions/checkout@689fcce700ae7ffc576f2b029b51b2ffb66d3abd # v2.1.0 additional comment diff --git a/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap b/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap index 78eb551c8c3..3f5ccf2f37c 100644 --- a/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap +++ b/lib/modules/manager/github-actions/__snapshots__/extract.spec.ts.snap @@ -6,11 +6,11 @@ exports[`modules/manager/github-actions/extract > extractPackageFile() > extract "autoReplaceStringTemplate": "{{depName}}/shellcheck@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}", "commitMessageTopic": "{{{depName}}} action", "currentValue": "master", - "datasource": "github-tags", + "datasource": "github-digest", "depName": "actions/bin", "depType": "action", "replaceString": "actions/bin/shellcheck@master", - "versioning": "docker", + "versioning": "exact", }, { "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 "autoReplaceStringTemplate": "{{depName}}/shellcheck@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}", "commitMessageTopic": "{{{depName}}} action", "currentValue": "master", - "datasource": "github-tags", + "datasource": "github-digest", "depName": "actions/bin", "depType": "action", "replaceString": "actions/bin/shellcheck@master", - "versioning": "docker", + "versioning": "exact", }, { "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", @@ -142,11 +142,11 @@ exports[`modules/manager/github-actions/extract > extractPackageFile() > extract "autoReplaceStringTemplate": "{{depName}}/cli@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}", "commitMessageTopic": "{{{depName}}} action", "currentValue": "master", - "datasource": "github-tags", + "datasource": "github-digest", "depName": "actions/docker", "depType": "action", "replaceString": "actions/docker/cli@master", - "versioning": "docker", + "versioning": "exact", }, { "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", diff --git a/lib/modules/manager/github-actions/extract.spec.ts b/lib/modules/manager/github-actions/extract.spec.ts index 791b28f8e71..6514936154a 100644 --- a/lib/modules/manager/github-actions/extract.spec.ts +++ b/lib/modules/manager/github-actions/extract.spec.ts @@ -70,7 +70,10 @@ describe('modules/manager/github-actions/extract', () => { expect(res?.deps).toMatchSnapshot(); expect( res?.deps.filter((d) => d.datasource === 'github-tags'), - ).toHaveLength(8); + ).toHaveLength(7); + expect( + res?.deps.filter((d) => d.datasource === 'github-digest'), + ).toHaveLength(1); }); it('use github.com as registry when no settings provided', () => { @@ -429,6 +432,11 @@ describe('modules/manager/github-actions/extract', () => { currentValue: 'v2.1.0', replaceString: 'actions/checkout@1e204e # v2.1.0', }, + { + currentDigestShort: '1e204e', + currentValue: 'some-ref-name', + replaceString: 'actions/checkout@1e204e # some-ref-name', + }, { currentValue: '01aecc#v2.1.0', replaceString: 'actions/checkout@01aecc#v2.1.0', @@ -473,6 +481,49 @@ describe('modules/manager/github-actions/extract', () => { expect(res!.deps[14]).not.toHaveProperty('skipReason'); }); + it('extracts non-semver ref automatically', () => { + const res = extractPackageFile( + ` + jobs: + build: + steps: + - uses: taiki-e/install-action@cargo-llvm-cov + `, + 'workflow.yml', + ); + expect(res?.deps[0]).toMatchObject({ + depName: 'taiki-e/install-action', + currentValue: 'cargo-llvm-cov', + datasource: 'github-digest', + versioning: 'exact', + autoReplaceStringTemplate: + '{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}', + }); + }); + + it('extracts pinned non-semver ref with digest', () => { + const res = extractPackageFile( + ` + jobs: + build: + steps: + - uses: taiki-e/install-action@4b1248585248751e3b12fd020cf7ac91540ca09c # cargo-llvm-cov + `, + 'workflow.yml', + ); + expect(res?.deps[0]).toMatchObject({ + depName: 'taiki-e/install-action', + currentValue: 'cargo-llvm-cov', + currentDigest: '4b1248585248751e3b12fd020cf7ac91540ca09c', + datasource: 'github-digest', + versioning: 'exact', + replaceString: + 'taiki-e/install-action@4b1248585248751e3b12fd020cf7ac91540ca09c # cargo-llvm-cov', + autoReplaceStringTemplate: + '{{depName}}@{{#if newDigest}}{{newDigest}}{{#if newValue}} # {{newValue}}{{/if}}{{/if}}{{#unless newDigest}}{{newValue}}{{/unless}}', + }); + }); + it('extracts actions with fqdn', () => { const res = extractPackageFile( codeBlock` diff --git a/lib/modules/manager/github-actions/extract.ts b/lib/modules/manager/github-actions/extract.ts index 37b90595ac5..79a89d571fb 100644 --- a/lib/modules/manager/github-actions/extract.ts +++ b/lib/modules/manager/github-actions/extract.ts @@ -5,10 +5,12 @@ import { detectPlatform } from '../../../util/common.ts'; import { newlineRegex, regEx } from '../../../util/regex.ts'; import { ForgejoTagsDatasource } from '../../datasource/forgejo-tags/index.ts'; import { GiteaTagsDatasource } from '../../datasource/gitea-tags/index.ts'; +import { GithubDigestDatasource } from '../../datasource/github-digest/index.ts'; import { GithubReleasesDatasource } from '../../datasource/github-releases/index.ts'; import { GithubRunnersDatasource } from '../../datasource/github-runners/index.ts'; import { GithubTagsDatasource } from '../../datasource/github-tags/index.ts'; import * as dockerVersioning from '../../versioning/docker/index.ts'; +import * as exactVersioning from '../../versioning/exact/index.ts'; import * as nodeVersioning from '../../versioning/node/index.ts'; import * as npmVersioning from '../../versioning/npm/index.ts'; import { getDep } from '../dockerfile/extract.ts'; @@ -19,7 +21,7 @@ import type { } from '../types.ts'; import { CommunityActions } from './community.ts'; import type { DockerReference, RepositoryReference } from './parse.ts'; -import { isSha, isShortSha, parseUsesLine } from './parse.ts'; +import { isSha, isShortSha, parseUsesLine, versionLikeRe } from './parse.ts'; import type { Steps } from './schema.ts'; import { Workflow } from './schema.ts'; @@ -84,7 +86,6 @@ function extractRepositoryAction( const dep: PackageDependency = { depName, commitMessageTopic: '{{{depName}}} action', - datasource: GithubTagsDatasource.id, versioning: dockerVersioning.id, depType: 'action', replaceString: valueString, @@ -99,10 +100,13 @@ function extractRepositoryAction( } // Extend replaceString to include relevant comment portions: - // - Pinned version: include only up to the version (truncate trailing text) + // - Pinned version or ref: include only up to the matched token (truncate trailing text) // - Ratchet exclude: include the full comment to preserve the marker + const pinComment = + commentData.pinnedVersion ?? + (isSha(ref) || isShortSha(ref) ? commentData.ref : undefined); if ( - commentData.pinnedVersion && + pinComment && !is.undefined(commentData.index) && !is.undefined(commentData.matchedString) ) { @@ -117,15 +121,24 @@ function extractRepositoryAction( } if (isSha(ref)) { - dep.currentValue = commentData.pinnedVersion; + dep.currentValue = commentData.pinnedVersion ?? commentData.ref; dep.currentDigest = ref; } else if (isShortSha(ref)) { - dep.currentValue = commentData.pinnedVersion; + dep.currentValue = commentData.pinnedVersion ?? commentData.ref; dep.currentDigestShort = ref; } else { dep.currentValue = ref; } + const isVersionLike = + dep.currentValue && versionLikeRe.test(dep.currentValue); + if (!dep.datasource && dep.currentValue && !isVersionLike) { + dep.datasource = GithubDigestDatasource.id; + dep.versioning = exactVersioning.id; + } + + dep.datasource ??= GithubTagsDatasource.id; + return dep; } diff --git a/lib/modules/manager/github-actions/index.ts b/lib/modules/manager/github-actions/index.ts index d8c69e6ada5..c926c72f134 100644 --- a/lib/modules/manager/github-actions/index.ts +++ b/lib/modules/manager/github-actions/index.ts @@ -1,5 +1,6 @@ import type { Category } from '../../../constants/index.ts'; import { GiteaTagsDatasource } from '../../datasource/gitea-tags/index.ts'; +import { GithubDigestDatasource } from '../../datasource/github-digest/index.ts'; import { GithubRunnersDatasource } from '../../datasource/github-runners/index.ts'; import { GithubTagsDatasource } from '../../datasource/github-tags/index.ts'; @@ -18,6 +19,7 @@ export const defaultConfig = { export const supportedDatasources = [ GiteaTagsDatasource.id, - GithubTagsDatasource.id, + GithubDigestDatasource.id, GithubRunnersDatasource.id, + GithubTagsDatasource.id, ]; diff --git a/lib/modules/manager/github-actions/parse.spec.ts b/lib/modules/manager/github-actions/parse.spec.ts index cc35bde84db..92258c22a5b 100644 --- a/lib/modules/manager/github-actions/parse.spec.ts +++ b/lib/modules/manager/github-actions/parse.spec.ts @@ -245,6 +245,28 @@ describe('modules/manager/github-actions/parse', () => { pinnedVersion: 'node/v20', }); }); + + it('parses bare non-semver ref', () => { + const result = parseComment(' cargo-llvm-cov'); + expect(result).toEqual({ + index: 0, + matchedString: ' cargo-llvm-cov', + ref: 'cargo-llvm-cov', + }); + }); + + it('parses bare branch name', () => { + const result = parseComment(' main'); + expect(result).toEqual({ + index: 0, + matchedString: ' main', + ref: 'main', + }); + }); + + it('ignores multi-word comments', () => { + expect(parseComment('do not update')).toEqual({}); + }); }); describe('parseQuote', () => { diff --git a/lib/modules/manager/github-actions/parse.ts b/lib/modules/manager/github-actions/parse.ts index 96492ed0b89..f8b73c5d53a 100644 --- a/lib/modules/manager/github-actions/parse.ts +++ b/lib/modules/manager/github-actions/parse.ts @@ -194,20 +194,20 @@ export function parseActionReference(uses: string): ActionReference | null { export interface CommentData { pinnedVersion?: string; + ref?: string; ratchetExclude?: boolean; matchedString?: string; index?: number; } -// Matches version strings with optional prefixes, e.g.: -// - "@v1.2.3", "v1.2.3", "1.2.3" -// - "renovate: pin @v1.2.3", "tag=v1.2.3" -// - "ratchet:owner/repo@v1.2.3" -// - "stable/v1.2.3", "stable-v1.2.3" -const pinnedVersionRe = regEx( +const pinTokenRe = regEx( /^\s*(?:(?:renovate\s*:\s*)?(?:pin\s+|tag\s*=\s*)?|(?:ratchet:[\w-]+\/[.\w-]+))?@?(?([\w-]*[-/])?v?\d+(?:\.\d+(?:\.\d+)?)?)/, ); +export const versionLikeRe = regEx(/^v?\d+/); + +const bareTokenRe = regEx(/^\s*(?\S+)\s*$/); + export function parseComment(commentBody: string): CommentData { const trimmed = commentBody.trim(); if (trimmed === 'ratchet:exclude') { @@ -215,7 +215,7 @@ export function parseComment(commentBody: string): CommentData { } // We use commentBody (with leading spaces) to get the correct index relative to the comment start - const match = pinnedVersionRe.exec(commentBody); + const match = pinTokenRe.exec(commentBody); if (match?.groups?.version) { return { pinnedVersion: match.groups.version, @@ -224,6 +224,15 @@ export function parseComment(commentBody: string): CommentData { }; } + const bareMatch = bareTokenRe.exec(commentBody); + if (bareMatch?.groups?.token) { + return { + ref: bareMatch.groups.token, + matchedString: bareMatch[0], + index: bareMatch.index, + }; + } + return {}; } @@ -290,7 +299,7 @@ export function parseUsesLine(line: string): ParsedUsesLine | null { ); const { value, quote } = parseQuote(rawValuePart); - // commentPart always starts with '#' since we found ' #' and sliced after the space + // commentPart always starts with '#' (see commentIndex search above) const cleanCommentBody = commentPart.slice(1); return { diff --git a/lib/modules/manager/github-actions/readme.md b/lib/modules/manager/github-actions/readme.md index b45afe3a649..5ebb28a86c1 100644 --- a/lib/modules/manager/github-actions/readme.md +++ b/lib/modules/manager/github-actions/readme.md @@ -39,6 +39,29 @@ If you want to automatically pin action digests add the `helpers:pinGitHubAction } ``` +### Non-semver refs (branches and feature tags) + +Renovate supports GitHub Actions that reference non-semver refs like branch names (`main`, `master`) or feature-oriented tags (`cargo-llvm-cov`). + +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. +Since these refs have no version ordering, only digest pinning updates are supported. + +**Routing logic:** + +- `actions/checkout@v4.2.0` → `github-tags` datasource (version updates) +- `actions/checkout@v4` → `github-tags` datasource (version updates) +- `taiki-e/install-action@cargo-llvm-cov` → `github-digest` datasource (digest pinning only) +- `actions/checkout@main` → `github-digest` datasource (digest pinning only) + +When pinning, Renovate adds a comment to preserve the original ref: + +```yaml +- uses: taiki-e/install-action@d8c10dae823f48238abff23fee4146b448aed2f1 # cargo-llvm-cov +``` + +Non-semver ref support is currently limited to GitHub-hosted actions. +Gitea and Forgejo support the same ref types, but Renovate does not yet handle them for these platforms. + ### Non-support of Variables Renovate ignores any GitHub runners which are configured in variables. diff --git a/lib/util/cache/package/backend.ts b/lib/util/cache/package/backend.ts index 813b733a867..ce62bd56d02 100644 --- a/lib/util/cache/package/backend.ts +++ b/lib/util/cache/package/backend.ts @@ -1,4 +1,5 @@ import type { AllConfig } from '../../../config/types.ts'; +import { instrument } from '../../../instrumentation/index.ts'; import { getEnv } from '../../env.ts'; import type { PackageCacheBase } from './impl/base.ts'; import { PackageCacheFile } from './impl/file.ts'; @@ -15,27 +16,28 @@ export function getCacheType(): typeof cacheType { export async function init(config: AllConfig): Promise { await destroy(); + await instrument('init PackageCache', async () => { + if (config.redisUrl) { + cacheProxy = await PackageCacheRedis.create( + config.redisUrl, + config.redisPrefix, + ); + cacheType = 'redis'; + return; + } - if (config.redisUrl) { - cacheProxy = await PackageCacheRedis.create( - config.redisUrl, - config.redisPrefix, - ); - cacheType = 'redis'; - return; - } + if (getEnv().RENOVATE_X_SQLITE_PACKAGE_CACHE && config.cacheDir) { + cacheProxy = await PackageCacheSqlite.create(config.cacheDir); + cacheType = 'sqlite'; + return; + } - if (getEnv().RENOVATE_X_SQLITE_PACKAGE_CACHE && config.cacheDir) { - cacheProxy = await PackageCacheSqlite.create(config.cacheDir); - cacheType = 'sqlite'; - return; - } - - if (config.cacheDir) { - cacheProxy = PackageCacheFile.create(config.cacheDir); - cacheType = 'file'; - return; - } + if (config.cacheDir) { + cacheProxy = PackageCacheFile.create(config.cacheDir); + cacheType = 'file'; + return; + } + }); } export async function get( @@ -55,10 +57,12 @@ export async function set( } export async function destroy(): Promise { - cacheType = undefined; - try { - await cacheProxy?.destroy(); - } finally { - cacheProxy = undefined; - } + await instrument('destroy PackageCache', async () => { + cacheType = undefined; + try { + await cacheProxy?.destroy(); + } finally { + cacheProxy = undefined; + } + }); } diff --git a/lib/util/cache/repository/index.ts b/lib/util/cache/repository/index.ts index 16eb01151bf..a9cbfa5b2bc 100644 --- a/lib/util/cache/repository/index.ts +++ b/lib/util/cache/repository/index.ts @@ -1,4 +1,5 @@ import { GlobalConfig } from '../../../config/global.ts'; +import { instrument } from '../../../instrumentation/index.ts'; import { logger } from '../../../logger/index.ts'; import { RepoCacheNull } from './impl/null.ts'; import type { RepoCache, RepoCacheData } from './types.ts'; @@ -23,7 +24,7 @@ export async function saveCache(): Promise { if (GlobalConfig.get('dryRun')) { logger.info(`DRY-RUN: Would save repository cache.`); } else { - await repoCache.save(); + await instrument('save RepoCache', () => repoCache.save()); } } diff --git a/lib/util/cache/repository/init.ts b/lib/util/cache/repository/init.ts index cd318c011e7..f9fdb1fa5db 100644 --- a/lib/util/cache/repository/init.ts +++ b/lib/util/cache/repository/init.ts @@ -1,3 +1,4 @@ +import { instrument } from '../../../instrumentation/index.ts'; import { CacheFactory } from './impl/cache-factory.ts'; import { RepoCacheNull } from './impl/null.ts'; import { resetCache, setCache } from './index.ts'; @@ -23,14 +24,14 @@ export async function initRepoCache(config: RepoCacheConfig): Promise { if (repositoryCache === 'enabled') { const cache = CacheFactory.get(repository!, repoFingerprint, type); - await cache.load(); + await instrument('load RepoCache', () => cache.load()); setCache(cache); return; } if (repositoryCache === 'reset') { const cache = CacheFactory.get(repository!, repoFingerprint, type); - await cache.save(); + await instrument('save RepoCache', () => cache.save()); setCache(cache); return; } diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index d85fa063a8d..3e46c19a2ec 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -5,19 +5,19 @@ ARG BASE_IMAGE_TYPE=slim # -------------------------------------- # slim image # -------------------------------------- -FROM ghcr.io/renovatebot/base-image:13.27.4@sha256:dfa62f5cc4919f5fad910e8b26fc5eacecb7689924a9aaf4d90722f222beeb5d AS slim-base +FROM ghcr.io/renovatebot/base-image:13.27.5@sha256:91221968ec09e5f9dee57551bb73056ea519cb59faa3b078306296dfb7cd08ec AS slim-base # -------------------------------------- # full image # -------------------------------------- -FROM ghcr.io/renovatebot/base-image:13.27.4-full@sha256:995c038dc9a79d598cf92980def59fd10dadabbd4fc5a3a79f8f46c131b96fe3 AS full-base +FROM ghcr.io/renovatebot/base-image:13.27.5-full@sha256:5982fa49eb0703eebaa483ebf154aa4ff9925d8edf0bbfd12966fceb90b7a086 AS full-base ENV RENOVATE_BINARY_SOURCE=global # -------------------------------------- # build image # -------------------------------------- -FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:13.27.4@sha256:dfa62f5cc4919f5fad910e8b26fc5eacecb7689924a9aaf4d90722f222beeb5d AS build +FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:13.27.5@sha256:91221968ec09e5f9dee57551bb73056ea519cb59faa3b078306296dfb7cd08ec AS build # We want a specific node version here # renovate: datasource=github-releases packageName=containerbase/node-prebuild versioning=node