Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions .github/workflows/toolchain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: Toolchain

# Weekly pnpm bump — dependabot's blind spot. It updates dependencies but never the
# `packageManager` pin, so this repo can sit a year behind on pnpm with every dependency
# PR green. corepack reads `packageManager`, so moving the pin IS the upgrade.
#
# It deliberately does NOT touch `engines.node`. That floor is what end users must run;
# raising it drops them and is a breaking release, so it stays a manual decision. The
# CI Node (24) is bumped by hand when a new Active LTS lands.
#
# TOKEN: with GITHUB_TOKEN the PR opens but its checks sit in `action_required` until
# approved by hand. That's why the gates run in-job below — the self-check is the real
# verification, not the PR's CI. Swap GH_TOKEN for a fine-grained PAT to make it
# hands-off.

on:
schedule:
- cron: '17 6 * * 1' # Mondays ~06:17 UTC — off the :00 mark everyone piles onto
workflow_dispatch:

concurrency:
group: toolchain
cancel-in-progress: false

permissions:
contents: write
pull-requests: write

jobs:
pnpm:
name: Bump pnpm
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: 24

- run: corepack enable

- name: Resolve current vs latest pnpm
id: v
run: |
set -euo pipefail
current=$(node -p "require('./package.json').packageManager.split('@')[1]")
latest=$(npm view pnpm version)
echo "current=$current" >> "$GITHUB_OUTPUT"
echo "latest=$latest" >> "$GITHUB_OUTPUT"
if [ "$current" = "$latest" ]; then
echo "up_to_date=true" >> "$GITHUB_OUTPUT"
echo "pnpm $current is current."
else
echo "up_to_date=false" >> "$GITHUB_OUTPUT"
echo "pnpm $current -> $latest"
fi

# A PR from a previous run that hasn't been merged yet would collide on the branch
# name and fail the push. Check before doing any work.
- name: Skip if a bump PR is already open
id: existing
if: steps.v.outputs.up_to_date == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
branch="ci/bump-pnpm-${{ steps.v.outputs.latest }}"
open=$(gh pr list --head "$branch" --state open --json number --jq 'length')
echo "open=$open" >> "$GITHUB_OUTPUT"
[ "$open" = "0" ] || echo "PR for $branch is already open — nothing to do."

- name: Bump the pin and refresh the lockfile
if: steps.v.outputs.up_to_date == 'false' && steps.existing.outputs.open == '0'
run: |
set -euo pipefail
corepack use "pnpm@${{ steps.v.outputs.latest }}"
pnpm install --lockfile-only

# The PR's own CI won't run under GITHUB_TOKEN, so prove the bump here instead —
# the same gates ci.yml requires. E2E is left out on purpose: a browser install for
# a package-manager pin is not worth six minutes every Monday.
- name: Verify the bump against the CI gates
if: steps.v.outputs.up_to_date == 'false' && steps.existing.outputs.open == '0'
run: |
pnpm install --frozen-lockfile
pnpm lint
pnpm format:check
pnpm typecheck
pnpm build
pnpm test

- name: Open the bump PR
if: steps.v.outputs.up_to_date == 'false' && steps.existing.outputs.open == '0'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
branch="ci/bump-pnpm-${{ steps.v.outputs.latest }}"
git config user.name "Paul Barahona"
git config user.email "johanpaulbarahona@gmail.com"
git checkout -b "$branch"
# `ci:` — the pin is toolchain only. It must not cut a release.
git commit -am "ci: bump pnpm ${{ steps.v.outputs.current }} -> ${{ steps.v.outputs.latest }}"
git push -u origin "$branch"
gh pr create --base main --head "$branch" \
--title "ci: bump pnpm to ${{ steps.v.outputs.latest }}" \
--body "Weekly toolchain bump: \`${{ steps.v.outputs.current }}\` -> \`${{ steps.v.outputs.latest }}\`.

corepack reads \`packageManager\`, so moving the pin is the upgrade. Lockfile refreshed.

Verified in-job (the PR's own checks won't run until approved): lint, format:check, typecheck, build, test. E2E was not run.

\`engines.node\` untouched — that floor is a manual decision."
Loading