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 docs/caveats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions docs/central-vs-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
58 changes: 50 additions & 8 deletions docs/repository-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,56 @@ A repository admin does this when a repository adopts the process. Set
`REPO=Algorithmiq/<name>` and `APP_ID=<the 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" <commit>
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" <commit>
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=<the pinned tag>" > "$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:
Expand All @@ -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
Expand Down
46 changes: 46 additions & 0 deletions tests/test_cliff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"]
Loading