Skip to content

Commit c97d017

Browse files
author
CometAPI
committed
fix: make release changelog ownership deterministic
1 parent c5eb2f9 commit c97d017

10 files changed

Lines changed: 414 additions & 91 deletions

AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ Post-stable invariants:
154154
documents must contain no exact CometAPI patch or recovery identity. The
155155
document/version checker must fail before merge or release when this
156156
boundary is violated.
157+
12. Keep `CHANGELOG.md` release-only: do not maintain an unmanaged `Unreleased`
158+
section. Record changes in Conventional Commits and let Release Please own
159+
the newest canonical dated release section after the changelog preamble.
160+
The version gate must reject any `Unreleased` level-two heading before merge
161+
or release.
162+
13. A validated release-evidence block binds one canonical publication workflow
163+
run to its machine-readable identity marker. Every ancillary Actions run URL
164+
in that block requires an exact workflow-reference marker. The document gate
165+
must reject non-canonical URLs and undeclared, unused, malformed, duplicate,
166+
or contradictory run identities regardless of prose or Markdown labeling.
157167

158168
## Repository independence
159169

ARCHITECTURE.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,13 @@ uses `node24`. The workflow semantic contract fixes that SHA and runtime
140140
disposition so GitHub does not need to force a deprecated Node 20 action onto a
141141
newer runtime.
142142

143-
The version gate accepts Release Please's native linked, dated changelog heading
144-
as well as the existing legacy dated form. It validates repository, previous
145-
tag, candidate tag, and calendar date rather than rewriting generated history.
143+
The changelog is release-only. Release Please owns the newest canonical dated
144+
section immediately after its preamble; contributors record pending changes in
145+
Conventional Commits and never maintain an `Unreleased` placeholder. The
146+
version gate rejects that structurally incompatible placeholder, accepts Release
147+
Please's native linked form and legacy dated history, and validates repository,
148+
previous tag, candidate tag, and calendar date without rewriting generated
149+
history.
146150

147151
The PyPI publisher remains directly in `publish.yml` and is pinned to its
148152
reviewed Node 24 maintenance release. Pinning its exact SHA prevents a syntactic
@@ -159,9 +163,12 @@ idempotent, while retrying immutable tag or GitHub Release creation could leave
159163
ambiguous external state and is forbidden.
160164

161165
Immutable run, tag, commit, registry, and digest records live only in the
162-
validated release-evidence blocks in `ROADMAP.md` and `RELEASING.md`.
163-
Architecture documents mechanisms and boundaries, not a second historical
164-
ledger.
166+
validated release-evidence blocks in `ROADMAP.md` and `RELEASING.md`. Each
167+
block binds the canonical publication run through its machine-readable identity
168+
and binds every ancillary workflow URL through an exact reference marker. The
169+
checker requires a canonical URL and rejects undeclared or unused run identities,
170+
so Markdown wording cannot disguise a contradictory workflow. Architecture
171+
documents mechanisms and boundaries, not a second historical ledger.
165172

166173
The scheduled/manual default-branch smoke is an operational canary only; it
167174
does not prove the release commit. `COMETAPI_KEY` is exposed only to the

CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ All notable changes to this project are documented in this file. The project
44
follows Semantic Versioning and uses Conventional Commits for release-PR
55
automation.
66

7-
## [Unreleased]
8-
97
## [0.1.3] - 2026-07-30
108

119
### Fixed

CONTRIBUTING.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ legacy client aliases to a 0.1 change.
6969

7070
Prefer the smallest complete change. Preserve precise type hints, update
7171
documentation and the compatibility matrix when public behavior changes, and
72-
add a changelog entry for user-visible changes.
72+
describe user-visible changes through Conventional Commits. Do not maintain an
73+
`Unreleased` changelog section; Release Please owns canonical dated release
74+
history.
7375

7476
## Commit and pull-request guidance
7577

RELEASING.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,20 @@ branch and missing pull-request permission. Treat that run as isolated
198198
transport-failure evidence; do not rerun it or reinterpret it as an
199199
authorization failure.
200200

