diff --git a/.github/workflows/toolchain.yml b/.github/workflows/toolchain.yml new file mode 100644 index 0000000..0abd260 --- /dev/null +++ b/.github/workflows/toolchain.yml @@ -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."