From 593d14054d2ff6c583144ef0d1a95cf3dde76acd Mon Sep 17 00:00:00 2001 From: Tim Beyer Date: Sat, 18 Jul 2026 16:56:22 +0200 Subject: [PATCH] ci: isolate benchmark comment permissions Run pull-request workflows with read-only tokens and discard checkout credentials. Publish sanitized benchmark artifacts from trusted default-branch code. --- .github/workflows/benchmark-comment.yml | 248 ++++++++++++++++++ .github/workflows/benchmark.yml | 317 +++++++++--------------- .github/workflows/build.yml | 31 ++- .github/workflows/commitlint.yml | 16 +- .github/workflows/lint.yml | 35 +-- .github/workflows/test.yml | 93 +++---- 6 files changed, 461 insertions(+), 279 deletions(-) create mode 100644 .github/workflows/benchmark-comment.yml diff --git a/.github/workflows/benchmark-comment.yml b/.github/workflows/benchmark-comment.yml new file mode 100644 index 0000000..7ef7120 --- /dev/null +++ b/.github/workflows/benchmark-comment.yml @@ -0,0 +1,248 @@ +name: Benchmark Comment + +on: + workflow_run: + workflows: [Benchmark] + types: [completed] + +# Artifact parsing and comment publication intentionally use separate tokens. +# Neither job checks out or executes code from the triggering pull request. +permissions: {} + +concurrency: + group: benchmark-comment-${{ github.event.workflow_run.head_sha }} + cancel-in-progress: true + +jobs: + prepare: + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + outputs: + report: ${{ steps.report.outputs.report }} + + steps: + - name: Download Bun benchmark artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + name: benchmark-results-bun-latest + path: ${{ runner.temp }}/benchmark-results + + - name: Download Node.js 22 benchmark artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + name: benchmark-results-node-22 + path: ${{ runner.temp }}/benchmark-results + + - name: Download Node.js 24 benchmark artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + name: benchmark-results-node-24 + path: ${{ runner.temp }}/benchmark-results + + - name: Download Node.js 25 benchmark artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + name: benchmark-results-node-25 + path: ${{ runner.temp }}/benchmark-results + + - name: Sanitize benchmark report + id: report + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + BENCHMARK_RESULTS: ${{ runner.temp }}/benchmark-results + with: + script: | + const fs = require('fs') + const path = require('path') + + const root = fs.realpathSync(process.env.BENCHMARK_RESULTS) + const expectedRuntimes = ['bun-latest', 'node-22', 'node-24', 'node-25'] + + const truncateUtf8 = (value, maxBytes) => { + const bytes = Buffer.from(value, 'utf8') + if (bytes.length <= maxBytes) return value + + let end = maxBytes + while (end > 0 && (bytes[end] & 0xc0) === 0x80) end-- + return bytes.subarray(0, end).toString('utf8') + } + const sanitize = value => + truncateUtf8(value, 13000) + .replace(//g, '') + .replace(/<[^>]*>/g, '') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '') + .replace(/[\u202a-\u202e\u2066-\u2069]/g, '') + .replaceAll('@', '@\u200b') + .replaceAll('](', ']\u200b(') + .replace(/https?:\/\//gi, match => match.replace('://', ':\u200b//')) + + const sections = expectedRuntimes.map(runtime => { + const name = `final-comparison-${runtime}.md` + const file = path.join(root, name) + const metadata = fs.lstatSync(file) + if (!metadata.isFile() || metadata.size > 50000) { + throw new Error(`Invalid benchmark comparison artifact: ${name}`) + } + if (path.dirname(fs.realpathSync(file)) !== root) { + throw new Error(`Benchmark artifact escapes its directory: ${name}`) + } + return sanitize(fs.readFileSync(file, 'utf8')) + }) + + const report = [ + '> The tables below were generated by pull-request-controlled benchmark code.', + '> Links, HTML, mentions, control characters, and bidirectional controls were sanitized.', + '', + ...sections + ].join('\n') + if (Buffer.byteLength(report, 'utf8') > 56000) { + throw new Error('Sanitized benchmark report exceeds the size limit') + } + + core.setOutput('report', Buffer.from(report, 'utf8').toString('base64')) + + publish: + needs: prepare + if: needs.prepare.result == 'success' && needs.prepare.outputs.report != '' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + issues: write + pull-requests: read + + steps: + - name: Create or update benchmark comment + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + BENCHMARK_REPORT: ${{ needs.prepare.outputs.report }} + with: + script: | + const marker = '' + const runIdMarker = '`, + '# 🚀 Benchmark Results', + '', + `Benchmark report for PR head [\`${shortHead}\`](${commitUrl}) from [workflow run ${run.id}](${run.html_url}).`, + '', + report, + '', + '
', + 'â„šī¸ Benchmark Details', + '', + `- **Base branch**: \`${pullRequest.base.ref}\``, + `- **PR head**: [\`${shortHead}\`](${commitUrl})`, + `- **Workflow run**: [${run.id}](${run.html_url})`, + `- **Completed**: ${run.updated_at}`, + '
' + ].join('\n').slice(0, 60000) + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + per_page: 100 + }) + const previous = comments.find(comment => + comment.user?.login === 'github-actions[bot]' && comment.body?.startsWith(marker) + ) + + if (previous) { + const previousRunId = Number( + previous.body?.match(//)?.[1] ?? 0 + ) + if (previousRunId > run.id) { + core.info(`Skipping older workflow run ${run.id}`) + return + } + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: previous.id, + body + }) + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body + }) + } diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ca43d60..1a72a84 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -2,67 +2,104 @@ name: Benchmark on: pull_request: - branches: [ main, master ] + branches: [main, master] + +# Pull-request code and benchmark dependencies are untrusted. Keep this workflow +# read-only; the trusted Benchmark Comment workflow publishes the artifacts. +permissions: + contents: read + +concurrency: + group: benchmark-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: benchmark: runs-on: namespace-profile-default + timeout-minutes: 40 # Only run PR comparison for pull requests if: github.event_name == 'pull_request' strategy: + fail-fast: false matrix: runtime: [node-22, node-24, node-25, bun-latest] steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Need full history to find merge-base - - - name: Parse runtime version - id: version - run: echo "value=${RUNTIME#*-}" >> $GITHUB_OUTPUT - env: - RUNTIME: ${{ matrix.runtime }} - - - name: Setup Node.js (node runtime) - if: startsWith(matrix.runtime, 'node-') - uses: actions/setup-node@v4 - with: - node-version: ${{ steps.version.outputs.value }} - cache: 'npm' - - - name: Setup Node.js (bun runtime) - if: startsWith(matrix.runtime, 'bun-') - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - - name: Setup Bun - if: startsWith(matrix.runtime, 'bun-') - uses: oven-sh/setup-bun@v2 - with: - bun-version: ${{ steps.version.outputs.value }} - - - name: Find merge base - id: merge-base - run: | - git fetch origin ${{ github.base_ref }} - MERGE_BASE=$(git merge-base HEAD origin/${{ github.base_ref }}) - echo "commit=$MERGE_BASE" >> $GITHUB_OUTPUT - echo "Merge base: $MERGE_BASE" - - - name: Benchmark baseline (merge-base) - run: | - echo "Benchmarking merge-base: ${{ steps.merge-base.outputs.commit }}" - git checkout ${{ steps.merge-base.outputs.commit }} - npm ci - - # Check if benchmark:json script exists in package.json - if [ -f package.json ] && grep -q '"benchmark:json"' package.json; then - echo "Found benchmark:json script, running benchmarks..." + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + fetch-depth: 0 # Need full history to find merge-base + persist-credentials: false + + - name: Parse runtime version + id: version + run: echo "value=${RUNTIME#*-}" >> $GITHUB_OUTPUT + env: + RUNTIME: ${{ matrix.runtime }} + + - name: Setup Node.js (node runtime) + if: startsWith(matrix.runtime, 'node-') + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: ${{ steps.version.outputs.value }} + cache: 'npm' + + - name: Setup Node.js (bun runtime) + if: startsWith(matrix.runtime, 'bun-') + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 22 + cache: 'npm' + + - name: Setup Bun + if: startsWith(matrix.runtime, 'bun-') + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: ${{ steps.version.outputs.value }} + + - name: Find merge base + id: merge-base + run: | + git fetch origin ${{ github.base_ref }} + MERGE_BASE=$(git merge-base HEAD origin/${{ github.base_ref }}) + echo "commit=$MERGE_BASE" >> $GITHUB_OUTPUT + echo "Merge base: $MERGE_BASE" + + - name: Benchmark baseline (merge-base) + run: | + echo "Benchmarking merge-base: ${{ steps.merge-base.outputs.commit }}" + git checkout ${{ steps.merge-base.outputs.commit }} + npm ci + + # Check if benchmark:json script exists in package.json + if [ -f package.json ] && grep -q '"benchmark:json"' package.json; then + echo "Found benchmark:json script, running benchmarks..." + # Use appropriate runtime for benchmark + if [[ "${{ matrix.runtime }}" == bun-* ]]; then + BENCHMARK_CMD="bun run benchmark:json" + else + BENCHMARK_CMD="npm run benchmark:json" + fi + + if timeout 900s $BENCHMARK_CMD baseline-results-${{ matrix.runtime }}.json 2>baseline-error-${{ matrix.runtime }}.log; then + echo "Baseline benchmark completed successfully" + else + echo "Baseline benchmark failed or timed out" + echo '[]' > baseline-results-${{ matrix.runtime }}.json + fi + else + echo "benchmark:json script not found in merge-base commit, skipping baseline" + echo "Merge-base does not have benchmark:json script" > baseline-error-${{ matrix.runtime }}.log + echo '[]' > baseline-results-${{ matrix.runtime }}.json + fi + + - name: Benchmark PR branch + run: | + echo "Benchmarking PR: ${{ github.sha }}" + git checkout ${{ github.sha }} + npm ci + # Use appropriate runtime for benchmark if [[ "${{ matrix.runtime }}" == bun-* ]]; then BENCHMARK_CMD="bun run benchmark:json" @@ -70,159 +107,35 @@ jobs: BENCHMARK_CMD="npm run benchmark:json" fi - if timeout 900s $BENCHMARK_CMD baseline-results-${{ matrix.runtime }}.json 2>baseline-error-${{ matrix.runtime }}.log; then - echo "Baseline benchmark completed successfully" + if timeout 900s $BENCHMARK_CMD pr-results-${{ matrix.runtime }}.json 2>pr-error-${{ matrix.runtime }}.log; then + echo "PR benchmark completed successfully" else - echo "Baseline benchmark failed or timed out" - echo '[]' > baseline-results-${{ matrix.runtime }}.json - fi - else - echo "benchmark:json script not found in merge-base commit, skipping baseline" - echo "Merge-base does not have benchmark:json script" > baseline-error-${{ matrix.runtime }}.log - echo '[]' > baseline-results-${{ matrix.runtime }}.json - fi - - - name: Benchmark PR branch - run: | - echo "Benchmarking PR: ${{ github.sha }}" - git checkout ${{ github.sha }} - npm ci - - # Use appropriate runtime for benchmark - if [[ "${{ matrix.runtime }}" == bun-* ]]; then - BENCHMARK_CMD="bun run benchmark:json" - else - BENCHMARK_CMD="npm run benchmark:json" - fi - - if timeout 900s $BENCHMARK_CMD pr-results-${{ matrix.runtime }}.json 2>pr-error-${{ matrix.runtime }}.log; then - echo "PR benchmark completed successfully" - else - echo "PR benchmark failed or timed out" - echo '[]' > pr-results-${{ matrix.runtime }}.json - fi - - - name: Compare results - id: comparison - run: | - # Generate comparison markdown (using Node.js since it's not performance-critical) - npm run compare-benchmarks baseline-results-${{ matrix.runtime }}.json pr-results-${{ matrix.runtime }}.json > comparison-${{ matrix.runtime }}.md - - # Add runtime version header - echo "### ${{ matrix.runtime }}" > final-comparison-${{ matrix.runtime }}.md - echo "" >> final-comparison-${{ matrix.runtime }}.md - cat comparison-${{ matrix.runtime }}.md >> final-comparison-${{ matrix.runtime }}.md - echo "" >> final-comparison-${{ matrix.runtime }}.md - - - name: Upload results as artifacts - uses: actions/upload-artifact@v4 - with: - name: benchmark-results-${{ matrix.runtime }} - path: | - baseline-results-${{ matrix.runtime }}.json - pr-results-${{ matrix.runtime }}.json - baseline-error-${{ matrix.runtime }}.log - pr-error-${{ matrix.runtime }}.log - final-comparison-${{ matrix.runtime }}.md - - # Job to collect all runtime results and post PR comment - comment: - runs-on: ubuntu-latest - needs: benchmark - if: github.event_name == 'pull_request' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Need full history to find merge-base - - - name: Find merge base - id: merge-base - run: | - git fetch origin ${{ github.base_ref }} - MERGE_BASE=$(git merge-base HEAD origin/${{ github.base_ref }}) - echo "commit=$MERGE_BASE" >> $GITHUB_OUTPUT - echo "short-commit=${MERGE_BASE:0:7}" >> $GITHUB_OUTPUT - echo "Merge base: $MERGE_BASE" - - - name: Download all benchmark results - uses: actions/download-artifact@v4 - with: - path: benchmark-results - - - name: Combine results and post comment - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Combine all runtime results (Node.js and Bun) - echo "" > combined-results.md - echo "# 🚀 Benchmark Results" >> combined-results.md - echo "" >> combined-results.md - echo "Performance comparison against merge-base [\`${{ steps.merge-base.outputs.short-commit }}\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ steps.merge-base.outputs.commit }})" >> combined-results.md - echo "" >> combined-results.md - - # Add results for each runtime (iterate over actual artifacts) - for result_dir in benchmark-results/benchmark-results-*; do - if [ -d "$result_dir" ]; then - # Extract runtime from directory name (benchmark-results-node-22 -> node-22) - runtime=$(basename "$result_dir" | sed 's/benchmark-results-//') - - if [ -f "$result_dir/final-comparison-${runtime}.md" ]; then - cat "$result_dir/final-comparison-${runtime}.md" >> combined-results.md - else - echo "### ${runtime}" >> combined-results.md - echo "âš ī¸ Benchmark failed or timed out" >> combined-results.md - echo "" >> combined-results.md - - # Add error details if available - if [ -f "$result_dir/baseline-error-${runtime}.log" ]; then - echo "
" >> combined-results.md - echo "Baseline Error Log" >> combined-results.md - echo "" >> combined-results.md - echo "\`\`\`" >> combined-results.md - head -20 "$result_dir/baseline-error-${runtime}.log" >> combined-results.md - echo "\`\`\`" >> combined-results.md - echo "
" >> combined-results.md - echo "" >> combined-results.md - fi - - if [ -f "$result_dir/pr-error-${runtime}.log" ]; then - echo "
" >> combined-results.md - echo "PR Error Log" >> combined-results.md - echo "" >> combined-results.md - echo "\`\`\`" >> combined-results.md - head -20 "$result_dir/pr-error-${runtime}.log" >> combined-results.md - echo "\`\`\`" >> combined-results.md - echo "
" >> combined-results.md - echo "" >> combined-results.md - fi - fi + echo "PR benchmark failed or timed out" + echo '[]' > pr-results-${{ matrix.runtime }}.json fi - done - - echo "" >> combined-results.md - echo "
" >> combined-results.md - echo "â„šī¸ Benchmark Details" >> combined-results.md - echo "" >> combined-results.md - echo "- **Baseline**: Merge-base commit [\`${{ steps.merge-base.outputs.short-commit }}\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ steps.merge-base.outputs.commit }}) where this PR branched from ${{ github.base_ref }}" >> combined-results.md - echo "- **Comparison**: Current PR head [\`${GITHUB_SHA:0:7}\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})" >> combined-results.md - echo "- **Timestamp**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> combined-results.md - echo "
" >> combined-results.md - - # Find existing comment or create new one - COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.number }}/comments" \ - --jq '.[] | select(.body | contains("")) | .id' | head -1) - - if [ -n "$COMMENT_ID" ]; then - echo "Updating existing comment: $COMMENT_ID" - gh api "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" \ - -X PATCH \ - -f body="$(cat combined-results.md)" - else - echo "Creating new comment" - gh api "repos/${{ github.repository }}/issues/${{ github.event.number }}/comments" \ - -X POST \ - -f body="$(cat combined-results.md)" - fi + - name: Compare results + id: comparison + run: | + # Generate comparison markdown (using Node.js since it's not performance-critical) + npm run compare-benchmarks baseline-results-${{ matrix.runtime }}.json pr-results-${{ matrix.runtime }}.json > comparison-${{ matrix.runtime }}.md + + # Add runtime version header + echo "### ${{ matrix.runtime }}" > final-comparison-${{ matrix.runtime }}.md + echo "" >> final-comparison-${{ matrix.runtime }}.md + cat comparison-${{ matrix.runtime }}.md >> final-comparison-${{ matrix.runtime }}.md + echo "" >> final-comparison-${{ matrix.runtime }}.md + + - name: Upload results as artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: benchmark-results-${{ matrix.runtime }} + path: | + baseline-results-${{ matrix.runtime }}.json + pr-results-${{ matrix.runtime }}.json + baseline-error-${{ matrix.runtime }}.log + pr-error-${{ matrix.runtime }}.log + final-comparison-${{ matrix.runtime }}.md + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0d4adf..dfb1f1b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,27 +2,32 @@ name: Build on: push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ main, master ] + branches: [main, master] workflow_call: +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 25 - cache: 'npm' + - name: Setup Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 25 + cache: 'npm' - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Build project - run: npm run build \ No newline at end of file + - name: Build project + run: npm run build diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 1d897a3..cd059d7 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -2,15 +2,21 @@ name: Commitlint on: pull_request: - branches: [ main, master ] + branches: [main, master] + +permissions: + contents: read + pull-requests: read jobs: commitlint: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false - - name: Lint commit messages - uses: wagoid/commitlint-github-action@v6 \ No newline at end of file + - name: Lint commit messages + uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fb1100f..54b82ba 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,30 +2,35 @@ name: Lint on: push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ main, master ] + branches: [main, master] workflow_call: +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 25 - cache: 'npm' + - name: Setup Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 25 + cache: 'npm' - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Run linting - run: npm run lint + - name: Run linting + run: npm run lint - - name: Check formatting - run: npm run format:check \ No newline at end of file + - name: Check formatting + run: npm run format:check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b0128e..74d9ee0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,11 +2,14 @@ name: Test on: push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ main, master ] + branches: [main, master] workflow_call: +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest @@ -16,54 +19,56 @@ jobs: runtime: [node-22, node-24, node-25, bun-latest] steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false - - name: Parse runtime version - id: version - run: echo "value=${RUNTIME#*-}" >> $GITHUB_OUTPUT - env: - RUNTIME: ${{ matrix.runtime }} + - name: Parse runtime version + id: version + run: echo "value=${RUNTIME#*-}" >> $GITHUB_OUTPUT + env: + RUNTIME: ${{ matrix.runtime }} - - name: Setup Node.js (node runtime) - if: startsWith(matrix.runtime, 'node-') - uses: actions/setup-node@v4 - with: - node-version: ${{ steps.version.outputs.value }} - cache: 'npm' + - name: Setup Node.js (node runtime) + if: startsWith(matrix.runtime, 'node-') + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: ${{ steps.version.outputs.value }} + cache: 'npm' - - name: Setup Node.js (bun runtime) - if: startsWith(matrix.runtime, 'bun-') - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' + - name: Setup Node.js (bun runtime) + if: startsWith(matrix.runtime, 'bun-') + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 22 + cache: 'npm' - - name: Setup Bun - if: startsWith(matrix.runtime, 'bun-') - uses: oven-sh/setup-bun@v2 - with: - bun-version: ${{ steps.version.outputs.value }} + - name: Setup Bun + if: startsWith(matrix.runtime, 'bun-') + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: ${{ steps.version.outputs.value }} - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Run tests with coverage (Node.js) - if: startsWith(matrix.runtime, 'node-') - run: npm run cover + - name: Run tests with coverage (Node.js) + if: startsWith(matrix.runtime, 'node-') + run: npm run cover - - name: Run tests with coverage (Bun) - if: startsWith(matrix.runtime, 'bun-') - run: bun run cover + - name: Run tests with coverage (Bun) + if: startsWith(matrix.runtime, 'bun-') + run: bun run cover - - name: Display coverage summary - run: npx nyc report --reporter=text-summary + - name: Display coverage summary + run: npx nyc report --reporter=text-summary - - name: Upload coverage to artifacts - uses: actions/upload-artifact@v4 - if: matrix.runtime == 'node-22' - with: - name: coverage-report - path: | - coverage.lcov - .nyc_output/ \ No newline at end of file + - name: Upload coverage to artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: matrix.runtime == 'node-22' + with: + name: coverage-report + path: | + coverage.lcov + .nyc_output/