201+
`CHANGELOG.md` is release-only: do not maintain an `Unreleased` placeholder.
202+
Conventional Commits carry pending changes, and Release Please owns the newest
203+
canonical dated section immediately after the preamble. The version gate rejects
204+
any unmanaged `Unreleased` level-two heading so the generated layout remains
205+
valid on every patch release.
206+
207+
Each validated release-evidence block contains the immutable release identity
208+
and workflow history bound by machine-readable markers. The canonical
209+
publication run is part of the identity; every ancillary implementation CI,
210+
Release Please, failed-publication, or recovery URL requires an exact workflow
211+
reference marker. The document gate rejects non-canonical URLs and undeclared,
212+
unused, malformed, duplicate, or contradictory run identities regardless of
213+
prose or Markdown labeling.
214+
201215
Release mode (`check_version.py --require-releasable-docs`) also fails closed
202216
until project authorship, the canonical GitHub repository URL, the copyright
203217
holder, security and support contacts, a publication-neutral README, and a
@@ -473,6 +487,9 @@ authorized.
473487

474488
<!-- cometapi-release-evidence:start version=0.1.0 date=2026-07-28 -->
475489
<!-- cometapi-release-identity tag=v0.1.0 commit=6f42981edcc6c252f8db997606671c3da84d1dd8 workflow-run=30359383715 wheel-sha256=8eae758688bb6c98274e48d8d81f882eeae760f69cfd2f5e125004881d60e90f sdist-sha256=e9308b44f6091200b5121e24d1a0e1b9ea3e6bcccc109d6de87554b1ab2a8bca -->
490+
<!-- cometapi-release-workflow-reference run=30358662050 -->
491+
<!-- cometapi-release-workflow-reference run=30358990834 -->
492+
476493

