Skip to content
Merged
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
139 changes: 96 additions & 43 deletions MAINTAINING.md
Original file line number Diff line number Diff line change
@@ -1,82 +1,135 @@
# 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 <N> --merge # correct
gh pr merge <N> --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.

```
<type>(<scope>): <description>
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"
```

```bash
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)
```
Loading