Semver policy: the declaration file owns prefix, changelog and assemblies; first release and measured set are derived
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
23d7beac20
commit
38ead8c4b0
1 changed files with 68 additions and 61 deletions
|
|
@ -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
|
||||
`<prefix>-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
|
||||
`<prefix>-v*` tag exists (older or newer than the version) and
|
||||
`--tag-prefix` names the directory that holds the changelog
|
||||
(`packages/<prefix>/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 `<prefix>-v`; one that is not
|
||||
`<prefix>-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 `## [<version>]` 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 <repo-relative directory
|
||||
of tracked DLLs>` and one `--api-built <dll>` per assembly they built (plus
|
||||
A package whose declaration names an `assemblies` directory is measured, and
|
||||
the release must hand over one `--api-built <dll>` per assembly it built (plus
|
||||
`--api-refs <dir>` for directories that hold the built assemblies'
|
||||
dependencies, and `--api-new <dll name>` 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 <name>
|
||||
--changelog <path> --version <x.y.z> --tag-prefix <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 <x.y.z> [--cwd <dir>] [--api-built <dll>... [--api-refs <dir>...]]`.
|
||||
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 <declared path>") 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue