🧭 Runtime Probe #16
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: 🧭 Runtime Probe | |
| on: | |
| schedule: | |
| # Weekly at 01:00 UTC on Sunday. | |
| - cron: '0 1 * * 0' | |
| workflow_dispatch: | |
| inputs: | |
| candidates: | |
| description: Space-separated future Node majors to probe | |
| type: string | |
| default: '24 26 28' | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: runtime-probe | |
| cancel-in-progress: false | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| OWNER_NAME: oxyzenQ | |
| OWNER_EMAIL: 130107241+oxyzenQ@users.noreply.github.com | |
| RUNTIME_CANDIDATES: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.candidates || '24 26 28' }} | |
| jobs: | |
| runtime-probe: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Detect default branch | |
| id: branch | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "name=${{ github.event.repository.default_branch }}" >> "$GITHUB_OUTPUT" | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ steps.branch.outputs.name }} | |
| token: ${{ secrets.PAT_WORKFLOW || secrets.GITHUB_TOKEN }} | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| - name: Detect current runtime | |
| id: current | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| current="$(grep -m1 -E '^[[:space:]]+NODE_VERSION:[[:space:]]+[0-9]+' .github/workflows/ci.yml | awk '{print $2}')" | |
| if [ -z "$current" ]; then | |
| echo "Unable to detect NODE_VERSION from .github/workflows/ci.yml" >&2 | |
| exit 1 | |
| fi | |
| echo "node=$current" >> "$GITHUB_OUTPUT" | |
| echo "Current Node runtime: $current" | |
| - name: Setup current Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ steps.current.outputs.node }} | |
| - name: Resolve Bun version | |
| id: bun-version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="$(node -p "require('./package.json').packageManager?.match(/^bun@(.+)$/)?.[1] || ''")" | |
| if [ -z "$version" ]; then | |
| echo "package.json packageManager must be bun@<version>" >&2 | |
| exit 1 | |
| fi | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: ${{ steps.bun-version.outputs.version }} | |
| - name: Cache Bun dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Probe future runtime | |
| id: probe | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| current="${{ steps.current.outputs.node }}" | |
| selected="" | |
| for candidate in ${RUNTIME_CANDIDATES}; do | |
| if ! [[ "$candidate" =~ ^[0-9]+$ ]]; then | |
| echo "Skipping invalid candidate: $candidate" | |
| continue | |
| fi | |
| if [ "$candidate" -le "$current" ]; then | |
| echo "Skipping Node $candidate because current runtime is Node $current." | |
| continue | |
| fi | |
| runtime="nodejs${candidate}.x" | |
| echo "Probing ${runtime}..." | |
| if VERCEL_NODE_RUNTIME="$runtime" bun run format:check \ | |
| && VERCEL_NODE_RUNTIME="$runtime" bun run check \ | |
| && VERCEL_NODE_RUNTIME="$runtime" bun run lint \ | |
| && VERCEL_NODE_RUNTIME="$runtime" bun run test:ci \ | |
| && VERCEL_NODE_RUNTIME="$runtime" bun run build; then | |
| selected="$candidate" | |
| break | |
| fi | |
| echo "Runtime ${runtime} is not ready yet." | |
| done | |
| if [ -z "$selected" ]; then | |
| { | |
| echo "upgraded=false" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "No future runtime passed the quality gate." | |
| exit 0 | |
| fi | |
| runtime="nodejs${selected}.x" | |
| echo "Selected future runtime: ${runtime}" | |
| sed -i -E "s/(NODE_VERSION:[[:space:]]*)[0-9]+/\1${selected}/g" .github/workflows/*.yml | |
| sed -i -E "s/(VERCEL_NODE_RUNTIME:[[:space:]]*)nodejs[0-9]+\.x/\1${runtime}/g" .github/workflows/*.yml | |
| { | |
| echo "node=$selected" | |
| echo "runtime=$runtime" | |
| echo "upgraded=true" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Format workflow files | |
| if: steps.probe.outputs.upgraded == 'true' | |
| run: bunx prettier --write .github/workflows/*.yml | |
| - name: Check changed runtime files | |
| if: steps.probe.outputs.upgraded == 'true' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git diff -- .github/workflows | |
| git diff --check | |
| - name: Commit runtime upgrade | |
| if: steps.probe.outputs.upgraded == 'true' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git config user.name "${OWNER_NAME}" | |
| git config user.email "${OWNER_EMAIL}" | |
| git add .github/workflows | |
| if git diff --cached --quiet; then | |
| echo "No staged runtime changes." | |
| exit 0 | |
| fi | |
| git commit -m "ci: upgrade Node runtime to ${{ steps.probe.outputs.runtime }}" | |
| git push origin "HEAD:${{ steps.branch.outputs.name }}" | |
| - name: Summary | |
| if: always() | |
| run: | | |
| { | |
| echo "## Runtime Probe" | |
| echo "" | |
| echo "Current Node: ${{ steps.current.outputs.node }}" | |
| echo "Candidates: ${RUNTIME_CANDIDATES}" | |
| echo "Strategy: direct to ${{ steps.branch.outputs.name }}" | |
| echo "Upgraded: ${{ steps.probe.outputs.upgraded || 'false' }}" | |
| if [ "${{ steps.probe.outputs.upgraded || 'false' }}" = "true" ]; then | |
| echo "Selected runtime: ${{ steps.probe.outputs.runtime }}" | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" |