477494
- The immutable non-draft [GitHub release](https://github.com/cometapi-dev/cometapi-python/releases/tag/v0.1.0)
478495
and lightweight tag `v0.1.0` resolve to release commit
@@ -508,6 +525,12 @@ authorized.
508525

509526
<!-- cometapi-release-evidence:start version=0.1.1 date=2026-07-29 -->
510527
<!-- cometapi-release-identity tag=v0.1.1 commit=576e7503a0a8c1103faca5143e4b8d576f8e8b44 workflow-run=30429821548 wheel-sha256=27e7904542f82fbbcd60e0de23a4a62c042420b6d004d00286d1f37d2ec4c5e5 sdist-sha256=64c7cb87745032703b3374cc562ea00b979416c54908862dbcebd116b2dc44c8 -->
528+
<!-- cometapi-release-workflow-reference run=30419881169 -->
529+
<!-- cometapi-release-workflow-reference run=30420057230 -->
530+
<!-- cometapi-release-workflow-reference run=30423490399 -->
531+
<!-- cometapi-release-workflow-reference run=30424732041 -->
532+
<!-- cometapi-release-workflow-reference run=30429821579 -->
533+
511534

512535
- Configuration fix [PR #25](https://github.com/cometapi-dev/cometapi-python/pull/25)
513536
passed [pull-request CI run 30419881169](https://github.com/cometapi-dev/cometapi-python/actions/runs/30419881169)
@@ -557,6 +580,13 @@ authorized.
557580

558581
<!-- cometapi-release-evidence:start version=0.1.2 date=2026-07-30 -->
559582
<!-- cometapi-release-identity tag=v0.1.2 commit=710c56491d9ef5f47cccff3ce837ab7e799455b0 workflow-run=30515861246 wheel-sha256=3f12c26ae1ae7a1de5ac19d8ef27a784b2bf592143c716493f1b0f35ec19daca sdist-sha256=21c8edc0586610de1a9a8cd39b54ed23d2b1e20552100f69f53938cb7678da3d -->
583+
<!-- cometapi-release-workflow-reference run=30509063138 -->
584+
<!-- cometapi-release-workflow-reference run=30509764960 -->
585+
<!-- cometapi-release-workflow-reference run=30510887049 -->
586+
<!-- cometapi-release-workflow-reference run=30511071674 -->
587+
<!-- cometapi-release-workflow-reference run=30511373822 -->
588+
<!-- cometapi-release-workflow-reference run=30515861285 -->
589+
560590

561591
- Metadata and runtime [PR #29](https://github.com/cometapi-dev/cometapi-python/pull/29)
562592
made packaged long descriptions release-neutral, added artifact assertions,
@@ -612,6 +642,12 @@ authorized.
612642

613643
<!-- cometapi-release-evidence:start version=0.1.3 date=2026-07-30 -->
614644
<!-- cometapi-release-identity tag=v0.1.3 commit=45429f373bbd11314ec43ba81904fdbb78db2522 workflow-run=30550536000 wheel-sha256=9ac2f8062a8554943649bffd7ec859fc90491f76bbe2b0165327722201417d6f sdist-sha256=07ded54606d50f44b689dad38cf93a74e1175370efaa33be84a3c01240d48e66 -->
645+
<!-- cometapi-release-workflow-reference run=30547956809 -->
646+
<!-- cometapi-release-workflow-reference run=30548315785 -->
647+
<!-- cometapi-release-workflow-reference run=30548348489 -->
648+
<!-- cometapi-release-workflow-reference run=30548842807 -->
649+
<!-- cometapi-release-workflow-reference run=30550533622 -->
650+
615651

616652
- Mutable-release-claim [PR #34](https://github.com/cometapi-dev/cometapi-python/pull/34)
617653
removed the published patch number from persistent guidance and extended the

ROADMAP.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,8 @@ Accepted release evidence:
416416

417417
<!-- cometapi-release-evidence:start version=0.1.0a1 date=2026-07-27 -->
418418
<!-- cometapi-release-identity tag=v0.1.0-alpha.1+recovery.1 commit=31b68904141489ca04932edbf305ccf88af09372 workflow-run=30261746138 wheel-sha256=a6820347317943ca22f7632acbe354dd992f31a122a6172dfe45b57960e3a093 sdist-sha256=98d86829ef14771e8b7ec180d452c6638289f49c14a39b7207be5c47cb64cde7 -->
419+
<!-- cometapi-release-workflow-reference run=30261497883 -->
420+
419421

420422
- Metadata fix [PR #16](https://github.com/cometapi-dev/cometapi-python/pull/16)
421423
merged as `6344c2d0e2e975360b42c887275c1950b82918ee`; recovery contract
@@ -449,6 +451,12 @@ Accepted release evidence:
449451

450452
<!-- cometapi-release-evidence:start version=0.1.0 date=2026-07-28 -->
451453
<!-- cometapi-release-identity tag=v0.1.0 commit=6f42981edcc6c252f8db997606671c3da84d1dd8 workflow-run=30359383715 wheel-sha256=8eae758688bb6c98274e48d8d81f882eeae760f69cfd2f5e125004881d60e90f sdist-sha256=e9308b44f6091200b5121e24d1a0e1b9ea3e6bcccc109d6de87554b1ab2a8bca -->
454+
<!-- cometapi-release-workflow-reference run=30348177128 -->
455+
<!-- cometapi-release-workflow-reference run=30353657522 -->
456+
<!-- cometapi-release-workflow-reference run=30357111315 -->
457+
<!-- cometapi-release-workflow-reference run=30358662050 -->
458+
<!-- cometapi-release-workflow-reference run=30358990834 -->
459+
452460

453461
Canonical [GitHub release](https://github.com/cometapi-dev/cometapi-python/releases/tag/v0.1.0)
454462
and [PyPI release](https://pypi.org/project/cometapi/0.1.0/) identity.
@@ -529,6 +537,12 @@ the only remaining release-related repository variable.
529537

530538
<!-- cometapi-release-evidence:start version=0.1.1 date=2026-07-29 -->
531539
<!-- cometapi-release-identity tag=v0.1.1 commit=576e7503a0a8c1103faca5143e4b8d576f8e8b44 workflow-run=30429821548 wheel-sha256=27e7904542f82fbbcd60e0de23a4a62c042420b6d004d00286d1f37d2ec4c5e5 sdist-sha256=64c7cb87745032703b3374cc562ea00b979416c54908862dbcebd116b2dc44c8 -->
540+
<!-- cometapi-release-workflow-reference run=30419881169 -->
541+
<!-- cometapi-release-workflow-reference run=30420057230 -->
542+
<!-- cometapi-release-workflow-reference run=30423490399 -->
543+
<!-- cometapi-release-workflow-reference run=30424732041 -->
544+
<!-- cometapi-release-workflow-reference run=30429821579 -->
545+
532546

533547
Maintenance release `0.1.1` rejects explicitly blank API keys and base URLs,
534548
treats a blank environment key as missing, and uses the default CometAPI URL
@@ -591,6 +605,13 @@ variables are absent. No recovery tag or recovery workflow was used.
591605

592606
<!-- cometapi-release-evidence:start version=0.1.2 date=2026-07-30 -->
593607
<!-- cometapi-release-identity tag=v0.1.2 commit=710c56491d9ef5f47cccff3ce837ab7e799455b0 workflow-run=30515861246 wheel-sha256=3f12c26ae1ae7a1de5ac19d8ef27a784b2bf592143c716493f1b0f35ec19daca sdist-sha256=21c8edc0586610de1a9a8cd39b54ed23d2b1e20552100f69f53938cb7678da3d -->
608+
<!-- cometapi-release-workflow-reference run=30509764960 -->
609+
<!-- cometapi-release-workflow-reference run=30509063138 -->
610+
<!-- cometapi-release-workflow-reference run=30510887049 -->
611+
<!-- cometapi-release-workflow-reference run=30511071674 -->
612+
<!-- cometapi-release-workflow-reference run=30511373822 -->
613+
<!-- cometapi-release-workflow-reference run=30515861285 -->
614+
594615

595616
[Release Please run 30509764960](https://github.com/cometapi-dev/cometapi-python/actions/runs/30509764960)
596617
failed while maintaining the `0.1.2` release PR. The pinned v5 action had built
@@ -665,6 +686,13 @@ used for `0.1.2`.
665686

666687
<!-- cometapi-release-evidence:start version=0.1.3 date=2026-07-30 -->
667688
<!-- cometapi-release-identity tag=v0.1.3 commit=45429f373bbd11314ec43ba81904fdbb78db2522 workflow-run=30550536000 wheel-sha256=9ac2f8062a8554943649bffd7ec859fc90491f76bbe2b0165327722201417d6f sdist-sha256=07ded54606d50f44b689dad38cf93a74e1175370efaa33be84a3c01240d48e66 -->
689+
<!-- cometapi-release-workflow-reference run=30547956809 -->
690+
<!-- cometapi-release-workflow-reference run=30548315922 -->
691+
<!-- cometapi-release-workflow-reference run=30548315785 -->
692+
<!-- cometapi-release-workflow-reference run=30548348489 -->
693+
<!-- cometapi-release-workflow-reference run=30548842807 -->
694+
<!-- cometapi-release-workflow-reference run=30550533622 -->
695+
668696

669697
[Implementation PR #34](https://github.com/cometapi-dev/cometapi-python/pull/34)
670698
removed the mutable published-patch claim from persistent guidance and extended

scripts/_checks.py

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@
8181
r"wheel-sha256=(?P<wheel>[0-9a-f]{64}) "
8282
r"sdist-sha256=(?P<sdist>[0-9a-f]{64}) -->$"
8383
)
84+
RELEASE_EVIDENCE_WORKFLOW_REFERENCE = re.compile(
85+
r"^<!-- cometapi-release-workflow-reference "
86+
r"run=(?P<run>[1-9]\d*) -->$"
87+
)
88+
_ANY_RELEASE_EVIDENCE_WORKFLOW_REFERENCE = re.compile(
89+
r"(?m)^.*cometapi-release-workflow-reference.*$"
90+
)
8491
_ANY_RELEASE_EVIDENCE_IDENTITY = re.compile(r"(?m)^.*cometapi-release-identity.*$")
8592
_ANY_RELEASE_EVIDENCE_MARKER = re.compile(r"(?m)^.*cometapi-release-evidence:.*$")
8693
_EXACT_VERSION = (
@@ -131,7 +138,8 @@
131138
_RECOVERY_TAGS = {"0.1.0a1": "v0.1.0-alpha.1+recovery.1"}
132139
_FULL_COMMIT = re.compile(r"(?<![0-9a-f])[0-9a-f]{40}(?![0-9a-f])", re.IGNORECASE)
133140
_ACTIONS_RUN = re.compile(
134-
rf"{re.escape(CANONICAL_REPOSITORY)}/actions/runs/[1-9]\d*(?:/attempts/[1-9]\d*)?"
141+
rf"(?<![^\s(<]){re.escape(CANONICAL_REPOSITORY)}/actions/runs/"
142+
r"[1-9]\d*(?:/attempts/[1-9]\d*)?(?=$|[\s)>])"
135143
)
136144
_WHEEL_DIGEST = re.compile(
137145
r"\bwheel\s+sha256\b[^0-9a-f]{0,96}(?P<digest>[0-9a-f]{64})(?![0-9a-f])",
@@ -504,6 +512,39 @@ def _identity_violations(
504512
sdist_sha256=match.group("sdist"),
505513
)
506514
findings: list[tuple[int, str]] = []
515+
workflow_references: set[str] = set()
516+
workflow_reference_lines = [
517+
value for value in nonempty if "cometapi-release-workflow-reference" in value
518+
]
519+
for reference_line in _ANY_RELEASE_EVIDENCE_WORKFLOW_REFERENCE.findall(body):
520+
if reference_line.strip() not in workflow_reference_lines:
521+
findings.append(
522+
(line, f"release-evidence block for {version} has malformed workflow reference")
523+
)
524+
for reference_line in workflow_reference_lines:
525+
reference = RELEASE_EVIDENCE_WORKFLOW_REFERENCE.fullmatch(reference_line)
526+
if reference is None:
527+
findings.append(
528+
(line, f"release-evidence block for {version} has malformed workflow reference")
529+
)
530+
continue
531+
run = reference.group("run")
532+
if run in workflow_references:
533+
findings.append(
534+
(
535+
line,
536+
f"release-evidence block for {version} duplicates workflow reference run {run}",
537+
)
538+
)
539+
workflow_references.add(run)
540+
if identity.workflow_run in workflow_references:
541+
findings.append(
542+
(
543+
line,
544+
f"release-evidence block for {version} must not classify its canonical "
545+
"publication run as an ancillary workflow reference",
546+
)
547+
)
507548
expected_tag = _canonical_release_tag(version)
508549
if identity.tag != expected_tag:
509550
findings.append(
@@ -569,26 +610,32 @@ def _identity_violations(
569610
before_commit,
570611
):
571612
release_commit_values.add(commit.group(0).lower())
572-
release_run_values: set[str] = set()
573-
for run in _ACTIONS_RUN.finditer(prose):
574-
line_start = prose.rfind("\n", 0, run.start()) + 1
575-
prior_line_start = prose.rfind("\n", 0, max(0, line_start - 1)) + 1
576-
context = prose[prior_line_start : run.start()]
577-
label = re.search(r"(?i)\[([^\]]+)\]\([^\n]*$", context)
578-
label_text = label.group(1) if label is not None else context.splitlines()[-1]
579-
if re.search(
580-
r"(?i)\b(?:release|publish(?:ing)?|publication|registry)"
581-
r"(?:[ -]+(?:workflow|job|pipeline))?[ -]+run\b"
582-
r"|\b(?:release|publish(?:ing)?|publication|registry)"
583-
r"(?:[ -]+(?:job|pipeline))?[ -]+workflow\b"
584-
r"|\bworkflow[ -]+run\b"
585-
r"|\bgithub[ -]+actions[ -]+run\b",
586-
label_text,
587-
):
588-
release_run_values.add(run.group(0).split("/actions/runs/", 1)[1].split("/", 1)[0])
613+
# Prose may cite only the canonical publication run or an exact ancillary run
614+
# declared by a typed machine-readable reference.
615+
release_run_values = {
616+
run.group(0).split("/actions/runs/", 1)[1].split("/", 1)[0]
617+
for run in _ACTIONS_RUN.finditer(prose)
618+
}
619+
unreferenced_workflow_runs = set(workflow_references) - release_run_values
620+
if unreferenced_workflow_runs:
621+
findings.append(
622+
(
623+
line,
624+
f"release-evidence block for {version} declares ancillary workflow runs "
625+
"that have no exact canonical Actions URL in the block",
626+
)
627+
)
628+
undeclared_release_runs = release_run_values - {
629+
identity.workflow_run,
630+
*workflow_references,
631+
}
589632
labeled_values = (
590633
("release commit", release_commit_values, identity.commit),
591-
("release workflow run", release_run_values, identity.workflow_run),
634+
(
635+
"release workflow run",
636+
undeclared_release_runs | {identity.workflow_run},
637+
identity.workflow_run,
638+
),
592639
(
593640
"wheel SHA256",
594641
{

0 commit comments

Comments
 (0)