Update dependencies and workflows #441
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | |
| }; | |
| } | |
| 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); | |
| // Filter LTS only, strip 'v' prefix | |
| 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] }; | |
| }); | |
| // Sort majors descending | |
| latestPerMajor.sort((a, b) => b.major - a.major); | |
| // Take latest 4 LTS majors | |
| const top4 = latestPerMajor.slice(0, 4); | |
| const includedVersions = new Set(top4.map(e => e.version)); | |
| const extra = []; | |
| if (minVersion !== null) { | |
| // Resolve the exact minimum version string to test: | |
| // - patch specified (>= 18.18.2): use exactly 18.18.2 | |
| // - minor specified, no patch (>= 18.18): use latest 18.18.x | |
| // - only major (>= 18): use latest 18.x.x (same as latestPerMajor entry) | |
| let minExact = null; | |
| if (minVersion.patchSpecified) { | |
| // Use the exact version if it exists in LTS releases | |
| const candidate = `${minVersion.major}.${minVersion.minor}.${minVersion.patch}`; | |
| if (lts.includes(candidate)) minExact = candidate; | |
| } else if (minVersion.minorSpecified) { | |
| // Find the latest patch for this major.minor | |
| const candidates = lts.filter(v => { | |
| const [maj, min] = v.split('.').map(Number); | |
| return maj === minVersion.major && min === minVersion.minor; | |
| }); | |
| candidates.sort(compareSemver); | |
| if (candidates.length > 0) minExact = candidates[0]; // highest patch | |
| } | |
| // If only major specified, minExact stays null — latestPerMajor already covers it | |
| // Add the resolved minimum version if not already included | |
| if (minExact && !includedVersions.has(minExact)) { | |
| extra.push({ major: minVersion.major, version: minExact }); | |
| includedVersions.add(minExact); | |
| } | |
| // Always also add the latest patch of the minimum major if not already included | |
| const latestOfMinMajor = latestPerMajor.find(e => e.major === minVersion.major); | |
| if (latestOfMinMajor && !includedVersions.has(latestOfMinMajor.version)) { | |
| extra.push(latestOfMinMajor); | |
| includedVersions.add(latestOfMinMajor.version); | |
| } | |
| } | |
| // Merge and sort ascending (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 |