Skip to content

feat: auto-generate changesets from PR descriptions#828

Merged
binoy14 merged 3 commits intomainfrom
chore/auto-changeset-from-pr
Apr 8, 2026
Merged

feat: auto-generate changesets from PR descriptions#828
binoy14 merged 3 commits intomainfrom
chore/auto-changeset-from-pr

Conversation

@joshuaellis
Copy link
Copy Markdown
Member

@joshuaellis joshuaellis commented Mar 27, 2026

Summary

Auto-generate changeset files from PR descriptions so contributors never need to run pnpm changeset manually. Adds a PR description template and a workflow that parses it to create changesets automatically.

Stacked on #827.

How It Works

When a PR is opened or updated, the generate-changeset workflow:

  1. Parses the PR title for the conventional commit type → bump type (feat: → minor, fix: → patch, feat!: → major)
  2. Extracts release notes from the "Notes for release" section in the PR description
  3. Detects affected packages dynamically by matching changed files against packages/*/package.json
  4. Commits a changeset (.changeset/pr-<number>.md) to the PR branch

The changeset is kept in sync — if the PR title or description changes, the changeset is updated or removed accordingly.

Scenario Behaviour
feat: title + release notes filled in Changeset created with release notes as changelog entry
feat: title + release notes empty Changeset created using the PR title as changelog entry
chore: / docs: / test: title No changeset (cleaned up if one existed)
Release notes starts with N/A Explicit opt-out, no changeset
Only root files changed No changeset (no public packages affected)
Bot PRs (Renovate, Dependabot) Skipped — handled by changesets-from-conventional-commits.yml

Security

Uses pull_request_target to get write access for pushing changesets to PR branches. The workflow YAML runs from main (not the PR), and PR title/body are passed as environment variables — never interpolated into shell commands. No code from the PR is executed.

@joshuaellis joshuaellis requested a review from a team as a code owner March 27, 2026 21:29
@joshuaellis joshuaellis requested review from cngonzalez and removed request for a team March 27, 2026 21:29
@claude
Copy link
Copy Markdown

claude bot commented Mar 27, 2026

Claude finished @binoy14's task in 2m 29s —— View job


Code Review

  • Gather context
  • Read changed files
  • Post review

Issues

1. Misleading log + incorrect exit on signal-killed git process.github/scripts/generate-changeset.mjs:192

const {status} = spawnSync('git', ['diff', '--cached', '--quiet'], {stdio: 'ignore'})
if (status !== 1) {
  console.log('No changes to changeset file')
  process.exit(status ?? 1)
}

When the git process is killed by a signal, spawnSync returns status: null. The status !== 1 condition is truthy, so it logs "No changes to changeset file" and exits with 1 — a confusing failure message. The exit-code handling itself (status ?? 1) is fine, but the condition should distinguish the no-changes case (status === 0) from the error case:

if (status === null) throw new Error('git diff was killed by signal')
if (status === 0) {
  console.log('No changes to changeset file')
  process.exit(0)
}

Fix this →


2. git push --force-with-lease without explicit refspec.github/scripts/generate-changeset.mjs:39,199

Both push calls use git('push', '--force-with-lease') without specifying origin HEAD. Since persist-credentials: false is set in checkout and the remote URL is manually reconfigured (line 29), the tracking branch should still resolve correctly — but relying on implicit tracking is fragile. Explicit is safer:

git('push', '--force-with-lease', 'origin', 'HEAD')

Fix this →

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

📦 Bundle Stats — @sanity/cli

Compared against main (8960e68e) · v6.3.1 (npm)

@sanity/cli

Metric Value vs main (8960e68) vs v6.3.1
Internal (raw) 2.1 KB - -
Internal (gzip) 799 B - -
Bundled (raw) 10.95 MB - +9.01 MB, +464.9%
Bundled (gzip) 2.06 MB - +1.58 MB, +331.9%
Import time 823ms -10ms, -1.2% +20ms, +2.5%

bin:sanity

Metric Value vs main (8960e68) vs v6.3.1
Internal (raw) 975 B - -
Internal (gzip) 460 B - -
Bundled (raw) 9.84 MB - +9.13 MB, +1287.6%
Bundled (gzip) 1.77 MB - +1.60 MB, +940.8%
Import time 1.92s -3ms, -0.2% +1.04s, +118.3% ⚠️

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-core

Compared against main (8960e68e) · v1.3.0 (npm)

Metric Value vs main (8960e68) vs v1.3.0
Internal (raw) 92.3 KB - +93 B, +0.1%
Internal (gzip) 21.6 KB - +28 B, +0.1%
Bundled (raw) 21.53 MB - +9.02 MB, +72.0%
Bundled (gzip) 3.41 MB - +1.58 MB, +85.9%
Import time 781ms -3ms, -0.4% +43ms, +5.9%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@joshuaellis joshuaellis force-pushed the chore/changesets-migration branch 2 times, most recently from e3604c3 to 052d17c Compare March 27, 2026 21:36
@joshuaellis joshuaellis force-pushed the chore/auto-changeset-from-pr branch from 4c59c39 to 356ecd4 Compare March 27, 2026 21:36
@joshuaellis joshuaellis requested review from binoy14 and removed request for cngonzalez March 27, 2026 21:38
@joshuaellis joshuaellis force-pushed the chore/auto-changeset-from-pr branch from 356ecd4 to b001dc7 Compare March 27, 2026 21:40
@joshuaellis joshuaellis force-pushed the chore/changesets-migration branch from 052d17c to b704592 Compare March 27, 2026 21:43
@joshuaellis joshuaellis force-pushed the chore/auto-changeset-from-pr branch 2 times, most recently from 4da6e80 to d85643b Compare March 27, 2026 21:44
@joshuaellis joshuaellis force-pushed the chore/changesets-migration branch from b704592 to 47677a0 Compare March 31, 2026 08:37
@joshuaellis joshuaellis force-pushed the chore/auto-changeset-from-pr branch from d85643b to 11688f6 Compare March 31, 2026 08:38
@joshuaellis joshuaellis force-pushed the chore/changesets-migration branch from 47677a0 to e55bd40 Compare March 31, 2026 08:48
@joshuaellis joshuaellis force-pushed the chore/auto-changeset-from-pr branch 2 times, most recently from 7ba88aa to 7c58f23 Compare March 31, 2026 08:49
Base automatically changed from chore/changesets-migration to main March 31, 2026 13:53
@joshuaellis joshuaellis force-pushed the chore/auto-changeset-from-pr branch from 7c58f23 to ebdc691 Compare March 31, 2026 13:54
@binoy14 binoy14 force-pushed the chore/auto-changeset-from-pr branch 2 times, most recently from f0c23cb to 22b23d0 Compare April 1, 2026 04:01
Miriad and others added 2 commits April 1, 2026 00:03
chore: replace release-please with changesets
Add ecospark[bot] to gitIgnoredAuthors so Renovate doesn't treat
changeset commits as foreign modifications that block rebasing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@binoy14 binoy14 force-pushed the chore/auto-changeset-from-pr branch from 22b23d0 to ee14154 Compare April 1, 2026 04:03
…workflow (#861)

* refactor: replace inline bash with node script in generate-changeset workflow

Move the changeset generation logic from an inline bash script to
.github/scripts/generate-changeset.mjs for readability and maintainability.
Add setup-node step with Node 24.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback

- Lazy git configuration: only write token to .git/config when actually
  pushing, not on early-exit paths
- Auto-discover scoped package dirs under packages/ instead of
  hardcoding @sanity and @repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: guard readdirSync and drop redundant GITHUB_REPOSITORY env

- Wrap readdirSync('packages') with existsSync check
- Remove GITHUB_REPOSITORY from workflow env block (already a default
  GitHub Actions env var)
- Remove it from the required env var check accordingly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent fork code execution with pull_request_target

Sparse-checkout the script from the base branch so that fork PRs
cannot replace generate-changeset.mjs to exfiltrate the token.

Also replace try/catch control flow with spawnSync exit code check
for the git diff --cached --quiet guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate shell injection by using execFileSync

Replace all execSync calls with execFileSync argument arrays to avoid
shell interpretation of untrusted input (PR_REPO is fork-controlled).
Also restore GITHUB_REPOSITORY validation and remove unused scope field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: handle signal-killed status in spawnSync diff check

spawnSync returns status: null when killed by a signal. The previous
check (status === 0) would fall through to git commit in that case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

📦 Bundle Stats — @sanity/cli

Compared against main (8960e68e)

@sanity/cli

Metric Value vs main (8960e68)
Internal (raw) 2.1 KB -
Internal (gzip) 799 B -
Bundled (raw) 10.95 MB -
Bundled (gzip) 2.06 MB -
Import time 829ms -14ms, -1.6%

bin:sanity

Metric Value vs main (8960e68)
Internal (raw) 975 B -
Internal (gzip) 460 B -
Bundled (raw) 9.84 MB -
Bundled (gzip) 1.77 MB -
Import time 1.97s -40ms, -2.0%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-core

Compared against main (8960e68e)

Metric Value vs main (8960e68)
Internal (raw) 92.3 KB -
Internal (gzip) 21.6 KB -
Bundled (raw) 21.53 MB -
Bundled (gzip) 3.41 MB -
Import time 790ms -8ms, -1.0%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — create-sanity

Compared against main (8960e68e)

Metric Value vs main (8960e68)
Internal (raw) 976 B -
Internal (gzip) 507 B -
Bundled (raw) 50.7 KB -
Bundled (gzip) 12.6 KB -
Import time ❌ ChildProcess denied: node -
Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub/​actions/​setup-node@​49933ea5288caeca8642d1e84afbd3f7d68200209910010010080

View full report

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

Coverage Delta

No covered files changed in this PR.

Overall Coverage

Metric Coverage
Statements 83.1% (±0%)
Branches 72.9% (±0%)
Functions 83.3% (±0%)
Lines 83.5% (±0%)

@binoy14 binoy14 merged commit b70668c into main Apr 8, 2026
44 checks passed
@binoy14 binoy14 deleted the chore/auto-changeset-from-pr branch April 8, 2026 18:42
joshuaellis added a commit that referenced this pull request Apr 9, 2026
* feat: auto-generate changesets from PR descriptions

chore: replace release-please with changesets

* fix: allow renovate to rebase PRs with auto-generated changesets

Add ecospark[bot] to gitIgnoredAuthors so Renovate doesn't treat
changeset commits as foreign modifications that block rebasing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: replace inline bash with node script in generate-changeset workflow (#861)

* refactor: replace inline bash with node script in generate-changeset workflow

Move the changeset generation logic from an inline bash script to
.github/scripts/generate-changeset.mjs for readability and maintainability.
Add setup-node step with Node 24.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback

- Lazy git configuration: only write token to .git/config when actually
  pushing, not on early-exit paths
- Auto-discover scoped package dirs under packages/ instead of
  hardcoding @sanity and @repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: guard readdirSync and drop redundant GITHUB_REPOSITORY env

- Wrap readdirSync('packages') with existsSync check
- Remove GITHUB_REPOSITORY from workflow env block (already a default
  GitHub Actions env var)
- Remove it from the required env var check accordingly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent fork code execution with pull_request_target

Sparse-checkout the script from the base branch so that fork PRs
cannot replace generate-changeset.mjs to exfiltrate the token.

Also replace try/catch control flow with spawnSync exit code check
for the git diff --cached --quiet guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate shell injection by using execFileSync

Replace all execSync calls with execFileSync argument arrays to avoid
shell interpretation of untrusted input (PR_REPO is fork-controlled).
Also restore GITHUB_REPOSITORY validation and remove unused scope field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: handle signal-killed status in spawnSync diff check

spawnSync returns status: null when killed by a signal. The previous
check (status === 0) would fall through to git commit in that case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Binoy Patel <me@binoy.io>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants