Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://github.com/Hanalyx/kensa/compare/v0.8.0...v0.9.0> (swap the tags for
<https://github.com/Hanalyx/kensa/compare/v0.10.0...v0.11.0> (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
Expand Down Expand Up @@ -179,6 +193,27 @@ 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

- 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
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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.10.0
0.11.0
95 changes: 70 additions & 25 deletions VERSIONING_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +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, 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+)
Expand All @@ -252,44 +268,73 @@ 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 — <date>
$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 (<date>) 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. 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. 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.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"

# 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).
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
`sed '$d'` then deletes it. The result is an empty notes file and a release
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

Expand Down
40 changes: 22 additions & 18 deletions cmd/kensa/coverage_finalization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
})

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/09-reference.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
21 changes: 18 additions & 3 deletions specs/cli/coverage-command-finalization.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -338,18 +338,33 @@ 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
framework_coverage_stale_clauses_marked_superseded: true
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: |
Expand Down
Loading