mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-26 07:20:39 +00:00
6d747ea8ff
* feat(manager): add apm manager for Agent Package Manager Add a new `apm` manager that updates git-pinned dependencies in APM (Agent Package Manager) `apm.yml` manifests, as requested in renovatebot/renovate#42507. The manager parses the string entries under `dependencies.apm` and `devDependencies.apm` (`[host/]owner/repo[/subpath]#<ref>`) and selects the datasource dynamically from the git host, reusing the existing github-tags, gitlab-tags and git-tags datasources. MCP entries carry no pinnable version and are ignored. When an `apm.lock.yaml` lockfile is present, it is refreshed by running `apm install` via a new `apm` tool constraint (pypi `apm-cli`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vfjt3BpqDriiVsvwqVe9M9 * fix(manager/apm): resolve apm tool from github releases APM ships self-contained binaries as GitHub release assets (apm-linux-<arch>.tar.gz under vX.Y.Z tags), not as the PyPI apm-cli wheel. Point the apm tool constraint at the github-releases datasource so the version Renovate resolves matches the artifact that actually gets installed, stripping the leading `v` from the tag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vfjt3BpqDriiVsvwqVe9M9 * docs(manager/apm): point to canonical microsoft/apm repository The APM project's canonical home is github.com/microsoft/apm (originally danielmeppiel/apm). Update the manager URL and readme accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vfjt3BpqDriiVsvwqVe9M9 * refactor(manager/apm): validate manifest with zod schema Address review feedback on the apm manager: - parse `apm.yml` with a dedicated zod schema via `parseSingleYaml(content, { customSchema })` instead of casting the parsed YAML to `ApmManifest` - `LooseArray(z.string())` keeps only the string dependency entries, so MCP objects and other non-string values are dropped by the schema and the manual `Array.isArray`/`is.string` guards are no longer needed - drop the `as SkipReason` casts now that the literals are inferred - remove the now-unused `types.ts` Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UbMAQBiMRNkCNxc8EGQw1C * Update schema.ts Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com> * Update readme.md Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com> * Update readme.md Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com> * refactor(manager/apm): simplify manifest schema and host detection Address review feedback: - default the `apm` array to `[]` in the schema (`LooseArray(...).catch([])`) since LooseArray already discards non-string entries, so downstream code no longer needs to handle `undefined` - use `coerceArray` when mapping the optional dependency sections - drop the redundant `host === 'github.com'` / `host === 'gitlab.com'` checks in datasource selection; `detectPlatform` already resolves those hosts (the explicit host comparison is only kept to decide registryUrls for self-hosted instances) - document that APM only supports remote git references and that dots in repo names / subpaths are handled, with a regression test Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UbMAQBiMRNkCNxc8EGQw1C * docs(manager/apm): fix readme trailing newline Remove the trailing blank line left after the readme edits so it passes `prettier` and `markdown-lint`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UbMAQBiMRNkCNxc8EGQw1C * refactor(manager/apm): parse dependency path with named capture groups Replace the manual host/owner/repo splitting (and the standalone dot regex) with a single named-capture regex, as suggested in review. `owner` excludes dots so the optional hostname segment is unambiguous, while repo names and subpaths may still contain dots. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UbMAQBiMRNkCNxc8EGQw1C * test(manager/apm): align admin config with InternalGlobalConfigOptions `localDir` moved out of `RepoGlobalConfig` into `InternalGlobalConfigOptions` on main, so annotate the test admin config as `RepoGlobalConfig & InternalGlobalConfigOptions`, matching the other manager artifacts specs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UbMAQBiMRNkCNxc8EGQw1C * fix(manager/apm): resolve GitLab nested groups and recognize SHA-pinned refs - resolve nested GitLab project slugs (e.g. `group/subgroup/project`) instead of capping at `owner/repo`, splitting the project from any virtual-package subpath using APM's primitive-dir / virtual-file markers - recognize the documented SHA-pinned form and skip it (`unversioned-reference`) rather than treating the SHA as a version or rewriting it * feat(manager/apm): support digest pinning for SHA-pinned deps Recover the release tag from the trailing YAML comment of a SHA-pinned entry (`owner/repo#<sha> # v2.0.0`) - which the structured parse strips - so it extracts as a digest (`currentDigest` + `currentValue`) and updates both via `pinDigests`, mirroring the github-actions manager. A bare SHA with no tag comment (or a quoted form we can't rewrite verbatim) is left skipped. * fix(manager/apm): map SHA-pin tag recovery per line to handle duplicates The raw-manifest scan matched the first line for the entry regardless of section, so the same `owner/repo#<sha>` pinned in both `dependencies` and `devDependencies` resolved to the first line's tag/replaceString. Consume each line at most once (in file order) so duplicate pins map to distinct lines. * fix(manager/apm): recognize `.chatmode.md` virtual files APM's VIRTUAL_FILE_EXTENSIONS includes `.chatmode.md`; add it to the subpath-boundary regex so a `*.chatmode.md` virtual dependency on a nested-group host (GitLab, Azure DevOps) is not folded into the repo path. * test(manager/apm): cover SHA-pin digest updates on git-tags hosts Lock in that a SHA-pinned dep on a non-github/gitlab host resolves via the git-tags datasource (which supports `getDigest` through `git ls-remote`), so the commit is preserved on a bump rather than dropped. * fix(manager/apm): commit regenerated harness files, not just the lockfile `apm install` re-deploys the harness directories APM consumers commit (`.github/`, `.claude/`, ...), but updateArtifacts only returned `apm.lock.yaml`, leaving instruction files stale after a bump. Capture every file the install changed via `getRepoStatus` (additions + deletions); the gitignored `apm_modules/` cache is excluded, and the manifest stays with `updatedPackageFiles`. * fix(manager/apm): declare lockFileMaintenanceIsDelegatedToPackageManager main now requires managers with `supportsLockFileMaintenance` to set this field. apm regenerates the lockfile via `apm install`, so it delegates to the package manager (`true`). * refactor(manager/apm): reuse shared regex and git-sha helpers Adopt the shared utilities per review: - use the exported `newlineRegex` from `util/regex` instead of a local copy - use `isLongCommitSha` from `util/schema-utils/git` instead of a bespoke SHA regex - filter path segments with `isTruthy` instead of `Boolean` * test(manager/apm): align docker exec snapshot with current base-image wrapper The shared `util/exec` docker wrapper now injects `-e CI` and single-quotes the `bash -l -c` payload. Update the docker-mode expectation to match, so the snapshot tracks the current wrapper output. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
402 lines
9.9 KiB
TypeScript
402 lines
9.9 KiB
TypeScript
import { isString } from '@sindresorhus/is';
|
|
import { quote } from 'shlex';
|
|
import { GlobalConfig } from '../../config/global.ts';
|
|
import { logger } from '../../logger/index.ts';
|
|
import type { ReleaseResult } from '../../modules/datasource/index.ts';
|
|
import type { VersioningApi } from '../../modules/versioning/types.ts';
|
|
import { coerceArray } from '../array.ts';
|
|
import { getEnv } from '../env.ts';
|
|
import { regEx } from '../regex.ts';
|
|
import type { Opt, ToolConfig, ToolConstraint, ToolName } from './types.ts';
|
|
|
|
export const allToolConfig: Record<ToolName, ToolConfig> = {
|
|
apm: {
|
|
datasource: 'github-releases',
|
|
packageName: 'microsoft/apm',
|
|
versioning: 'semver',
|
|
extractVersion: '^v(?<version>.*)$',
|
|
},
|
|
bazelisk: {
|
|
datasource: 'github-releases',
|
|
packageName: 'bazelbuild/bazelisk',
|
|
versioning: 'semver',
|
|
},
|
|
bun: {
|
|
datasource: 'github-releases',
|
|
packageName: 'oven-sh/bun',
|
|
extractVersion: '^bun-v(?<version>.*)$',
|
|
versioning: 'npm',
|
|
},
|
|
bundler: {
|
|
datasource: 'rubygems',
|
|
packageName: 'bundler',
|
|
versioning: 'ruby',
|
|
},
|
|
cocoapods: {
|
|
datasource: 'rubygems',
|
|
packageName: 'cocoapods',
|
|
versioning: 'ruby',
|
|
},
|
|
composer: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/composer-prebuild',
|
|
versioning: 'composer',
|
|
},
|
|
conan: {
|
|
datasource: 'pypi',
|
|
packageName: 'conan',
|
|
versioning: 'pep440',
|
|
},
|
|
copier: {
|
|
datasource: 'pypi',
|
|
packageName: 'copier',
|
|
versioning: 'pep440',
|
|
},
|
|
corepack: {
|
|
datasource: 'npm',
|
|
packageName: 'corepack',
|
|
versioning: 'npm',
|
|
},
|
|
deno: {
|
|
datasource: 'github-releases',
|
|
packageName: 'denoland/deno',
|
|
versioning: 'deno',
|
|
},
|
|
devbox: {
|
|
datasource: 'github-releases',
|
|
packageName: 'jetify-com/devbox',
|
|
versioning: 'semver',
|
|
},
|
|
dotnet: {
|
|
datasource: 'dotnet-version',
|
|
packageName: 'dotnet-sdk',
|
|
versioning: 'semver',
|
|
},
|
|
erlang: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/erlang-prebuild',
|
|
versioning: 'semver-coerced',
|
|
},
|
|
elixir: {
|
|
datasource: 'github-releases',
|
|
packageName: 'elixir-lang/elixir',
|
|
versioning: 'semver',
|
|
},
|
|
flux: {
|
|
datasource: 'github-releases',
|
|
packageName: 'fluxcd/flux2',
|
|
versioning: 'semver',
|
|
},
|
|
gh: {
|
|
datasource: 'github-releases',
|
|
packageName: 'cli/cli',
|
|
versioning: 'semver',
|
|
},
|
|
gleam: {
|
|
datasource: 'github-releases',
|
|
packageName: 'gleam-lang/gleam',
|
|
versioning: 'semver',
|
|
},
|
|
golang: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/golang-prebuild',
|
|
versioning: 'npm',
|
|
},
|
|
gradle: {
|
|
datasource: 'gradle-version',
|
|
packageName: 'gradle',
|
|
versioning: 'gradle',
|
|
},
|
|
hashin: {
|
|
datasource: 'pypi',
|
|
packageName: 'hashin',
|
|
versioning: 'pep440',
|
|
},
|
|
helm: {
|
|
datasource: 'github-releases',
|
|
packageName: 'helm/helm',
|
|
versioning: 'semver',
|
|
},
|
|
helmfile: {
|
|
datasource: 'github-releases',
|
|
packageName: 'helmfile/helmfile',
|
|
versioning: 'semver',
|
|
},
|
|
java: {
|
|
datasource: 'java-version',
|
|
packageName: 'java?system=true',
|
|
versioning: 'npm',
|
|
},
|
|
/* not used in Renovate */
|
|
'java-maven': {
|
|
datasource: 'java-version',
|
|
packageName: 'java?system=true',
|
|
versioning: 'maven',
|
|
},
|
|
jb: {
|
|
datasource: 'github-releases',
|
|
packageName: 'jsonnet-bundler/jsonnet-bundler',
|
|
versioning: 'semver',
|
|
},
|
|
kustomize: {
|
|
datasource: 'github-releases',
|
|
packageName: 'kubernetes-sigs/kustomize',
|
|
extractVersion: '^kustomize/v(?<version>.*)$',
|
|
versioning: 'semver',
|
|
},
|
|
maven: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/maven-prebuild',
|
|
versioning: 'maven',
|
|
},
|
|
mise: {
|
|
datasource: 'github-releases',
|
|
packageName: 'jdx/mise',
|
|
versioning: 'npm',
|
|
},
|
|
nix: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/nix-prebuild',
|
|
versioning: 'semver',
|
|
},
|
|
node: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/node-prebuild',
|
|
versioning: 'node',
|
|
},
|
|
npm: {
|
|
datasource: 'npm',
|
|
packageName: 'npm',
|
|
versioning: 'npm',
|
|
},
|
|
pdm: {
|
|
datasource: 'github-releases',
|
|
packageName: 'pdm-project/pdm',
|
|
versioning: 'semver',
|
|
},
|
|
php: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/php-prebuild',
|
|
versioning: 'composer',
|
|
},
|
|
'pip-tools': {
|
|
datasource: 'pypi',
|
|
packageName: 'pip-tools',
|
|
versioning: 'pep440',
|
|
},
|
|
pipenv: {
|
|
datasource: 'pypi',
|
|
packageName: 'pipenv',
|
|
versioning: 'pep440',
|
|
},
|
|
pnpm: {
|
|
datasource: 'npm',
|
|
packageName: 'pnpm',
|
|
versioning: 'npm',
|
|
},
|
|
pixi: {
|
|
datasource: 'github-releases',
|
|
packageName: 'prefix-dev/pixi',
|
|
versioning: 'conda',
|
|
extractVersion: '^v(?<version>.*)$',
|
|
},
|
|
poetry: {
|
|
datasource: 'pypi',
|
|
packageName: 'poetry',
|
|
versioning: 'pep440',
|
|
},
|
|
python: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/python-prebuild',
|
|
versioning: 'python',
|
|
},
|
|
ruby: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/ruby-prebuild',
|
|
versioning: 'ruby',
|
|
},
|
|
rust: {
|
|
datasource: 'rust-version',
|
|
packageName: 'rust',
|
|
versioning: 'rust-release-channel',
|
|
},
|
|
uv: {
|
|
datasource: 'pypi',
|
|
packageName: 'uv',
|
|
versioning: 'pep440',
|
|
},
|
|
yarn: {
|
|
datasource: 'npm',
|
|
packageName: 'yarn',
|
|
versioning: 'npm',
|
|
},
|
|
'yarn-slim': {
|
|
datasource: 'npm',
|
|
packageName: 'yarn',
|
|
versioning: 'npm',
|
|
},
|
|
dart: {
|
|
datasource: 'dart-version',
|
|
packageName: 'dart',
|
|
versioning: 'npm',
|
|
},
|
|
flutter: {
|
|
datasource: 'github-releases',
|
|
packageName: 'containerbase/flutter-prebuild',
|
|
versioning: 'npm',
|
|
},
|
|
vendir: {
|
|
datasource: 'github-releases',
|
|
packageName: 'carvel-dev/vendir',
|
|
versioning: 'semver',
|
|
},
|
|
} as const;
|
|
|
|
let _getPkgReleases: Promise<
|
|
typeof import('../../modules/datasource/index.ts')
|
|
> | null = null;
|
|
|
|
async function getPkgReleases(
|
|
toolConfig: ToolConfig,
|
|
): Promise<ReleaseResult | null> {
|
|
_getPkgReleases ??= import('../../modules/datasource/index.ts');
|
|
const { getPkgReleases } = await _getPkgReleases;
|
|
return getPkgReleases(toolConfig);
|
|
}
|
|
|
|
export function getToolConfig(toolName: ToolName): ToolConfig {
|
|
return allToolConfig[toolName];
|
|
}
|
|
|
|
export function supportsDynamicInstall(toolName: ToolName): boolean {
|
|
return !!allToolConfig[toolName];
|
|
}
|
|
|
|
export function isContainerbase(): boolean {
|
|
return !!getEnv().CONTAINERBASE;
|
|
}
|
|
|
|
export function isDynamicInstall(
|
|
toolConstraints?: Opt<ToolConstraint[]>,
|
|
): boolean {
|
|
if (GlobalConfig.get('binarySource') !== 'install') {
|
|
return false;
|
|
}
|
|
if (!isContainerbase()) {
|
|
logger.debug('Falling back to binarySource=global');
|
|
return false;
|
|
}
|
|
return (
|
|
!toolConstraints ||
|
|
toolConstraints.every((toolConstraint) =>
|
|
supportsDynamicInstall(toolConstraint.toolName),
|
|
)
|
|
);
|
|
}
|
|
|
|
function isStable(
|
|
version: string,
|
|
versioningApi: VersioningApi,
|
|
latest?: string,
|
|
): boolean {
|
|
if (!versioningApi.isStable(version)) {
|
|
return false;
|
|
}
|
|
if (isString(latest) && versioningApi.isGreaterThan(version, latest)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export async function resolveConstraint(
|
|
toolConstraint: ToolConstraint,
|
|
): Promise<string> {
|
|
const { toolName } = toolConstraint;
|
|
const toolConfig = allToolConfig[toolName];
|
|
if (!toolConfig) {
|
|
throw new Error(`Invalid tool to install: ${toolName}`);
|
|
}
|
|
|
|
const { get: getVersioning } =
|
|
await import('../../modules/versioning/index.ts');
|
|
const versioning = getVersioning(toolConfig.versioning);
|
|
let constraint = toolConstraint.constraint;
|
|
if (constraint) {
|
|
if (versioning.isValid(constraint)) {
|
|
if (versioning.isSingleVersion(constraint)) {
|
|
return constraint.replace(regEx(/^=+/), '').trim();
|
|
}
|
|
} else {
|
|
logger.warn(
|
|
{ toolName, constraint, versioning: toolConfig.versioning },
|
|
'Invalid tool constraint',
|
|
);
|
|
constraint = undefined;
|
|
}
|
|
}
|
|
|
|
const pkgReleases = await getPkgReleases(toolConfig);
|
|
const releases = coerceArray(pkgReleases?.releases);
|
|
|
|
if (!releases?.length) {
|
|
logger.warn({ toolConfig }, 'No tool releases found.');
|
|
throw new Error('No tool releases found.');
|
|
}
|
|
|
|
const matchingReleases = releases.filter(
|
|
(r) => !constraint || versioning.matches(r.version, constraint),
|
|
);
|
|
|
|
const stableMatchingVersion = matchingReleases
|
|
.filter((r) => isStable(r.version, versioning, pkgReleases?.tags?.latest))
|
|
.pop()?.version;
|
|
if (stableMatchingVersion) {
|
|
logger.debug(
|
|
{ toolName, constraint, resolvedVersion: stableMatchingVersion },
|
|
'Resolved stable matching version',
|
|
);
|
|
return stableMatchingVersion;
|
|
}
|
|
|
|
const unstableMatchingVersion = matchingReleases.pop()?.version;
|
|
if (unstableMatchingVersion) {
|
|
logger.debug(
|
|
{ toolName, constraint, resolvedVersion: unstableMatchingVersion },
|
|
'Resolved unstable matching version',
|
|
);
|
|
return unstableMatchingVersion;
|
|
}
|
|
|
|
const stableVersion = releases
|
|
.filter((r) => isStable(r.version, versioning, pkgReleases?.tags?.latest))
|
|
.pop()?.version;
|
|
if (stableVersion) {
|
|
logger.warn(
|
|
{ toolName, constraint, stableVersion },
|
|
'No matching tool versions found for constraint - using latest stable version',
|
|
);
|
|
}
|
|
|
|
const highestVersion = releases.pop()!.version;
|
|
logger.warn(
|
|
{ toolName, constraint, highestVersion },
|
|
'No matching or stable tool versions found - using an unstable version',
|
|
);
|
|
return highestVersion;
|
|
}
|
|
|
|
export async function generateInstallCommands(
|
|
toolConstraints: Opt<ToolConstraint[]>,
|
|
): Promise<string[]> {
|
|
const installCommands: string[] = [];
|
|
if (toolConstraints?.length) {
|
|
for (const toolConstraint of toolConstraints) {
|
|
const toolVersion = await resolveConstraint(toolConstraint);
|
|
const { toolName } = toolConstraint;
|
|
const installCommand = `install-tool ${toolName} ${quote(toolVersion)}`;
|
|
installCommands.push(installCommand);
|
|
}
|
|
}
|
|
return installCommands;
|
|
}
|