From 159048d198e6a2007055dd9d845012a0800a274f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:36:06 -0400 Subject: [PATCH] fix(renovate): improve extraction and add tests Add Node.js test suite for renovate custom manager regex patterns, covering npm CDN URL extraction from Jekyll, Sphinx, JS, JSON, and HTML files, plus cdnjs CSS imports. Wire tests into the validation workflow and add a `cdnurl` manager for CSS assets. --- .../workflows/__validate-renovate-config.yml | 4 + package.json | 1 + renovate-config.json5 | 19 ++- tests/renovate-config.test.mjs | 146 ++++++++++++++++++ 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 tests/renovate-config.test.mjs diff --git a/.github/workflows/__validate-renovate-config.yml b/.github/workflows/__validate-renovate-config.yml index df0d3408..90347172 100644 --- a/.github/workflows/__validate-renovate-config.yml +++ b/.github/workflows/__validate-renovate-config.yml @@ -66,3 +66,7 @@ jobs: # exit with error if any file failed exit ${status} + + - name: renovate extraction tests + if: steps.find-files.outputs.found != '' + run: npm test diff --git a/package.json b/package.json index 0fb16fb9..2a4fbb93 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "description": "LizardByte .github repository", "scripts": { + "test": "node --test", "validate-dependabot": "validate-dependabot-yaml", "validate-renovate": "renovate-config-validator --strict" }, diff --git a/renovate-config.json5 b/renovate-config.json5 index 824e2999..f4926c1f 100644 --- a/renovate-config.json5 +++ b/renovate-config.json5 @@ -2,6 +2,11 @@ // see https://docs.renovatebot.com/configuration-options "$schema": "https://docs.renovatebot.com/renovate-schema.json", "branchNameStrict": true, // remove special characters from branch names + "cdnurl": { + "managerFilePatterns": [ + "/(^|/)assets/.*[.]css$/", + ], + }, "cloneSubmodules": true, // needed for pyproject.toml that depend on submodules "cloneSubmodulesFilter": [ "packaging/linux/flatpak/deps/flatpak-builder-tools", // Sunshine @@ -153,12 +158,24 @@ "/^(?:_config(?:_[^/]+)?[.]ya?ml|_data/.*[.]ya?ml|_layouts/.*[.]html|[^/]+[.](?:html|md)|blog/.*[.]html|_posts/.*[.]md|docs/.*[.]md|gh-pages-template/(?:_data/.*[.]ya?ml|.*[.](?:html|md)))$/", ], "matchStrings": [ - "(?:(?:href|src):\\s*[\"']?|(?:href|src)=[\"'])https://(?:cdn\\.jsdelivr\\.net/npm|unpkg\\.com)/(?(?:@[^@/\"'\\s]+/)?[^@/\"'\\s]+)@(?[^/\"'\\s?#]+)", + "(?:(?:href|src|icon_img):\\s*[\"']?|(?:href|src)=[\"'])https://(?:cdn\\.jsdelivr\\.net/npm|unpkg\\.com)/(?(?:@[^@/\"'\\s]+/)?[^@/\"'\\s]+)@(?[^/\"'\\s?#]+)", "(?:^|\\r?\\n)[ \\t]*-[ \\t]*[\"']?https://(?:cdn\\.jsdelivr\\.net/npm|unpkg\\.com)/(?(?:@[^@/\"'\\s]+/)?[^@/\"'\\s]+)@(?[^/\"'\\s?#]+)", ], "datasourceTemplate": "npm", "versioningTemplate": "npm", }, + // Versioned npm CDN URLs in selected source/data files without SRI. + { + "customType": "regex", + "managerFilePatterns": [ + "/^(?:_includes/.*[.]html|apps/.*[.]json|assets/.*[.]js|docs/source/conf[.]py)$/", + ], + "matchStrings": [ + "https://(?:cdn\\.jsdelivr\\.net/npm|unpkg\\.com)/(?(?:@[^@/\"'\\s]+/)?[^@/\"'\\s]+)@(?v?\\d[^/\"'\\s?#]*)", + ], + "datasourceTemplate": "npm", + "versioningTemplate": "npm", + }, // Annotated YAML version fields such as _data/licenses.yml in LizardByte.github.io { "customType": "regex", diff --git a/tests/renovate-config.test.mjs b/tests/renovate-config.test.mjs new file mode 100644 index 00000000..5745737d --- /dev/null +++ b/tests/renovate-config.test.mjs @@ -0,0 +1,146 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +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'; + +const renovateConfig = JSON5.parse(fs.readFileSync('renovate-config.json5', 'utf8')); +const jekyllNpmCdnManager = renovateConfig.customManagers.find( + manager => manager.datasourceTemplate === 'npm' + && manager.managerFilePatterns.some(pattern => pattern.includes('gh-pages-template')), +); +const sourceNpmCdnManager = renovateConfig.customManagers.find( + manager => manager.datasourceTemplate === 'npm' + && manager.managerFilePatterns.some(pattern => pattern.includes('docs/source/conf')), +); + +function matchesManagerFilePattern(fileName, patterns) { + return patterns.some(pattern => { + const closingSlash = pattern.lastIndexOf('/'); + assert.equal(pattern[0], '/', `Expected a regex managerFilePattern: ${pattern}`); + assert.ok(closingSlash > 0, `Expected a closing slash in managerFilePattern: ${pattern}`); + + return new RegExp(pattern.slice(1, closingSlash), pattern.slice(closingSlash + 1)).test(fileName); + }); +} + +function extractNpmDependencies(manager, fileName, content) { + assert.ok(manager, 'Expected to find the npm CDN custom manager'); + assert.ok( + matchesManagerFilePattern(fileName, manager.managerFilePatterns), + `Expected the npm CDN manager to scan ${fileName}`, + ); + + return extractRegexPackageFile(content, fileName, manager)?.deps ?? []; +} + +test('extracts versioned npm CDN URLs from supported file types', () => { + const cases = [ + { + fileName: 'docs/source/conf.py', + content: ` +html_css_files = [ + 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2026.314.32913/dist/styles.css', +] +`, + expected: [['@lizardbyte/shared-web', '2026.314.32913']], + }, + { + fileName: 'assets/js/projects.js', + content: ` +const sharedWeb = 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@v2026.726.204939'; +const icon = 'https://cdn.jsdelivr.net/npm/simple-icons@v15/icons/readthedocs.svg'; +`, + expected: [ + ['@lizardbyte/shared-web', 'v2026.726.204939'], + ['simple-icons', 'v15'], + ], + }, + { + fileName: 'apps/moonlight/client.json', + content: '{"src":"https://cdn.jsdelivr.net/npm/simple-icons@13/icons/example.svg"}', + expected: [['simple-icons', '13']], + }, + { + fileName: '_includes/commentbox.html', + content: '', + expected: [['commentbox.io', '2.1.0']], + }, + ]; + + for (const {fileName, content, expected} of cases) { + const actual = extractNpmDependencies(sourceNpmCdnManager, fileName, content).map(dependency => [ + dependency.depName, + dependency.currentValue, + ]); + + assert.deepEqual(actual, expected, fileName); + } +}); + +test('extracts every shared-web pin from this repository Sphinx config', () => { + const dependencies = extractNpmDependencies( + sourceNpmCdnManager, + 'docs/source/conf.py', + fs.readFileSync('docs/source/conf.py', 'utf8'), + ); + + assert.deepEqual( + dependencies.map(dependency => [dependency.depName, dependency.currentValue]), + [ + ['@lizardbyte/shared-web', '2026.314.32913'], + ['@lizardbyte/shared-web', '2026.314.32913'], + ['@lizardbyte/shared-web', '2026.314.32913'], + ], + ); +}); + +test('ignores moving tags and interpolated npm CDN versions', () => { + const dependencies = extractNpmDependencies( + sourceNpmCdnManager, + 'assets/js/example.js', + ` +const latest = 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@latest/dist/example.js'; +const dynamic = \`https://cdn.jsdelivr.net/npm/@lizardbyte/gamepad-helper@\${gamepadHelperVersion}/example.js\`; +`, + ); + + assert.deepEqual(dependencies, []); +}); + +test('keeps commented Sphinx examples outside the selected Python path', () => { + assert.equal( + matchesManagerFilePattern('examples/sphinx/source/conf.py', sourceNpmCdnManager.managerFilePatterns), + false, + ); +}); + +test('extracts npm CDN URLs from known Jekyll YAML keys', () => { + const dependencies = extractNpmDependencies( + jekyllNpmCdnManager, + 'gh-pages-template/_data/features.yml', + 'icon_img: "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/icons/windows.svg"', + ); + + assert.deepEqual( + dependencies.map(dependency => [dependency.depName, dependency.currentValue]), + [['bootstrap-icons', '1.13.1']], + ); +}); + +test('extracts cdnjs imports from asset stylesheets', () => { + const fileName = 'docs/assets/css/style.css'; + assert.ok(matchesManagerFilePattern(fileName, renovateConfig.cdnurl.managerFilePatterns)); + + const dependencies = extractCdnUrlPackageFile( + "@import '//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.1/normalize.min.css';", + fileName, + ).deps; + + assert.deepEqual( + dependencies.map(dependency => [dependency.depName, dependency.currentValue]), + [['normalize', '3.0.1']], + ); +});