From 57570db58299b0a5cfcff423656d370609dacee3 Mon Sep 17 00:00:00 2001 From: Nick Earnshaw Date: Fri, 12 Jun 2026 23:16:48 +0100 Subject: [PATCH 1/2] ci: merge-and-auto-release workflow (draft-release approval) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the hand-tagged release flow with a merge-and-approve one so a maintainer's only per-PR action is Merge/Reject, and releases auto-draft for one-click approval — preserving contributor attribution (PRs get *merged*, not re-applied). - add release-drafter: a draft GitHub Release is kept up to date as labelled PRs merge to main; next semver version inferred from PR labels; PRs auto-labelled from branch/title prefix - publish.yml: trigger on `release: published` instead of `v*` tag push; stamp package.json version from the release tag (no hand-bump, no tag/version mismatch) and drop the tag-vs-package.json guard; upload the .vsix to the existing Release without regenerating its notes - docs: CONTRIBUTING.md + CLAUDE.md describe the new flow; PR template maps change type -> release label This is "option A" (release-drafter) of two approaches; release-please is the documented alternative in the proposal issue. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/PULL_REQUEST_TEMPLATE.md | 12 +++-- .github/release-drafter.yml | 63 +++++++++++++++++++++++++ .github/workflows/publish.yml | 67 +++++++++++++-------------- .github/workflows/release-drafter.yml | 36 ++++++++++++++ CLAUDE.md | 19 ++++---- CONTRIBUTING.md | 17 ++++++- 6 files changed, 165 insertions(+), 49 deletions(-) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/release-drafter.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 612bb200..d7135175 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,11 +14,13 @@ Keep the section headings; fill what applies, delete what doesn't. ## Type of change - -- [ ] Bug fix (non-breaking) -- [ ] New feature (non-breaking) -- [ ] Breaking change (existing behaviour differs afterwards) -- [ ] Documentation / CI / chore + +- [ ] Bug fix (non-breaking) → `fix` (patch) +- [ ] New feature (non-breaking) → `feature` (minor) +- [ ] Breaking change (existing behaviour differs afterwards) → `breaking` (major) +- [ ] Documentation / CI / chore → `docs` / `ci` / `chore` (patch) ## How was this tested? diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..87aed9df --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,63 @@ +# Release Drafter config. +# +# Keeps a single DRAFT GitHub Release continuously up to date as PRs merge to +# main. The maintainer reviews that draft and clicks "Publish" to ship — which +# creates the tag and triggers publish.yml. Nothing publishes automatically. +# +# The next version is inferred from the labels on the merged PRs: +# major → "breaking" minor → "feature"/"enhancement" +# patch → everything else (the default) +# +# PRs are auto-labelled from their branch/title prefix (see `autolabeler` below), +# so contributors don't have to label by hand and maintainers can just merge. + +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' + +categories: + - title: '✨ Features' + labels: ['feature', 'enhancement'] + - title: '🐛 Fixes' + labels: ['fix', 'bug'] + - title: '📝 Docs / CI / chore' + labels: ['docs', 'ci', 'chore', 'dependencies'] + +change-template: '- $TITLE (#$NUMBER) @$AUTHOR' +change-title-escapes: '\<*_&' + +version-resolver: + major: + labels: ['breaking', 'major'] + minor: + labels: ['feature', 'enhancement', 'minor'] + patch: + labels: ['fix', 'bug', 'docs', 'ci', 'chore', 'dependencies', 'patch'] + default: patch + +# Apply a label from the branch name or a conventional-commit-style title prefix. +autolabeler: + - label: 'breaking' + title: ['/!:/', '/^[a-z]+(\(.+\))?!:/i'] + - label: 'feature' + branch: ['/^feat(ure)?\//'] + title: ['/^feat(\(.+\))?:/i'] + - label: 'fix' + branch: ['/^(fix|bugfix|hotfix)\//'] + title: ['/^fix(\(.+\))?:/i'] + - label: 'docs' + branch: ['/^docs?\//'] + title: ['/^docs(\(.+\))?:/i'] + - label: 'ci' + branch: ['/^ci\//'] + title: ['/^ci(\(.+\))?:/i'] + - label: 'chore' + branch: ['/^chore\//'] + title: ['/^chore(\(.+\))?:/i'] + +template: | + ## What's changed + + $CHANGES + + **Contributors:** $CONTRIBUTORS + **Full changelog:** https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a3a5e94f..4b9840e8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,32 +1,30 @@ -# Publish on `v*` tag push. +# Publish when a GitHub Release is published. # -# What this does: -# 1. Checks out the tagged commit. -# 2. Verifies the tag matches package.json's version (catches "I pushed -# v2.0.1 but forgot to bump package.json" mistakes before they ship). -# 3. Compiles TypeScript, packages a .vsix. -# 4. Publishes to the VS Code Marketplace (vsce) using the VSCE_PAT secret. -# 5. Publishes to Open VSX Registry (ovsx) using the OVSX_PAT secret. -# 6. Attaches the .vsix to the GitHub Release for that tag, auto-generating -# release notes from PRs merged since the previous release. +# Merge-and-auto-release flow: +# 1. PRs merge to main; release-drafter keeps a DRAFT Release up to date with +# categorised notes + the next semver version (see release-drafter.yml). +# 2. When ready to ship, a maintainer reviews that draft and clicks +# "Publish release" — the single approval gesture. +# 3. Publishing fires this workflow, which: +# - checks out the released commit, +# - stamps package.json's version from the release tag (so nobody +# hand-bumps it — the tag is the source of truth), +# - compiles + packages a .vsix, +# - publishes to the VS Code Marketplace (VSCE_PAT) and Open VSX (OVSX_PAT), +# - attaches the .vsix to the Release. # -# Usage: -# git tag v2.0.1 && git push origin v2.0.1 -# ↳ this workflow then runs end to end. ~5 min wall-clock. -# -# Or trigger manually from Actions → Publish Extension → Run workflow -# (workflow_dispatch — useful if a publish fails and we want to retry). +# Trigger manually from Actions → Publish Extension → Run workflow +# (workflow_dispatch — useful to retry a failed Marketplace/Open VSX publish). name: Publish Extension on: - push: - tags: - - 'v*' + release: + types: [published] workflow_dispatch: permissions: - contents: write # needed by softprops/action-gh-release to create / attach assets + contents: write # needed by softprops/action-gh-release to attach the .vsix asset jobs: publish: @@ -43,18 +41,16 @@ jobs: - name: Install dependencies run: npm ci - # Belt-and-braces: refuse to publish if the tag doesn't match the - # version actually baked into package.json (which is what vsce reads). - - name: Verify tag matches package.json version - if: startsWith(github.ref, 'refs/tags/v') + # The release tag is the single source of truth for the version. Stamp it + # into package.json so vsce packages the right number — no human bump, no + # tag-vs-package.json mismatch possible. Build-time only (not committed). + - name: Set version from release tag + if: github.event_name == 'release' run: | - TAG=${GITHUB_REF#refs/tags/v} - VER=$(node -p "require('./package.json').version") - if [ "$TAG" != "$VER" ]; then - echo "::error::Tag v$TAG does not match package.json version $VER" - exit 1 - fi - echo "OK: tag v$TAG matches package.json $VER" + TAG="${{ github.event.release.tag_name }}" + VER="${TAG#v}" + npm version "$VER" --no-git-tag-version --allow-same-version + echo "package.json version set to $VER (from release $TAG)" - name: Compile run: npm run compile @@ -72,11 +68,14 @@ jobs: env: OVSX_PAT: ${{ secrets.OVSX_PAT }} - - name: Attach .vsix to GitHub Release - if: startsWith(github.ref, 'refs/tags/v') + # The Release already exists (the maintainer just published it) and already + # has release-drafter's notes — so we only upload the asset, never + # regenerate the body. + - name: Attach .vsix to the GitHub Release + if: github.event_name == 'release' uses: softprops/action-gh-release@v2 with: files: claude-code-usage.vsix - generate_release_notes: true + tag_name: ${{ github.event.release.tag_name }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..263baf81 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,36 @@ +# Drafts the next GitHub Release as PRs merge, and auto-labels PRs. +# +# On every push to main, release-drafter (re)builds a single DRAFT Release with +# categorised notes and the next semver version inferred from the merged PRs' +# labels (config in .github/release-drafter.yml). Nothing is published here — the +# maintainer reviews the draft and clicks "Publish release" when ready, which +# creates the tag and triggers publish.yml. +# +# On pull_request events it applies labels from the branch/title so contributors +# don't have to. NOTE: GitHub only grants the labelling token write access for +# PRs from branches in THIS repo; for PRs opened from forks the autolabeler is a +# no-op and a maintainer applies the label at merge time. (Switching to +# pull_request_target would cover forks but runs untrusted PR context with write +# scope, so we deliberately don't.) + +name: Release Drafter + +on: + push: + branches: [main] + pull_request: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write # create / update the draft release + pull-requests: write # apply autolabels + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CLAUDE.md b/CLAUDE.md index e3d9b33e..6d3a28af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,14 +53,17 @@ npx @vscode/vsce package # build a .vsix ``` - **F5** launches the Extension Development Host for manual testing. -- **Releases are automated.** Pushing a `v*` tag to the upstream repo triggers - `.github/workflows/publish.yml`, which compiles, packages, publishes to the - VS Code Marketplace + Open VSX, and attaches the `.vsix` to a GitHub Release. - The workflow verifies the tag matches `package.json`'s `version`. So to - release: bump `version`, update `CHANGELOG.md`, merge to `main`, then - `git tag vX.Y.Z && git push vX.Y.Z`. -- Versioning follows semver: bug-only → patch (`2.0.x`); new user-facing - feature → minor (`2.x.0`); breaking UX → major. +- **Releases are merge-and-auto-release.** As labelled PRs merge to `main`, + `.github/workflows/release-drafter.yml` keeps a **draft GitHub Release** up to + date (categorised notes + next semver version from the PR labels). To ship, a + maintainer reviews that draft and clicks **Publish** — which creates the tag + and triggers `.github/workflows/publish.yml`. Publish checks out the released + commit, stamps `package.json`'s version *from the release tag* (no hand-bump), + compiles, packages, publishes to the VS Code Marketplace + Open VSX, and + attaches the `.vsix`. So to release: just **Publish the draft Release** — no + manual `version` bump, no `git tag`. +- Versioning follows semver and is driven by PR labels: `fix`/none → patch + (`2.0.x`); `feature` → minor (`2.x.0`); `breaking` → major. ## Conventions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6cc2966..5686eba6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,8 +47,21 @@ welcome. ## Releases -Releases are automated: pushing a `v*` tag triggers the publish workflow -(Marketplace + Open VSX + GitHub Release). Maintainers handle tagging. +Releases are automated and contributor-friendly — you never touch versions or +tags: + +1. **Open a PR.** It's auto-labelled from your branch/title prefix (`fix/…`, + `feat/…`, `docs/…`); a maintainer can adjust the label. The label decides the + version bump (`feature` → minor, `breaking` → major, otherwise patch). +2. **A maintainer merges it.** [Release Drafter](https://github.com/release-drafter/release-drafter) + then adds your change to a continuously-updated **draft GitHub Release** and + recomputes the next version. +3. **To ship, a maintainer reviews that draft and clicks Publish.** That creates + the tag and triggers `publish.yml`, which packages and pushes to the VS Code + Marketplace + Open VSX and attaches the `.vsix`. + +Because changes ship by **merging** your PR — not by re-applying it — your commit +authorship and the PR's *merged* status are preserved. ## Code of conduct From 0d6c34f4159f8f9fa6155a683ca79ae2ca3895b8 Mon Sep 17 00:00:00 2001 From: Nick Earnshaw Date: Fri, 12 Jun 2026 23:43:17 +0100 Subject: [PATCH 2/2] ci: run release-drafter on push-to-main only Release Drafter reads its config from the default branch and the default GITHUB_TOKEN is read-only for fork PRs, so a pull_request autolabeler can't work for external contributions and fails on first introduction (config not yet on the default branch). Drop the pull_request trigger: drafting runs on push to main, maintainers set the version label at merge. Reword docs accordingly; the autolabeler block stays as a ready-to-enable option. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/PULL_REQUEST_TEMPLATE.md | 5 ++--- .github/release-drafter.yml | 6 ++++-- .github/workflows/release-drafter.yml | 20 +++++++++----------- CONTRIBUTING.md | 7 ++++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d7135175..5c00372f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,9 +14,8 @@ Keep the section headings; fill what applies, delete what doesn't. ## Type of change - + - [ ] Bug fix (non-breaking) → `fix` (patch) - [ ] New feature (non-breaking) → `feature` (minor) - [ ] Breaking change (existing behaviour differs afterwards) → `breaking` (major) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 87aed9df..2346206c 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -8,8 +8,10 @@ # major → "breaking" minor → "feature"/"enhancement" # patch → everything else (the default) # -# PRs are auto-labelled from their branch/title prefix (see `autolabeler` below), -# so contributors don't have to label by hand and maintainers can just merge. +# A version label is set by the maintainer when they merge (the PR template shows +# the mapping; unlabeled defaults to patch). The `autolabeler` block below is +# kept ready but only runs if a pull_request trigger is added to the workflow — +# see the note in .github/workflows/release-drafter.yml. name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 263baf81..d7e3dd74 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,4 +1,4 @@ -# Drafts the next GitHub Release as PRs merge, and auto-labels PRs. +# Drafts the next GitHub Release as changes land on main. # # On every push to main, release-drafter (re)builds a single DRAFT Release with # categorised notes and the next semver version inferred from the merged PRs' @@ -6,20 +6,19 @@ # maintainer reviews the draft and clicks "Publish release" when ready, which # creates the tag and triggers publish.yml. # -# On pull_request events it applies labels from the branch/title so contributors -# don't have to. NOTE: GitHub only grants the labelling token write access for -# PRs from branches in THIS repo; for PRs opened from forks the autolabeler is a -# no-op and a maintainer applies the label at merge time. (Switching to -# pull_request_target would cover forks but runs untrusted PR context with write -# scope, so we deliberately don't.) +# Why push-to-main only (no pull_request trigger): Release Drafter reads its +# config from the *default* branch, and GitHub forces the default GITHUB_TOKEN +# read-only for PRs opened from forks — so a pull_request autolabeler couldn't +# label external contributions anyway. Maintainers set the version label when +# they merge (the PR template shows the mapping; unlabeled defaults to patch). +# To enable branch/title autolabeling for same-repo PRs later, add a +# `pull_request_target` trigger plus `pull-requests: write`. name: Release Drafter on: push: branches: [main] - pull_request: - types: [opened, reopened, synchronize] permissions: contents: read @@ -27,8 +26,7 @@ permissions: jobs: update_release_draft: permissions: - contents: write # create / update the draft release - pull-requests: write # apply autolabels + contents: write # create / update the draft release runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v6 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5686eba6..ed6de714 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,9 +50,10 @@ welcome. Releases are automated and contributor-friendly — you never touch versions or tags: -1. **Open a PR.** It's auto-labelled from your branch/title prefix (`fix/…`, - `feat/…`, `docs/…`); a maintainer can adjust the label. The label decides the - version bump (`feature` → minor, `breaking` → major, otherwise patch). +1. **Open a PR** (a `fix/…`, `feat/…` or `docs/…` branch name keeps the intent + clear). A maintainer applies the matching label when merging — that label sets + the version bump (`feature` → minor, `breaking` → major, otherwise patch; + unlabeled → patch). 2. **A maintainer merges it.** [Release Drafter](https://github.com/release-drafter/release-drafter) then adds your change to a continuously-updated **draft GitHub Release** and recomputes the next version.