diff --git a/MAINTAINING.md b/MAINTAINING.md index e6667bc..1e1ff6c 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -1,66 +1,123 @@ # Maintaining Guide -This document explains how to maintain and release this library. +How this library is branched, released, and versioned. For contributing changes, see +[CONTRIBUTING.md](./CONTRIBUTING.md). ## Branch Strategy -### `main` - Active Development -- **Module Path:** `github.com/jasoet/pkg/v3` -- **Purpose:** Active development for v2.x releases -- **Go Version:** 1.26+ +| Branch | Module path | Status | Releases | +|---|---|---|---| +| `main` | `github.com/jasoet/pkg/v3` | Released v3 line | `v3.x.y` | +| `next` | `github.com/jasoet/pkg/v3` | v3 development | `v3.x.y-next.N` prereleases | +| `release/v2` | `github.com/jasoet/pkg/v2` | Frozen at v2.13.1 | `2.13.x` emergency patches only | +| `release/v1` | `github.com/jasoet/pkg` | Closed at v1.6.0 | none | + +Day-to-day work targets **`next`**, not `main`. Feature and fix branches are cut from +`next` and squash-merged back into it; each merge publishes a `v3.x.y-next.N` prerelease. +`main` only moves when a prerelease line is promoted (see below). + +Go's module-path versioning is what makes this work: `/v2` and `/v3` are different modules, +so consumers can import both while migrating. See +[ADR 0001](./docs/adr/0001-freeze-v2-and-ship-v3-as-one-big-bang.md). ## Releasing -Releases are fully automated via [semantic-release](https://github.com/semantic-release/semantic-release) on every push to `main`. +Releases are automated by [semantic-release](https://github.com/semantic-release/semantic-release) +on every push to `main`, `next`, and `release/v2`. ### What triggers a release -| Commit Type | Release | Example | +| Commit type | Bump | Example | |---|---|---| -| `feat` | Minor (v2.x.0) | `feat(server): add gRPC interceptor` | -| `fix` | Patch (v2.0.x) | `fix(compress): handle empty input` | -| `perf` | Patch | `perf(db): reduce query allocations` | -| `refactor` | Patch | `refactor(otel): simplify provider setup` | -| Breaking change | Major (vX.0.0) | `feat!: remove deprecated API` or footer `BREAKING CHANGE:` | +| `feat` | minor | `feat(server): add gRPC interceptor` | +| `fix` | patch | `fix(compress): handle empty input` | +| `perf` | patch | `perf(db): reduce query allocations` | +| `refactor` | patch | `refactor(otel): simplify provider setup` | +| Breaking | major | `feat(api)!: remove deprecated method`, or a `BREAKING CHANGE:` footer | -### What does NOT trigger a release +`docs`, `test`, `ci`, `chore`, `style` and `build` never trigger a release. -`docs`, `test`, `ci`, `chore`, `style`, `build` commits are excluded. +### Normal changes: squash merge into `next` -### Workflow +The PR title becomes the commit message, so it must be a valid Conventional Commit. Write +the PR description carefully — it becomes the release notes. -1. Merge PR to `main` with conventional commit title -2. CI runs tests -3. semantic-release analyzes commits since last tag -4. If a release is warranted, it creates a GitHub release with notes -5. Go module proxy is warmed automatically +**A breaking change needs both** a `BREAKING CHANGE:` footer *and* an entry in +[MIGRATION.md](./MIGRATION.md). Do not rely on the footer alone: several v3 breaks shipped +in `fix:`-typed commits without footers and never reached the generated notes. The +migration guide is the backstop. -## CI Pipelines +### Promoting a line: merge commit into `main`, never squash -- **`ci.yml`** - Runs on PRs: test (with race detector) + lint -- **`release.yml`** - Runs on push to `main`: test + semantic-release +**When merging `next` into `main`, use a merge commit.** -## Conventional Commits +```bash +gh pr merge --merge # correct +gh pr merge --squash # WRONG — silently produces the wrong version +``` -All PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/): +Squashing collapses the whole line into a single commit and **destroys every +`BREAKING CHANGE` footer in it**. semantic-release then analyses one commit against the last +tag on `main` and computes a bump from that alone. -``` -(): +This is measured, not theoretical. Simulating both merges of the v3 line locally and running +`semantic-release --dry-run`: -[optional body] +| Merge strategy | Surviving `BREAKING CHANGE` footers | Computed version | +|---|---|---| +| `--merge` | 22 | **3.0.0** | +| `--squash` | **0** | **2.14.0** | + +A `v2.14.0` tag on a module whose path is `/v3` is not installable — and the mistake is only +visible after the tag is published. + +You can re-run that check before any promotion, without pushing anything: -[optional footer(s)] +```bash +git checkout main && git reset --hard origin/main +git merge --no-ff --no-edit origin/next +GITHUB_TOKEN=$(gh auth token) bunx semantic-release --dry-run --no-ci +git reset --hard origin/main # discard the simulation ``` -### Best Practices for PR Authors -- Write detailed PR descriptions (they become release notes when squash-merged) -- Use conventional commit format in PR title -- Include scope when the change targets a specific package +A merge commit also produces complete release notes, since every `feat`/`fix` on the line +stays individually attributed. + +### Pre-promotion checklist + +1. `task ci:check` and `go vet -tags='example integration argo' ./...` clean on `next`. +2. Integration suite green: `task test:integration`. +3. `MIGRATION.md` covers every break on the line, including any that shipped without a + footer. +4. Package coverage figures in `README.md` regenerated. +5. Open the PR `next` → `main` and confirm the **API compatibility check reports + informationally, not blocking** — `ci.yml` keys that off `head_ref == 'next'`. + gorelease reporting `Inferred base version: none` is expected until the first + non-prerelease tag exists on the new major. +6. Merge with `--merge`, then confirm the published tag is what you expected before + announcing anything. + +## CI Pipelines + +| Workflow | Triggers | Does | +|---|---|---| +| `ci.yml` | push to `main`/`next`/`release/v2` and tags; PRs targeting them | lint, race tests, `go vet` over build-tagged code, gorelease API check | +| `release.yml` | push to `main`/`next`/`release/v2`; manual dispatch | race tests, integration tests, semantic-release, Go proxy warmup | + +Both run on the self-hosted `[self-hosted, local, macOS, ARM64]` runner. + +The gorelease API check is **blocking** everywhere except where `next` is involved +(`ref_name`, `base_ref`, or `head_ref` equal to `next`), because breaking changes are the +point of the v3 line. It is pinned to the `golang.org/x/exp` pseudo-version in `go.mod`; +bump both together. + +`ci.yml` runs `go vet` with `-tags='example integration argo'` as a separate step. `task +check` compiles only untagged code, and example- or integration-only breakage has slipped +through that gap before. ## Import Paths ```go -import "github.com/jasoet/pkg/v3/compress" import "github.com/jasoet/pkg/v3/server" ``` @@ -68,15 +125,11 @@ import "github.com/jasoet/pkg/v3/server" go get github.com/jasoet/pkg/v3@latest ``` -## Testing - -Before any release: - -1. **Unit Tests:** `task test` -2. **Integration Tests:** `task test:integration` -3. **Linting:** `task lint` +## Testing Before a Release -Or run everything: ```bash -task test:complete # Runs all tests with coverage +task test # unit +task test:integration # integration (Docker or Podman required) +task lint +task test:complete # everything, including argo (needs a k8s cluster) ```