Skip to content

Update dependencies and workflows #442

Update dependencies and workflows

Update dependencies and workflows #442

Workflow file for this run

name: Tests
on: [push, pull_request]
jobs:
resolve-node-versions:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
node <<'EOF' >> $GITHUB_OUTPUT
const https = require('https');
const fs = require('fs');
function compareSemver(a, b) {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0);
}
return 0;
}
// Returns { major, minor, patch, minorSpecified, patchSpecified } or null
function parseEnginesVersion(enginesNode) {
if (!enginesNode) return null;
const match = enginesNode.match(/>=\s*(\d+)(?:\.(\d+)(?:\.(\d+))?)?/);
if (!match) return null;
return {
major: parseInt(match[1], 10),
minor: match[2] !== undefined ? parseInt(match[2], 10) : null,
patch: match[3] !== undefined ? parseInt(match[3], 10) : null,
minorSpecified: match[2] !== undefined,
patchSpecified: match[3] !== undefined,
};
}
// Build the version spec to pass to setup-node for the minimum version.
// We let setup-node resolve the latest patch when patch is omitted.
// e.g. ">= 18" -> "18"
// ">= 18.18" -> "18.18"
// ">= 18.18.2" -> "18.18.2"
function minVersionSpec(minVersion) {
if (minVersion.patchSpecified) return `${minVersion.major}.${minVersion.minor}.${minVersion.patch}`;
if (minVersion.minorSpecified) return `${minVersion.major}.${minVersion.minor}`;
return `${minVersion.major}`;
}
let enginesNode = null;
try {
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
enginesNode = pkg?.engines?.node ?? null;
} catch {}
const minVersion = parseEnginesVersion(enginesNode);
https.get('https://nodejs.org/dist/index.json', res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const releases = JSON.parse(data);
const lts = releases
.filter(r => r.lts)
.map(r => r.version.replace(/^v/, ''));
// Group by major, pick highest semver per major
const byMajor = {};
for (const v of lts) {
const major = parseInt(v.split('.')[0], 10);
if (!byMajor[major]) byMajor[major] = [];
byMajor[major].push(v);
}
const latestPerMajor = Object.entries(byMajor)
.map(([major, versions]) => {
versions.sort(compareSemver);
return { major: parseInt(major, 10), version: versions[0] };
});
latestPerMajor.sort((a, b) => b.major - a.major);
// Take latest 4 LTS majors (these are exact resolved versions)
const top4 = latestPerMajor.slice(0, 4);
const top4Majors = new Set(top4.map(e => e.major));
const extra = [];
if (minVersion !== null) {
const spec = minVersionSpec(minVersion);
// Check if this spec is already fully covered:
// - If only major specified and that major is in top4, it's covered.
// - If minor or patch specified, we always add it — even if the major
// is in top4, because the top4 entry is the *latest* of that major,
// which may differ from the minimum spec.
const alreadyCovered =
!minVersion.minorSpecified && top4Majors.has(minVersion.major);
if (!alreadyCovered) {
// Check for exact duplicate against resolved top4 versions
const alreadyExact = top4.some(e => e.version === spec);
if (!alreadyExact) {
extra.push({ major: minVersion.major, version: spec });
}
}
// Also ensure the latest of the min major is in the list,
// in case it wasn't in top4 (e.g. engines requires an older major)
const latestOfMinMajor = latestPerMajor.find(e => e.major === minVersion.major);
if (latestOfMinMajor && !top4Majors.has(minVersion.major)) {
extra.push(latestOfMinMajor);
}
}
// Merge, sort ascending by semver (oldest -> newest)
const all = [...top4, ...extra];
all.sort((a, b) => compareSemver(b.version, a.version));
const matrix = { "node-version": all.map(e => e.version) };
console.log(`matrix=${JSON.stringify(matrix)}`);
});
});
EOF
test:
needs: resolve-node-versions
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.resolve-node-versions.outputs.matrix) }}
name: Node ${{ matrix.node-version }}
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Enable Corepack
run: corepack enable
- name: Detect Yarn version
id: yarn-version
run: |
YARN_VERSION=$(node -p "
try {
const pm = require('./package.json').packageManager ?? '';
const match = pm.match(/^yarn@(\d+)/);
match ? match[1] : '';
} catch { '' }
")
if [ -z "$YARN_VERSION" ]; then
YARN_VERSION=$(yarn --version | cut -d. -f1)
fi
echo "major=$YARN_VERSION" >> "$GITHUB_OUTPUT"
echo "Detected Yarn major version: $YARN_VERSION"
- name: Install dependencies
run: |
if [ "${{ steps.yarn-version.outputs.major }}" = "1" ]; then
yarn install --frozen-lockfile --non-interactive --prefer-offline
else
yarn install --immutable
fi
- name: Lint
run: yarn lint
- name: Test
run: yarn test