Compare commits

...

22 commits

Author SHA1 Message Date
Fotis Papadogeorgopoulos
889f796123
Merge 8a9d54e160 into adede1d309 2025-01-08 21:28:14 +01:00
renovate[bot]
adede1d309
chore(deps): update otel/opentelemetry-collector-contrib docker tag to v0.117.0 (#33483)
Some checks are pending
Build / setup (push) Waiting to run
Build / setup-build (push) Waiting to run
Build / prefetch (push) Blocked by required conditions
Build / lint-eslint (push) Blocked by required conditions
Build / lint-prettier (push) Blocked by required conditions
Build / lint-docs (push) Blocked by required conditions
Build / lint-other (push) Blocked by required conditions
Build / (push) Blocked by required conditions
Build / codecov (push) Blocked by required conditions
Build / coverage-threshold (push) Blocked by required conditions
Build / test-success (push) Blocked by required conditions
Build / build (push) Blocked by required conditions
Build / build-docs (push) Blocked by required conditions
Build / test-e2e (push) Blocked by required conditions
Build / release (push) Blocked by required conditions
Code scanning / CodeQL-Build (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
whitesource-scan / WS_SCAN (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 19:16:59 +00:00
renovate[bot]
2eca39ad90
chore(deps): update dependency memfs to v4.15.3 (#33482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 19:11:29 +00:00
renovate[bot]
88e2336945
fix(deps): update ghcr.io/renovatebot/base-image docker tag to v9.29.1 (#33480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 18:10:27 +00:00
ssams
766d0c37cf
refactor(manager/flux): extract helm repo handling to helper functions (#33462) 2025-01-08 15:17:04 +00:00
Borja Domínguez
8683eeb7ad
feat(datasource/azure-pipelines-tasks): Azure DevOps API based datasource (#32966) 2025-01-08 16:04:34 +01:00
Fotis Papadogeorgopoulos
8a9d54e160
chore: tidy up pnpm extraction test and TODO 2025-01-02 18:34:51 +02:00
Fotis Papadogeorgopoulos
c598c6ffa1
test(npm): mirror specifier tests to catalog update 2025-01-02 18:26:43 +02:00
Fotis Papadogeorgopoulos
8e37c8f100
chore: remove done TODO 2025-01-02 11:27:26 +02:00
Fotis Papadogeorgopoulos
e50bc2f9b5
feat(npm): implement replacement for pnpm catalog updates 2025-01-02 11:26:33 +02:00
Fotis Papadogeorgopoulos
ea5c14f003
test(npm): add tests for pnpm catalog updates 2025-01-02 10:59:52 +02:00
Fotis Papadogeorgopoulos
c9657a0040
chore(npm): tidy up TODOs for review 2025-01-01 23:54:11 +02:00
Fotis Papadogeorgopoulos
8ba3bd6eee
chore: remove TODO 2025-01-01 23:44:58 +02:00
Fotis Papadogeorgopoulos
9f23483b4f
fix(npm): set either implicit or named default catalog 2025-01-01 23:39:12 +02:00
Fotis Papadogeorgopoulos
f40a42dd68
refactor(npm,utils): split out parseSingleYamlDocument
This keeps YAML parsing centralised, while allowing use of the
Document represenation, in addition to the JS one.
2025-01-01 23:17:03 +02:00
Fotis Papadogeorgopoulos
4da6b6daf4
chore: small test and typechecking fixups 2025-01-01 22:53:20 +02:00
Fotis Papadogeorgopoulos
794f042068
refactor(npm): move git and npm alias resolution to common module 2025-01-01 22:35:32 +02:00
Fotis Papadogeorgopoulos
17efd68390
refactor: move updatePnpmCatalogDependency to its own module 2025-01-01 22:06:34 +02:00
Fotis Papadogeorgopoulos
591b401368
Implement updatePnpmCatalogDependency in updateDependency 2025-01-01 22:01:22 +02:00
Fotis Papadogeorgopoulos
50b0d82fb5
Remove pnpmCatalog from api.ts 2025-01-01 15:50:44 +02:00
Fotis Papadogeorgopoulos
891256f71a
Move pnpm catalog extraction to npm manager 2025-01-01 11:04:00 +02:00
Fotis Papadogeorgopoulos
0702e3d85b
WIP: prototype extraction in its own manager 2024-12-31 20:41:26 +02:00
20 changed files with 1584 additions and 107 deletions

View file

@ -36,7 +36,7 @@ services:
otel-collector:
# Using the Contrib version to access the spanmetrics connector.
# If you don't need the spanmetrics connector, you can use the standard version
image: otel/opentelemetry-collector-contrib:0.116.1
image: otel/opentelemetry-collector-contrib:0.117.0
volumes:
- ./otel-collector-config.yml:/etc/otelcol-contrib/config.yaml
ports:

View file

@ -0,0 +1,575 @@
{
"count": 3,
"value": [
{
"visibility": [
"Build",
"Release"
],
"runsOn": [
"Agent",
"DeploymentGroup"
],
"id": "e213ff0f-5d5c-4791-802d-52ea3e7be1f1",
"name": "PowerShell",
"version": {
"major": 2,
"minor": 247,
"patch": 1,
"isTest": false
},
"serverOwned": true,
"contentsUploaded": true,
"iconUrl": "https://dev.azure.com/test_organization/_apis/distributedtask/tasks/e213ff0f-5d5c-4791-802d-52ea3e7be1f1/2.247.1/icon",
"minimumAgentVersion": "2.115.0",
"friendlyName": "PowerShell",
"description": "Run a PowerShell script on Linux, macOS, or Windows",
"category": "Utility",
"helpMarkDown": "[Learn more about this task](https://go.microsoft.com/fwlink/?LinkID=613736)",
"helpUrl": "https://docs.microsoft.com/azure/devops/pipelines/tasks/utility/powershell",
"releaseNotes": "Script task consistency. Added support for macOS and Linux.",
"definitionType": "task",
"showEnvironmentVariables": true,
"author": "Microsoft Corporation",
"demands": [],
"groups": [
{
"name": "preferenceVariables",
"displayName": "Preference Variables",
"isExpanded": false
},
{
"name": "advanced",
"displayName": "Advanced",
"isExpanded": false
}
],
"inputs": [
{
"options": {
"filePath": "File Path",
"inline": "Inline"
},
"name": "targetType",
"label": "Type",
"defaultValue": "filePath",
"type": "radio",
"helpMarkDown": "Target script type: File Path or Inline"
},
{
"name": "filePath",
"label": "Script Path",
"defaultValue": "",
"required": true,
"type": "filePath",
"helpMarkDown": "Path of the script to execute. Must be a fully qualified path or relative to $(System.DefaultWorkingDirectory).",
"visibleRule": "targetType = filePath"
},
{
"name": "arguments",
"label": "Arguments",
"defaultValue": "",
"type": "string",
"helpMarkDown": "Arguments passed to the PowerShell script. Either ordinal parameters or named parameters.",
"visibleRule": "targetType = filePath"
},
{
"properties": {
"resizable": "true",
"rows": "10",
"maxLength": "20000"
},
"name": "script",
"label": "Script",
"defaultValue": "# Write your PowerShell commands here.\n\nWrite-Host \"Hello World\"\n",
"required": true,
"type": "multiLine",
"helpMarkDown": "",
"visibleRule": "targetType = inline"
},
{
"options": {
"default": "Default",
"stop": "Stop",
"continue": "Continue",
"silentlyContinue": "SilentlyContinue"
},
"name": "errorActionPreference",
"label": "ErrorActionPreference",
"defaultValue": "stop",
"type": "pickList",
"helpMarkDown": "When not `Default`, prepends the line `$ErrorActionPreference = 'VALUE'` at the top of your script.",
"groupName": "preferenceVariables"
},
{
"options": {
"default": "Default",
"stop": "Stop",
"continue": "Continue",
"silentlyContinue": "SilentlyContinue"
},
"name": "warningPreference",
"label": "WarningPreference",
"defaultValue": "default",
"type": "pickList",
"helpMarkDown": "When not `Default`, prepends the line `$WarningPreference = 'VALUE'` at the top of your script.",
"groupName": "preferenceVariables"
},
{
"options": {
"default": "Default",
"stop": "Stop",
"continue": "Continue",
"silentlyContinue": "SilentlyContinue"
},
"name": "informationPreference",
"label": "InformationPreference",
"defaultValue": "default",
"type": "pickList",
"helpMarkDown": "When not `Default`, prepends the line `$InformationPreference = 'VALUE'` at the top of your script.",
"groupName": "preferenceVariables"
},
{
"options": {
"default": "Default",
"stop": "Stop",
"continue": "Continue",
"silentlyContinue": "SilentlyContinue"
},
"name": "verbosePreference",
"label": "VerbosePreference",
"defaultValue": "default",
"type": "pickList",
"helpMarkDown": "When not `Default`, prepends the line `$VerbosePreference = 'VALUE'` at the top of your script.",
"groupName": "preferenceVariables"
},
{
"options": {
"default": "Default",
"stop": "Stop",
"continue": "Continue",
"silentlyContinue": "SilentlyContinue"
},
"name": "debugPreference",
"label": "DebugPreference",
"defaultValue": "default",
"type": "pickList",
"helpMarkDown": "When not `Default`, prepends the line `$DebugPreference = 'VALUE'` at the top of your script.",
"groupName": "preferenceVariables"
},
{
"options": {
"default": "Default",
"stop": "Stop",
"continue": "Continue",
"silentlyContinue": "SilentlyContinue"
},
"name": "progressPreference",
"label": "ProgressPreference",
"defaultValue": "silentlyContinue",
"type": "pickList",
"helpMarkDown": "When not `Default`, prepends the line `$ProgressPreference = 'VALUE'` at the top of your script.",
"groupName": "preferenceVariables"
},
{
"name": "failOnStderr",
"label": "Fail on Standard Error",
"defaultValue": "false",
"type": "boolean",
"helpMarkDown": "If this is true, this task will fail if any errors are written to the error pipeline, or if any data is written to the Standard Error stream. Otherwise the task will rely on the exit code to determine failure.",
"groupName": "advanced"
},
{
"name": "showWarnings",
"label": "Show warnings as Azure DevOps warnings",
"defaultValue": "false",
"type": "boolean",
"helpMarkDown": "If this is true, and your script writes a warnings - they are shown as warnings also in pipeline logs",
"groupName": "advanced"
},
{
"name": "ignoreLASTEXITCODE",
"label": "Ignore $LASTEXITCODE",
"defaultValue": "false",
"type": "boolean",
"helpMarkDown": "If this is false, the line `if ((Test-Path -LiteralPath variable:\\LASTEXITCODE)) { exit $LASTEXITCODE }` is appended to the end of your script. This will cause the last exit code from an external command to be propagated as the exit code of powershell. Otherwise the line is not appended to the end of your script.",
"groupName": "advanced"
},
{
"name": "pwsh",
"label": "Use PowerShell Core",
"defaultValue": "false",
"type": "boolean",
"helpMarkDown": "If this is true, then on Windows the task will use pwsh.exe from your PATH instead of powershell.exe.",
"groupName": "advanced"
},
{
"name": "workingDirectory",
"label": "Working Directory",
"defaultValue": "",
"type": "filePath",
"helpMarkDown": "Working directory where the script is run.",
"groupName": "advanced"
},
{
"name": "runScriptInSeparateScope",
"label": "Run script in the separate scope",
"defaultValue": "false",
"type": "boolean",
"helpMarkDown": "This input allows executing PowerShell scripts using '&' operator instead of the default '.'. If this input set to the true script will be executed in separate scope and globally scoped PowerShell variables won't be updated",
"groupName": "advanced"
}
],
"satisfies": [],
"sourceDefinitions": [],
"dataSourceBindings": [],
"instanceNameFormat": "PowerShell Script",
"preJobExecution": {},
"execution": {
"PowerShell3": {
"target": "powershell.ps1",
"platforms": [
"windows"
]
},
"Node10": {
"target": "powershell.js",
"argumentFormat": ""
},
"Node16": {
"target": "powershell.js",
"argumentFormat": ""
},
"Node20_1": {
"target": "powershell.js",
"argumentFormat": ""
}
},
"postJobExecution": {},
"_buildConfigMapping": {
"Default": "2.247.0",
"Node20-225": "2.247.1"
}
},
{
"visibility": [
"Build",
"Release"
],
"runsOn": [
"Agent",
"DeploymentGroup"
],
"id": "e213ff0f-5d5c-4791-802d-52ea3e7be1f1",
"name": "PowerShell",
"deprecated": true,
"version": {
"major": 1,
"minor": 2,
"patch": 3,
"isTest": false
},
"serverOwned": true,
"contentsUploaded": true,
"iconUrl": "https://dev.azure.com/test_organization/_apis/distributedtask/tasks/e213ff0f-5d5c-4791-802d-52ea3e7be1f1/1.2.3/icon",
"minimumAgentVersion": "1.102",
"friendlyName": "PowerShell",
"description": "Run a PowerShell script",
"category": "Utility",
"helpMarkDown": "[More Information](https://go.microsoft.com/fwlink/?LinkID=613736)",
"definitionType": "task",
"author": "Microsoft Corporation",
"demands": [
"DotNetFramework"
],
"groups": [
{
"name": "advanced",
"displayName": "Advanced",
"isExpanded": false
}
],
"inputs": [
{
"options": {
"inlineScript": "Inline Script",
"filePath": "File Path"
},
"name": "scriptType",
"label": "Type",
"defaultValue": "filePath",
"required": true,
"type": "pickList",
"helpMarkDown": "Type of the script: File Path or Inline Script"
},
{
"name": "scriptName",
"label": "Script Path",
"defaultValue": "",
"required": true,
"type": "filePath",
"helpMarkDown": "Path of the script to execute. Should be fully qualified path or relative to the default working directory.",
"visibleRule": "scriptType = filePath"
},
{
"name": "arguments",
"label": "Arguments",
"defaultValue": "",
"type": "string",
"helpMarkDown": "Arguments passed to the PowerShell script. Either ordinal parameters or named parameters"
},
{
"name": "workingFolder",
"label": "Working folder",
"defaultValue": "",
"type": "filePath",
"helpMarkDown": "Current working directory when script is run. Defaults to the folder where the script is located.",
"groupName": "advanced"
},
{
"properties": {
"resizable": "true",
"rows": "10",
"maxLength": "500"
},
"name": "inlineScript",
"label": "Inline Script",
"defaultValue": "# You can write your powershell scripts inline here. \n# You can also pass predefined and custom variables to this scripts using arguments\n\n Write-Host \"Hello World\"",
"required": true,
"type": "multiLine",
"helpMarkDown": "",
"visibleRule": "scriptType = inlineScript"
},
{
"name": "failOnStandardError",
"label": "Fail on Standard Error",
"defaultValue": "true",
"type": "boolean",
"helpMarkDown": "If this is true, this task will fail if any errors are written to the error pipeline, or if any data is written to the Standard Error stream. Otherwise the task will rely solely on $LASTEXITCODE and the exit code to determine failure.",
"groupName": "advanced"
}
],
"satisfies": [],
"sourceDefinitions": [],
"dataSourceBindings": [],
"instanceNameFormat": "PowerShell Script",
"preJobExecution": {},
"execution": {
"PowerShellExe": {
"target": "$(scriptName)",
"argumentFormat": "$(arguments)",
"workingDirectory": "$(workingFolder)",
"inlineScript": "$(inlineScript)",
"scriptType": "$(scriptType)",
"failOnStandardError": "$(failOnStandardError)"
}
},
"postJobExecution": {},
"_buildConfigMapping": {}
},
{
"visibility": [
"Build",
"Release"
],
"runsOn": [
"Agent",
"DeploymentGroup"
],
"id": "72a1931b-effb-4d2e-8fd8-f8472a07cb62",
"name": "AzurePowerShell",
"version": {
"major": 5,
"minor": 248,
"patch": 3,
"isTest": false
},
"serverOwned": true,
"contentsUploaded": true,
"iconUrl": "https://dev.azure.com/test_organization/_apis/distributedtask/tasks/72a1931b-effb-4d2e-8fd8-f8472a07cb62/5.248.3/icon",
"minimumAgentVersion": "2.115.0",
"friendlyName": "Azure PowerShell",
"description": "Run a PowerShell script within an Azure environment",
"category": "Deploy",
"helpMarkDown": "[Learn more about this task](https://go.microsoft.com/fwlink/?LinkID=613749)",
"helpUrl": "https://aka.ms/azurepowershelltroubleshooting",
"releaseNotes": "Added support for Az Module and cross platform agents.",
"definitionType": "task",
"author": "Microsoft Corporation",
"demands": [],
"groups": [
{
"name": "AzurePowerShellVersionOptions",
"displayName": "Azure PowerShell version options",
"isExpanded": true
},
{
"name": "advanced",
"displayName": "Advanced",
"isExpanded": false
}
],
"inputs": [
{
"aliases": [
"azureSubscription"
],
"properties": {
"EndpointFilterRule": "ScopeLevel != AzureMLWorkspace"
},
"name": "ConnectedServiceNameARM",
"label": "Azure Subscription",
"defaultValue": "",
"required": true,
"type": "connectedService:AzureRM",
"helpMarkDown": "Azure Resource Manager subscription to configure before running PowerShell"
},
{
"options": {
"FilePath": "Script File Path",
"InlineScript": "Inline Script"
},
"name": "ScriptType",
"label": "Script Type",
"defaultValue": "FilePath",
"type": "radio",
"helpMarkDown": "Type of the script: File Path or Inline Script"
},
{
"name": "ScriptPath",
"label": "Script Path",
"defaultValue": "",
"type": "filePath",
"helpMarkDown": "Path of the script. Should be fully qualified path or relative to the default working directory.",
"visibleRule": "ScriptType = FilePath"
},
{
"properties": {
"resizable": "true",
"rows": "10",
"maxLength": "5000"
},
"name": "Inline",
"label": "Inline Script",
"defaultValue": "# You can write your azure powershell scripts inline here. \n# You can also pass predefined and custom variables to this script using arguments",
"type": "multiLine",
"helpMarkDown": "Enter the script to execute.",
"visibleRule": "ScriptType = InlineScript"
},
{
"properties": {
"editorExtension": "ms.vss-services-azure.parameters-grid"
},
"name": "ScriptArguments",
"label": "Script Arguments",
"defaultValue": "",
"type": "string",
"helpMarkDown": "Additional parameters to pass to PowerShell. Can be either ordinal or named parameters.",
"visibleRule": "ScriptType = FilePath"
},
{
"options": {
"stop": "Stop",
"continue": "Continue",
"silentlyContinue": "SilentlyContinue"
},
"name": "errorActionPreference",
"label": "ErrorActionPreference",
"defaultValue": "stop",
"type": "pickList",
"helpMarkDown": "Select the value of the ErrorActionPreference variable for executing the script."
},
{
"name": "FailOnStandardError",
"label": "Fail on Standard Error",
"defaultValue": "false",
"type": "boolean",
"helpMarkDown": "If this is true, this task will fail if any errors are written to the error pipeline, or if any data is written to the Standard Error stream."
},
{
"aliases": [
"azurePowerShellVersion"
],
"options": {
"LatestVersion": "Latest installed version",
"OtherVersion": "Specify other version"
},
"name": "TargetAzurePs",
"label": "Azure PowerShell Version",
"defaultValue": "OtherVersion",
"type": "radio",
"helpMarkDown": "In case of hosted agents, the supported Azure PowerShell Version is: 1.0.0, 1.6.0, 2.3.2, 2.6.0, 3.1.0 (Hosted VS2017 Queue).\nTo pick the latest version available on the agent, select \"Latest installed version\".\n\nFor private agents you can specify preferred version of Azure PowerShell using \"Specify version\"",
"groupName": "AzurePowerShellVersionOptions"
},
{
"aliases": [
"preferredAzurePowerShellVersion"
],
"name": "CustomTargetAzurePs",
"label": "Preferred Azure PowerShell Version",
"defaultValue": "",
"required": true,
"type": "string",
"helpMarkDown": "Preferred Azure PowerShell Version needs to be a proper semantic version eg. 1.2.3. Regex like 2.\\*,2.3.\\* is not supported. The Hosted VS2017 Pool currently supports Az module version: 1.0.0, 1.6.0, 2.3.2, 2.6.0, 3.1.0",
"visibleRule": "TargetAzurePs = OtherVersion",
"groupName": "AzurePowerShellVersionOptions"
},
{
"name": "pwsh",
"label": "Use PowerShell Core",
"defaultValue": "false",
"type": "boolean",
"helpMarkDown": "If this is true, then on Windows the task will use pwsh.exe from your PATH instead of powershell.exe.",
"groupName": "advanced"
},
{
"name": "validateScriptSignature",
"label": "Validate script signature",
"defaultValue": "false",
"type": "boolean",
"helpMarkDown": "If this is true, then the task will first check to make sure specified script is signed and valid before executing it.",
"visibleRule": "ScriptType = FilePath",
"groupName": "advanced"
},
{
"name": "workingDirectory",
"label": "Working Directory",
"defaultValue": "",
"type": "filePath",
"helpMarkDown": "Working directory where the script is run.",
"groupName": "advanced"
}
],
"satisfies": [],
"sourceDefinitions": [],
"dataSourceBindings": [],
"instanceNameFormat": "Azure PowerShell script: $(ScriptType)",
"preJobExecution": {},
"execution": {
"PowerShell3": {
"target": "azurepowershell.ps1",
"platforms": [
"windows"
]
},
"Node16": {
"target": "azurepowershell.js",
"argumentFormat": ""
},
"Node10": {
"target": "azurepowershell.js",
"argumentFormat": ""
},
"Node20_1": {
"target": "azurepowershell.js",
"argumentFormat": ""
}
},
"postJobExecution": {},
"_buildConfigMapping": {
"Default": "5.248.2",
"Node20_229_2": "5.248.3"
}
}
]
}

View file

@ -1,5 +1,9 @@
import { getPkgReleases } from '..';
import { Fixtures } from '../../../../test/fixtures';
import * as httpMock from '../../../../test/http-mock';
import { GlobalConfig } from '../../../config/global';
import * as hostRules from '../../../util/host-rules';
import { AzurePipelinesTask } from './schema';
import { AzurePipelinesTasksDatasource } from '.';
const gitHubHost = 'https://raw.githubusercontent.com';
@ -9,6 +13,11 @@ const marketplaceTasksPath =
'/renovatebot/azure-devops-marketplace/main/azure-pipelines-marketplace-tasks.json';
describe('modules/datasource/azure-pipelines-tasks/index', () => {
beforeEach(() => {
GlobalConfig.reset();
hostRules.clear();
});
it('returns null for unknown task', async () => {
httpMock
.scope(gitHubHost)
@ -64,4 +73,103 @@ describe('modules/datasource/azure-pipelines-tasks/index', () => {
}),
).toEqual({ releases: [{ version: '0.171.0' }, { version: '0.198.0' }] });
});
it('returns organization task with single version', async () => {
GlobalConfig.set({
platform: 'azure',
endpoint: 'https://my.custom.domain',
});
hostRules.add({
hostType: AzurePipelinesTasksDatasource.id,
matchHost: 'my.custom.domain',
token: '123test',
});
httpMock
.scope('https://my.custom.domain')
.get('/_apis/distributedtask/tasks/')
.reply(200, Fixtures.get('tasks.json'));
expect(
await getPkgReleases({
datasource: AzurePipelinesTasksDatasource.id,
packageName: 'AzurePowerShell',
}),
).toEqual({ releases: [{ version: '5.248.3' }] });
});
it('returns organization task with multiple versions', async () => {
GlobalConfig.set({
platform: 'azure',
endpoint: 'https://my.custom.domain',
});
hostRules.add({
hostType: AzurePipelinesTasksDatasource.id,
matchHost: 'my.custom.domain',
token: '123test',
});
httpMock
.scope('https://my.custom.domain')
.get('/_apis/distributedtask/tasks/')
.reply(200, Fixtures.get('tasks.json'));
expect(
await getPkgReleases({
datasource: AzurePipelinesTasksDatasource.id,
packageName: 'PowerShell',
}),
).toEqual({
releases: [
{ isDeprecated: true, version: '1.2.3' },
{ isDeprecated: undefined, version: '2.247.1' },
],
});
});
describe('compare semver', () => {
it.each`
a | exp
${[]} | ${[]}
${['']} | ${['']}
${['', '']} | ${['', '']}
${['1.0.0']} | ${['1.0.0']}
${['1.0.1', '1.1.0', '1.0.0']} | ${['1.0.0', '1.0.1', '1.1.0']}
`('when versions is $a', ({ a, exp }) => {
const azureVersions = a.map((x: string) => {
const splitted = x.split('.');
const version =
splitted.length === 3
? {
major: Number(splitted[0]),
minor: Number(splitted[1]),
patch: Number(splitted[2]),
}
: null;
return AzurePipelinesTask.parse({
name: '',
deprecated: false,
version,
});
});
const azureSortedVersions = azureVersions.sort(
AzurePipelinesTasksDatasource.compareSemanticVersions('version'),
);
expect(
azureSortedVersions.map((x: any) => {
const data = AzurePipelinesTask.parse(x);
return data.version === null
? ''
: `${data.version.major}.${data.version.minor}.${data.version.patch}`;
}),
).toStrictEqual(exp);
});
});
});

View file

@ -1,7 +1,16 @@
import type { TypeOf, ZodType } from 'zod';
import { GlobalConfig } from '../../../config/global';
import { cache } from '../../../util/cache/package/decorator';
import * as hostRules from '../../../util/host-rules';
import type { HttpOptions } from '../../../util/http/types';
import { id as versioning } from '../../versioning/loose';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import {
AzurePipelinesFallbackTasks,
AzurePipelinesJSON,
AzurePipelinesTaskVersion,
} from './schema';
const TASKS_URL_BASE =
'https://raw.githubusercontent.com/renovatebot/azure-devops-marketplace/main';
@ -22,13 +31,58 @@ export class AzurePipelinesTasksDatasource extends Datasource {
async getReleases({
packageName,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
const versions =
(await this.getTasks(BUILT_IN_TASKS_URL))[packageName.toLowerCase()] ??
(await this.getTasks(MARKETPLACE_TASKS_URL))[packageName.toLowerCase()];
const platform = GlobalConfig.get('platform');
const endpoint = GlobalConfig.get('endpoint');
const { token } = hostRules.find({
hostType: AzurePipelinesTasksDatasource.id,
url: endpoint,
});
if (versions) {
const releases = versions.map((version) => ({ version }));
return { releases };
if (platform === 'azure' && endpoint && token) {
const auth = Buffer.from(`renovate:${token}`).toString('base64');
const opts: HttpOptions = {
headers: { authorization: `Basic ${auth}` },
};
const results = await this.getTasks(
`${endpoint}/_apis/distributedtask/tasks/`,
opts,
AzurePipelinesJSON,
);
const result: ReleaseResult = { releases: [] };
results.value
.filter((task) => task.name === packageName)
.sort(AzurePipelinesTasksDatasource.compareSemanticVersions('version'))
.forEach((task) => {
result.releases.push({
version: `${task.version!.major}.${task.version!.minor}.${task.version!.patch}`,
isDeprecated: task.deprecated,
});
});
return result;
} else {
const versions =
(
await this.getTasks(
BUILT_IN_TASKS_URL,
{},
AzurePipelinesFallbackTasks,
)
)[packageName.toLowerCase()] ??
(
await this.getTasks(
MARKETPLACE_TASKS_URL,
{},
AzurePipelinesFallbackTasks,
)
)[packageName.toLowerCase()];
if (versions) {
const releases = versions.map((version) => ({ version }));
return { releases };
}
}
return null;
@ -39,8 +93,39 @@ export class AzurePipelinesTasksDatasource extends Datasource {
key: (url: string) => url,
ttlMinutes: 24 * 60,
})
async getTasks(url: string): Promise<Record<string, string[]>> {
const { body } = await this.http.getJson<Record<string, string[]>>(url);
async getTasks<ResT, Schema extends ZodType<ResT> = ZodType<ResT>>(
url: string,
opts: HttpOptions,
schema: Schema,
): Promise<TypeOf<Schema>> {
const { body } = await this.http.getJson(url, opts, schema);
return body;
}
static compareSemanticVersions = (key: string) => (a: any, b: any) => {
const a1Version = AzurePipelinesTaskVersion.safeParse(a[key]).data;
const b1Version = AzurePipelinesTaskVersion.safeParse(b[key]).data;
const a1 =
a1Version === undefined
? ''
: `${a1Version.major}.${a1Version.minor}.${a1Version.patch}`;
const b1 =
b1Version === undefined
? ''
: `${b1Version.major}.${b1Version.minor}.${b1Version.patch}`;
const len = Math.min(a1.length, b1.length);
for (let i = 0; i < len; i++) {
const a2 = +a1[i] || 0;
const b2 = +b1[i] || 0;
if (a2 !== b2) {
return a2 > b2 ? 1 : -1;
}
}
return b1.length - a1.length;
};
}

View file

@ -0,0 +1,19 @@
import { z } from 'zod';
export const AzurePipelinesTaskVersion = z.object({
major: z.number(),
minor: z.number(),
patch: z.number(),
});
export const AzurePipelinesTask = z.object({
name: z.string(),
deprecated: z.boolean().optional(),
version: AzurePipelinesTaskVersion.nullable(),
});
export const AzurePipelinesJSON = z.object({
value: AzurePipelinesTask.array(),
});
export const AzurePipelinesFallbackTasks = z.record(z.string().array());

View file

@ -1,4 +1,6 @@
import { regEx } from '../../../util/regex';
import type { HelmRepository } from './schema';
import type { FluxManifest } from './types';
export const systemManifestFileNameRegex = '(?:^|/)gotk-components\\.ya?ml$';
@ -8,3 +10,19 @@ export const systemManifestHeaderRegex =
export function isSystemManifest(file: string): boolean {
return regEx(systemManifestFileNameRegex).test(file);
}
export function collectHelmRepos(manifests: FluxManifest[]): HelmRepository[] {
const helmRepositories: HelmRepository[] = [];
for (const manifest of manifests) {
if (manifest.kind === 'resource') {
for (const resource of manifest.resources) {
if (resource.kind === 'HelmRepository') {
helmRepositories.push(resource);
}
}
}
}
return helmRepositories;
}

View file

@ -21,7 +21,11 @@ import type {
PackageFile,
PackageFileContent,
} from '../types';
import { isSystemManifest, systemManifestHeaderRegex } from './common';
import {
collectHelmRepos,
isSystemManifest,
systemManifestHeaderRegex,
} from './common';
import { FluxResource, type HelmRepository } from './schema';
import type {
FluxManagerData,
@ -102,6 +106,39 @@ function resolveGitRepositoryPerSourceTag(
}
}
function resolveHelmRepository(
dep: PackageDependency,
matchingRepositories: HelmRepository[],
registryAliases: Record<string, string> | undefined,
): void {
if (matchingRepositories.length) {
dep.registryUrls = matchingRepositories
.map((repo) => {
if (repo.spec.type === 'oci' || isOCIRegistry(repo.spec.url)) {
// Change datasource to Docker
dep.datasource = DockerDatasource.id;
// Ensure the URL is a valid OCI path
dep.packageName = getDep(
`${removeOCIPrefix(repo.spec.url)}/${dep.depName}`,
false,
registryAliases,
).depName;
return null;
} else {
return repo.spec.url;
}
})
.filter(is.string);
// if registryUrls is empty, delete it from dep
if (!dep.registryUrls?.length) {
delete dep.registryUrls;
}
} else {
dep.skipReason = 'unknown-registry';
}
}
function resolveSystemManifest(
manifest: SystemFluxManifest,
): PackageDependency<FluxManagerData>[] {
@ -126,7 +163,8 @@ function resolveResourceManifest(
for (const resource of manifest.resources) {
switch (resource.kind) {
case 'HelmRelease': {
const depName = resource.spec.chart.spec.chart;
const chartSpec = resource.spec.chart.spec;
const depName = chartSpec.chart;
const dep: PackageDependency = {
depName,
currentValue: resource.spec.chart.spec.version,
@ -142,40 +180,12 @@ function resolveResourceManifest(
const matchingRepositories = helmRepositories.filter(
(rep) =>
rep.kind === resource.spec.chart.spec.sourceRef?.kind &&
rep.metadata.name === resource.spec.chart.spec.sourceRef.name &&
rep.kind === chartSpec.sourceRef?.kind &&
rep.metadata.name === chartSpec.sourceRef.name &&
rep.metadata.namespace ===
(resource.spec.chart.spec.sourceRef.namespace ??
resource.metadata?.namespace),
(chartSpec.sourceRef.namespace ?? resource.metadata?.namespace),
);
if (matchingRepositories.length) {
dep.registryUrls = matchingRepositories
.map((repo) => {
if (repo.spec.type === 'oci' || isOCIRegistry(repo.spec.url)) {
// Change datasource to Docker
dep.datasource = DockerDatasource.id;
// Ensure the URL is a valid OCI path
dep.packageName = getDep(
`${removeOCIPrefix(repo.spec.url)}/${
resource.spec.chart.spec.chart
}`,
false,
registryAliases,
).depName;
return null;
} else {
return repo.spec.url;
}
})
.filter(is.string);
// if registryUrls is empty, delete it from dep
if (!dep.registryUrls?.length) {
delete dep.registryUrls;
}
} else {
dep.skipReason = 'unknown-registry';
}
resolveHelmRepository(dep, matchingRepositories, registryAliases);
deps.push(dep);
break;
}
@ -252,14 +262,7 @@ export function extractPackageFile(
if (!manifest) {
return null;
}
const helmRepositories: HelmRepository[] = [];
if (manifest.kind === 'resource') {
for (const resource of manifest.resources) {
if (resource.kind === 'HelmRepository') {
helmRepositories.push(resource);
}
}
}
const helmRepositories = collectHelmRepos([manifest]);
let deps: PackageDependency[] | null = null;
switch (manifest.kind) {
case 'system':
@ -293,16 +296,7 @@ export async function extractAllPackageFiles(
}
}
const helmRepositories: HelmRepository[] = [];
for (const manifest of manifests) {
if (manifest.kind === 'resource') {
for (const resource of manifest.resources) {
if (resource.kind === 'HelmRepository') {
helmRepositories.push(resource);
}
}
}
}
const helmRepositories = collectHelmRepos(manifests);
for (const manifest of manifests) {
let deps: PackageDependency[] | null = null;

View file

@ -17,6 +17,7 @@ import type {
import type { NpmLockFiles, NpmManagerData } from '../types';
import { getExtractedConstraints } from './common/dependency';
import { extractPackageJson } from './common/package-file';
import { extractPnpmWorkspaceFile } from './pnpm';
import { postExtract } from './post';
import type { NpmPackage } from './types';
import { isZeroInstall } from './yarn';
@ -229,12 +230,24 @@ export async function extractAllPackageFiles(
const content = await readLocalFile(packageFile, 'utf8');
// istanbul ignore else
if (content) {
const deps = await extractPackageFile(content, packageFile, config);
if (deps) {
npmFiles.push({
...deps,
packageFile,
});
// TODO(fpapado): for PR dicussion, consider this vs. a post hook, where
// we look for pnpm-workspace.yaml in the siblings
if (packageFile === 'pnpm-workspace.yaml') {
const deps = extractPnpmWorkspaceFile(content, packageFile);
if (deps) {
npmFiles.push({
...deps,
packageFile,
});
}
} else {
const deps = await extractPackageFile(content, packageFile, config);
if (deps) {
npmFiles.push({
...deps,
packageFile,
});
}
}
} else {
logger.debug({ packageFile }, `No content found`);

View file

@ -1,3 +1,4 @@
import { codeBlock } from 'common-tags';
import { Fixtures } from '../../../../../test/fixtures';
import { getFixturePath, logger, partial } from '../../../../../test/util';
import { GlobalConfig } from '../../../../config/global';
@ -8,6 +9,7 @@ import type { NpmManagerData } from '../types';
import {
detectPnpmWorkspaces,
extractPnpmFilters,
extractPnpmWorkspaceFile,
findPnpmWorkspace,
getPnpmLock,
} from './pnpm';
@ -270,4 +272,62 @@ describe('modules/manager/npm/extract/pnpm', () => {
expect(res.lockedVersionsWithPath).toBeUndefined();
});
});
describe('.extractPnpmWorkspaceFile()', () => {
it('ignores invalid pnpm-workspace.yaml file', () => {
expect(extractPnpmWorkspaceFile('', 'pnpm-workspace.yaml')).toBeNull();
});
it('handles empty catalog entries', () => {
expect(
extractPnpmWorkspaceFile(
codeBlock`
catalog:
catalogs:
`,
'pnpm-workspace.yaml',
),
).toBeNull();
});
it('parses valid pnpm-workspace.yaml file', () => {
expect(
extractPnpmWorkspaceFile(
codeBlock`
catalog:
react: 18.3.0
catalogs:
react17:
react: 17.0.2
`,
'pnpm-workspace.yaml',
),
).toMatchObject({
deps: [
{
currentValue: '18.3.0',
datasource: 'npm',
depName: 'react',
depType: 'pnpm.catalog',
prettyDepType: 'pnpm.catalog',
managerData: {
catalogName: 'default',
},
},
{
currentValue: '17.0.2',
datasource: 'npm',
depName: 'react',
depType: 'pnpm.catalog',
prettyDepType: 'pnpm.catalog',
managerData: {
catalogName: 'react17',
},
},
],
packageFile: 'pnpm-workspace.yaml',
});
});
});
});

View file

@ -1,6 +1,7 @@
import is from '@sindresorhus/is';
import { findPackages } from 'find-packages';
import upath from 'upath';
import { z } from 'zod';
import { GlobalConfig } from '../../../../config/global';
import { logger } from '../../../../logger';
import {
@ -10,10 +11,19 @@ import {
readLocalFile,
} from '../../../../util/fs';
import { parseSingleYaml } from '../../../../util/yaml';
import type { PackageFile } from '../../types';
import type {
PackageDependency,
PackageFile,
PackageFileContent,
} from '../../types';
import type { PnpmDependencySchema, PnpmLockFile } from '../post-update/types';
import type { NpmManagerData } from '../types';
import type { LockFile, PnpmWorkspaceFile } from './types';
import { extractDependency, parseDepName } from './common/dependency';
import type {
LockFile,
NpmPackageDependency,
PnpmWorkspaceFile,
} from './types';
function isPnpmLockfile(obj: any): obj is PnpmLockFile {
return is.plainObject(obj) && 'lockfileVersion' in obj;
@ -86,8 +96,8 @@ export async function detectPnpmWorkspaces(
const packagePathCache = new Map<string, string[] | null>();
for (const p of packageFiles) {
const { packageFile, managerData } = p;
const { pnpmShrinkwrap } = managerData as NpmManagerData;
const { packageFile, managerData = {} } = p;
const { pnpmShrinkwrap } = managerData as Partial<NpmManagerData>;
// check if pnpmShrinkwrap-file has already been provided
if (pnpmShrinkwrap) {
@ -222,3 +232,108 @@ function getLockedDependencyVersions(
return res;
}
/**
* A pnpm catalog is either the default catalog (catalog:, catlog:default), or a
* named one (under catalogs:)
*/
type PnpmCatalog = { name: string; dependencies: NpmPackageDependency };
export function extractPnpmWorkspaceFile(
content: string,
packageFile: string,
): PackageFile | null {
logger.trace(`pnpm.extractPnpmWorkspaceFile(${packageFile})`);
let pnpmCatalogs: Array<PnpmCatalog>;
try {
pnpmCatalogs = parsePnpmCatalogs(content);
} catch {
logger.debug({ packageFile }, `Invalid pnpm workspace YAML.`);
return null;
}
const extracted = extractPnpmCatalogDeps(pnpmCatalogs);
if (!extracted) {
logger.debug({ packageFile }, 'No dependencies found');
return null;
}
logger.debug(extracted, 'Extracted catalog dependencies.');
return {
...extracted,
packageFile,
};
}
function extractPnpmCatalogDeps(
catalogs: Array<PnpmCatalog>,
): PackageFileContent<NpmManagerData> | null {
const CATALOG_DEPENDENCY = 'pnpm.catalog';
const deps: PackageDependency[] = [];
for (const catalog of catalogs) {
for (const [key, val] of Object.entries(catalog.dependencies)) {
const depName = parseDepName(CATALOG_DEPENDENCY, key);
let dep: PackageDependency = {
depType: CATALOG_DEPENDENCY,
// TODO(fpapado): for PR discussion, consider how users might be able to
// match on specific catalogs for their config.
//
// For example, we could change depType to `pnpm.catalog.${string}`, so
// that users can match use `{matchDepTypes: ["pnpm.catalog.default"]}`,
// `{matchDepTypes: ["pnpm.catalog.react17"]}` and so on.
//
// Another option would be to mess with depName/packageName.
//
// Is there precedence for something similar?
depName,
managerData: {
// We assign the name of the catalog, in order to know which fields to
// update later on.
catalogName: catalog.name,
},
};
if (depName !== key) {
dep.managerData!.key = key;
}
// TODO: fix type #22198
dep = {
...dep,
...extractDependency(CATALOG_DEPENDENCY, depName, val!),
prettyDepType: CATALOG_DEPENDENCY,
};
dep.prettyDepType = CATALOG_DEPENDENCY;
deps.push(dep);
}
}
return {
deps,
};
}
export const pnpmCatalogsSchema = z.object({
catalog: z.optional(z.record(z.string())),
catalogs: z.optional(z.record(z.record(z.string()))),
});
function parsePnpmCatalogs(content: string): Array<PnpmCatalog> {
const { catalog: defaultCatalogDeps, catalogs: namedCatalogs } =
parseSingleYaml(content, { customSchema: pnpmCatalogsSchema });
return [
{
name: 'default',
dependencies: defaultCatalogDeps ?? {},
},
...Object.entries(namedCatalogs ?? {}).map(([name, dependencies]) => ({
name,
dependencies,
})),
];
}

View file

@ -36,6 +36,8 @@ export interface LockFile {
export interface PnpmWorkspaceFile {
packages: string[];
catalog?: Partial<Record<string, string>>;
catalogs?: Partial<Record<string, Partial<Record<string, string>>>>;
}
export type OverrideDependency = Record<string, RecursiveOverride>;

View file

@ -20,7 +20,8 @@ export const url = 'https://docs.npmjs.com';
export const categories: Category[] = ['js'];
export const defaultConfig = {
fileMatch: ['(^|/)package\\.json$'],
// TODO(fpapado): for PR dicussion, consider this vs. a post hook
fileMatch: ['(^|/)package\\.json$', '(^|/)pnpm-workspace\\.yaml$'],
digest: {
prBodyDefinitions: {
Change:

View file

@ -0,0 +1,33 @@
import { logger } from '../../../../../logger';
import type { Upgrade } from '../../../types';
export function getNewGitValue(upgrade: Upgrade): string | undefined {
if (!upgrade.currentRawValue) {
return;
}
if (upgrade.currentDigest) {
logger.debug('Updating git digest');
return upgrade.currentRawValue.replace(
upgrade.currentDigest,
// TODO #22198
upgrade.newDigest!.substring(0, upgrade.currentDigest.length),
);
} else {
logger.debug('Updating git version tag');
return upgrade.currentRawValue.replace(
upgrade.currentValue,
upgrade.newValue,
);
}
}
export function getNewNpmAliasValue(
value: string | undefined,
upgrade: Upgrade,
): string | undefined {
if (!upgrade.npmPackageAlias) {
return;
}
return `npm:${upgrade.packageName}@${value}`;
}

View file

@ -11,6 +11,8 @@ import type {
RecursiveOverride,
} from '../../extract/types';
import type { NpmDepType, NpmManagerData } from '../../types';
import { getNewGitValue, getNewNpmAliasValue } from './common';
import { updatePnpmCatalogDependency } from './pnpm';
function renameObjKey(
oldObj: DependenciesMeta,
@ -115,29 +117,16 @@ export function updateDependency({
fileContent,
upgrade,
}: UpdateDependencyConfig): string | null {
if (upgrade.depType === 'pnpm.catalog') {
return updatePnpmCatalogDependency({ fileContent, upgrade });
}
const { depType, managerData } = upgrade;
const depName: string = managerData?.key || upgrade.depName;
let { newValue } = upgrade;
if (upgrade.currentRawValue) {
if (upgrade.currentDigest) {
logger.debug('Updating package.json git digest');
newValue = upgrade.currentRawValue.replace(
upgrade.currentDigest,
// TODO #22198
upgrade.newDigest!.substring(0, upgrade.currentDigest.length),
);
} else {
logger.debug('Updating package.json git version tag');
newValue = upgrade.currentRawValue.replace(
upgrade.currentValue,
upgrade.newValue,
);
}
}
if (upgrade.npmPackageAlias) {
newValue = `npm:${upgrade.packageName}@${newValue}`;
}
newValue = getNewGitValue(upgrade) ?? newValue;
newValue = getNewNpmAliasValue(newValue, upgrade) ?? newValue;
logger.debug(`npm.updateDependency(): ${depType}.${depName} = ${newValue}`);
try {

View file

@ -0,0 +1,336 @@
import { codeBlock } from 'common-tags';
import * as npmUpdater from '../..';
/**
* Per the YAML spec, a document ends with a newline. The 'yaml' library always
* uses that when serialising, but `codeBlock` strips the last indentation. This
* helper makes assertions simpler.
*/
function yamlCodeBlock(
literals: TemplateStringsArray,
...placeholders: any[]
): string {
return codeBlock(literals, placeholders) + '\n';
}
describe('modules/manager/npm/update/dependency/pnpm', () => {
it('handles implicit default catalog dependency', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'react',
newValue: '19.0.0',
managerData: {
catalogName: 'default',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
catalog:
react: 18.3.1
`;
const expected = yamlCodeBlock`
packages:
- pkg-a
catalog:
react: 19.0.0
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
});
it('handles explicit default catalog dependency', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'react',
newValue: '19.0.0',
managerData: {
catalogName: 'default',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
catalogs:
default:
react: 18.3.1
`;
const expected = yamlCodeBlock`
packages:
- pkg-a
catalogs:
default:
react: 19.0.0
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
});
it('handles explicit named catalog dependency', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'react',
newValue: '19.0.0',
managerData: {
catalogName: 'react17',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
catalog:
react: 18.3.1
catalogs:
react17:
react: 17.0.0
`;
const expected = yamlCodeBlock`
packages:
- pkg-a
catalog:
react: 18.3.1
catalogs:
react17:
react: 19.0.0
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
});
it('replaces package', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'config',
newName: 'abc',
newValue: '2.0.0',
managerData: {
catalogName: 'default',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
catalog:
config: 1.21.0
`;
const expected = yamlCodeBlock`
packages:
- pkg-a
catalog:
abc: 2.0.0
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
});
it('replaces a github dependency value', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'gulp',
currentValue: 'v4.0.0-alpha.2',
currentRawValue: 'gulpjs/gulp#v4.0.0-alpha.2',
newValue: 'v4.0.0',
managerData: {
catalogName: 'default',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
catalog:
gulp: gulpjs/gulp#v4.0.0-alpha.2
`;
const expected = yamlCodeBlock`
packages:
- pkg-a
catalog:
gulp: gulpjs/gulp#v4.0.0
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
});
it('replaces a npm package alias', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'hapi',
npmPackageAlias: true,
packageName: '@hapi/hapi',
currentValue: '18.3.0',
newValue: '18.3.1',
managerData: {
catalogName: 'default',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
catalog:
hapi: npm:@hapi/hapi@18.3.0
`;
const expected = yamlCodeBlock`
packages:
- pkg-a
catalog:
hapi: npm:@hapi/hapi@18.3.1
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
});
it('replaces a github short hash', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'gulp',
currentDigest: 'abcdef7',
currentRawValue: 'gulpjs/gulp#abcdef7',
newDigest: '0000000000111111111122222222223333333333',
managerData: {
catalogName: 'default',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
catalog:
gulp: gulpjs/gulp#abcdef7
`;
const expected = yamlCodeBlock`
packages:
- pkg-a
catalog:
gulp: gulpjs/gulp#0000000
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
});
it('replaces a github fully specified version', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'n',
currentValue: 'v1.0.0',
currentRawValue: 'git+https://github.com/owner/n#v1.0.0',
newValue: 'v1.1.0',
managerData: {
catalogName: 'default',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
catalog:
n: git+https://github.com/owner/n#v1.0.0
`;
const expected = yamlCodeBlock`
packages:
- pkg-a
catalog:
n: git+https://github.com/owner/n#v1.1.0
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
});
it('returns null if the dependency is not present in the target catalog', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'react-not',
newValue: '19.0.0',
managerData: {
catalogName: 'default',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
catalog:
react: 18.3.1
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toBeNull();
});
it('returns null if catalogs are missing', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'react',
newValue: '19.0.0',
managerData: {
catalogName: 'default',
},
};
const pnpmWorkspaceYaml = yamlCodeBlock`
packages:
- pkg-a
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toBeNull();
});
it('returns null if empty file', () => {
const upgrade = {
depType: 'pnpm.catalog',
depName: 'react',
newValue: '19.0.0',
managerData: {
catalogName: 'default',
},
};
const testContent = npmUpdater.updateDependency({
fileContent: null as never,
upgrade,
});
expect(testContent).toBeNull();
});
});

View file

@ -0,0 +1,104 @@
import is from '@sindresorhus/is';
import { stringify } from 'yaml';
import { logger } from '../../../../../logger';
import { parseSingleYamlDocument } from '../../../../../util/yaml';
import type { UpdateDependencyConfig } from '../../../types';
import { pnpmCatalogsSchema } from '../../extract/pnpm';
import { getNewGitValue, getNewNpmAliasValue } from './common';
export function updatePnpmCatalogDependency({
fileContent,
upgrade,
}: UpdateDependencyConfig): string | null {
const { depType, managerData, depName } = upgrade;
const catalogName = managerData?.catalogName;
if (!is.string(catalogName)) {
logger.error(
'No catalogName was found; this is likely an extraction error.',
);
return null;
}
let { newValue } = upgrade;
newValue = getNewGitValue(upgrade) ?? newValue;
newValue = getNewNpmAliasValue(newValue, upgrade) ?? newValue;
logger.debug(
`npm.updatePnpmCatalogDependency(): ${depType}:${managerData?.catalogName}.${depName} = ${newValue}`,
);
let document;
let parsedContents;
try {
document = parseSingleYamlDocument(fileContent);
parsedContents = pnpmCatalogsSchema.parse(document.toJS());
} catch (err) {
logger.debug({ err }, 'Could not parse pnpm-workspace YAML file.');
return null;
}
// In pnpm-workspace.yaml, the default catalog can be either `catalog` or
// `catalog.default`, but not both (pnpm throws outright with a config error).
// Thus, we must check which entry is being used, to reference it from the
// right place.
const usesImplicitDefaultCatalog = parsedContents.catalog !== undefined;
// Save the old version
const oldVersion =
catalogName === 'default' && usesImplicitDefaultCatalog
? parsedContents.catalog?.[depName!]
: parsedContents.catalogs?.[catalogName]?.[depName!];
if (oldVersion === newValue) {
logger.trace('Version is already updated');
return fileContent;
}
// Update the value
const path = getDepPath({
depName: depName!,
catalogName,
usesImplicitDefaultCatalog,
});
if (!document.hasIn(path)) {
return null;
}
document.setIn(path, newValue);
// Update the name, for replacements
if (upgrade.newName) {
const newPath = getDepPath({
depName: upgrade.newName,
catalogName,
usesImplicitDefaultCatalog,
});
const oldValue = document.getIn(path);
document.deleteIn(path);
document.setIn(newPath, oldValue);
}
return stringify(document);
}
function getDepPath({
catalogName,
depName,
usesImplicitDefaultCatalog,
}: {
usesImplicitDefaultCatalog: boolean;
catalogName: string;
depName: string;
}): string[] {
if (catalogName === 'default' && usesImplicitDefaultCatalog) {
return ['catalog', depName];
} else {
return ['catalogs', catalogName, depName];
}
}

View file

@ -1,5 +1,6 @@
import type {
CreateNodeOptions,
Document,
DocumentOptions,
ParseOptions,
SchemaOptions,
@ -20,6 +21,13 @@ interface YamlOptions<
removeTemplates?: boolean;
}
interface YamlParseDocumentOptions
extends ParseOptions,
DocumentOptions,
SchemaOptions {
removeTemplates?: boolean;
}
interface YamlOptionsMultiple<
ResT = unknown,
Schema extends ZodType<ResT> = ZodType<ResT>,
@ -117,6 +125,29 @@ export function parseSingleYaml<ResT = unknown>(
content: string,
options?: YamlOptions<ResT>,
): ResT {
const rawDocument = parseSingleYamlDocument(content, options);
const document = rawDocument.toJS({ maxAliasCount: 10000 });
const schema = options?.customSchema;
if (!schema) {
return document as ResT;
}
return schema.parse(document);
}
/**
* Parse a YAML string into a Document representation.
*
* Only a single document is supported.
*
* @param content
* @param options
*/
export function parseSingleYamlDocument(
content: string,
options?: YamlParseDocumentOptions,
): Document {
const massagedContent = massageContent(content, options);
const rawDocument = parseDocument(
massagedContent,
@ -127,13 +158,7 @@ export function parseSingleYaml<ResT = unknown>(
throw new AggregateError(rawDocument.errors, 'Failed to parse YAML file');
}
const document = rawDocument.toJS({ maxAliasCount: 10000 });
const schema = options?.customSchema;
if (!schema) {
return document as ResT;
}
return schema.parse(document);
return rawDocument;
}
export function dump(obj: any, opts?: DumpOptions): string {

View file

@ -336,7 +336,7 @@
"jest-mock-extended": "3.0.7",
"jest-snapshot": "29.7.0",
"markdownlint-cli2": "0.17.1",
"memfs": "4.15.2",
"memfs": "4.15.3",
"nock": "13.5.6",
"npm-run-all2": "7.0.2",
"nyc": "17.1.0",

View file

@ -581,8 +581,8 @@ importers:
specifier: 0.17.1
version: 0.17.1
memfs:
specifier: 4.15.2
version: 4.15.2
specifier: 4.15.3
version: 4.15.3
nock:
specifier: 13.5.6
version: 13.5.6
@ -4555,8 +4555,8 @@ packages:
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
memfs@4.15.2:
resolution: {integrity: sha512-n8/qP8AT6CtY6kxCPYgYVusT5rS6axaT66dD3tYi2lm+l1iMH7YYpmW8H/qL5bfV4YvInCCgUDAWIRvrNS7kbQ==}
memfs@4.15.3:
resolution: {integrity: sha512-vR/g1SgqvKJgAyYla+06G4p/EOcEmwhYuVb1yc1ixcKf8o/sh7Zngv63957ZSNd1xrZJoinmNyDf2LzuP8WJXw==}
engines: {node: '>= 4.0.0'}
memorystream@0.3.1:
@ -11769,7 +11769,7 @@ snapshots:
mdurl@2.0.0: {}
memfs@4.15.2:
memfs@4.15.3:
dependencies:
'@jsonjoy.com/json-pack': 1.1.1(tslib@2.8.1)
'@jsonjoy.com/util': 1.5.0(tslib@2.8.1)

View file

@ -5,19 +5,19 @@ ARG BASE_IMAGE_TYPE=slim
# --------------------------------------
# slim image
# --------------------------------------
FROM ghcr.io/renovatebot/base-image:9.29.0@sha256:10e27273241a0ba63d3a298a7b1e178dbb75b84da6bc2ea7a71db7c9d1a4971c AS slim-base
FROM ghcr.io/renovatebot/base-image:9.29.1@sha256:db4b70c00fb197babca9dd92be612bef044d7a35d933d19c668864f84b52d1f8 AS slim-base
# --------------------------------------
# full image
# --------------------------------------
FROM ghcr.io/renovatebot/base-image:9.29.0-full@sha256:7b2353855c0f59b9efdb93ce9356aff5dad7d5102f8947c4ebc906855be9177c AS full-base
FROM ghcr.io/renovatebot/base-image:9.29.1-full@sha256:4880c7aae10ed892d49c6c5573418014605ce2824c978dbcc04382a2c26bb0df AS full-base
ENV RENOVATE_BINARY_SOURCE=global
# --------------------------------------
# build image
# --------------------------------------
FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:9.29.0@sha256:10e27273241a0ba63d3a298a7b1e178dbb75b84da6bc2ea7a71db7c9d1a4971c AS build
FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:9.29.1@sha256:db4b70c00fb197babca9dd92be612bef044d7a35d933d19c668864f84b52d1f8 AS build
# We want a specific node version here
# renovate: datasource=node-version