Update dependencies and workflows #442
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, | |
| }; | |
| } | |
| // 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 |