Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/__validate-renovate-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
19 changes: 18 additions & 1 deletion renovate-config.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)/(?<depName>(?:@[^@/\"'\\s]+/)?[^@/\"'\\s]+)@(?<currentValue>[^/\"'\\s?#]+)",
"(?:(?:href|src|icon_img):\\s*[\"']?|(?:href|src)=[\"'])https://(?:cdn\\.jsdelivr\\.net/npm|unpkg\\.com)/(?<depName>(?:@[^@/\"'\\s]+/)?[^@/\"'\\s]+)@(?<currentValue>[^/\"'\\s?#]+)",
"(?:^|\\r?\\n)[ \\t]*-[ \\t]*[\"']?https://(?:cdn\\.jsdelivr\\.net/npm|unpkg\\.com)/(?<depName>(?:@[^@/\"'\\s]+/)?[^@/\"'\\s]+)@(?<currentValue>[^/\"'\\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)/(?<depName>(?:@[^@/\"'\\s]+/)?[^@/\"'\\s]+)@(?<currentValue>v?\\d[^/\"'\\s?#]*)",
],
"datasourceTemplate": "npm",
"versioningTemplate": "npm",
},
// Annotated YAML version fields such as _data/licenses.yml in LizardByte.github.io
{
"customType": "regex",
Expand Down
146 changes: 146 additions & 0 deletions tests/renovate-config.test.mjs
Original file line number Diff line number Diff line change
@@ -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: '<script src="https://unpkg.com/commentbox.io@2.1.0/dist/commentBox.min.js"></script>',
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']],
);
});