Files
pnpm/.github/workflows/ci.yml
Zoltan Kochan bf77ed25ea ci: run required checks on merge_group so the merge queue works (#12627)
* ci: run required checks on merge_group so the merge queue works

The merge queue dispatches a merge_group event against a temporary
gh-readonly-queue/main ref, but neither TS CI (ci.yml) nor Rust CI
(pacquet-ci.yml) listened for it. Their required status checks therefore
never ran in the queue, so every queued PR waited forever on the missing
contexts (e.g. Rust CI / Success never starting).

Add merge_group to both workflows' triggers and force the change
detection true for that event. Forcing matters because TS CI / Compile &
Lint is itself a required context: a skipped job never reports its
context, which would keep the queue waiting, so it has to actually run.
It also makes the queue test the fully merged result, which is the point
of a merge queue. The Rust deny job's nested path filter, which has no
push/PR base to diff against in the queue, runs unconditionally on
merge_group instead.

* ci: gate compile-and-lint and build-pnpr on merge_group explicitly

The merge queue tests the merged commit, so the gating jobs must run on
merge_group. build-pnpr (added by the Windows-sharding work) carries the
same event guard as compile-and-lint and feeds test-smoke/test/test-windows,
so it needs the merge_group clause too — otherwise the queue would skip the
whole test suite.
2026-06-24 10:23:46 +00:00

254 lines
9.8 KiB
YAML

name: TS CI
on: [push, pull_request, merge_group]
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
changes:
name: TS CI / Detect Changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
ts: ${{ steps.force.outputs.ts || steps.filter.outputs.ts }}
steps:
# In the merge queue the full suite must run: `TS CI / Compile & Lint`
# is itself a required check, and a skipped job never reports its
# context, which would leave the queue waiting forever. Forcing
# detection true also tests the merged result, which is the point of
# the queue.
- name: Force TS CI for merge queue
if: github.event_name == 'merge_group'
id: force
run: echo "ts=true" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
if: github.event_name != 'merge_group'
with:
persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
if: github.event_name != 'merge_group'
id: filter
with:
# The TypeScript pnpm stack lives under pnpm11/, plus the
# workspace-root tooling that drives its build/lint/test.
filters: |
ts:
- 'pnpm11/**'
- '.meta-updater/**'
- '__patches__/**'
- 'package.json'
- 'pnpm-workspace.yaml'
- 'pnpm-lock.yaml'
- '.pnpmfile.cjs'
- 'eslint.config.mjs'
- 'tsconfig.lint.json'
- 'cspell.json'
- '.github/workflows/ci.yml'
- '.github/workflows/test.yml'
- '.github/scripts/**'
compile-and-lint:
needs: changes
# Run only when TypeScript-relevant files changed. Also skip
# pull_request events from PRs in the same repo to prevent duplicate
# build jobs (the push event already covers them).
# Exception: chore/update-lockfile PRs (created by automation with GITHUB_TOKEN, which doesn't trigger push events)
if: ${{ !cancelled() && needs.changes.outputs.ts == 'true' && (github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name != github.repository || github.head_ref == 'chore/update-lockfile') }}
name: TS CI / Compile & Lint
runs-on: ubuntu-latest
steps:
- name: Checkout Commit
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/setup@b1cac37306e39c21283b9dd6cb0ac288fb35ba6b
- name: Compile TypeScript
run: pn compile-only
- name: Lint
run: pn lint
- name: Package compiled artifacts
shell: bash
run: |
mapfile -d '' -t lib_dirs < <(find . -type d -name lib -not -path '*/node_modules/*' -print0)
mapfile -d '' -t tsbuildinfo_files < <(find . -name 'tsconfig.tsbuildinfo' -not -path '*/node_modules/*' -print0)
tar -czf compiled.tar.gz --exclude='node_modules' "${lib_dirs[@]}" "${tsbuildinfo_files[@]}" pnpm11/pnpm/dist
- name: Upload compiled artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: compiled-packages
path: compiled.tar.gz
retention-days: 1
# Build the pnpr server and registry fixture preparer once per OS, then
# share the binaries with every test job/shard as an artifact. Building
# inside each test job instead rebuilt pnpr once per shard, because the
# parallel shards all miss the build cache at job start.
build-pnpr:
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.ts == 'true' && (github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name != github.repository || github.head_ref == 'chore/update-lockfile') }}
name: TS CI / Build pnpr / ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Free up runner disk space
if: ${{ runner.os == 'Linux' }}
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
df -h /
- name: Checkout Commit
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# The binaries are cached and keyed on the Rust sources that produce them,
# so a run that only touches TypeScript restores them in seconds instead of
# recompiling. They are copied out of `target/` into a stable dir so
# `Swatinem/rust-cache`'s `target/` cleanup can't strip them before the
# cache saves.
- name: Restore prebuilt pnpr binaries
id: pnpr-bins
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .pnpr-bin
key: pnpr-bins-${{ runner.os }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock', '**/Cargo.toml', 'pnpr/**/*.rs', 'pacquet/**/*.rs') }}
- name: Install Rust
if: steps.pnpr-bins.outputs.cache-hit != 'true'
uses: ./.github/actions/rustup
with:
save-cache: ${{ github.ref_name == 'main' }}
shared-key: registry-prepare
- name: Build the pnpr server and registry fixture preparer
if: steps.pnpr-bins.outputs.cache-hit != 'true'
shell: bash
run: |
cargo build --locked --release -p pnpr --bin pnpr -p pnpr-fixtures --bin pnpr-prepare
mkdir -p .pnpr-bin
ext=""
[ -f target/release/pnpr.exe ] && ext=".exe"
cp "target/release/pnpr$ext" "target/release/pnpr-prepare$ext" .pnpr-bin/
# Save the binaries to the cache right after they build, not in a post-job
# step: a later step failing would otherwise skip the save and force a full
# Rust rebuild on the next run.
- name: Save prebuilt pnpr binaries
if: steps.pnpr-bins.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .pnpr-bin
key: ${{ steps.pnpr-bins.outputs.cache-primary-key }}
- name: Upload pnpr binaries
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pnpr-bins-${{ runner.os }}
path: .pnpr-bin
# `.pnpr-bin` is a dotfile dir, which upload-artifact skips by default.
include-hidden-files: true
if-no-files-found: error
retention-days: 1
test-smoke:
name: TS CI / Test / ubuntu
needs: [compile-and-lint, build-pnpr]
uses: ./.github/workflows/test.yml
with:
node: '24.0.0'
node_major: '24'
platform: ubuntu-latest
garnet: true
secrets:
GARNET_API_TOKEN: ${{ secrets.GARNET_API_TOKEN }}
test:
name: TS CI / Test / ${{ matrix.platform_label }}
needs: test-smoke
strategy:
fail-fast: false
matrix:
include:
- node: '22.13.0'
node_major: '22'
platform: ubuntu-latest
platform_label: ubuntu
test_chunk: '1'
test_chunk_total: '1'
- node: '26.3.1'
node_major: '26'
platform: ubuntu-latest
platform_label: ubuntu
test_chunk: '1'
test_chunk_total: '1'
uses: ./.github/workflows/test.yml
with:
node: ${{ matrix.node }}
node_major: ${{ matrix.node_major }}
platform: ${{ matrix.platform }}
test_chunk: ${{ matrix.test_chunk }}
test_chunk_total: ${{ matrix.test_chunk_total }}
# Windows is the slowest platform. Gate it only on compile-and-lint so its
# shards run in parallel with the ubuntu smoke job, instead of waiting for
# the smoke job like the other Node.js versions do.
test-windows:
name: TS CI / Test / ${{ matrix.platform_label }}
needs: [compile-and-lint, build-pnpr]
strategy:
fail-fast: false
matrix:
include:
- node: '22.13.0'
node_major: '22'
platform: windows-latest
platform_label: windows 1/3
test_chunk: '1'
test_chunk_total: '3'
- node: '22.13.0'
node_major: '22'
platform: windows-latest
platform_label: windows 2/3
test_chunk: '2'
test_chunk_total: '3'
- node: '22.13.0'
node_major: '22'
platform: windows-latest
platform_label: windows 3/3
test_chunk: '3'
test_chunk_total: '3'
uses: ./.github/workflows/test.yml
with:
node: ${{ matrix.node }}
node_major: ${{ matrix.node_major }}
platform: ${{ matrix.platform }}
test_chunk: ${{ matrix.test_chunk }}
test_chunk_total: ${{ matrix.test_chunk_total }}
# Single aggregate gate — the only TS CI context branch protection needs
# to require. Listing the individual jobs as required checks does not
# work: they skip on non-TypeScript PRs, and a matrix job skipped at the
# job level never expands its `${{ matrix.* }}` name, so the per-platform
# contexts it would report never appear and the PR blocks forever waiting
# for them. This job always runs (it reports under a static name in every
# state) and fails only if a dependency actually failed or was cancelled —
# skipped dependencies count as a pass.
success:
name: TS CI / Success
if: ${{ always() }}
needs:
- changes
- compile-and-lint
- build-pnpr
- test-smoke
- test
- test-windows
runs-on: ubuntu-latest
steps:
- name: Fail if any dependency failed or was cancelled
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1