diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 612bb200..5c00372f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,11 +14,12 @@ 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..2346206c --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,65 @@ +# 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) +# +# 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' + +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..d7e3dd74 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,34 @@ +# 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' +# 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. +# +# 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] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write # create / update the draft release + 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..ed6de714 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,8 +47,22 @@ 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** (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. +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