diff --git a/docs/semver-policy.md b/docs/semver-policy.md index 15a95913..6083c209 100644 --- a/docs/semver-policy.md +++ b/docs/semver-policy.md @@ -21,15 +21,20 @@ caught before it ships instead of written up afterward. ## Packages in scope -| Package | Kind | Tag prefix | Published by | -| --- | --- | --- | --- | -| `org.gamecult.cultlib` | Unity UPM | `cultlib-unity` | `scripts/build-unity-package.ps1`, consumed by git tag | -| `org.gamecult.caching.unity` | Unity UPM | `caching-unity` | tagged directly (no build script; see "Coverage gaps") | -| `org.gamecult.cultmath` | Unity UPM | `cultmath-unity` | `packages/cultmath/scripts/build-unity-package.ps1`, consumed by git tag | -| `@gamecult/cultcache-ts` | npm | `cultcache-ts` | `.github/workflows/publish-packages.yml` | -| `cultcache-py` | PyPI | `cultcache-py` | `.github/workflows/publish-packages.yml` | -| `cultnet-py` | PyPI | `cultnet-py` | `.github/workflows/publish-packages.yml` | -| `cultmesh-py` | PyPI | `cultmesh-py` | `.github/workflows/publish-packages.yml` | +| Package | Kind | Published by | +| --- | --- | --- | +| `org.gamecult.cultlib` | Unity UPM | `scripts/build-unity-package.ps1`, consumed by git tag | +| `org.gamecult.caching.unity` | Unity UPM | tagged directly (no build script; see "Coverage gaps") | +| `org.gamecult.cultmath` | Unity UPM | `packages/cultmath/scripts/build-unity-package.ps1`, consumed by git tag | +| `@gamecult/cultcache-ts` | npm | `.github/workflows/publish-packages.yml` | +| `cultcache-py` | PyPI | `.github/workflows/publish-packages.yml` | +| `cultnet-py` | PyPI | `.github/workflows/publish-packages.yml` | +| `cultmesh-py` | PyPI | `.github/workflows/publish-packages.yml` | + +Each package's tag prefix, changelog path and, for the packages that ship +assemblies, the directory of tracked DLLs are declared in +`scripts/release-packages.mjs`, the one copy. The check reads them by package +name; no release script or workflow passes one. Rust crates are consumed as pinned git revisions (see `publish-packages.yml`'s own header comment) and are out of scope for tagged-release semver policing; @@ -137,23 +142,25 @@ mislabelled one. ## Enforcement `scripts/check-changelog-semver.mjs` is the mechanical check. Given a -package name, its `CHANGELOG.md` path, the version being released, and a git -tag prefix, it: +package name and the version being released, it reads the package's +declaration from `scripts/release-packages.mjs` in `--cwd` (an unknown package +or a missing declaration refuses), and: -1. resolves the previous published version from the highest existing - `-v*` tag strictly older than the version being released. A tag - equal to the version does not exempt it and does not count as a previous - release: there is no "already published" skip, so a rebuild or a CI re-run - at a tagged version is measured against that version's predecessor, exactly - as at release, and gets the same verdict. A first release is never - inferred: with no older tag, or with tags that cannot be read (a `--cwd` - that is not a git repository), the check refuses unless the caller declares - `--first-release`. The declaration is believed only when no other - `-v*` tag exists (older or newer than the version) and - `--tag-prefix` names the directory that holds the changelog - (`packages//CHANGELOG.md`, the workflow's own spelling of a - package). A deleted predecessor tag leaves nothing in git to find, so under - the right prefix it cannot be told from a first release; +1. derives the release record from `git tag -l` in `--cwd`. A same-prefix tag + is one that starts `-v`; one that is not + `-vMAJOR.MINOR.PATCH` refuses, naming the tag, and is never + skipped. The previous release is the newest same-prefix tag strictly older + than the version being released. A tag equal to the version does not exempt + it and does not count as a previous release: there is no "already + published" skip, so a rebuild or a CI re-run at a tagged version is + measured against that version's predecessor, exactly as at release, and + gets the same verdict. With no older tag, a newer one refuses (the version + is older than every release), and a repository that holds no tag other than + the version's own refuses, because a first release cannot be told from a + missing record. Otherwise it is a first release, which no caller declares. + Tags that cannot be read (a `--cwd` that is not a git repository) refuse. A + deleted predecessor tag leaves nothing in git to find, so in a checkout that + holds other tags it cannot be told from a first release; 2. requires a `## []` changelog entry to exist at all; 3. classifies the version bump (major/minor/patch) against the previous version, refusing anything that is not exactly the next version in some @@ -177,28 +184,29 @@ above), and the check neither refuses nor demands a bump for one. ### Measured input -The Unity release scripts pass `--api-baseline-path ` and one `--api-built ` per assembly they built (plus +A package whose declaration names an `assemblies` directory is measured, and +the release must hand over one `--api-built ` per assembly it built (plus `--api-refs ` for directories that hold the built assemblies' -dependencies, and `--api-new ` for an assembly that is new in this -release). For each built DLL the checker reads its namesake under the baseline -path at the previous tag as a git blob (no worktree, no network, unaffected by -what the working tree tracks now) and compares public API with -`Microsoft.DotNet.ApiCompat.Tool`. Each `CPnnnn` diagnostic is one measured -break. Every assembly the previous tag tracked under the baseline path that -the build no longer produces is itself a measured break, whatever its name, so -a release script passes every assembly it ships, not only its own. +dependencies). A package with no such directory takes no `--api-built`, and one +that has it cannot omit it. The measured set is every managed assembly tracked +under that directory, at any depth, at the previous tag, read as git blobs (no +worktree, no network, unaffected by what the working tree tracks now) and keyed +by file name. A DLL is managed when its PE header carries a CLI header; a +native DLL has none and is not measured, and a tracked `.dll` that is not a PE +image refuses. Two managed assemblies with one name refuse. The checker +compares each built assembly with its namesake using +`Microsoft.DotNet.ApiCompat.Tool`; each `CPnnnn` diagnostic is one measured +break. Every managed assembly the previous tag tracked that the build no +longer hands over is itself a measured break, whatever its name or place, so a +release script passes every assembly it ships. A built assembly with no +namesake at the tag is an addition and is not examined. Measuring nothing is a refusal, never a pass. The check refuses, naming what -is missing, when the previous tag tracks no DLL under the baseline path (a -wrong or moved path, or a tag that predates it); when git cannot read the -tracked assemblies; when a built assembly has no namesake at the tag and was -not declared with `--api-new`; when an assembly declared new is already -tracked; when no built assembly has a namesake at the tag, so that no -comparison ran (renaming the only assembly and declaring it new is this case); -and when the tool fails to measure (not installed, an unreadable assembly, a -crash: a non-zero exit with no diagnostic line). A verified first release -measures nothing. +is missing, when the previous tag tracks no managed assembly under the +declared directory (a wrong or moved directory, or a tag that predates it); +when git cannot read the tracked assemblies; and when the tool fails to +measure (not installed, an unreadable assembly, a crash: a non-zero exit with +no diagnostic line). A first release measures nothing. The tool and the `NETStandard.Library.Ref` 2.1.0 reference assemblies are pinned in `scripts/api-gate/` (`.config/dotnet-tools.json` and @@ -229,17 +237,17 @@ The release scripts run the check once the assemblies under judgement exist, after the build and before anything is copied into the package. Run it directly: `node scripts/check-changelog-semver.mjs --package ---changelog --version --tag-prefix ` (CI's TS and Python -jobs and `verify-caching-unity-release.ps1` pass no measured input). Its own tests -live in `scripts/check-changelog-semver.test.mjs` (`node --test +--version [--cwd ] [--api-built ... [--api-refs ...]]`. +Any other option is a usage error (exit 2). Its own tests live in +`scripts/check-changelog-semver.test.mjs` (`node --test scripts/check-changelog-semver.test.mjs`). It is wired into: - `.github/workflows/publish-packages.yml`, right after each job's existing "tag matches manifest version" check, for `cultcache-ts`, `cultcache-py`, - `cultnet-py`, and `cultmesh-py`; a job passes `--first-release` when no - other tag of its prefix exists, and the check verifies that claim itself; + `cultnet-py`, and `cultmesh-py`, with `fetch-depth: 0` so the checkout holds + the tags; - `scripts/build-unity-package.ps1`, for `org.gamecult.cultlib`; - `packages/cultmath/scripts/build-unity-package.ps1`, for `org.gamecult.cultmath`; @@ -281,14 +289,13 @@ described above, not mechanically checked. Extending the checker to a Rust crate's `Cargo.toml`/`CHANGELOG.md` or to an automated CultNet interop compatibility gate is future work, not part of this change. -`@gamecult/cultcache-ts`, `cultcache-py`, `cultnet-py`, `cultmesh-py`, and -`org.gamecult.cultmath` currently have **no `CHANGELOG.md` file at all**. This -is a real, pre-existing gap, not something introduced by this policy: their -next tagged release will fail the new check for exactly that reason ("no -changelog entry for version X") until each package's `CHANGELOG.md` exists. -That failure is intended — a release with no changelog is not a smaller -problem than a mislabelled one — but it does mean the very next release of -any of these five packages needs a `CHANGELOG.md` created first. Backfilling -changelog content for their past releases is not part of this change: it -would mean inventing release-note prose for history this pass has no -first-hand knowledge of, which is worse than leaving the gap named. +`@gamecult/cultcache-ts`, `cultcache-py`, `cultnet-py`, and `cultmesh-py` +currently have **no `CHANGELOG.md` file at all**; the check refuses their next +tagged release ("changelog not found at ") until each +package's file exists. A release with no changelog is not a smaller problem +than a mislabelled one. The next release of each creates its file: +`packages/cultcache-ts/CHANGELOG.md` arrives with the document-variants work +(`hands/variants-c2a`), and whoever cuts the next release of `cultcache-py`, +`cultnet-py` or `cultmesh-py` creates that package's file. Backfilling +changelog content for past releases would mean inventing release-note prose +for history nobody here knows first-hand, so the gap stays named.