diff --git a/docs/caveats.md b/docs/caveats.md index f4d60da..b986a5f 100644 --- a/docs/caveats.md +++ b/docs/caveats.md @@ -20,6 +20,11 @@ `Migration notes` of its own copy must switch its rulesets to the new names right after merging the stub: `pull_request_target` runs the workflow file of `main`, so the PR adding the stub still reports the old names, and every later PR the new ones. +- **Workflows on `release:` events no longer run.** `publish-release` publishes the + GitHub release with `GITHUB_TOKEN`, and events caused by `GITHUB_TOKEN` start no + workflows. A workflow that ran on `release: published` (tests, a container image, + documentation) runs on `push: tags: ["v*"]` instead, or becomes a job of the publish + workflow that `publish_release` needs. - **`job.workflow_*` is GitHub.com-only** (not on GitHub Enterprise Server), and actionlint 1.7.12 doesn't know it yet: [`.github/actionlint.yaml`](https://github.com/Algorithmiq/release-tools/blob/main/.github/actionlint.yaml) ignores the two properties in `release.yml` and `backport.yml`. Consumers' stubs don't diff --git a/docs/central-vs-local.md b/docs/central-vs-local.md index ebf7450..a6f16a2 100644 --- a/docs/central-vs-local.md +++ b/docs/central-vs-local.md @@ -20,7 +20,8 @@ PR titles use the types `feat fix deprecate perf docs refactor test build ci cho in every repository. The first five appear in the changelog; the others are left out unless they are breaking (`!`). A [gitmoji](https://gitmoji.dev) after the type (`feat: ✨ add X`, or `:sparkles:`) is optional and stripped from the changelog. The -changelog starts at the newest final tag, so a repository tags `v0.0.0` where it adopts -the process. Each entry ends with an absolute link to its PR (`[#12](https://github.com/…/pull/12)`), -built from `GITHUB_REPOSITORY`: GitHub doesn't autolink `#12` in `CHANGELOG.md`, nor reliably -in release notes. The `just changelog` recipes take the repository from the `origin` remote. +changelog starts at the newest final tag; see [Per-repository setup](repository-setup.md), +step 2, for where it starts in a repository that adopts the process. Each entry ends with +an absolute link to its PR (`[#12](https://github.com/…/pull/12)`), built from +`GITHUB_REPOSITORY`: GitHub doesn't autolink `#12` in `CHANGELOG.md`, nor reliably in +release notes. The `just changelog` recipes take the repository from the `origin` remote. diff --git a/docs/repository-setup.md b/docs/repository-setup.md index 605be89..44cfd05 100644 --- a/docs/repository-setup.md +++ b/docs/repository-setup.md @@ -4,19 +4,56 @@ A repository admin does this when a repository adopts the process. Set `REPO=Algorithmiq/` and `APP_ID=` first. Steps 1 to 7 come before merging the PR that adds the stubs, 8 after it. +The repository's default branch must be `main`: the release workflow starts releases +from it and adds the changelog to it. Rename it first if it isn't (Settings → Branches), +which retargets the open PRs; everyone then updates their clones. + 1. **Install the app and share the credentials** with the repository: add it to the app's installation (Organisation settings → GitHub Apps → algorithmiq-release-bot → Configure), and to the `--repos` lists of the variable and the secret. -2. **Tag `v0.0.0`, the start of the changelog**, on the last commit that should not appear - in it, before the tag ruleset (step 5) exists. The first release is then `0.1.0` (or - `1.0.0` when started with **major**). +2. **The baseline: where versions count from and the changelog starts.** The release + workflow counts versions from the newest final tag `vX.Y.Z`, and the changelog starts + there. Settle it before the tag ruleset (step 5) exists. + + - **A repository that released by hand has final tags**: the newest one is the + baseline; tag nothing. If its newest `v*` tag is a pre-release without its final + (`v0.4.0rc1` but no `v0.4.0`), release that final the old way first. If the package + index has a final version newer than the newest final tag, tag that version's commit + `vX.Y.Z` first. + - **A repository with no final tags and nothing published** tags `v0.0.0` on the last + commit that should not appear in the changelog. The first release is then `0.1.0` + (or `1.0.0` when started with **major**). Pushing a tag runs the workflows of the + tagged commit: if one of them publishes on tag pushes, disable it while you push + (`gh workflow disable`, then `gh workflow enable`). + + ```bash + git tag -a v0.0.0 -m "Start of the changelog" + git push origin v0.0.0 + just changelog # what the first release will contain + ``` + + **The releases before the baseline** belong in `CHANGELOG.md` too. Generate their + sections in the PR that adds the stubs (step 4), with the `cliff.toml` of the + release-tools tag the stubs pin, from a worktree at the baseline tag so that later + commits stay out. The command replaces an existing `CHANGELOG.md`: keep or merge a + hand-written one first. ```bash - git tag -a v0.0.0 -m "Start of the changelog" - git push origin v0.0.0 - just changelog # what the first release will contain + tmp=$(mktemp -d) + git worktree add "$tmp/base" vX.Y.Z # the baseline tag + gh api -H "Accept: application/vnd.github.raw" \ + "repos/Algorithmiq/release-tools/contents/cliff.toml?ref=" > "$tmp/cliff.toml" + GITHUB_REPOSITORY=$REPO uvx --from git-cliff==2.14.2 git-cliff \ + --config "$tmp/cliff.toml" --repository "$tmp/base" --output CHANGELOG.md + git worktree remove "$tmp/base" ``` + Each final tag gets a `## X.Y.Z (date)` section; pre-release tags are skipped. Titles + that are not conventional commits are left out, and a capitalised type (`Fix: …`) + lands in a group of its own (`### Fix`): compare each section with that release's + GitHub notes (`gh release view vX.Y.Z`), move stray entries into their groups and add + what is missing. Release PRs add their sections above these. + 3. **Settings.** Squash-merge only, with the PR title and description as the commit message (otherwise the migration notes never reach the changelog), auto-merge, automatic deletion of head branches, immutable releases, and the `backport` label: @@ -31,8 +68,13 @@ merging the PR that adds the stubs, 8 after it. ``` 4. **Files.** The three stubs, the publish workflow, the `justfile` recipe, the Migration - section of the PR template, an empty `CHANGELOG.md` if there is none, and the - Dependabot group. Delete any local copies of `release.py` and `cliff.toml`. + section of the PR template, the `CHANGELOG.md` of step 2 (empty if the baseline is + `v0.0.0`), and the Dependabot group. Delete any local copies of `release.py` and + `cliff.toml`. Retire publishing by hand: the publish workflow no longer runs on + `release: published`, other workflows with a `release:` trigger move to + `push: tags: ["v*"]` (see [Caveats](caveats.md)), and `.github/release.yml`, the + configuration of GitHub's generated release notes, goes: the `release-notes` action + writes them. 5. **Tag ruleset**: only the release bot creates, moves or deletes `v*` tags. ```bash diff --git a/tests/test_cliff.py b/tests/test_cliff.py index fd4f9b8..46e3292 100644 --- a/tests/test_cliff.py +++ b/tests/test_cliff.py @@ -28,7 +28,11 @@ import pytest from conftest import ROOT +from release.changelog import CHANGELOG_SECTION + if TYPE_CHECKING: + from pathlib import Path + from conftest import Repo GIT_CLIFF_VERSION = "2.14.2" @@ -325,3 +329,45 @@ def tag(day: int, name: str) -> None: assert entries(patch) == ["- F (#4)"] assert entries(final) == ["- B (#3)", "- F (#4)"] + + +def test_history_before_adoption( + repo: Repo, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + # docs/repository-setup.md, step 2: the releases made before adoption, generated + # from a worktree at the baseline tag. + repo.commit("chore: start") + repo.commit("feat: first feature (#1)") + repo.git("tag", "-a", "v0.1.0", "-m", "Release 0.1.0") + repo.commit("Fix: capitalised type (#2)") + repo.commit("fix: conventional fix (#3)") + repo.commit("Merge an unconventional commit") + repo.git("tag", "-a", "v0.2.0rc1", "-m", "Release 0.2.0rc1") + repo.commit("feat: later feature (#4)") + repo.git("tag", "-a", "v0.2.0", "-m", "Release 0.2.0") + repo.commit("feat: after the baseline (#5)") + base = tmp_path / "base" + repo.git("worktree", "add", "--quiet", str(base), "v0.2.0") + monkeypatch.setenv("GITHUB_REPOSITORY", "Algorithmiq/example") + + cliff("--repository", str(base), "--output", "CHANGELOG.md") + + history = (repo.path / "CHANGELOG.md").read_text(encoding="utf-8") + assert history.startswith(HEADER) + headings = [line for line in history.splitlines() if line.startswith("## ")] + assert [h.split()[1] for h in headings] == ["0.2.0", "0.1.0"] + assert all(CHANGELOG_SECTION.match(h) for h in headings) + assert "after the baseline" not in history.lower() + assert "unconventional commit" not in history + # A capitalised type is not skipped: it lands in a group of its own, which the docs + # tell the admin to move. + assert "### Fix\n\n- Capitalised type" in history + assert "([#1](https://github.com/Algorithmiq/example/pull/1))" in history + + # The first release PR adds its section above them, under the one header. + monkeypatch.delenv("GITHUB_REPOSITORY") + cliff("--unreleased", "--tag", "v0.3.0", "--prepend", "CHANGELOG.md") + changelog = (repo.path / "CHANGELOG.md").read_text(encoding="utf-8") + assert changelog.count("# Changelog") == 1 + headings = [line for line in changelog.splitlines() if line.startswith("## ")] + assert [h.split()[1] for h in headings] == ["0.3.0", "0.2.0", "0.1.0"]