Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/remove-version-bump-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@deessejs/errors": patch
---

Remove the `version bump` label gate from the release workflow. Every PR merged to `main` now produces a release if it contains `.changeset/*.md` files in its diff. The `has_changesets` detection step is the only condition. Simplifies the release engineer's job — no more remembering to label.
5 changes: 2 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
# Only run when PR is merged into main and has version bump label OR manually triggered
# Run when a PR is merged into main OR manually triggered
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
contains(github.event.pull_request.labels.*.name, 'version bump'))
github.event.pull_request.base.ref == 'main')

steps:
- name: Checkout
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ This is **`@deessejs/errors`**, a TypeScript library that reimagines error handl
This project uses a **staging-first** branching model. The convention is:

- **`staging`** is the integration branch. Developers open their feature/fix/chore PRs targeting `staging`. Every PR to `staging` must include a `.changeset/*.md` file (enforced by the CI lint in `.github/workflows/ci.yml`).
- **`main`** is the release branch. The release engineer cherry-picks curated batches of commits from `staging` into a `release/*` branch, opens a release PR targeting `main`, and applies the `version bump` label. Merging a `version bump` PR triggers the release workflow: `pnpm changeset version`, then `pnpm changeset publish`, then push the `@deessejs/errors@X.Y.Z` tag.
- **`main`** is the release branch. The release engineer cherry-picks curated batches of commits from `staging` into a `release/*` branch, opens a release PR targeting `main`, and merges. Merging a release PR to `main` triggers the release workflow: `pnpm changeset version`, then `pnpm changeset publish`, then push the `@deessejs/errors@X.Y.Z` tag. No label is required — every merge to `main` with at least one `.changeset/*.md` in the diff produces a release.
- **`dev`** is **deprecated** and will be archived. It is not part of the current flow.

