From 51c9458636c9bb6a413727fd213ba63716636ff7 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Tue, 17 Mar 2026 23:58:12 +0100 Subject: [PATCH 1/8] ci: pre-release workflow --- .github/workflows/autofix.yml | 2 +- .github/workflows/release.yml | 93 ++++++----- scripts/create-github-release.mjs | 265 ++++++++++++++++++++++++++++++ 3 files changed, 319 insertions(+), 41 deletions(-) create mode 100644 scripts/create-github-release.mjs diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index e92492c7cad..9f324da71b6 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -3,7 +3,7 @@ name: autofix.ci # needed to securely identify the workflow on: pull_request: push: - branches: [main, alpha, beta, rc, v4] + branches: [main, v4, '*-pre', '*-maint'] concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af61b1a1711..272948f86dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,13 +2,11 @@ name: Release on: push: - branches: [main, alpha, beta, rc, v4] - repository_dispatch: - types: [release] + branches: [main, v4, '*-pre', '*-maint'] concurrency: - group: ${{ github.workflow }}-${{ github.event.number || github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} @@ -21,52 +19,67 @@ permissions: jobs: release: name: Release - if: github.repository_owner == 'TanStack' + if: "!contains(github.event.head_commit.message, 'ci: changeset release')" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 + - name: Check for changesets + id: changesets + run: | + CHANGESET_FILES=$(ls .changeset/*.md 2>/dev/null | grep -v README.md || true) + if [ -z "$CHANGESET_FILES" ]; then + echo "has_changesets=false" >> "$GITHUB_OUTPUT" + else + echo "has_changesets=true" >> "$GITHUB_OUTPUT" + fi - name: Start Nx Agents + if: steps.changesets.outputs.has_changesets == 'true' run: npx nx-cloud start-ci-run --distribute-on=".nx/workflows/dynamic-changesets.yaml" - name: Setup Tools uses: TanStack/config/.github/setup@main - name: Run Tests + if: steps.changesets.outputs.has_changesets == 'true' run: pnpm run test:ci - name: Stop Nx Agents - if: ${{ always() }} + if: ${{ always() && steps.changesets.outputs.has_changesets == 'true' }} run: npx nx-cloud stop-all-agents - # - name: Check for Changesets marked as major - # id: major - # run: | - # echo "found=false" >> $GITHUB_OUTPUT - # regex="(major)" - # shopt -s nullglob - # for file in .changeset/*.md; do - # if [[ $(cat $file) =~ $regex ]]; then - # echo "found=true" >> $GITHUB_OUTPUT - # fi - # done - - name: Run Changesets (version or publish) - id: changesets - uses: changesets/action@v1.7.0 - with: - version: pnpm run changeset:version - publish: pnpm run changeset:publish - commit: 'ci: Version Packages' - title: 'ci: Version Packages' - # - name: Auto-merge Changesets PR - # if: steps.changesets.outputs.hasChangesets == 'true' && steps.major.outputs.found == 'false' - # run: | - # gh pr merge --squash "$PR_NUMBER" - # gh api --method POST /repos/$REPO/dispatches -f 'event_type=release' - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # REPO: ${{ github.repository }} - # PR_NUMBER: ${{ steps.changesets.outputs.pullRequestNumber }} - - name: Comment on PRs about release - if: steps.changesets.outputs.published == 'true' - uses: TanStack/config/.github/comment-on-release@main - with: - published-packages: ${{ steps.changesets.outputs.publishedPackages }} + - name: Version Packages + run: pnpm run changeset:version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Commit and Push Version Changes + id: commit + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add . + if git commit -m "ci: changeset release"; then + git push + echo "committed=true" >> "$GITHUB_OUTPUT" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Determine dist-tag + if: steps.commit.outputs.committed == 'true' + id: dist-tag + run: | + BRANCH="${GITHUB_REF_NAME}" + if [[ "$BRANCH" == *-pre ]]; then + echo "prerelease=true" >> "$GITHUB_OUTPUT" + elif [[ "$BRANCH" == *-maint ]]; then + echo "tag=maint" >> "$GITHUB_OUTPUT" + elif [[ "$BRANCH" == "v4" ]]; then + echo "tag=v4" >> "$GITHUB_OUTPUT" + fi + - name: Publish Packages + if: steps.commit.outputs.committed == 'true' + run: pnpm run changeset:publish ${{ steps.dist-tag.outputs.tag && format('--tag {0}', steps.dist-tag.outputs.tag) }} + - name: Create GitHub Release + if: steps.commit.outputs.committed == 'true' + run: node scripts/create-github-release.mjs ${{ steps.dist-tag.outputs.prerelease == 'true' && '--prerelease' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/create-github-release.mjs b/scripts/create-github-release.mjs new file mode 100644 index 00000000000..cd40c3930a1 --- /dev/null +++ b/scripts/create-github-release.mjs @@ -0,0 +1,265 @@ +// @ts-nocheck +import fs from 'fs' +import path from 'node:path' +import { globSync } from 'node:fs' +import { execSync } from 'node:child_process' +import { tmpdir } from 'node:os' + +const rootDir = path.join(import.meta.dirname, '..') +const ghToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN + +// Resolve GitHub usernames from commit author emails +const usernameCache = {} +async function resolveUsername(email) { + if (!ghToken || !email) return null + if (usernameCache[email] !== undefined) return usernameCache[email] + + try { + const res = await fetch(`https://api.github.com/search/users?q=${email}`, { + headers: { Authorization: `token ${ghToken}` }, + }) + const data = await res.json() + const login = data?.items?.[0]?.login || null + usernameCache[email] = login + return login + } catch { + usernameCache[email] = null + return null + } +} + +// Resolve author from a PR number via GitHub API +const prAuthorCache = {} +async function resolveAuthorForPR(prNumber) { + if (prAuthorCache[prNumber] !== undefined) return prAuthorCache[prNumber] + + if (!ghToken) { + prAuthorCache[prNumber] = null + return null + } + + try { + const res = await fetch( + `https://api.github.com/repos/TanStack/query/pulls/${prNumber}`, + { headers: { Authorization: `token ${ghToken}` } }, + ) + const data = await res.json() + const login = data?.user?.login || null + prAuthorCache[prNumber] = login + return login + } catch { + prAuthorCache[prNumber] = null + return null + } +} + +// Get the previous release commit to diff against. +// This script runs right after the "ci: changeset release" commit is pushed, +// so HEAD is the release commit. +const releaseLogs = execSync( + 'git log --oneline --grep="ci: changeset release" --format=%H', +) + .toString() + .trim() + .split('\n') + .filter(Boolean) + +const currentRelease = releaseLogs[0] || 'HEAD' +const previousRelease = releaseLogs[1] + +// Find packages that were actually bumped by comparing versions +const packagesDir = path.join(rootDir, 'packages') +const allPkgJsonPaths = globSync('*/package.json', { cwd: packagesDir }) + +const bumpedPackages = [] +for (const relPath of allPkgJsonPaths) { + const fullPath = path.join(packagesDir, relPath) + const currentPkg = JSON.parse(fs.readFileSync(fullPath, 'utf-8')) + if (currentPkg.private) continue + + // Get the version from the previous release commit + if (previousRelease) { + try { + const prevContent = execSync( + `git show ${previousRelease}:packages/${relPath}`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }, + ) + const prevPkg = JSON.parse(prevContent) + if (prevPkg.version !== currentPkg.version) { + bumpedPackages.push({ + name: currentPkg.name, + version: currentPkg.version, + prevVersion: prevPkg.version, + dir: path.dirname(relPath), + }) + } + } catch { + // Package didn't exist in previous release — it's new + bumpedPackages.push({ + name: currentPkg.name, + version: currentPkg.version, + prevVersion: null, + dir: path.dirname(relPath), + }) + } + } else { + // No previous release — include all non-private packages + bumpedPackages.push({ + name: currentPkg.name, + version: currentPkg.version, + prevVersion: null, + dir: path.dirname(relPath), + }) + } +} + +bumpedPackages.sort((a, b) => a.name.localeCompare(b.name)) + +// Build changelog from git log between releases (conventional commits) +const rangeFrom = previousRelease || `${currentRelease}~1` +const rawLog = execSync( + `git log ${rangeFrom}..${currentRelease} --pretty=format:"%h %ae %s" --no-merges`, + { encoding: 'utf-8' }, +).trim() + +const typeOrder = [ + 'feat', + 'fix', + 'perf', + 'refactor', + 'docs', + 'chore', + 'test', + 'ci', +] +const typeLabels = { + feat: 'Features', + fix: 'Fix', + perf: 'Performance', + refactor: 'Refactor', + docs: 'Documentation', + chore: 'Chore', + test: 'Tests', + ci: 'CI', +} +const typeIndex = (t) => { + const i = typeOrder.indexOf(t) + return i === -1 ? 99 : i +} + +const groups = {} +const commits = rawLog ? rawLog.split('\n') : [] + +for (const line of commits) { + const match = line.match(/^(\w+)\s+(\S+)\s+(.*)$/) + if (!match) continue + const [, hash, email, subject] = match + + // Skip release commits + if (subject.startsWith('ci: changeset release')) continue + + // Parse conventional commit: type(scope): message + const conventionalMatch = subject.match(/^(\w+)(?:\(([^)]*)\))?:\s*(.*)$/) + const type = conventionalMatch ? conventionalMatch[1] : 'other' + const scope = conventionalMatch ? conventionalMatch[2] || '' : '' + const message = conventionalMatch ? conventionalMatch[3] : subject + + // Only include user-facing change types + if (['feat', 'fix', 'perf', 'refactor', 'build', 'chore'].includes(type)) continue + + // Extract PR number if present + const prMatch = message.match(/\(#(\d+)\)/) + const prNumber = prMatch ? prMatch[1] : null + + if (!groups[type]) groups[type] = [] + groups[type].push({ hash, email, scope, message, prNumber }) +} + +// Build markdown grouped by conventional commit type +const sortedTypes = Object.keys(groups).sort( + (a, b) => typeIndex(a) - typeIndex(b), +) + +let changelogMd = '' +for (const type of sortedTypes) { + const label = typeLabels[type] || type.charAt(0).toUpperCase() + type.slice(1) + changelogMd += `### ${label}\n\n` + + for (const commit of groups[type]) { + const scopePrefix = commit.scope ? `${commit.scope}: ` : '' + const cleanMessage = commit.message.replace(/\s*\(#\d+\)/, '') + const prRef = commit.prNumber ? ` (#${commit.prNumber})` : '' + const username = commit.prNumber + ? await resolveAuthorForPR(commit.prNumber) + : await resolveUsername(commit.email) + const authorSuffix = username ? ` by @${username}` : '' + + changelogMd += `- ${scopePrefix}${cleanMessage}${prRef} (${commit.hash})${authorSuffix}\n` + } + changelogMd += '\n' +} + +if (!changelogMd.trim()) { + changelogMd = '- No changelog entries\n\n' +} + +const now = new Date() +const date = now.toISOString().slice(0, 10) +const time = now.toISOString().slice(11, 16).replace(':', '') +const tagName = `release-${date}-${time}` +const titleDate = `${date} ${now.toISOString().slice(11, 16)}` + +const isPrerelease = process.argv.includes('--prerelease') + +const body = `Release ${titleDate} + +## Changes + +${changelogMd} +## Packages + +${bumpedPackages.map((p) => `- ${p.name}@${p.version}`).join('\n')} +` + +// Create the release +// Check if tag already exists — if so, try to create the release for it +// (handles retries where the tag was pushed but release creation failed) +let tagExists = false +try { + execSync(`git rev-parse ${tagName}`, { stdio: 'ignore' }) + tagExists = true +} catch { + // Tag doesn't exist yet +} + +if (!tagExists) { + execSync(`git tag -a -m "${tagName}" ${tagName}`) + execSync('git push --tags') +} + +const prereleaseFlag = isPrerelease ? '--prerelease' : '' +const latestFlag = isPrerelease ? '' : ' --latest' +const tmpFile = path.join(tmpdir(), `release-notes-${tagName}.md`) +fs.writeFileSync(tmpFile, body) + +try { + execSync( + `gh release create ${tagName} ${prereleaseFlag} --title "Release ${titleDate}" --notes-file ${tmpFile}${latestFlag}`, + { stdio: 'inherit' }, + ) + console.info(`GitHub release ${tagName} created.`) +} catch (err) { + // Clean up the tag if we created it but release failed + if (!tagExists) { + console.info(`Release creation failed, cleaning up tag ${tagName}...`) + try { + execSync(`git push --delete origin ${tagName}`, { stdio: 'ignore' }) + execSync(`git tag -d ${tagName}`, { stdio: 'ignore' }) + } catch { + // Best effort cleanup + } + } + throw err +} finally { + fs.unlinkSync(tmpFile) +} From b73a127e2e655e9d6020bb8639c3a91307edc3c5 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:02:16 +0000 Subject: [PATCH 2/8] ci: apply automated fixes --- scripts/create-github-release.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/create-github-release.mjs b/scripts/create-github-release.mjs index cd40c3930a1..f250f2b6c4a 100644 --- a/scripts/create-github-release.mjs +++ b/scripts/create-github-release.mjs @@ -165,7 +165,8 @@ for (const line of commits) { const message = conventionalMatch ? conventionalMatch[3] : subject // Only include user-facing change types - if (['feat', 'fix', 'perf', 'refactor', 'build', 'chore'].includes(type)) continue + if (['feat', 'fix', 'perf', 'refactor', 'build', 'chore'].includes(type)) + continue // Extract PR number if present const prMatch = message.match(/\(#(\d+)\)/) From a8658454c8ba2df6c6bbf68c9d941501c65d723d Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 18 Mar 2026 00:03:05 +0100 Subject: [PATCH 3/8] add v[0-9] --- .github/workflows/autofix.yml | 2 +- .github/workflows/release.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 9f324da71b6..fd98e9fb08b 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -3,7 +3,7 @@ name: autofix.ci # needed to securely identify the workflow on: pull_request: push: - branches: [main, v4, '*-pre', '*-maint'] + branches: [main, 'v[0-9]', '*-pre', '*-maint'] concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 272948f86dd..f1f6cdd5a85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release on: push: - branches: [main, v4, '*-pre', '*-maint'] + branches: [main, 'v[0-9]', '*-pre', '*-maint'] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -71,8 +71,8 @@ jobs: echo "prerelease=true" >> "$GITHUB_OUTPUT" elif [[ "$BRANCH" == *-maint ]]; then echo "tag=maint" >> "$GITHUB_OUTPUT" - elif [[ "$BRANCH" == "v4" ]]; then - echo "tag=v4" >> "$GITHUB_OUTPUT" + elif [[ "$BRANCH" =~ ^v[0-9]+$ ]]; then + echo "tag=$BRANCH" >> "$GITHUB_OUTPUT" fi - name: Publish Packages if: steps.commit.outputs.committed == 'true' From fee2bc25d18136663b40b8fa7601dd9f6037ac7f Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 18 Mar 2026 00:12:34 +0100 Subject: [PATCH 4/8] changelog filter --- scripts/create-github-release.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/create-github-release.mjs b/scripts/create-github-release.mjs index f250f2b6c4a..1f520dbb6eb 100644 --- a/scripts/create-github-release.mjs +++ b/scripts/create-github-release.mjs @@ -165,7 +165,7 @@ for (const line of commits) { const message = conventionalMatch ? conventionalMatch[3] : subject // Only include user-facing change types - if (['feat', 'fix', 'perf', 'refactor', 'build', 'chore'].includes(type)) + if (!['feat', 'fix', 'perf', 'refactor', 'build', 'chore'].includes(type)) continue // Extract PR number if present From f2a2770f65a44ef56d41aabe9324b64c384029f9 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 18 Mar 2026 01:12:18 +0100 Subject: [PATCH 5/8] don't use latest for maint branches --- .github/workflows/release.yml | 4 +++- scripts/create-github-release.mjs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1f6cdd5a85..d13a299d26a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,13 +73,15 @@ jobs: echo "tag=maint" >> "$GITHUB_OUTPUT" elif [[ "$BRANCH" =~ ^v[0-9]+$ ]]; then echo "tag=$BRANCH" >> "$GITHUB_OUTPUT" + else + echo "latest=true" >> "$GITHUB_OUTPUT" fi - name: Publish Packages if: steps.commit.outputs.committed == 'true' run: pnpm run changeset:publish ${{ steps.dist-tag.outputs.tag && format('--tag {0}', steps.dist-tag.outputs.tag) }} - name: Create GitHub Release if: steps.commit.outputs.committed == 'true' - run: node scripts/create-github-release.mjs ${{ steps.dist-tag.outputs.prerelease == 'true' && '--prerelease' }} + run: node scripts/create-github-release.mjs ${{ steps.dist-tag.outputs.prerelease == 'true' && '--prerelease' }} ${{ steps.dist-tag.outputs.latest == 'true' && '--latest' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/create-github-release.mjs b/scripts/create-github-release.mjs index 1f520dbb6eb..b07467a4b5b 100644 --- a/scripts/create-github-release.mjs +++ b/scripts/create-github-release.mjs @@ -211,6 +211,7 @@ const tagName = `release-${date}-${time}` const titleDate = `${date} ${now.toISOString().slice(11, 16)}` const isPrerelease = process.argv.includes('--prerelease') +const isLatest = process.argv.includes('--latest') const body = `Release ${titleDate} @@ -239,7 +240,7 @@ if (!tagExists) { } const prereleaseFlag = isPrerelease ? '--prerelease' : '' -const latestFlag = isPrerelease ? '' : ' --latest' +const latestFlag = isLatest ? ' --latest' : '' const tmpFile = path.join(tmpdir(), `release-notes-${tagName}.md`) fs.writeFileSync(tmpFile, body) From bf5766e0df9f5b96b0bd558f8897ff71c42bc15b Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 18 Mar 2026 01:32:49 +0100 Subject: [PATCH 6/8] fix teh sember breaking --- scripts/create-github-release.mjs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/create-github-release.mjs b/scripts/create-github-release.mjs index b07467a4b5b..5e6ed05e3c0 100644 --- a/scripts/create-github-release.mjs +++ b/scripts/create-github-release.mjs @@ -123,6 +123,7 @@ const rawLog = execSync( ).trim() const typeOrder = [ + 'breaking', 'feat', 'fix', 'perf', @@ -133,6 +134,7 @@ const typeOrder = [ 'ci', ] const typeLabels = { + breaking: '⚠️ Breaking Changes', feat: 'Features', fix: 'Fix', perf: 'Performance', @@ -158,11 +160,12 @@ for (const line of commits) { // Skip release commits if (subject.startsWith('ci: changeset release')) continue - // Parse conventional commit: type(scope): message - const conventionalMatch = subject.match(/^(\w+)(?:\(([^)]*)\))?:\s*(.*)$/) + // Parse conventional commit: type(scope)!: message + const conventionalMatch = subject.match(/^(\w+)(?:\(([^)]*)\))?(!)?:\s*(.*)$/) const type = conventionalMatch ? conventionalMatch[1] : 'other' + const isBreaking = conventionalMatch ? !!conventionalMatch[3] : false const scope = conventionalMatch ? conventionalMatch[2] || '' : '' - const message = conventionalMatch ? conventionalMatch[3] : subject + const message = conventionalMatch ? conventionalMatch[4] : subject // Only include user-facing change types if (!['feat', 'fix', 'perf', 'refactor', 'build', 'chore'].includes(type)) @@ -172,8 +175,9 @@ for (const line of commits) { const prMatch = message.match(/\(#(\d+)\)/) const prNumber = prMatch ? prMatch[1] : null - if (!groups[type]) groups[type] = [] - groups[type].push({ hash, email, scope, message, prNumber }) + const bucket = isBreaking ? 'breaking' : type + if (!groups[bucket]) groups[bucket] = [] + groups[bucket].push({ hash, email, scope, message, prNumber }) } // Build markdown grouped by conventional commit type From a746c4b5df50841fba02895457a84aa60c6813ee Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 18 Mar 2026 01:35:37 +0100 Subject: [PATCH 7/8] ci: pnpm changeset pre enter rc --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d13a299d26a..08f17a0f040 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,6 +46,9 @@ jobs: - name: Stop Nx Agents if: ${{ always() && steps.changesets.outputs.has_changesets == 'true' }} run: npx nx-cloud stop-all-agents + - name: Enter Pre-Release Mode + if: "contains(github.ref_name, '-pre') && !hashFiles('.changeset/pre.json')" + run: pnpm changeset pre enter pre - name: Version Packages run: pnpm run changeset:version env: From 8bd3c96a95b4042b4c5466a49a91c96938367703 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 18 Mar 2026 01:40:36 +0100 Subject: [PATCH 8/8] ci: guard against shell injection --- scripts/create-github-release.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/create-github-release.mjs b/scripts/create-github-release.mjs index 5e6ed05e3c0..6ded1208aff 100644 --- a/scripts/create-github-release.mjs +++ b/scripts/create-github-release.mjs @@ -2,7 +2,7 @@ import fs from 'fs' import path from 'node:path' import { globSync } from 'node:fs' -import { execSync } from 'node:child_process' +import { execSync, execFileSync } from 'node:child_process' import { tmpdir } from 'node:os' const rootDir = path.join(import.meta.dirname, '..') @@ -80,8 +80,9 @@ for (const relPath of allPkgJsonPaths) { // Get the version from the previous release commit if (previousRelease) { try { - const prevContent = execSync( - `git show ${previousRelease}:packages/${relPath}`, + const prevContent = execFileSync( + 'git', + ['show', `${previousRelease}:packages/${relPath}`], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }, ) const prevPkg = JSON.parse(prevContent)