From 359e5515872e8c63e783eb3a8fe4da34367873fa Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:12:07 -0400 Subject: [PATCH] feat(renovate): add regex manager for conda env pins Introduces a new Renovate custom regex manager to detect pinned conda-forge dependencies in `environment.yml`/`environment.yaml` files and update them using conda versioning. Adds tests to verify dependency extraction and replacement behavior, including handling spacing variants and preserving unrelated entries/comments. --- renovate-config.json5 | 15 +++++++ tests/renovate-config.test.mjs | 73 ++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/renovate-config.json5 b/renovate-config.json5 index 2b1067c9..6dfb5c57 100644 --- a/renovate-config.json5 +++ b/renovate-config.json5 @@ -213,6 +213,21 @@ ], "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{/if}}", }, + // Pinned conda-forge dependencies in Conda environment files. + { + "customType": "regex", + "description": "Update pinned conda-forge dependencies in Conda environment files", + "managerFilePatterns": [ + "/(^|/)environment[.]ya?ml$/", + ], + "matchStrings": [ + "(?[ \\t]*)-[ \\t]*(?[A-Za-z0-9_.-]+)[ \\t]*=[ \\t]*(?[^=\\s#][^\\s#]*)", + ], + "datasourceTemplate": "conda", + "packageNameTemplate": "conda-forge/{{{depName}}}", + "versioningTemplate": "conda", + "autoReplaceStringTemplate": "{{{indentation}}}- {{{depName}}}={{{newVersion}}}", + }, // CPM Package Lock { "customType": "regex", diff --git a/tests/renovate-config.test.mjs b/tests/renovate-config.test.mjs index 2b7af6d5..4885d6cf 100644 --- a/tests/renovate-config.test.mjs +++ b/tests/renovate-config.test.mjs @@ -5,6 +5,7 @@ import test from 'node:test'; import JSON5 from 'json5'; import {extractPackageFile as extractCdnUrlPackageFile} from 'renovate/dist/modules/manager/cdnurl/index.js'; import {extractPackageFile as extractRegexPackageFile} from 'renovate/dist/modules/manager/custom/regex/index.js'; +import {compile} from 'renovate/dist/util/template/index.js'; const renovateConfig = JSON5.parse(fs.readFileSync('renovate-config.json5', 'utf8')); const githubRefManager = renovateConfig.customManagers.find( @@ -18,6 +19,10 @@ const sourceNpmCdnManager = renovateConfig.customManagers.find( manager => manager.datasourceTemplate === 'npm' && manager.managerFilePatterns.some(pattern => pattern.includes('docs/source/conf')), ); +const condaEnvironmentManager = renovateConfig.customManagers.find( + manager => manager.datasourceTemplate === 'conda' + && manager.managerFilePatterns.some(pattern => pattern.includes('environment')), +); function matchesManagerFilePattern(fileName, patterns) { return patterns.some(pattern => { @@ -56,6 +61,16 @@ function extractGitHubRefDependencies(fileName, content) { })); } +function extractCondaDependencies(fileName, content) { + assert.ok(condaEnvironmentManager, 'Expected to find the Conda environment custom manager'); + assert.ok( + matchesManagerFilePattern(fileName, condaEnvironmentManager.managerFilePatterns), + `Expected the Conda environment manager to scan ${fileName}`, + ); + + return extractRegexPackageFile(content, fileName, condaEnvironmentManager)?.deps ?? []; +} + test('extracts the actionlint release and jsDelivr commit', () => { const fileName = '.github/workflows/__call-common-lint.yml'; const content = fs.readFileSync(fileName, 'utf8'); @@ -90,6 +105,64 @@ test('extracts a GitHub branch and jsDelivr commit from a composite action', () ); }); +test('extracts and updates pinned conda-forge dependencies from environment files', () => { + const fileName = 'environment.yml'; + const content = ` +--- +name: RTD +channels: + - conda-forge + - defaults +dependencies: + - doxygen=1.2.3 + - graphviz = 4.5.6 # Keep an inline comment. + - nodejs + - pip: + - example==7.8.9 +`; + const dependencies = extractCondaDependencies(fileName, content); + + assert.deepEqual( + dependencies.map(dependency => ({ + currentValue: dependency.currentValue, + datasource: dependency.datasource, + depName: dependency.depName, + packageName: dependency.packageName, + versioning: dependency.versioning, + })), + [ + { + currentValue: '1.2.3', + datasource: 'conda', + depName: 'doxygen', + packageName: 'conda-forge/doxygen', + versioning: 'conda', + }, + { + currentValue: '4.5.6', + datasource: 'conda', + depName: 'graphviz', + packageName: 'conda-forge/graphviz', + versioning: 'conda', + }, + ], + ); + + const dependency = dependencies[0]; + const newVersion = nextTestVersion(dependency.currentValue); + const updatedReplaceString = compile( + condaEnvironmentManager.autoReplaceStringTemplate, + {...dependency, newVersion}, + false, + ); + const updatedContent = content.replace(dependency.replaceString, updatedReplaceString); + const updatedDependencies = extractCondaDependencies(fileName, updatedContent); + + assert.equal(updatedDependencies[0].currentValue, newVersion); + assert.match(updatedContent, new RegExp(` - doxygen=${newVersion.replaceAll('.', '\\.')}`)); + assert.match(updatedContent, / - graphviz = 4\.5\.6 # Keep an inline comment\./); +}); + function nextTestVersion(currentValue) { const match = /^(.*?)(\d+)$/.exec(currentValue); assert.ok(match, `Expected a version ending in a number: ${currentValue}`);