### Hotfix path
Expand All @@ -31,7 +31,7 @@ For urgent fixes that must skip the staging queue: branch from `main` as `releas

### Release cadence

Each merge of a `version bump` PR to `main` publishes one release per package that has pending changesets. Multiple changesets in a single merge become one version bump per affected package (Changesets default behavior).
Each merge to `main` that contains at least one `.changeset/*.md` publishes one release per package with pending changesets. Multiple changesets in a single merge become one version bump per affected package (Changesets default behavior). A merge without changesets is a no-op.

## Web Search

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Thank you for your interest in contributing to this project!
This project uses a **staging-first** branching model:

- **`staging`** is the integration branch. Developers open their feature/fix/chore PRs targeting `staging`. Every PR to `staging` must include a `.changeset/*.md` file (enforced by the CI lint).
- **`main`** is the release branch. The release engineer cherry-picks curated batches from `staging` into a `release/*` branch, opens a release PR targeting `main`, and applies the `version bump` label. Merging a `version bump` PR triggers the release workflow.
- **`main`** is the release branch. The release engineer cherry-picks curated batches from `staging` into a `release/*` branch, opens a release PR targeting `main`, and merges. Merging a release PR to `main` triggers the release workflow. No label is required — every merge to `main` with at least one `.changeset/*.md` in the diff produces a release.
- **`dev`** is deprecated and archived.

### Pull Requests
Expand Down
49 changes: 24 additions & 25 deletions docs/internal/engineering/plans/release-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ Keep Changesets. The configuration changes, not the tool.

The release workflow is the _only_ place that touches tags and `npm publish`. It runs on two triggers:

1. A PR merged into `main` that carries the `version bump` label — this is the normal release path. The release engineer cherry-picks a batch of commits from `staging`, opens a PR to `main`, applies the `version bump` label, and merges. The workflow then versions and publishes the changesets included in that PR.
2. `workflow_dispatch` — for hotfixes, dry runs, and selective re-publishes.
1. **Any PR merged into `main`** — this is the normal release path. The release engineer cherry-picks a batch of commits from `staging`, opens a PR to `main`, and merges. The workflow then versions and publishes the changesets included in that PR. There is no label gate; the changeset detection step is the only condition.
2. `workflow_dispatch` — for dry runs, selective re-publishes, and emergency hotfixes that need to bypass the staging queue.

The `version bump` label is the release engineer's explicit gate. Without it, a merge to `main` is silent. This is the current behaviour and it is **kept deliberately** — it keeps the human in the loop and prevents accidental releases.
Any merge to `main` with at least one `.changeset/*.md` in the diff triggers a release. A merge without changesets is a no-op (the workflow's `has_changesets` step short-circuits the publish). This keeps the human in the loop: the release engineer still cherry-picks intentionally, but they no longer need to remember a label.

```yaml
# .github/workflows/release.yml (new version)
Expand Down Expand Up @@ -126,8 +126,7 @@ jobs:
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
contains(github.event.pull_request.labels.*.name, 'version bump'))
github.event.pull_request.base.ref == 'main')

steps:
- name: Checkout
Expand Down Expand Up @@ -208,8 +207,8 @@ jobs:

Key changes from the current workflow:

- **Trigger kept hybrid** — `workflow_dispatch` + PR `closed` on `main` with label `version bump`. This is the current behaviour; it is preserved because it gives the release engineer explicit control.
- **Explicit changeset detection** — the job walks the diff of the merge commit and toggles `has_changesets` accordingly. All subsequent steps are gated on this. If a `version bump` PR somehow contains no changesets (or the changesets were dropped during cherry-pick), the workflow becomes a no-op and does not publish.
- **Trigger** — `workflow_dispatch` + PR `closed` on `main`. No label required. Any merge to `main` with changesets publishes a release.
- **Explicit changeset detection** — the job walks the diff of the merge commit and toggles `has_changesets` accordingly. All subsequent steps are gated on this. If a PR to `main` somehow contains no changesets (or the changesets were dropped during cherry-pick), the workflow becomes a no-op and does not publish.
- **Tag points at the version bump commit** — the `chore(release): version packages` commit is what pushes the tag, not the cherry-pick merge commit. This fixes the `@deessejs/errors@1.1.1` tag drift.
- **`pnpm build` and `pnpm test` after versioning** — kept from the current workflow. They run against the bumped sources, not the pre-bump ones.
- **`body_path` keeps `packages/errors/CHANGELOG.md`** — using the freshly generated changelog rather than GitHub auto-notes, so the GitHub Release body matches what was actually published to npm.
Expand Down Expand Up @@ -253,7 +252,7 @@ jobs:
- CI / workflow changes (paths under `.github/`)
- PRs labeled `no-changeset-required` by a maintainer

**Note on `main`**: the lint does not run on PRs to `main`. Those are cherry-pick PRs from the release engineer; they are either label-gated (`version bump`) or hotfixes, both of which the release engineer handles explicitly. Re-linting them would only cause false positives for rebase commits.
**Note on `main`**: the lint does not run on PRs to `main`. Those are cherry-pick PRs from the release engineer or hotfixes. Re-linting them would only cause false positives for rebase commits. The changeset detection in the release workflow (`has_changesets` step) is the safety net for accidental empty merges.

### 5. Documentation updates

Expand All @@ -278,7 +277,7 @@ Release loop (release engineer, in batches):
2. Cherry-pick them into a release/* branch off main
3. Open a PR release/* → main
4. PR body lists the changesets being released (auto-generated from the cherry-picked files)
5. Apply the `version bump` label to the PR
5. Approve and merge the PR (no label required; the changeset diff is the trigger)
6. Approve and merge the PR
7. The release workflow fires on the merge event:
- Detects pending changesets in the merge commit diff
Expand Down Expand Up @@ -390,7 +389,7 @@ All four open questions are resolved:
Confirmed at the time of the inventory (commit `569c96d` on `main`):

- [x] `.changeset/config.json` committed on `main`. `baseBranch: "main"`, `access: "public"`, `commit: false`. OK.
- [x] `.github/workflows/release.yml` committed on `main`. Hybrid trigger (`workflow_dispatch` + PR `closed` with label `version bump`). To be rewritten per Section 3.
- [x] `.github/workflows/release.yml` committed on `main`. Hybrid trigger (`workflow_dispatch` + PR `closed` on `main` with label `version bump`). To be rewritten per Section 3 (label removed in a follow-up).
- [ ] `.github/workflows/ci.yml` does not exist. To be created per Section 4.
- [x] `packages/errors/package.json` has `version: "1.1.1"`. No `publishConfig` block — `access: "public"` is propagated by Changesets instead. OK.
- [x] `packages/errors/CHANGELOG.md` exists and is generated by Changesets (`@changesets/cli/changelog`). Format.
Expand All @@ -404,18 +403,18 @@ Confirmed at the time of the inventory (commit `569c96d` on `main`):

## Appendix B — Decision log

| Date | Decision | Rationale |
| ---- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| TBD | Keep Changesets; do not migrate to `release-please` | Tool is fine; the issues are configuration and branch drift |
| TBD | `staging` is the integration branch; `main` is release-only | Matches the actual release engineer's workflow |
| TBD | `feature/*` → `staging` via PR; `staging` → `main` via cherry-pick PR | Specified by the release engineer; this plan adopts it |
| TBD | Release workflow keeps the `version bump` label gate on PRs to `main` | Current behaviour; the release engineer is the explicit gate. Without the label, a merge to `main` is silent. |
| TBD | Release workflow also accepts `workflow_dispatch` for hotfixes, dry runs, selective re-publishes | Operational escape hatch |
| TBD | Each `version bump` PR → one release per package with pending changesets | Specified by the release engineer; multiple changesets become one bump per package |
| TBD | Hotfixes branch from `main` as `release/hotfix-<slug>`, PR directly to `main` with `[hotfix]` label | Bypass the staging queue for urgent fixes |
| TBD | The release workflow detects changesets in the merge commit diff before publishing | A `version bump` PR with no changesets is a no-op |
| TBD | The tag points at the version bump commit, not at the cherry-pick merge commit | Fixes the `@deessejs/errors@1.1.1` tag drift |
| TBD | Add a CI lint `ci.yml` that requires `.changeset/*.md` on every PR to `staging` (unless exempted) | Force the contract; reduce release-day surprises |
| TBD | The CI lint does NOT run on PRs to `main` (those are cherry-picks or hotfixes) | Avoid false positives on rebase commits |
| TBD | Archive `dev` | `dev` is unused in the current flow |
| TBD | Update `CLAUDE.md` and `CONTRIBUTING.md` to describe the actual branching model and the `version bump` label | End the documentation drift |
| Date | Decision | Rationale |
| ---- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| TBD | Keep Changesets; do not migrate to `release-please` | Tool is fine; the issues are configuration and branch drift |
| TBD | `staging` is the integration branch; `main` is release-only | Matches the actual release engineer's workflow |
| TBD | `feature/*` → `staging` via PR; `staging` → `main` via cherry-pick PR | Specified by the release engineer; this plan adopts it |
| TBD | Release workflow fires on every PR merged to `main` (no label required) | Simplified: the changeset detection step is the only condition. Removes the cognitive load of remembering to label. |
| TBD | Release workflow also accepts `workflow_dispatch` for hotfixes, dry runs, selective re-publishes | Operational escape hatch |
| TBD | Each merge to `main` with changesets → one release per package | Specified by the release engineer; multiple changesets become one bump per package |
| TBD | Hotfixes branch from `main` as `release/hotfix-<slug>`, PR directly to `main` with `[hotfix]` label | Bypass the staging queue for urgent fixes |
| TBD | The release workflow detects changesets in the merge commit diff before publishing | A merge without changesets is a no-op (no empty releases) |
| TBD | The tag points at the version bump commit, not at the cherry-pick merge commit | Fixes the `@deessejs/errors@1.1.1` tag drift |
| TBD | Add a CI lint `ci.yml` that requires `.changeset/*.md` on every PR to `staging` (unless exempted) | Force the contract; reduce release-day surprises |
| TBD | The CI lint does NOT run on PRs to `main` (those are cherry-picks or hotfixes) | Avoid false positives on rebase commits |
| TBD | Archive `dev` | `dev` is unused in the current flow |
| TBD | Update `CLAUDE.md` and `CONTRIBUTING.md` to describe the actual branching model | End the documentation drift |
Loading