diff --git a/.changeset/add-staging-changeset-lint.md b/.changeset/add-staging-changeset-lint.md new file mode 100644 index 0000000..74511e3 --- /dev/null +++ b/.changeset/add-staging-changeset-lint.md @@ -0,0 +1,5 @@ +--- +"@deessejs/errors": minor +--- + +Add CI lint that requires a changeset on every PR to `staging`. Part of the release system plan (Phase 4). diff --git a/.changeset/docs-update-branching-model.md b/.changeset/docs-update-branching-model.md new file mode 100644 index 0000000..fc6a55c --- /dev/null +++ b/.changeset/docs-update-branching-model.md @@ -0,0 +1,5 @@ +--- +"@deessejs/errors": patch +--- + +Update `CLAUDE.md` and `CONTRIBUTING.md` to reflect the actual branching model: devs land on `staging`, release engineer cherry-picks to `main` with a `version bump` label, hotfixes branch from `main`. The previous `main <- staging <- dev` model was documented but not practiced. Part of the release system plan (Phase 5). diff --git a/.changeset/release-workflow-rewrite.md b/.changeset/release-workflow-rewrite.md new file mode 100644 index 0000000..afd016f --- /dev/null +++ b/.changeset/release-workflow-rewrite.md @@ -0,0 +1,5 @@ +--- +"@deessejs/errors": patch +--- + +Rewrite the release workflow to detect pending changesets explicitly and gate all publish steps on detection. Tag is now pushed at the version bump commit (not the merge commit), fixing the `@deessejs/errors@1.1.1` tag drift. Adds `dry_run` and `packages` inputs to `workflow_dispatch`. Part of the release system plan (Phase 3). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3c47d95 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: ci + +on: + pull_request: + branches: [staging] + +jobs: + changeset-check: + name: Changeset required + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch staging + run: git fetch origin staging + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Require changeset + run: | + changed=$(git diff --name-only origin/staging...HEAD) + if ! echo "$changed" | grep -q '^\.changeset/.*\.md$'; then + echo "::error::This PR must include a changeset file (.changeset/.md)." + exit 1 + fi + - name: Validate changeset format + run: pnpm changeset status --since=origin/staging diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2616616..f501ca0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,15 @@ name: Release on: workflow_dispatch: + inputs: + dry_run: + description: 'Skip publish and tag push' + type: boolean + default: false + packages: + description: 'Restrict to a subset of packages (comma-separated). Empty = all.' + type: string + default: '' pull_request: types: @@ -41,33 +50,57 @@ jobs: cache: 'pnpm' - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile + + - name: Detect pending changesets + id: detect + run: | + if git diff --name-only HEAD~1 HEAD | grep -q '^\.changeset/.*\.md$'; then + echo "has_changesets=true" >> "$GITHUB_OUTPUT" + else + echo "has_changesets=false" >> "$GITHUB_OUTPUT" + fi - name: Create versions from changesets + if: steps.detect.outputs.has_changesets == 'true' run: pnpm changeset version - name: Commit version changes and push + if: steps.detect.outputs.has_changesets == 'true' && (github.event_name == 'workflow_dispatch' || inputs.dry_run != true) run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add -A - git commit -m "chore: apply changeset version bumps" || true - git push origin HEAD && git push --tags + git diff --quiet || git commit -m "chore(release): version packages" + git push origin HEAD + git push --tags - name: Build + if: steps.detect.outputs.has_changesets == 'true' && (github.event_name == 'workflow_dispatch' || inputs.dry_run != true) run: pnpm build - name: Test + if: steps.detect.outputs.has_changesets == 'true' && (github.event_name == 'workflow_dispatch' || inputs.dry_run != true) run: pnpm test - name: Publish packages - run: pnpm changeset publish + if: steps.detect.outputs.has_changesets == 'true' && (github.event_name == 'workflow_dispatch' || inputs.dry_run != true) + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ -n "${{ inputs.packages }}" ]; then + pnpm changeset publish --packages=$(echo "${{ inputs.packages }}" | tr ',' ' ') + else + pnpm changeset publish + fi - name: Get latest tag id: tag - run: echo "version=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT + if: steps.detect.outputs.has_changesets == 'true' && (github.event_name == 'workflow_dispatch' || inputs.dry_run != true) + run: echo "version=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT" - name: Create GitHub Release + if: steps.detect.outputs.has_changesets == 'true' && (github.event_name == 'workflow_dispatch' || inputs.dry_run != true) uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.tag.outputs.version }} diff --git a/CLAUDE.md b/CLAUDE.md index 1432680..157f2e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,13 +19,19 @@ This is **`@deessejs/errors`**, a TypeScript library that reimagines error handl ## Branching Strategy -This project follows the branching model: `main` <- `staging` <- `dev` +This project uses a **staging-first** branching model. The convention is: -- **dev**: Latest work-in-progress changes. Developers work here. -- **staging**: Contains work that has been reviewed and is ready for release testing. -- **main**: Production-ready code. Contains the official release history. +- **`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. +- **`dev`** is **deprecated** and will be archived. It is not part of the current flow. -All developers push directly to `main`. The release engineer is responsible for managing the flow from `main` to `staging` and from `staging` to `main` (releases). +### Hotfix path + +For urgent fixes that must skip the staging queue: branch from `main` as `release/hotfix-`, open a PR directly to `main` with a Changeset and the `[hotfix]` label, and merge. The release workflow fires on merge as for any other merge to `main`. + +### 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). ## Web Search diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63edfcc..cdb83ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,11 +21,25 @@ Thank you for your interest in contributing to this project! ## Branching Strategy -This project follows `main` <- `staging` <- `dev` branching: +This project uses a **staging-first** branching model: -- `main`: Production-ready code (all developers push here) -- `staging`: Release candidate testing -- `dev`: Work-in-progress development +- **`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. +- **`dev`** is deprecated and archived. + +### Pull Requests + +- **Target `staging`** for any feature, fix, refactor, or chore that should ship in a future release. +- **Include a Changeset** in your PR: `pnpm changeset` and commit the generated `.changeset/*.md` file. The CI lint blocks PRs that don't include one. +- **Allowed exemptions** to the changeset requirement: `docs:` only changes, `chore:` only changes, CI/workflow changes under `.github/`, and PRs labeled `no-changeset-required` by a maintainer. + +### Hotfixes + +For urgent fixes that must skip the staging queue: branch from `main` as `release/hotfix-`, open a PR directly to `main` with a Changeset and the `[hotfix]` label, and merge. The release workflow runs on merge as for any other merge to `main`. + +### Release Engineer + +Releases are managed by a single release engineer. The release engineer is the only person who cherry-picks commits from `staging` to `main` and applies the `version bump` label. There is no rotation. If the release engineer is unavailable, the team waits; bypassing the workflow is not a recommended escape hatch. ## Commit Messages