Skip to content

feat: CHANGELOG enforcement infrastructure#32

Merged
rocketman-code merged 4 commits intomainfrom
feat/changelog-enforcement
Apr 15, 2026
Merged

feat: CHANGELOG enforcement infrastructure#32
rocketman-code merged 4 commits intomainfrom
feat/changelog-enforcement

Conversation

@rocketman-code
Copy link
Copy Markdown
Owner

@rocketman-code rocketman-code commented Apr 15, 2026

Summary

Establishes a compile-time-enforced CHANGELOG architecture across four atomic commits:

  1. Workspace migration (atomic-rollback moves into crates/atomic-rollback/)
  2. Back-fill: changelog-core crate holds Fragment + VersionId + Status + Section + generator; all v0.1.1-v0.4.0 history ported (56 fragments); CHANGELOG.md now generated; build.rs enforces drift via rustc panic
  3. PR-time gate: structural paths-filter requires Fragment addition whenever crates/atomic-rollback/src/** is modified
  4. CONTRIBUTING.md + docs/standards/changelog-fragments.md documenting the contract

Design derived from one axiom (make invalid states unrepresentable) applied to each decision. Implementation spec at docs/plans/changelog-enforcement.md (gitignored).

Why

Without this infrastructure, CHANGELOG.md is a hand-written artifact whose correspondence to actual code changes depends on maintainer discipline. Failure modes:

  • User-facing change merged without CHANGELOG entry: silent until release
  • CHANGELOG.md inconsistent with reality: no mechanical gate
  • Release notes generated manually: prone to omission

After this PR:

  • crates/atomic-rollback/src/** modified without Fragment = CI fails on PR (structural gate)
  • Fragment variant added without classification = cargo build fails (rustc exhaustive match)
  • Source of truth changed without CHANGELOG.md regen = cargo build fails (build.rs drift check)
  • CHANGELOG.md is a mechanically generated artifact

Each failure mode is rustc- or CI-enforced. No trust, no self-reporting.

Architecture

Workspace layout:

crates/
  atomic-rollback/      (user binary; tests; unchanged)
  changelog-core/       (Fragment enum, VersionId enum, Section enum, Status enum, generate())
  changelog/            (bin + build.rs; depends on changelog-core)

changelog-core is isolated from atomic-rollback. A bug in changelog code cannot reach the user binary — they share zero compiled code.

Status has three variants:

  • Released { version, section, text } — shipped in a past release
  • Unreleased { section, text } — pending the next bump
  • InternalOnly { description } — acknowledged change with no user-perceivable effect; not emitted to CHANGELOG.md

Test plan

All verified locally and confirmed by CI:

  • Commit 1: cargo build --release clean; cargo test --release 23/23 atomic-rollback tests pass; binary reports atomic-rollback v0.4.0; atomic-rollback.spec / .copr/Makefile / .github/workflows/ci.yml unaffected
  • Commit 2: cargo build --release clean across all 3 crates; cargo test --release 23/23 pass; cargo run -p changelog > /tmp/out.md && diff CHANGELOG.md /tmp/out.md zero output
  • Drift check actually fires: edit CHANGELOG.md without regen causes cargo build to panic with the expected message
  • Type system catches missing status arm: adding a Fragment variant without a match arm produces rustc E0004 non-exhaustive
  • InternalOnly variant compiles AND is filtered from CHANGELOG.md output
  • atomic-rollback binary smoke test: --version and --help work
  • actionlint on .github/workflows/pr-fragment.yml: clean
  • CONTRIBUTING.md links to docs/standards/changelog-fragments.md; standard is internally consistent
  • CI tests (ubuntu-latest + macos-latest), x86_64 (Fedora VM), aarch64-cross, task-list-completed all pass on this PR
  • CI fragment-required passes on this PR (commit 2 modifies both crates/atomic-rollback/src/** and crates/changelog-core/src/lib.rs per paths-filter logic)

Known follow-up

Issue #31: actionlint reports two pre-existing SC2086 shellcheck info-level warnings in .github/workflows/ci.yml (unrelated to this PR's scope). Will be addressed in a dedicated small PR after this merges.

Post-merge motion (binding; same pattern as PRs #23, #29)

The workflow existing in the repo is advisory until added to required checks. Completion contract:

  1. After merge, run:
gh api -X POST repos/rocketman-code/atomic-rollback/branches/main/protection/required_status_checks/contexts \
  --input - <<'IN'
{
  "contexts": [
    "fragment-required"
  ]
}
IN
  1. Verify with:
gh api repos/rocketman-code/atomic-rollback/branches/main/protection --jq '.required_status_checks.contexts'

Expect six contexts: x86_64, aarch64-cross, tests (ubuntu-latest), tests (macos-latest), task-list-completed, fragment-required.

The merge is incomplete until the API call runs and verification passes.

Move atomic-rollback from root-crate to crates/atomic-rollback/
workspace member. Root Cargo.toml becomes a workspace manifest with
resolver = "3" and one member.

No functional change: binary builds to target/release/atomic-rollback
as before; atomic-rollback.spec and .copr/Makefile reference paths
that are unchanged by the move; CI workflow invokes cargo at
workspace root and produces the same artifact.

This commit prepares the tree for adding changelog-core and
changelog crates in the next commit.
Add changelog-core crate (Fragment + VersionId + Status + Section +
generator) and changelog crate (binary + build.rs drift check).
Back-fill all v0.1.1-v0.4.0 entries (56 fragments) so the existing
CHANGELOG.md is now produced by the generator.

CHANGELOG.md changes:
- Add `## [Unreleased]` section header (always emitted per design;
  empty until the next user-facing change ships)
- Reorder sections within v0.3.1 and v0.3.2 to KaC canonical order
  (Added/Changed/Deprecated/Removed/Fixed/Security)
- Drop "Initial release." prose under v0.1.1 (no equivalent field in
  the data model; not load-bearing)
- Add trailing newline

Architecture:
- Source of truth: Fragment enum variants + Status assignments
- crates/changelog-core/src/lib.rs holds all changelog content
- Adding/transitioning/removing fragments in lib.rs is the only way
  to change CHANGELOG.md
- crates/changelog/build.rs runs at compile time, regenerates the
  expected CHANGELOG.md from source, panics on mismatch with the
  committed file
- "CHANGELOG.md and source disagree" is now an unrepresentable state
  for any release build (cargo build fails)

Manual workflow when fragments change:
  cargo run -p changelog > CHANGELOG.md
  git add CHANGELOG.md crates/changelog-core/src/lib.rs

Status::InternalOnly variant exists for changes with no
user-perceivable effect (escape hatch for pure private refactors);
not emitted to CHANGELOG.md.
PR-time gate enforcing that any modification to crates/atomic-rollback/src/**
also modifies crates/changelog-core/src/lib.rs. No commit-subject parsing,
no self-reporting, no labels: the gate reads the diff directly and the
diff cannot lie about what files changed.

By the axiom (invalid states unrepresentable applied to git/PR domain):
"PR modifies user-facing source without fragment" is the invalid state.
Commit-type parsing allows "lied in subject" as a representable bypass;
structural proxy eliminates that bypass entirely.

Failure mode coverage:
- New feat or fix touching user-facing code: gate fires; fragment required
- Pure refactor in user-facing code: gate fires; Status::InternalOnly
  available as the explicit acknowledgment escape hatch
- Doc-only or workflow-only change: gate does not fire; no fragment
  required (those changes do not modify crates/atomic-rollback/src/**)

Error message includes copy-pasteable example for both Unreleased and
InternalOnly variants, plus regen instructions and pointer to
docs/standards/changelog-fragments.md (added in next commit).
CONTRIBUTING.md is the entry-point doc covering build, test, commits,
PRs, and a brief on the changelog-fragments contract. Links out to
the standards directory for depth.

docs/standards/changelog-fragments.md is the full contract referenced
by the pr-fragment workflow's error message. Documents:

- when a fragment is required (structural proxy, no self-reporting)
- how to add one (variant + match arm + regenerate CHANGELOG.md)
- when Status::InternalOnly is appropriate (rare; explicit list of
  what does and does not qualify)
- text conventions (matches existing CHANGELOG style)
- variant naming (change-description PascalCase, not PR-prefixed)
- how releases consume fragments at bump time

The layered structure (CONTRIBUTING entry + docs/standards/ depth)
gives the CI error message a stable URL anchor to link to and gives
future standards (testing, code style, releases, etc.) a natural home
without bloating CONTRIBUTING.md.
@rocketman-code rocketman-code merged commit 5612ab8 into main Apr 15, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant