From eff263082e28aebbd15ef2e87c87c9014528bcc0 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Apr 2026 10:42:31 +0800 Subject: [PATCH 1/9] feat(workflow): generate accurate upgrade-deps PR descriptions via Claude Previously the `Upgrade Upstream Dependencies` workflow produced a generic template commit message and PR body (see #1401) that didn't reflect what actually changed. This wires up a second claude-code-action pass whose only job is to write a diff-accurate commit message and PR body. - `.github/scripts/upgrade-deps.mjs` now captures old -> new for every dependency it touches (including rolldown/vite tag + short SHA) and writes `versions.json`, `commit-message.txt`, and `pr-body.md` to `$UPGRADE_DEPS_META_DIR` (outside the repo, so not committed). - `.github/workflows/upgrade-deps.yml`: - Exports `UPGRADE_DEPS_META_DIR` via `$GITHUB_ENV` in a setup step. - Adds an "Enhance PR description with Claude" step that reads the baseline files + `git diff` and overwrites them with a concrete summary, a dependency table, and a real code-changes section. - Adds a "Read generated PR content" step that exposes the files as multi-line step outputs. - `peter-evans/create-pull-request` now consumes those outputs instead of a static template body. - Tightens the `check-upgrade-dependencies` final check to require BOTH `just build` AND `pnpm bootstrap-cli:ci && pnpm test` to pass (with manual snap-test diff inspection). - Bumps both `claude-code-action` pins to v1.0.99 (Claude Code 2.1.112). If the enhancement step fails, `continue-on-error` keeps the workflow going and the baseline content produced by the Node script is shipped instead of a generic message. --- .github/scripts/upgrade-deps.mjs | 197 ++++++++++++++++++++++------- .github/workflows/upgrade-deps.yml | 127 +++++++++++++++++-- 2 files changed, 268 insertions(+), 56 deletions(-) diff --git a/.github/scripts/upgrade-deps.mjs b/.github/scripts/upgrade-deps.mjs index c0551bd19b..5e58ddf908 100644 --- a/.github/scripts/upgrade-deps.mjs +++ b/.github/scripts/upgrade-deps.mjs @@ -2,9 +2,24 @@ import fs from 'node:fs'; import path from 'node:path'; const ROOT = process.cwd(); +const META_DIR = process.env.UPGRADE_DEPS_META_DIR; + +/** @type {Map} */ +const changes = new Map(); + +function recordChange(name, oldValue, newValue, tag) { + const entry = { old: oldValue ?? null, new: newValue }; + if (tag) entry.tag = tag; + changes.set(name, entry); + if (oldValue !== newValue) { + console.log(` ${name}: ${oldValue ?? '(unset)'} -> ${newValue}`); + } else { + console.log(` ${name}: ${newValue} (unchanged)`); + } +} // ============ GitHub API ============ -async function getLatestTagCommit(owner, repo) { +async function getLatestTag(owner, repo) { const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/tags`, { headers: { Authorization: `token ${process.env.GITHUB_TOKEN}`, @@ -18,11 +33,11 @@ async function getLatestTagCommit(owner, repo) { if (!Array.isArray(tags) || !tags.length) { throw new Error(`No tags found for ${owner}/${repo}`); } - if (!tags[0]?.commit?.sha) { - throw new Error(`Invalid tag structure for ${owner}/${repo}: missing commit SHA`); + if (!tags[0]?.commit?.sha || !tags[0]?.name) { + throw new Error(`Invalid tag structure for ${owner}/${repo}: missing SHA or name`); } - console.log(`${repo} -> ${tags[0].name}`); - return tags[0].commit.sha; + console.log(`${repo} -> ${tags[0].name} (${tags[0].commit.sha.slice(0, 7)})`); + return { sha: tags[0].commit.sha, tag: tags[0].name }; } // ============ npm Registry ============ @@ -45,11 +60,16 @@ async function updateUpstreamVersions() { const filePath = path.join(ROOT, 'packages/tools/.upstream-versions.json'); const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); - // rolldown -> rolldown/rolldown - data.rolldown.hash = await getLatestTagCommit('rolldown', 'rolldown'); - - // vite -> vitejs/vite - data['vite'].hash = await getLatestTagCommit('vitejs', 'vite'); + const oldRolldownHash = data.rolldown.hash; + const oldViteHash = data['vite'].hash; + const [rolldown, vite] = await Promise.all([ + getLatestTag('rolldown', 'rolldown'), + getLatestTag('vitejs', 'vite'), + ]); + data.rolldown.hash = rolldown.sha; + data['vite'].hash = vite.sha; + recordChange('rolldown', oldRolldownHash, rolldown.sha, rolldown.tag); + recordChange('vite', oldViteHash, vite.sha, vite.tag); fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n'); console.log('Updated .upstream-versions.json'); @@ -60,38 +80,59 @@ async function updatePnpmWorkspace(versions) { const filePath = path.join(ROOT, 'pnpm-workspace.yaml'); let content = fs.readFileSync(filePath, 'utf8'); - // Update vitest-dev override (handle pre-release versions like -beta.1, -rc.0) - content = content.replace( - /vitest-dev: npm:vitest@\^[\d.]+(-[\w.]+)?/, - `vitest-dev: npm:vitest@^${versions.vitest}`, - ); - - // Update tsdown in catalog (handle pre-release versions) - content = content.replace(/tsdown: \^[\d.]+(-[\w.]+)?/, `tsdown: ^${versions.tsdown}`); - - // Update @oxc-node/cli in catalog - content = content.replace( - /'@oxc-node\/cli': \^[\d.]+(-[\w.]+)?/, - `'@oxc-node/cli': ^${versions.oxcNodeCli}`, - ); - - // Update @oxc-node/core in catalog - content = content.replace( - /'@oxc-node\/core': \^[\d.]+(-[\w.]+)?/, - `'@oxc-node/core': ^${versions.oxcNodeCore}`, - ); - - // Update oxfmt in catalog - content = content.replace(/oxfmt: =[\d.]+(-[\w.]+)?/, `oxfmt: =${versions.oxfmt}`); - - // Update oxlint in catalog (but not oxlint-tsgolint) - content = content.replace(/oxlint: =[\d.]+(-[\w.]+)?\n/, `oxlint: =${versions.oxlint}\n`); + // The capture regex returns the current version in $1; the replacement string + // substitutes the new version into the same anchor text. + // oxlint's trailing \n disambiguates from oxlint-tsgolint. + const entries = [ + { + name: 'vitest', + pattern: /vitest-dev: npm:vitest@\^([\d.]+(?:-[\w.]+)?)/, + replacement: `vitest-dev: npm:vitest@^${versions.vitest}`, + newVersion: versions.vitest, + }, + { + name: 'tsdown', + pattern: /tsdown: \^([\d.]+(?:-[\w.]+)?)/, + replacement: `tsdown: ^${versions.tsdown}`, + newVersion: versions.tsdown, + }, + { + name: '@oxc-node/cli', + pattern: /'@oxc-node\/cli': \^([\d.]+(?:-[\w.]+)?)/, + replacement: `'@oxc-node/cli': ^${versions.oxcNodeCli}`, + newVersion: versions.oxcNodeCli, + }, + { + name: '@oxc-node/core', + pattern: /'@oxc-node\/core': \^([\d.]+(?:-[\w.]+)?)/, + replacement: `'@oxc-node/core': ^${versions.oxcNodeCore}`, + newVersion: versions.oxcNodeCore, + }, + { + name: 'oxfmt', + pattern: /oxfmt: =([\d.]+(?:-[\w.]+)?)/, + replacement: `oxfmt: =${versions.oxfmt}`, + newVersion: versions.oxfmt, + }, + { + name: 'oxlint', + pattern: /oxlint: =([\d.]+(?:-[\w.]+)?)\n/, + replacement: `oxlint: =${versions.oxlint}\n`, + newVersion: versions.oxlint, + }, + { + name: 'oxlint-tsgolint', + pattern: /oxlint-tsgolint: =([\d.]+(?:-[\w.]+)?)/, + replacement: `oxlint-tsgolint: =${versions.oxlintTsgolint}`, + newVersion: versions.oxlintTsgolint, + }, + ]; - // Update oxlint-tsgolint in catalog - content = content.replace( - /oxlint-tsgolint: =[\d.]+(-[\w.]+)?/, - `oxlint-tsgolint: =${versions.oxlintTsgolint}`, - ); + for (const { name, pattern, replacement, newVersion } of entries) { + const oldVersion = content.match(pattern)?.[1]; + content = content.replace(pattern, replacement); + recordChange(name, oldVersion, newVersion); + } fs.writeFileSync(filePath, content); console.log('Updated pnpm-workspace.yaml'); @@ -128,15 +169,83 @@ async function updateCorePackage(devtoolsVersion) { const filePath = path.join(ROOT, 'packages/core/package.json'); const pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); - // Update @vitejs/devtools in devDependencies - if (pkg.devDependencies?.['@vitejs/devtools']) { + const currentDevtools = pkg.devDependencies?.['@vitejs/devtools']; + if (currentDevtools) { pkg.devDependencies['@vitejs/devtools'] = `^${devtoolsVersion}`; + recordChange('@vitejs/devtools', currentDevtools.replace(/^[\^~]/, ''), devtoolsVersion); } fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n'); console.log('Updated packages/core/package.json'); } +// ============ Write metadata files for PR description ============ +function writeMetaFiles() { + if (!META_DIR) return; + + fs.mkdirSync(META_DIR, { recursive: true }); + + const versionsObj = Object.fromEntries(changes); + fs.writeFileSync( + path.join(META_DIR, 'versions.json'), + JSON.stringify(versionsObj, null, 2) + '\n', + ); + + const changed = [...changes.entries()].filter(([, v]) => v.old !== v.new); + const unchanged = [...changes.entries()].filter(([, v]) => v.old === v.new); + + const isFullSha = (s) => /^[0-9a-f]{40}$/.test(s); + const formatVersion = (v) => { + if (v.tag) return `${v.tag} (${v.new.slice(0, 7)})`; + if (isFullSha(v.new)) return v.new.slice(0, 7); + return v.new; + }; + const formatOld = (v) => { + if (!v.old) return '(unset)'; + if (isFullSha(v.old)) return v.old.slice(0, 7); + return v.old; + }; + + const commitLines = ['feat(deps): upgrade upstream dependencies', '']; + if (changed.length) { + for (const [name, v] of changed) { + commitLines.push(`- ${name}: ${formatOld(v)} -> ${formatVersion(v)}`); + } + } else { + commitLines.push('- no version changes detected'); + } + commitLines.push(''); + fs.writeFileSync(path.join(META_DIR, 'commit-message.txt'), commitLines.join('\n')); + + const bodyLines = ['## Summary', '']; + if (changed.length) { + bodyLines.push('Automated daily upgrade of upstream dependencies.'); + } else { + bodyLines.push('Automated daily upgrade run — no upstream version changes detected.'); + } + bodyLines.push('', '## Dependency updates', ''); + if (changed.length) { + bodyLines.push('| Package | From | To |'); + bodyLines.push('| --- | --- | --- |'); + for (const [name, v] of changed) { + bodyLines.push(`| \`${name}\` | \`${formatOld(v)}\` | \`${formatVersion(v)}\` |`); + } + } else { + bodyLines.push('_No version changes._'); + } + if (unchanged.length) { + bodyLines.push('', '
Unchanged dependencies', ''); + for (const [name, v] of unchanged) { + bodyLines.push(`- \`${name}\`: \`${formatVersion(v)}\``); + } + bodyLines.push('', '
'); + } + bodyLines.push('', '## Code changes', '', '_No additional code changes recorded._', ''); + fs.writeFileSync(path.join(META_DIR, 'pr-body.md'), bodyLines.join('\n')); + + console.log(`Wrote metadata files to ${META_DIR}`); +} + console.log('Fetching latest versions…'); const [ @@ -181,4 +290,6 @@ await updatePnpmWorkspace({ await updateTestPackage(vitestVersion); await updateCorePackage(devtoolsVersion); +writeMetaFiles(); + console.log('Done!'); diff --git a/.github/workflows/upgrade-deps.yml b/.github/workflows/upgrade-deps.yml index fc21b0513e..adb0e55bd0 100644 --- a/.github/workflows/upgrade-deps.yml +++ b/.github/workflows/upgrade-deps.yml @@ -20,6 +20,9 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/clone + - name: Set up metadata directory + run: echo "UPGRADE_DEPS_META_DIR=${RUNNER_TEMP}/upgrade-deps-meta" >> "$GITHUB_ENV" + - uses: oxc-project/setup-rust@23f38cfb0c04af97a055f76acee94d5be71c7c82 # v1.0.16 with: save-cache: ${{ github.ref_name == 'main' }} @@ -59,7 +62,9 @@ jobs: env: RELEASE_BUILD: 'true' - - uses: anthropics/claude-code-action@eb99fb38f09dedf69f423f1315d6c0272ace56a0 # Claude Code to 2.1.72 + - name: Check upgrade dependencies + id: check-upgrade-dependencies + uses: anthropics/claude-code-action@c3d45e8e941e1b2ad7b278c57482d9c5bf1f35b3 # Claude Code to 2.1.112 env: RELEASE_BUILD: 'true' with: @@ -82,8 +87,6 @@ jobs: - `pnpm bootstrap-cli:ci` - `echo "$HOME/.vite-plus/bin" >> $GITHUB_PATH` - Run `pnpm run lint` to check if there are any issues after the build, if has, deep investigate it and fix it. You need to run `just build` before you can run `pnpm run lint`. - - Run `pnpm run test` after `just build` to ensure all tests are successful. - - The snapshot tests in `pnpm run test` are always successful, you need to check the snapshot diffs in git to see if there is anything wrong after our deps upgrade. - If deps in our `Cargo.toml` need to be upgraded, you can refer to the `./.claude/agents/cargo-workspace-merger.md` - If `Cargo.toml` has been modified, you need to run `cargo shear` to ensure there is nothing wrong with our dependencies. - Run `cargo check --all-targets --all-features` to ensure everything works fine if any Rust related codes are modified. @@ -95,7 +98,23 @@ jobs: vp build -h vp fmt -h vp pack -h - - Your final step is to run `just build` to ensure all builds are successful. + + ### Final check (BOTH must succeed before you exit) + This step is only complete when BOTH of the following pass. If either one fails, + diagnose and fix the root cause, then re-run both until they both succeed. + 1. `just build` — must exit 0. If it fails, investigate the error, fix the + underlying issue, and run it again. + 2. `pnpm bootstrap-cli:ci && pnpm test` — must exit 0. If it fails, investigate + the error, fix the underlying issue, and run it again. Note that the snap + tests inside `pnpm test` always exit 0 even when their outputs differ, so + after the command passes you MUST also inspect the `git diff` on + `packages/cli/snap-tests/**/snap.txt` and `packages/cli/snap-tests-global/**/snap.txt` + and decide whether each change is an acceptable consequence of the upstream + upgrade (e.g. a version string bump) or a real regression that needs fixing. + + Only exit after you have confirmed both of the above succeed. Do not consider + the task complete if either `just build` fails, `pnpm bootstrap-cli:ci && pnpm test` + fails, or the snap-test diff shows an apparent regression. Help me fix the errors in `build-upstream` steps if exists. No need to commit changes after your fixing we have a following step to commit all file changes. @@ -117,6 +136,95 @@ jobs: - name: Format code run: pnpm fmt + - name: Enhance PR description with Claude + id: enhance-pr-description + continue-on-error: true + uses: anthropics/claude-code-action@c3d45e8e941e1b2ad7b278c57482d9c5bf1f35b3 # Claude Code to 2.1.112 + with: + claude_code_oauth_token: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + show_full_output: 'true' + prompt: | + Your task is to generate an accurate commit message and PR description for this + automated upstream-dependency upgrade PR. All output must be written in English. + + ### Inputs + - Version changes (old -> new) in JSON: `${{ env.UPGRADE_DEPS_META_DIR }}/versions.json` + - Baseline commit message: `${{ env.UPGRADE_DEPS_META_DIR }}/commit-message.txt` + - Baseline PR body: `${{ env.UPGRADE_DEPS_META_DIR }}/pr-body.md` + - The working directory is the project root; all upgraded files are staged or modified. + Use `git status` and `git diff` (including `git diff --stat` and focused per-file diffs) + to discover what actually changed beyond the raw version bumps. + + ### Build outcomes from earlier steps + - Sync remote and build: ${{ steps.build.outcome }} + - build-upstream action: ${{ steps.build-upstream.outcome }} + + ### Output files to OVERWRITE (these live outside the repo and will NOT be committed) + - `${{ env.UPGRADE_DEPS_META_DIR }}/commit-message.txt` + - `${{ env.UPGRADE_DEPS_META_DIR }}/pr-body.md` + + ### Commit message format + - First line (title, <= 72 chars): `feat(deps): upgrade upstream dependencies` + - Blank line. + - A concise bulleted list of notable version changes, one bullet per package. + For `rolldown` and `vite`, include the tag name plus short SHA when available + (e.g. `rolldown: -> v1.2.3 ()`). Skip packages whose + old and new values are equal. + - If non-version code changes were required (e.g., tsdown CLI option sync, + rolldown workspace re-merge, snapshot updates, cargo shear cleanup), + add a trailing `Code changes:` block listing each change with a file path. + + ### PR body format (Markdown) + Use these sections in order: + 1. `## Summary` — 2-4 bullets describing what this PR does at a high level, + naming the most impactful upgrades (e.g., rolldown tag bump, vite bump, + vitest/tsdown bumps) and any required code adjustments. + 2. `## Dependency updates` — a Markdown table with columns `Package | From | To`. + Use short 7-char SHAs for rolldown/vite hashes, and include the tag in the `To` + column when available. Omit packages whose old and new values are equal. + 3. `## Code changes` — bullet list of the actual non-version edits, each naming + the file(s) touched. If the only changes are version bumps + lockfile + generated + bindings, write `- None beyond version bumps, lockfile, and formatter output.`. + 4. `## Build status` — bullet list: + - `sync-remote-and-build`: ${{ steps.build.outcome }} + - `build-upstream`: ${{ steps.build-upstream.outcome }} + + ### Rules + - Be factual. Only describe changes that are actually present in `git diff`. Do not + invent feature descriptions or behavior changes. + - Keep the tone terse and mechanical — this is an automated upgrade PR, not a + feature announcement. + - Do NOT run `git commit`, `git push`, or create any PR comment; simply write the + two output files. A later workflow step will consume them. + - Do NOT modify any file inside the repository working tree. + claude_args: | + --model opus --allowedTools "Bash,Read,Write,Edit" + additional_permissions: | + actions: read + + - name: Read generated PR content + id: pr-content + run: | + set -euo pipefail + COMMIT_FILE="${UPGRADE_DEPS_META_DIR}/commit-message.txt" + BODY_FILE="${UPGRADE_DEPS_META_DIR}/pr-body.md" + + echo '--- commit-message.txt ---' + cat "${COMMIT_FILE}" + echo '--- pr-body.md ---' + cat "${BODY_FILE}" + echo '---' + + { + echo 'commit-message<> "${GITHUB_OUTPUT}" + - name: Close and delete previous PR env: GH_TOKEN: ${{ secrets.AUTO_UPDATE_BRANCH_TOKEN }} @@ -140,12 +248,5 @@ jobs: sign-commits: true token: ${{ secrets.AUTO_UPDATE_BRANCH_TOKEN }} branch-token: ${{ secrets.GITHUB_TOKEN }} - body: | - Automated daily upgrade of upstream dependencies: - - rolldown (latest tag) - - vite (latest tag) - - vitest (latest npm version) - - tsdown (latest npm version) - - Build status: ${{ steps.build.outcome }} - commit-message: 'feat(deps): upgrade upstream dependencies' + body: ${{ steps.pr-content.outputs.body }} + commit-message: ${{ steps.pr-content.outputs.commit-message }} From f043b362a40ec88d01070b17d2c4b2a029732419 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Apr 2026 10:47:45 +0800 Subject: [PATCH 2/9] chore(workflow): broaden final-error checklist for upgrade-deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The closing prompt instruction only pointed at `build-upstream` failures, but the step also runs lint, `just build`, `pnpm test`, and snap tests. Enumerate every error class Claude is expected to fix, and call out snap-test regressions explicitly since `pnpm test` exits 0 even when snapshot outputs diverge — the agent must inspect the snap-test git diff and fix real regressions while leaving cosmetic version-string drift in place. --- .github/workflows/upgrade-deps.yml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/upgrade-deps.yml b/.github/workflows/upgrade-deps.yml index adb0e55bd0..413a828873 100644 --- a/.github/workflows/upgrade-deps.yml +++ b/.github/workflows/upgrade-deps.yml @@ -116,8 +116,26 @@ jobs: the task complete if either `just build` fails, `pnpm bootstrap-cli:ci && pnpm test` fails, or the snap-test diff shows an apparent regression. - Help me fix the errors in `build-upstream` steps if exists. - No need to commit changes after your fixing we have a following step to commit all file changes. + Help me fix every error that surfaces during this step. That includes, at + minimum: + - Failures in the `build-upstream` action (non-zero exit from any step inside + `.github/actions/build-upstream/action.yml`). + - `pnpm run lint` errors. + - `just build` failures. + - `pnpm bootstrap-cli:ci && pnpm test` failures. + - Snap-test regressions: `pnpm test` always exits 0 even when snapshot outputs + differ, so you MUST inspect the `git diff` on + `packages/cli/snap-tests/**/snap.txt` and + `packages/cli/snap-tests-global/**/snap.txt` and fix any change that looks + like a real regression (e.g., unexpected stack traces, missing output, + diverging CLI behavior). Cosmetic drift caused by the upstream upgrade + itself (e.g., a bumped version string in help output) is acceptable — + leave those in place. + - `cargo check --all-targets --all-features` errors if any Rust code was + modified, and stale `cargo shear` findings if `Cargo.toml` changed. + + Do NOT commit any changes — a later workflow step handles the commit for all + modified files. claude_args: | --model opus --allowedTools "Bash,Edit,Replace,NotebookEditCell" additional_permissions: | From 13652433a2b08020b2336c0e04858031c089fa77 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Apr 2026 10:51:42 +0800 Subject: [PATCH 3/9] chore(scripts): satisfy oxlint curly + consistent-function-scoping rules Bare-return and single-line `if` statements tripped the `curly` rule, and `isFullSha` was declared inside `writeMetaFiles` even though it didn't capture any parent-scope bindings. Hoist the helper to module scope and add braces to every single-statement `if`. --- .github/scripts/upgrade-deps.mjs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/scripts/upgrade-deps.mjs b/.github/scripts/upgrade-deps.mjs index 5e58ddf908..f8e0216645 100644 --- a/.github/scripts/upgrade-deps.mjs +++ b/.github/scripts/upgrade-deps.mjs @@ -4,12 +4,16 @@ import path from 'node:path'; const ROOT = process.cwd(); const META_DIR = process.env.UPGRADE_DEPS_META_DIR; +const isFullSha = (s) => /^[0-9a-f]{40}$/.test(s); + /** @type {Map} */ const changes = new Map(); function recordChange(name, oldValue, newValue, tag) { const entry = { old: oldValue ?? null, new: newValue }; - if (tag) entry.tag = tag; + if (tag) { + entry.tag = tag; + } changes.set(name, entry); if (oldValue !== newValue) { console.log(` ${name}: ${oldValue ?? '(unset)'} -> ${newValue}`); @@ -181,7 +185,9 @@ async function updateCorePackage(devtoolsVersion) { // ============ Write metadata files for PR description ============ function writeMetaFiles() { - if (!META_DIR) return; + if (!META_DIR) { + return; + } fs.mkdirSync(META_DIR, { recursive: true }); @@ -194,15 +200,22 @@ function writeMetaFiles() { const changed = [...changes.entries()].filter(([, v]) => v.old !== v.new); const unchanged = [...changes.entries()].filter(([, v]) => v.old === v.new); - const isFullSha = (s) => /^[0-9a-f]{40}$/.test(s); const formatVersion = (v) => { - if (v.tag) return `${v.tag} (${v.new.slice(0, 7)})`; - if (isFullSha(v.new)) return v.new.slice(0, 7); + if (v.tag) { + return `${v.tag} (${v.new.slice(0, 7)})`; + } + if (isFullSha(v.new)) { + return v.new.slice(0, 7); + } return v.new; }; const formatOld = (v) => { - if (!v.old) return '(unset)'; - if (isFullSha(v.old)) return v.old.slice(0, 7); + if (!v.old) { + return '(unset)'; + } + if (isFullSha(v.old)) { + return v.old.slice(0, 7); + } return v.old; }; From 9446a5f50a7f30c6fdb02caf9dac24f040d32ba5 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Apr 2026 10:57:33 +0800 Subject: [PATCH 4/9] fix(workflow): address Cursor Bugbot findings on upgrade-deps PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guarantee a trailing newline on commit-message.txt and pr-body.md before piping them through the heredoc into `$GITHUB_OUTPUT`. If Claude overwrites either file without a final `\n`, `cat` leaves the last line adjacent to the closing delimiter and GitHub Actions never terminates the multi-line value — producing a garbled body/commit. - Fail fast in `updatePnpmWorkspace` when a version regex no longer matches the YAML. Previously we silently recorded `(unset) -> new` even though `.replace` was a no-op, which would ship a misleading dependency table. A stale pattern now surfaces as an explicit error so the script can be updated. --- .github/scripts/upgrade-deps.mjs | 6 ++++++ .github/workflows/upgrade-deps.yml | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/.github/scripts/upgrade-deps.mjs b/.github/scripts/upgrade-deps.mjs index f8e0216645..ca282c03f8 100644 --- a/.github/scripts/upgrade-deps.mjs +++ b/.github/scripts/upgrade-deps.mjs @@ -134,6 +134,12 @@ async function updatePnpmWorkspace(versions) { for (const { name, pattern, replacement, newVersion } of entries) { const oldVersion = content.match(pattern)?.[1]; + if (!oldVersion) { + throw new Error( + `Failed to match ${name} in pnpm-workspace.yaml — the pattern ${pattern} is stale, ` + + `please update it in .github/scripts/upgrade-deps.mjs`, + ); + } content = content.replace(pattern, replacement); recordChange(name, oldVersion, newVersion); } diff --git a/.github/workflows/upgrade-deps.yml b/.github/workflows/upgrade-deps.yml index 413a828873..f93395b40b 100644 --- a/.github/workflows/upgrade-deps.yml +++ b/.github/workflows/upgrade-deps.yml @@ -228,6 +228,17 @@ jobs: COMMIT_FILE="${UPGRADE_DEPS_META_DIR}/commit-message.txt" BODY_FILE="${UPGRADE_DEPS_META_DIR}/pr-body.md" + # GitHub Actions heredoc outputs require the terminator on its own line, + # so guarantee a trailing newline regardless of what the generator wrote. + ensure_trailing_newline() { + local f="$1" + if [ -s "$f" ] && [ -n "$(tail -c1 "$f")" ]; then + printf '\n' >> "$f" + fi + } + ensure_trailing_newline "${COMMIT_FILE}" + ensure_trailing_newline "${BODY_FILE}" + echo '--- commit-message.txt ---' cat "${COMMIT_FILE}" echo '--- pr-body.md ---' From 1175d99a1bb0098e2f8ff59f00a1ea0393dc0b31 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Apr 2026 11:27:42 +0800 Subject: [PATCH 5/9] refactor(workflow): tighten upgrade-deps script + prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consolidate the `check-upgrade-dependencies` Claude prompt: the three overlapping sections (old Instructions, Final check, "Help me fix") all described the same test/build/snap-test checks and sometimes contradicted each other. Replace with a single structured prompt (Background → Fixups → Final validation → Commit rule) where each requirement appears exactly once. - `updatePnpmWorkspace` now uses a single-pass `String.replace` with a callback, so the current version is captured during the substitution instead of via a separate `match` call. Drops the redundant `replacement` field from each entry and removes the "matched once but replace didn't match" edge case. - `getLatestTag` fetches only the newest tag (`?per_page=1`) instead of the default 30. - `updateCorePackage` now early-returns when `@vitejs/devtools` is absent, skipping the no-op JSON rewrite. --- .github/scripts/upgrade-deps.mjs | 60 ++++++----------- .github/workflows/upgrade-deps.yml | 105 ++++++++++++----------------- 2 files changed, 64 insertions(+), 101 deletions(-) diff --git a/.github/scripts/upgrade-deps.mjs b/.github/scripts/upgrade-deps.mjs index ca282c03f8..a2eaaace18 100644 --- a/.github/scripts/upgrade-deps.mjs +++ b/.github/scripts/upgrade-deps.mjs @@ -24,7 +24,7 @@ function recordChange(name, oldValue, newValue, tag) { // ============ GitHub API ============ async function getLatestTag(owner, repo) { - const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/tags`, { + const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/tags?per_page=1`, { headers: { Authorization: `token ${process.env.GITHUB_TOKEN}`, Accept: 'application/vnd.github.v3+json', @@ -84,64 +84,47 @@ async function updatePnpmWorkspace(versions) { const filePath = path.join(ROOT, 'pnpm-workspace.yaml'); let content = fs.readFileSync(filePath, 'utf8'); - // The capture regex returns the current version in $1; the replacement string - // substitutes the new version into the same anchor text. - // oxlint's trailing \n disambiguates from oxlint-tsgolint. + // The capture regex puts the current version in $1 and matches exactly what + // the new string will replace. oxlint's trailing \n disambiguates from oxlint-tsgolint. const entries = [ { name: 'vitest', - pattern: /vitest-dev: npm:vitest@\^([\d.]+(?:-[\w.]+)?)/, - replacement: `vitest-dev: npm:vitest@^${versions.vitest}`, + pattern: /(vitest-dev: npm:vitest@\^)([\d.]+(?:-[\w.]+)?)/, newVersion: versions.vitest, }, - { - name: 'tsdown', - pattern: /tsdown: \^([\d.]+(?:-[\w.]+)?)/, - replacement: `tsdown: ^${versions.tsdown}`, - newVersion: versions.tsdown, - }, + { name: 'tsdown', pattern: /(tsdown: \^)([\d.]+(?:-[\w.]+)?)/, newVersion: versions.tsdown }, { name: '@oxc-node/cli', - pattern: /'@oxc-node\/cli': \^([\d.]+(?:-[\w.]+)?)/, - replacement: `'@oxc-node/cli': ^${versions.oxcNodeCli}`, + pattern: /('@oxc-node\/cli': \^)([\d.]+(?:-[\w.]+)?)/, newVersion: versions.oxcNodeCli, }, { name: '@oxc-node/core', - pattern: /'@oxc-node\/core': \^([\d.]+(?:-[\w.]+)?)/, - replacement: `'@oxc-node/core': ^${versions.oxcNodeCore}`, + pattern: /('@oxc-node\/core': \^)([\d.]+(?:-[\w.]+)?)/, newVersion: versions.oxcNodeCore, }, - { - name: 'oxfmt', - pattern: /oxfmt: =([\d.]+(?:-[\w.]+)?)/, - replacement: `oxfmt: =${versions.oxfmt}`, - newVersion: versions.oxfmt, - }, - { - name: 'oxlint', - pattern: /oxlint: =([\d.]+(?:-[\w.]+)?)\n/, - replacement: `oxlint: =${versions.oxlint}\n`, - newVersion: versions.oxlint, - }, + { name: 'oxfmt', pattern: /(oxfmt: =)([\d.]+(?:-[\w.]+)?)/, newVersion: versions.oxfmt }, + { name: 'oxlint', pattern: /(oxlint: =)([\d.]+(?:-[\w.]+)?)(\n)/, newVersion: versions.oxlint }, { name: 'oxlint-tsgolint', - pattern: /oxlint-tsgolint: =([\d.]+(?:-[\w.]+)?)/, - replacement: `oxlint-tsgolint: =${versions.oxlintTsgolint}`, + pattern: /(oxlint-tsgolint: =)([\d.]+(?:-[\w.]+)?)/, newVersion: versions.oxlintTsgolint, }, ]; - for (const { name, pattern, replacement, newVersion } of entries) { - const oldVersion = content.match(pattern)?.[1]; - if (!oldVersion) { + for (const { name, pattern, newVersion } of entries) { + let matched = false; + content = content.replace(pattern, (_match, prefix, oldVersion, suffix = '') => { + matched = true; + recordChange(name, oldVersion, newVersion); + return `${prefix}${newVersion}${suffix}`; + }); + if (!matched) { throw new Error( `Failed to match ${name} in pnpm-workspace.yaml — the pattern ${pattern} is stale, ` + `please update it in .github/scripts/upgrade-deps.mjs`, ); } - content = content.replace(pattern, replacement); - recordChange(name, oldVersion, newVersion); } fs.writeFileSync(filePath, content); @@ -180,10 +163,11 @@ async function updateCorePackage(devtoolsVersion) { const pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); const currentDevtools = pkg.devDependencies?.['@vitejs/devtools']; - if (currentDevtools) { - pkg.devDependencies['@vitejs/devtools'] = `^${devtoolsVersion}`; - recordChange('@vitejs/devtools', currentDevtools.replace(/^[\^~]/, ''), devtoolsVersion); + if (!currentDevtools) { + return; } + pkg.devDependencies['@vitejs/devtools'] = `^${devtoolsVersion}`; + recordChange('@vitejs/devtools', currentDevtools.replace(/^[\^~]/, ''), devtoolsVersion); fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n'); console.log('Updated packages/core/package.json'); diff --git a/.github/workflows/upgrade-deps.yml b/.github/workflows/upgrade-deps.yml index f93395b40b..2208cf5a3f 100644 --- a/.github/workflows/upgrade-deps.yml +++ b/.github/workflows/upgrade-deps.yml @@ -72,70 +72,49 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} show_full_output: 'true' prompt: | - Check if the build-upstream steps failed and fix them. + Your goal: after the daily upstream-dependency upgrade, bring the project back + to a fully green state. The upgrade script has already bumped every dep to the + latest version and the `build-upstream` action has attempted a build — your job + is to diagnose and fix every error that surfaced, then prove the fix is complete + by running a final validation pass. + ### Background - - The build-upstream steps are at ./.github/actions/build-upstream/action.yml - - The deps upgrade script is at ./.github/scripts/upgrade-deps.mjs - - ### Instructions - - We are using `pnpm` as the package manager - - We are aiming to upgrade all dependencies to the latest versions in this workflow, so don't downgrade any dependencies. - - Compare tsdown CLI options with `vp pack` and sync any new or removed options. Follow the instructions in `.claude/skills/sync-tsdown-cli/SKILL.md`. - - Check `.claude/agents/cargo-workspace-merger.md` if rolldown hash is changed. - - Run the steps in `build-upstream` action.yml after your fixing. If no errors are found, you can safe to exit. - - Install global CLI after the build-upstream steps are successful, by running the following commands: - - `pnpm bootstrap-cli:ci` - - `echo "$HOME/.vite-plus/bin" >> $GITHUB_PATH` - - Run `pnpm run lint` to check if there are any issues after the build, if has, deep investigate it and fix it. You need to run `just build` before you can run `pnpm run lint`. - - If deps in our `Cargo.toml` need to be upgraded, you can refer to the `./.claude/agents/cargo-workspace-merger.md` - - If `Cargo.toml` has been modified, you need to run `cargo shear` to ensure there is nothing wrong with our dependencies. - - Run `cargo check --all-targets --all-features` to ensure everything works fine if any Rust related codes are modified. - - Run the following commands to ensure everything works fine: - vp -h - vp run -h - vp lint -h - vp test -h - vp build -h - vp fmt -h - vp pack -h - - ### Final check (BOTH must succeed before you exit) - This step is only complete when BOTH of the following pass. If either one fails, - diagnose and fix the root cause, then re-run both until they both succeed. - 1. `just build` — must exit 0. If it fails, investigate the error, fix the - underlying issue, and run it again. - 2. `pnpm bootstrap-cli:ci && pnpm test` — must exit 0. If it fails, investigate - the error, fix the underlying issue, and run it again. Note that the snap - tests inside `pnpm test` always exit 0 even when their outputs differ, so - after the command passes you MUST also inspect the `git diff` on - `packages/cli/snap-tests/**/snap.txt` and `packages/cli/snap-tests-global/**/snap.txt` - and decide whether each change is an acceptable consequence of the upstream - upgrade (e.g. a version string bump) or a real regression that needs fixing. - - Only exit after you have confirmed both of the above succeed. Do not consider - the task complete if either `just build` fails, `pnpm bootstrap-cli:ci && pnpm test` - fails, or the snap-test diff shows an apparent regression. - - Help me fix every error that surfaces during this step. That includes, at - minimum: - - Failures in the `build-upstream` action (non-zero exit from any step inside - `.github/actions/build-upstream/action.yml`). - - `pnpm run lint` errors. - - `just build` failures. - - `pnpm bootstrap-cli:ci && pnpm test` failures. - - Snap-test regressions: `pnpm test` always exits 0 even when snapshot outputs - differ, so you MUST inspect the `git diff` on - `packages/cli/snap-tests/**/snap.txt` and - `packages/cli/snap-tests-global/**/snap.txt` and fix any change that looks - like a real regression (e.g., unexpected stack traces, missing output, - diverging CLI behavior). Cosmetic drift caused by the upstream upgrade - itself (e.g., a bumped version string in help output) is acceptable — - leave those in place. - - `cargo check --all-targets --all-features` errors if any Rust code was - modified, and stale `cargo shear` findings if `Cargo.toml` changed. - - Do NOT commit any changes — a later workflow step handles the commit for all - modified files. + - Upgrade script: `./.github/scripts/upgrade-deps.mjs` + - Build-upstream action: `./.github/actions/build-upstream/action.yml` + - Package manager: `pnpm`. Do NOT downgrade any dep — we want the latest. + + ### Fixups to perform (in order) + 1. Re-run the steps in `./.github/actions/build-upstream/action.yml`; fix any + non-zero exits. + 2. If the rolldown hash changed, follow `.claude/agents/cargo-workspace-merger.md` + to resync the workspace. + 3. Compare tsdown CLI options with `vp pack` and sync new/removed options per + `.claude/skills/sync-tsdown-cli/SKILL.md`. + 4. Install the global CLI: + - `pnpm bootstrap-cli:ci` + - `echo "$HOME/.vite-plus/bin" >> $GITHUB_PATH` + 5. If any Rust code or `Cargo.toml` was modified, run `cargo check + --all-targets --all-features` and `cargo shear`; fix anything they report. + 6. Run `pnpm run lint` (requires a prior `just build`); fix any errors. + 7. Smoke-test the CLI: `vp -h`, `vp run -h`, `vp lint -h`, `vp test -h`, + `vp build -h`, `vp fmt -h`, `vp pack -h`. + + ### Final validation (this step is complete ONLY when all pass) + 1. `just build` exits 0. + 2. `pnpm bootstrap-cli:ci && pnpm test` exits 0. + 3. `git diff` on `packages/cli/snap-tests/**/snap.txt` and + `packages/cli/snap-tests-global/**/snap.txt` contains no real regressions. + IMPORTANT: `pnpm test` always exits 0 even when snap outputs differ, so you + MUST inspect the diff yourself. Cosmetic drift from the upgrade (e.g. a + bumped version string in help output) is acceptable; unexpected stack + traces, missing output, or diverging CLI behavior are regressions to fix. + + If any of the three above fails, diagnose the root cause, fix it, and re-run + the final validation. Do not exit with the task marked complete otherwise. + + ### Commit rule + Do NOT run `git commit` or `git push`. A later workflow step commits every + modified file for you. claude_args: | --model opus --allowedTools "Bash,Edit,Replace,NotebookEditCell" additional_permissions: | From 65e709c0b8ca82abbbcdfb9c86f221832428a5e9 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Apr 2026 11:43:56 +0800 Subject: [PATCH 6/9] fix(workflow): cap Claude turns + forbid background/polling in upgrade-deps Run 24545325671 showed the `Check upgrade dependencies` step stuck for 40+ minutes in a busy-wait loop: Claude had backgrounded `just build` and was polling `ps aux | grep "just build"` / `pgrep` / `sleep` every few seconds to check if it was done. Each poll was a full model round-trip, so the session never ended even though no real work was happening. - Add a 200-turn cap via `--max-turns 200` so a runaway agent session can no longer burn an unbounded amount of Actions time. - Add an explicit "Running long commands" rule to the prompt that forbids backgrounding (`&`, `nohup`, `disown`, etc.) and forbids polling with `ps`/`pgrep`/`sleep` loops. The Bash tool's 20-minute per-call timeout is plenty for `just build` and `pnpm test`, and foreground calls give Claude the exit code and output in one shot. --- .github/workflows/upgrade-deps.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/upgrade-deps.yml b/.github/workflows/upgrade-deps.yml index 2208cf5a3f..d15a04f2d1 100644 --- a/.github/workflows/upgrade-deps.yml +++ b/.github/workflows/upgrade-deps.yml @@ -112,11 +112,29 @@ jobs: If any of the three above fails, diagnose the root cause, fix it, and re-run the final validation. Do not exit with the task marked complete otherwise. + ### Running long commands (IMPORTANT) + Run every long-running command (`just build`, `pnpm bootstrap-cli:ci`, + `pnpm test`, `cargo check`, etc.) in the FOREGROUND — a single Bash tool call + that blocks until the command exits. The Bash tool already gives you a 20-minute + timeout per call, which is enough for these builds. + + Do NOT do any of the following: + - Spawn a background process (`&`, `nohup`, `disown`, `setsid`, `screen`, `tmux`). + - Poll for process completion with `ps`, `pgrep`, `lsof`, `sleep` loops, or + repeated `ls` checks on build artifacts. Each polling Bash call costs a full + model round-trip and burns minutes without progress. + - "Monitor" a running build from a separate Bash call. If you want to know + whether a build succeeded, just run it in the foreground and read its exit + code and stdout/stderr in the single tool result. + + If a foreground command legitimately hits the 20-minute Bash timeout, report + that and stop — do not start polling. + ### Commit rule Do NOT run `git commit` or `git push`. A later workflow step commits every modified file for you. claude_args: | - --model opus --allowedTools "Bash,Edit,Replace,NotebookEditCell" + --model opus --max-turns 200 --allowedTools "Bash,Edit,Replace,NotebookEditCell" additional_permissions: | actions: read From 05af642bfb6f51615d047e3a5aef0d8337065130 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Apr 2026 11:44:41 +0800 Subject: [PATCH 7/9] fix(workflow): correct Bash tool timeout to 10 minutes in upgrade-deps prompt The Bash tool's maximum per-call timeout is 10 minutes (600000 ms), not 20 minutes as the previous wording stated. --- .github/workflows/upgrade-deps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/upgrade-deps.yml b/.github/workflows/upgrade-deps.yml index d15a04f2d1..6f491e28fc 100644 --- a/.github/workflows/upgrade-deps.yml +++ b/.github/workflows/upgrade-deps.yml @@ -115,7 +115,7 @@ jobs: ### Running long commands (IMPORTANT) Run every long-running command (`just build`, `pnpm bootstrap-cli:ci`, `pnpm test`, `cargo check`, etc.) in the FOREGROUND — a single Bash tool call - that blocks until the command exits. The Bash tool already gives you a 20-minute + that blocks until the command exits. The Bash tool already gives you a 10-minute timeout per call, which is enough for these builds. Do NOT do any of the following: @@ -127,7 +127,7 @@ jobs: whether a build succeeded, just run it in the foreground and read its exit code and stdout/stderr in the single tool result. - If a foreground command legitimately hits the 20-minute Bash timeout, report + If a foreground command legitimately hits the 10-minute Bash timeout, report that and stop — do not start polling. ### Commit rule From f8045cf73a9e0913b5ab21d60b87081bef4d442a Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Apr 2026 14:26:58 +0800 Subject: [PATCH 8/9] fix(scripts): correct corrupted versions in upgrade-deps replace callback Cursor Bugbot caught a high-severity bug introduced by the previous single-pass refactor. The replace callback used: (_match, prefix, oldVersion, suffix = '') => `${prefix}${newVersion}${suffix}` `String.prototype.replace` always passes the match offset (a number) as the next positional argument after the captures, so for the 6 patterns with only two captures (vitest, tsdown, @oxc-node/cli, @oxc-node/core, oxfmt, oxlint-tsgolint), `suffix` received the offset instead of `undefined`. The `= ''` default never kicked in, and the offset got appended to every version. Example: `tsdown: ^0.21.8` was rewritten to `tsdown: ^9.9.90`. The earlier smoke test missed this because it used substring `includes()`, which still matched. Restructure each entry to use ONE capture group (just the version) plus a literal `replacement` string. The callback signature is now `(_match, captured)` and ignores the trailing offset/string args, so the positional ambiguity is gone. Verified with a byte-exact equality test on a synthetic fixture covering non-zero offsets. --- .github/scripts/upgrade-deps.mjs | 52 ++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/.github/scripts/upgrade-deps.mjs b/.github/scripts/upgrade-deps.mjs index a2eaaace18..8945cee728 100644 --- a/.github/scripts/upgrade-deps.mjs +++ b/.github/scripts/upgrade-deps.mjs @@ -84,47 +84,67 @@ async function updatePnpmWorkspace(versions) { const filePath = path.join(ROOT, 'pnpm-workspace.yaml'); let content = fs.readFileSync(filePath, 'utf8'); - // The capture regex puts the current version in $1 and matches exactly what - // the new string will replace. oxlint's trailing \n disambiguates from oxlint-tsgolint. + // Each pattern has exactly one capture group (the current version) so the replace + // callback signature is `(match, captured, offset, string)` — no positional ambiguity. + // oxlint's trailing \n disambiguates from oxlint-tsgolint. const entries = [ { name: 'vitest', - pattern: /(vitest-dev: npm:vitest@\^)([\d.]+(?:-[\w.]+)?)/, + pattern: /vitest-dev: npm:vitest@\^([\d.]+(?:-[\w.]+)?)/, + replacement: `vitest-dev: npm:vitest@^${versions.vitest}`, newVersion: versions.vitest, }, - { name: 'tsdown', pattern: /(tsdown: \^)([\d.]+(?:-[\w.]+)?)/, newVersion: versions.tsdown }, + { + name: 'tsdown', + pattern: /tsdown: \^([\d.]+(?:-[\w.]+)?)/, + replacement: `tsdown: ^${versions.tsdown}`, + newVersion: versions.tsdown, + }, { name: '@oxc-node/cli', - pattern: /('@oxc-node\/cli': \^)([\d.]+(?:-[\w.]+)?)/, + pattern: /'@oxc-node\/cli': \^([\d.]+(?:-[\w.]+)?)/, + replacement: `'@oxc-node/cli': ^${versions.oxcNodeCli}`, newVersion: versions.oxcNodeCli, }, { name: '@oxc-node/core', - pattern: /('@oxc-node\/core': \^)([\d.]+(?:-[\w.]+)?)/, + pattern: /'@oxc-node\/core': \^([\d.]+(?:-[\w.]+)?)/, + replacement: `'@oxc-node/core': ^${versions.oxcNodeCore}`, newVersion: versions.oxcNodeCore, }, - { name: 'oxfmt', pattern: /(oxfmt: =)([\d.]+(?:-[\w.]+)?)/, newVersion: versions.oxfmt }, - { name: 'oxlint', pattern: /(oxlint: =)([\d.]+(?:-[\w.]+)?)(\n)/, newVersion: versions.oxlint }, + { + name: 'oxfmt', + pattern: /oxfmt: =([\d.]+(?:-[\w.]+)?)/, + replacement: `oxfmt: =${versions.oxfmt}`, + newVersion: versions.oxfmt, + }, + { + name: 'oxlint', + pattern: /oxlint: =([\d.]+(?:-[\w.]+)?)\n/, + replacement: `oxlint: =${versions.oxlint}\n`, + newVersion: versions.oxlint, + }, { name: 'oxlint-tsgolint', - pattern: /(oxlint-tsgolint: =)([\d.]+(?:-[\w.]+)?)/, + pattern: /oxlint-tsgolint: =([\d.]+(?:-[\w.]+)?)/, + replacement: `oxlint-tsgolint: =${versions.oxlintTsgolint}`, newVersion: versions.oxlintTsgolint, }, ]; - for (const { name, pattern, newVersion } of entries) { - let matched = false; - content = content.replace(pattern, (_match, prefix, oldVersion, suffix = '') => { - matched = true; - recordChange(name, oldVersion, newVersion); - return `${prefix}${newVersion}${suffix}`; + for (const { name, pattern, replacement, newVersion } of entries) { + let oldVersion; + content = content.replace(pattern, (_match, captured) => { + oldVersion = captured; + return replacement; }); - if (!matched) { + if (oldVersion === undefined) { throw new Error( `Failed to match ${name} in pnpm-workspace.yaml — the pattern ${pattern} is stale, ` + `please update it in .github/scripts/upgrade-deps.mjs`, ); } + recordChange(name, oldVersion, newVersion); } fs.writeFileSync(filePath, content); From f173b91ce7457b72870ff883fa71eade2652f4ec Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 17 Apr 2026 14:30:28 +0800 Subject: [PATCH 9/9] chore(workflow): minor upgrade-deps simplifications - Drop the redundant `[ -s ]` check in `ensure_trailing_newline`. If the file is empty, `tail -c1` returns nothing and `[ -n "" ]` is already false, so the size check adds nothing. - Trim the upgrade-deps regex-table comment to the only WHY worth preserving (oxlint's trailing `\n` disambiguates from oxlint-tsgolint). The callback-signature explanation just narrated the code and added noise. - Add `timeout-minutes: 180` to the `Check upgrade dependencies` step as a belt-and-suspenders guard. With `--max-turns 200` and the no-polling rule now in place, normal runs finish well under this cap; a runaway will be killed by Actions instead of consuming the full 6-hour job budget. --- .github/scripts/upgrade-deps.mjs | 4 +--- .github/workflows/upgrade-deps.yml | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/scripts/upgrade-deps.mjs b/.github/scripts/upgrade-deps.mjs index 8945cee728..c6e41398a0 100644 --- a/.github/scripts/upgrade-deps.mjs +++ b/.github/scripts/upgrade-deps.mjs @@ -84,9 +84,7 @@ async function updatePnpmWorkspace(versions) { const filePath = path.join(ROOT, 'pnpm-workspace.yaml'); let content = fs.readFileSync(filePath, 'utf8'); - // Each pattern has exactly one capture group (the current version) so the replace - // callback signature is `(match, captured, offset, string)` — no positional ambiguity. - // oxlint's trailing \n disambiguates from oxlint-tsgolint. + // oxlint's trailing \n in the pattern disambiguates from oxlint-tsgolint. const entries = [ { name: 'vitest', diff --git a/.github/workflows/upgrade-deps.yml b/.github/workflows/upgrade-deps.yml index 6f491e28fc..0e5e599c81 100644 --- a/.github/workflows/upgrade-deps.yml +++ b/.github/workflows/upgrade-deps.yml @@ -64,6 +64,7 @@ jobs: - name: Check upgrade dependencies id: check-upgrade-dependencies + timeout-minutes: 180 uses: anthropics/claude-code-action@c3d45e8e941e1b2ad7b278c57482d9c5bf1f35b3 # Claude Code to 2.1.112 env: RELEASE_BUILD: 'true' @@ -229,7 +230,7 @@ jobs: # so guarantee a trailing newline regardless of what the generator wrote. ensure_trailing_newline() { local f="$1" - if [ -s "$f" ] && [ -n "$(tail -c1 "$f")" ]; then + if [ -n "$(tail -c1 "$f")" ]; then printf '\n' >> "$f" fi }