From 25d6daab1e9dbbcf1653c5a8b17447d5399485f5 Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Sun, 20 Sep 2026 23:44:12 -0400 Subject: [PATCH 1/4] chore(release): v0.11.0 Stamps the Unreleased section as v0.11.0, keeps an empty Unreleased above it, and updates VERSION and the README version lines. Adds a release intro in operator terms and a Known limits section covering the three things an operator should know that no Fixed entry says on its own: records written by earlier versions on the two corrected paths are not repaired, rollback_results is serialized but not signed in v1, and both engine corrections were verified offline only. Corrects VERSIONING_PLAN.md in two places. The release-process section said releases are manual and source-only with automation planned for v1.0; every release since v0.2.0 has been tag-triggered GoReleaser publishing signed rpm, deb, tarballs, an SBOM and a cosign-signed checksums file. The section now describes that, including that the tag push is publication and that the release page body must be set from the CHANGELOG afterwards because changelog generation is disabled. Adds the pre-1.0 exception the founder approved on 2026-09-20 for an api/ change that moves no signature but changes what an existing value means to a consumer: MINOR during 0.x, with the change stated in consumer terms and the consumer's written confirmation. Stated as a granted exception with its first use, not as a reading of the existing rule. 09-reference's stamp read "v0.10.0 plus Unreleased changes"; those changes are this release, so it now reads v0.11.0. The other guide chapters keep their v0.9.0 stamps: they were not re-verified for v0.10.0 and are not re-verified here. --- CHANGELOG.md | 31 ++++++++++++++- README.md | 4 +- VERSION | 2 +- VERSIONING_PLAN.md | 78 ++++++++++++++++++++++++++------------ docs/guide/09-reference.md | 2 +- 5 files changed, 87 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cec63698..6037696b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,25 @@ The CLI is governed by GNU/POSIX conventions. Long-form flags are the canonical names; short forms are listed in `cmd/kensa/flags.go`. Compare any two releases at - (swap the tags for + (swap the tags for any pair). ## Unreleased +## v0.11.0 (2026-09-20) + +Two engine corrections lead this release, both to what Kensa reports rather +than to what it does on a host. Evidence records are signed over their final +contents, so `kensa verify` accepts records it used to reject. And a +remediation step that fails partway now reports the host as unconfirmed +instead of restored, which OpenWatch shows as "Partially applied" with an +inspection prompt where it used to show "Reverted, host unchanged". Expect +more of those prompts: a handler that failed without changing anything reports +the same way, because the engine has no evidence either way. The rest is two +CLI additions, a CLI change announced since v0.1, and a supply-chain +hardening pass over CI. The frozen `api/` package gains no field and loses +none; OpenWatch confirmed no consumer change is needed. + ### Changed - **CI builds specs with Specter v0.15.0**, up from v0.13.2, and verifies the @@ -179,6 +193,21 @@ any pair). fetches read a public repository anonymously, everything else is a local read, and the release upload authenticates with its own step-scoped token. +### Known limits + +- Evidence records written by earlier versions on the two affected paths, a + partially applied transaction with a stranded step and a persistence failure + after signing, stay unverifiable. They are not repaired or re-signed: + rewriting them would mean signing evidence after the fact. Records on every + other path are unaffected. +- The v1 evidence envelope serializes `rollback_results` but does not cover it + with the signature. Editing that field in a stored record does not + invalidate verification. Authenticating it changes the signed bytes and needs + a new schema version that a verifier can tell apart from v1; that work is + tracked and not in this release. +- Both engine corrections were verified offline, with isolated stores and real + signatures, not on a live host. + ## v0.10.0 (2026-08-10) ### Added diff --git a/README.md b/README.md index c5ff3d42..f1b31b51 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Requires Go 1.26+ and make. git clone git@github.com:Hanalyx/kensa.git cd kensa make build # builds all five binaries into bin/ -./bin/kensa --version # → kensa 0.10.0 (kensa) +./bin/kensa --version # → kensa 0.11.0 (kensa) ``` The five binaries: @@ -117,7 +117,7 @@ ldd bin/kensa # "not a dynamic executable" ## Status -`v0.10.0`, released and signed. The 0.x line is the pre-1.0 development phase. The +`v0.11.0`, released and signed. The 0.x line is the pre-1.0 development phase. The `api/` Go package is frozen under v1 semver for OpenWatch's consumption; the rest of the surface (CLI flags, rule schema additions, output formats) may change between MINOR versions with one release of deprecation warning. All 29 handlers diff --git a/VERSION b/VERSION index 78bc1abd..d9df1bbc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.10.0 +0.11.0 diff --git a/VERSIONING_PLAN.md b/VERSIONING_PLAN.md index beca141b..77db5b53 100644 --- a/VERSIONING_PLAN.md +++ b/VERSIONING_PLAN.md @@ -226,6 +226,15 @@ Current phase. Indicates: CHANGELOG entry and one-version deprecation warning** - Breaking changes to `api/` require a major bump (`1.0.0` or later); the `api/` contract is the load-bearing commitment to OpenWatch +- **Pre-1.0 exception, founder-approved 2026-09-20.** During 0.x, an + `api/` change that moves no Go signature, adds no field and removes none, + but changes what an existing value means to a consumer, ships in a MINOR + bump. Two conditions apply: the CHANGELOG entry states the change in + consumer terms, and the affected consumer (OpenWatch) has confirmed in + writing that it needs no change or has made the change. The first use is + v0.11.0, where a failed apply step reports `rollback_failed` instead of + `rolled_back`. This is an exception granted for the 0.x line, not a + reading of the rule above; at 1.0 and later such a change is MAJOR. - No long-term support commitment for 0.x lines ### Production Phase (1.x.x+) @@ -252,33 +261,50 @@ Indicates production-ready software: ### Tagging -Kensa releases are git tags of the form `v$(cat VERSION)`. The release -process is manual today; release automation (GoReleaser, RPM packaging, -Ed25519-signed checksums) is planned for v1.0. +Kensa releases are git tags of the form `v$(cat VERSION)`. Pushing a `v*` +tag triggers `.github/workflows/release.yml`, which runs GoReleaser per +`.goreleaser.yaml` and publishes the GitHub Release with its artifacts. +The tag push IS publication; there is no separate publish step and no way +to preview it, so everything below the tag line is a point of no return. ```bash # 1. Bump VERSION on a release branch -echo "0.2.0" > VERSION - -# 2. Update CHANGELOG.md: stamp ## Unreleased as ## v0.2.0 — -$EDITOR CHANGELOG.md - -# 3. Open a release PR -git checkout -b release/v0.2.0 -git add VERSION CHANGELOG.md -git commit -m "chore(release): v0.2.0" -git push -u origin release/v0.2.0 -gh pr create --title "chore(release): v0.2.0" --body "..." - -# 4. After CI green + PR merge, tag the merge commit and create the -# GitHub Release page from the CHANGELOG section. +echo "0.11.0" > VERSION + +# 2. Update CHANGELOG.md: stamp ## Unreleased as ## v0.11.0 () and +# keep an empty ## Unreleased above it. Update the README version lines. +$EDITOR CHANGELOG.md README.md + +# 3. Open a release PR; `make docs-check` verifies VERSION, CHANGELOG and +# README agree. CI on the PR runs the release snapshot job, which builds +# every artifact without publishing or signing. +git checkout -b chore/release-v0.11.0 +git add VERSION CHANGELOG.md README.md +git commit -m "chore(release): v0.11.0" +git push -u origin chore/release-v0.11.0 +gh pr create --title "chore(release): v0.11.0" --body "..." + +# 4. After CI green, PR merge, and founder release acceptance: tag the +# merge commit. The workflow refuses to run without GPG_PRIVATE_KEY, +# GPG_PASSPHRASE, COSIGN_PRIVATE_KEY and COSIGN_PASSWORD. git checkout main && git pull --ff-only -git tag -a "v0.2.0" -m "Release v0.2.0 — Sentinel" -git push origin "v0.2.0" -gh release create v0.2.0 --title "v0.2.0 — Sentinel" \ - --notes-file <(sed -n '/^## v0.2.0/,/^## v[0-9]/p' CHANGELOG.md | sed '$d') +git tag -a "v0.11.0" -m "Release v0.11.0 — Sentinel" +git push origin "v0.11.0" + +# 5. GoReleaser has changelog generation disabled, so the release page is +# published with an EMPTY body. Set it from the CHANGELOG section after +# the workflow finishes, and check the extracted notes are not empty +# first (see the sed note above). +sed -n '/^## v0.11.0/,/^## v[0-9]/p' CHANGELOG.md | sed '$d' > /tmp/notes.md +test -s /tmp/notes.md && gh release edit v0.11.0 --notes-file /tmp/notes.md ``` +What a release publishes, all signed: rpm and deb for linux/amd64 and +linux/arm64, a noarch `kensa-rules` rpm and deb, binary tarballs, `with-rules` +air-gap tarballs, a CycloneDX SBOM, and a sha256 checksums file with a cosign +signature over it. The GPG signing key is the Hanalyx LLC key in `KEYS`; +the checksums signature uses the Kensa cosign key, also in `KEYS`. + Use `sed` for the range, not `awk`. An awk range whose end pattern also matches its start line opens and closes on that one line, so `awk '/^## v0.2.0/,/^## v[0-9]/'` emits only the heading and the trailing @@ -286,10 +312,12 @@ matches its start line opens and closes on that one line, so page with no body, published without an error. A `sed` range never terminates on its start line. Check the output is not empty before tagging. -The 0.x line ships source-only. OpenWatch and other Go consumers -import the `api/` package via `go get github.com/Hanalyx/kensa@v0.x.y` -against the tag. Operators build from source per `README.md` → -**Building from source**. +Go consumers such as OpenWatch import the `api/` package via +`go get github.com/Hanalyx/kensa@v0.x.y` against the tag, independent of +the packages. Operators install the signed rpm or deb; building from source +per `README.md` remains supported but is not the shipped path. This section +said the 0.x line ships source-only until v0.11.0, which was untrue from +v0.2.0 on. ### Commit Message Format diff --git a/docs/guide/09-reference.md b/docs/guide/09-reference.md index 3a3ddbb1..79cc984d 100644 --- a/docs/guide/09-reference.md +++ b/docs/guide/09-reference.md @@ -1,6 +1,6 @@ # 09 · Command reference -_Applies to: Kensa v0.10.0 plus Unreleased changes. Last updated 2026-09-08._ +_Applies to: Kensa v0.11.0. Last updated 2026-09-20._ This chapter documents every `kensa` command and flag. It is the exhaustive counterpart to the task-focused chapters: for *how* to scan From fa2194d93008b41197f811c13cf4830b8206593c Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Sun, 20 Sep 2026 23:53:16 -0400 Subject: [PATCH 2/4] fix(spec): let the coverage-finalization contract survive a release cli-coverage-command-finalization AC-08 expected the CHANGELOG entry to sit under Unreleased and VERSION to be unchanged. Those were guards for the PR that landed the finalization, written into an approved criterion as permanent expectations. No release could satisfy them: releasing IS stamping Unreleased and changing VERSION. The v0.11.0 release PR failed its unit job on exactly this. The criterion now expects the entry at or after v0.11.0, the release that shipped it, and drops the VERSION guard. The test anchors the entry above the v0.10.0 heading, whichever section holds it, and no longer reads VERSION. Removing the entry fails the test. AC-08 carries approval_gate with approval_date 2026-09-08. That date is left as it was; the amended wording needs the founder's acceptance and is called out in the release PR. --- cmd/kensa/coverage_finalization_test.go | 40 ++++++++++--------- .../coverage-command-finalization.spec.yaml | 8 +++- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/cmd/kensa/coverage_finalization_test.go b/cmd/kensa/coverage_finalization_test.go index 9286afe4..3d0eeb40 100644 --- a/cmd/kensa/coverage_finalization_test.go +++ b/cmd/kensa/coverage_finalization_test.go @@ -632,25 +632,29 @@ func TestActiveDocsAgreeWithFinalContract(t *testing.T) { } }) - t.Run("changelog_and_version", func(t *testing.T) { + t.Run("changelog", func(t *testing.T) { c := read("CHANGELOG.md") - // Anchor on the heading at line start: the file's preamble mentions - // "## Unreleased" in prose, and matching that would slice the wrong - // region and pass or fail for the wrong reason. - const heading = "\n## Unreleased\n" - i := strings.Index(c, heading) - if i < 0 { - t.Fatal("CHANGELOG has no Unreleased heading") - } - unreleased := c[i+len(heading):] - if j := strings.Index(unreleased, "\n## "); j >= 0 { - unreleased = unreleased[:j] - } - if !strings.Contains(unreleased, "coverage") || !strings.Contains(unreleased, "--framework") { - t.Errorf("Unreleased does not record the finalization:\n%s", unreleased) - } - if v := strings.TrimSpace(read("VERSION")); v != "0.10.0" { - t.Errorf("VERSION = %q; this slice must not bump it", v) + // The finalization entry sits under Unreleased until the release + // that ships it stamps that section, which happened in v0.11.0. So + // the entry must appear above the v0.10.0 heading, whichever + // section it is in; pinning it to Unreleased, or pinning VERSION, + // would fail at every release. Anchor headings at line start: the + // preamble mentions "## Unreleased" in prose. + const entry = "**`kensa coverage` always reports framework control coverage.**" + const prior = "\n## v0.10.0 " + e := strings.Index(c, entry) + p := strings.Index(c, prior) + if e < 0 { + t.Fatal("CHANGELOG does not record the coverage finalization") + } + if p < 0 { + t.Fatal("CHANGELOG has no v0.10.0 heading to anchor on") + } + if e > p { + t.Errorf("the finalization entry sits below the v0.10.0 heading, in a release that predates it") + } + if !strings.Contains(c[e:p], "--framework") { + t.Error("the finalization entry does not name --framework") } }) diff --git a/specs/cli/coverage-command-finalization.spec.yaml b/specs/cli/coverage-command-finalization.spec.yaml index 18ba373f..d338cc74 100644 --- a/specs/cli/coverage-command-finalization.spec.yaml +++ b/specs/cli/coverage-command-finalization.spec.yaml @@ -338,9 +338,13 @@ spec: KENSA_NO_REPURPOSE_WARNINGS_active_entry: absent manpage_check: pass changelog: - Unreleased_records_finalization: true + # The entry lives under Unreleased until the release that + # ships it stamps that section; the finalization shipped + # in v0.11.0. "Unreleased_records_finalization: true" and + # "VERSION_changed: false" were PR-time guards written as + # permanent expectations, which no release could satisfy. + records_finalization_at_or_after: v0.11.0 historical_release_sections_changed: false - VERSION_changed: false specifications: old_rename_status: deprecated current_contract_points_to: cli-coverage-command-finalization From 7f0dee5a487533d9216965a2877a5aae1fe65aac Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Mon, 21 Sep 2026 08:27:01 -0400 Subject: [PATCH 3/4] docs(release): narrow the versioning exception, name the real gate, scope the old-evidence note Three corrections from review of the v0.11.0 release documents. The pre-1.0 exception in VERSIONING_PLAN.md read as a general license to change api/ value semantics whenever OpenWatch agreed. What was approved is narrower: a corrective change only, one that stops a value stating something false, approved per change, with the release documenting the compatibility assessment, the operator-visible effect and any migration. OpenWatch's written clearance is required and is evidence for OpenWatch alone; it says nothing about other consumers of api/. The text now says all of that. The release procedure said pull-request CI runs the release snapshot. It does not: the snapshot and govulncheck jobs run only on schedule or workflow_dispatch and show as skipped on a PR. The procedure now requires a candidate-specific dispatch, green on every job including those two, with the run id and head SHA recorded, before tagging. It also notes that nothing checks the tag against VERSION. The Known limits entry said persistence failures in earlier versions left unverifiable stored records. The old mutation rewrote the decision inside a RETURNED envelope after a committed, rolled-back or staged result was demoted because its persistence failed; Kensa did not store those, since storing them is what had failed, and a persistence failure that did not demote left the signature intact. Only the stranded-step case produced unverifiable STORED records. The entry now names the two cases separately and keeps the no-repair statement. --- CHANGELOG.md | 16 +++++++++++----- VERSIONING_PLAN.md | 47 +++++++++++++++++++++++++++++++--------------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6037696b..caf290e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -195,11 +195,17 @@ none; OpenWatch confirmed no consumer change is needed. ### Known limits -- Evidence records written by earlier versions on the two affected paths, a - partially applied transaction with a stranded step and a persistence failure - after signing, stay unverifiable. They are not repaired or re-signed: - rewriting them would mean signing evidence after the fact. Records on every - other path are unaffected. +- Two kinds of evidence from earlier versions fail verification, and neither + is repaired or re-signed, because rewriting them would mean signing evidence + after the fact. **Stored records** of a partially applied transaction with a + stranded step: those were persisted with a signature that no longer matched + their contents. **Returned envelopes** handed back to the caller after a + committed, rolled-back or staged result was demoted because its persistence + failed: the engine rewrote the decision inside a signed envelope. Those + envelopes were not stored by Kensa, since storing them is what had failed; + whether a caller kept one is not something Kensa can establish. A + persistence failure that did not demote the result left the signature + intact. Records on every other path are unaffected. - The v1 evidence envelope serializes `rollback_results` but does not cover it with the signature. Editing that field in a stored record does not invalidate verification. Authenticating it changes the signed bytes and needs diff --git a/VERSIONING_PLAN.md b/VERSIONING_PLAN.md index 77db5b53..5f3e395c 100644 --- a/VERSIONING_PLAN.md +++ b/VERSIONING_PLAN.md @@ -226,15 +226,22 @@ Current phase. Indicates: CHANGELOG entry and one-version deprecation warning** - Breaking changes to `api/` require a major bump (`1.0.0` or later); the `api/` contract is the load-bearing commitment to OpenWatch -- **Pre-1.0 exception, founder-approved 2026-09-20.** During 0.x, an - `api/` change that moves no Go signature, adds no field and removes none, - but changes what an existing value means to a consumer, ships in a MINOR - bump. Two conditions apply: the CHANGELOG entry states the change in - consumer terms, and the affected consumer (OpenWatch) has confirmed in - writing that it needs no change or has made the change. The first use is - v0.11.0, where a failed apply step reports `rollback_failed` instead of - `rolled_back`. This is an exception granted for the 0.x line, not a - reading of the rule above; at 1.0 and later such a change is MAJOR. +- **Pre-1.0 exception, founder-approved 2026-09-20, one change at a + time.** During 0.x, a *corrective* `api/` behavior change may ship in a + MINOR bump: one that moves no Go signature, adds no field and removes + none, and changes what an existing value means only to stop it stating + something false. It is not a general license to change value semantics. + Each use needs its own founder approval, and the release that carries it + must document three things: the compatibility assessment (which consumers + are known, what each sees, and what remains unassessed), the operator- + visible effect, and any migration a consumer must perform. Written + clearance from OpenWatch is required and is evidence for OpenWatch only; + it does not establish compatibility for any other consumer of `api/`. + The first use is v0.11.0, where a failed apply step reports + `rollback_failed` instead of `rolled_back` because `rolled_back` claimed + a restoration the engine had not performed. This is an exception granted + for the 0.x line, not a reading of the rule above; at 1.0 and later such + a change is MAJOR. - No long-term support commitment for 0.x lines ### Production Phase (1.x.x+) @@ -276,22 +283,32 @@ echo "0.11.0" > VERSION $EDITOR CHANGELOG.md README.md # 3. Open a release PR; `make docs-check` verifies VERSION, CHANGELOG and -# README agree. CI on the PR runs the release snapshot job, which builds -# every artifact without publishing or signing. +# README agree. Pull-request CI does NOT run the release snapshot or +# govulncheck jobs: both are gated to `schedule` and `workflow_dispatch` +# and show as skipped on the PR. git checkout -b chore/release-v0.11.0 git add VERSION CHANGELOG.md README.md git commit -m "chore(release): v0.11.0" git push -u origin chore/release-v0.11.0 gh pr create --title "chore(release): v0.11.0" --body "..." -# 4. After CI green, PR merge, and founder release acceptance: tag the -# merge commit. The workflow refuses to run without GPG_PRIVATE_KEY, -# GPG_PASSPHRASE, COSIGN_PRIVATE_KEY and COSIGN_PASSWORD. +# 4. Dispatch CI on the candidate branch and require it green on EVERY +# job, including "Release snapshot" (builds every artifact, no publish, +# no sign) and "Vulnerability Scan (govulncheck)". Record the run id and +# confirm its head SHA is the candidate. This is the packaging gate; a +# green pull-request run is not. +gh workflow run ci.yml --ref chore/release-v0.11.0 +gh run list --workflow ci.yml --branch chore/release-v0.11.0 --event workflow_dispatch --limit 1 + +# 5. After PR merge and founder release acceptance: tag the merge commit. +# The workflow refuses to run without GPG_PRIVATE_KEY, GPG_PASSPHRASE, +# COSIGN_PRIVATE_KEY and COSIGN_PASSWORD. Nothing checks that the tag +# matches VERSION; the tag name alone sets the published version. git checkout main && git pull --ff-only git tag -a "v0.11.0" -m "Release v0.11.0 — Sentinel" git push origin "v0.11.0" -# 5. GoReleaser has changelog generation disabled, so the release page is +# 6. GoReleaser has changelog generation disabled, so the release page is # published with an EMPTY body. Set it from the CHANGELOG section after # the workflow finishes, and check the extracted notes are not empty # first (see the sed note above). From cdcbad5f2e43a9876476f14dd7c620dd0f630b3d Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Tue, 22 Sep 2026 10:22:42 -0400 Subject: [PATCH 4/4] spec(cli): record the AC-08 amendment approval beside the original The criterion was accepted 2026-09-08 with changelog expectations that were PR-time guards written as permanent ones, and amended during the v0.11.0 release when no release could satisfy them. The amended wording carried the original approval date, so the record read as though the contract had been settled once. approval_date now carries the amendment acceptance, 2026-09-22, with a comment stating the original date, what changed and why. This follows the convention set by system-checkout-credential-non-persistence AC-01, which keeps both dates for the same reason. --- specs/cli/coverage-command-finalization.spec.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/specs/cli/coverage-command-finalization.spec.yaml b/specs/cli/coverage-command-finalization.spec.yaml index d338cc74..0c52cbc1 100644 --- a/specs/cli/coverage-command-finalization.spec.yaml +++ b/specs/cli/coverage-command-finalization.spec.yaml @@ -352,8 +352,19 @@ spec: quiet_and_manpage_drafts_match_active_surface: true references_constraints: [C-07] priority: high + # Accepted 2026-09-08, then revised: the changelog expectations + # were PR-time guards written as permanent ones. They required the + # entry to sit under ## Unreleased and VERSION to be unchanged, + # which no release can satisfy, since releasing IS stamping that + # section and changing VERSION. The v0.11.0 release failed its + # unit job on exactly that. The revised expectation is that the + # entry appears at or after v0.11.0, the release that shipped the + # finalization, and the VERSION guard is gone. The amendment was + # accepted on 2026-09-22, which is the date below. Both dates are + # kept so the record shows the contract changed after its first + # approval rather than appearing to have been settled once. approval_gate: true - approval_date: "2026-09-08" + approval_date: "2026-09-22" - id: AC-09 description: |