Skip to content

Commit dd921b2

Browse files
chore: GA-readiness fixes (F1-F6) + Specter 100% coverage gate (#602)
GA-readiness review fixes plus a 100% Specter coverage gate (structural + outcome at all tiers). - F6 hygiene: remove stray root binary, gitignore /openwatch, strip prototype login creds - F2 lint: warn when local golangci-lint != CI-pinned 1.64.8 - F1 isolation: self-contained SPA test fixture; gate native package-build tests behind OPENWATCH_PACKAGING_BUILD - F5 CI: hard-fail the build on frontend vitest failures - F4 release: fail closed on unsigned tag-push releases - F3 docs: reconcile guide versions to 0.2.0-rc.10 + CHANGELOG - Specter: enforce 100% structural AC coverage and raise all outcome tiers to 100, wiring the formerly env-gated tests (Kensa corpus AC-08; runtime-boot AC-14 via pgx + CSRF fix) to run in CI
1 parent 56ca8cd commit dd921b2

17 files changed

Lines changed: 228 additions & 34 deletions

File tree

.github/workflows/go-ci.yml

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,32 @@ jobs:
196196
exit 1
197197
fi
198198
199+
# 100% structural coverage gate. Every acceptance criterion in every spec
200+
# MUST have at least one test annotation — no AC may silently lose its
201+
# test. This runs in annotation strictness (structural, no ingest) so it
202+
# is independent of which tests run in this lane, and it fails fast and
203+
# with a clear message before the heavier ingest-based outcome gate. The
204+
# outcome gate (`specter sync` at the manifest's threshold strictness,
205+
# tiers all 100, below) separately requires every AC's test to PASS; the
206+
# ingest job provisions OPENWATCH_KENSA_RULES_DIR + OPENWATCH_PACKAGING_BUILD
207+
# so the formerly env-gated tests run there instead of skipping.
208+
#
209+
# NOTE: `specter sync --strictness annotation` would only enforce the
210+
# per-tier thresholds, so we assert 100% directly: any per-spec row
211+
# (a "… Tn …" line) not at 100% fails the build.
212+
- name: specter structural coverage must be 100%
213+
if: steps.paths.outputs.go == 'true'
214+
run: |
215+
report=$(specter coverage --strictness annotation 2>&1)
216+
echo "$report"
217+
below=$(echo "$report" | grep -E '\sT[0-9]+\s' | grep -vE '\s100%' || true)
218+
if [ -n "$below" ]; then
219+
echo "::error::Specter structural coverage is below 100% — every acceptance criterion must carry a // @ac (Go) or // @ac (frontend) annotation. Specs below 100%:"
220+
echo "$below"
221+
exit 1
222+
fi
223+
echo "All specs at 100% structural coverage."
224+
199225
# Single full-suite run that BOTH detects data races and emits the
200226
# JSON specter ingests — replacing the previous two end-to-end passes
201227
# (`make test-race` + a separate non-race `go test -json`), which ran
@@ -206,8 +232,18 @@ jobs:
206232
# to 600s because race instrumentation slows the hot DB packages.
207233
- name: go test (race + json) for specter ingest
208234
if: steps.paths.outputs.go == 'true'
235+
# OPENWATCH_PACKAGING_BUILD=1 opts the native package-build + runtime-boot
236+
# tests into this run so their AC coverage is enforced in CI; a plain
237+
# local `go test ./...` skips them (they take minutes and write into
238+
# dist/). See packaging/tests/package_test.go:requirePackagingBuild.
239+
env:
240+
OPENWATCH_PACKAGING_BUILD: "1"
209241
run: |
210242
set +e
243+
# Point the Kensa variable-catalog tests at the real rule corpus that
244+
# ships inside the kensa module, so api-system-scan-config/AC-08 runs
245+
# here instead of skipping (100% outcome coverage requires it).
246+
export OPENWATCH_KENSA_RULES_DIR="$(go list -m -f '{{.Dir}}' github.com/Hanalyx/kensa)/rules"
211247
go test -race -json -timeout 600s -p 4 ./... > /tmp/go-test.json
212248
EXIT=$?
213249
if [ "$EXIT" -ne 0 ]; then
@@ -242,12 +278,14 @@ jobs:
242278
set +e
243279
npx vitest run --reporter=junit --outputFile=/tmp/vitest-junit.xml --reporter=default
244280
EXIT=$?
245-
# Don't block the pipeline on Vitest failure — surface it but
246-
# let specter sync report which ACs are uncovered. The Go test
247-
# step above already fails the build on test failure; this
248-
# mirror keeps the gate's failure source observable.
281+
# Record the exit code but DON'T fail here: specter ingest (next
282+
# step) still needs to read the JUnit file, and specter sync should
283+
# report which ACs the failures left uncovered before we abort.
284+
# The "Enforce frontend tests passed" step below turns a non-zero
285+
# result into a hard build failure once specter has run.
286+
echo "$EXIT" > /tmp/vitest-exit
249287
if [ "$EXIT" -ne 0 ]; then
250-
echo "::warning::Vitest reported test failures — see specter coverage for affected ACs"
288+
echo "::warning::Vitest reported test failures — specter coverage below shows affected ACs; the build fails at the enforcement step."
251289
fi
252290
253291
- name: specter ingest
@@ -264,6 +302,22 @@ jobs:
264302
# are skipped at parse time.
265303
run: specter sync --tests '**/*'
266304

305+
# Hard frontend-test gate. Vitest failures are deferred above so
306+
# specter can ingest the JUnit results and report coverage first; this
307+
# step makes a real frontend test failure fail the build. A frontend
308+
# regression that does not happen to map to a spec AC would otherwise
309+
# slip through (specter only gates annotated ACs), so this is the
310+
# backstop that keeps `npm test` authoritative in CI.
311+
- name: Enforce frontend tests passed
312+
if: steps.paths.outputs.go == 'true'
313+
run: |
314+
EXIT=$(cat /tmp/vitest-exit 2>/dev/null || echo 1)
315+
if [ "$EXIT" -ne 0 ]; then
316+
echo "::error::Frontend vitest suite failed (exit $EXIT). See the 'frontend vitest' step above."
317+
exit "$EXIT"
318+
fi
319+
echo "frontend vitest suite passed"
320+
267321
- name: upload specter artifacts
268322
if: always() && steps.paths.outputs.go == 'true'
269323
uses: actions/upload-artifact@v4

.github/workflows/release.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,16 @@ jobs:
8282
run: |
8383
if [ -n "$GPG_PRIVATE_KEY" ]; then
8484
echo "gpg=true" >> "$GITHUB_OUTPUT"
85+
elif [ "$GITHUB_EVENT_NAME" = "push" ]; then
86+
# A v* tag push is an intentional release cut — publishing it
87+
# unsigned would ship packages operators cannot verify. Fail
88+
# closed. Manual workflow_dispatch runs (dry-runs / trials) stay
89+
# permissive below so an unsigned trial build is still possible.
90+
echo "::error::No GPG_PRIVATE_KEY secret configured, but this is a tag-push release ($GITHUB_REF_NAME). Refusing to publish unsigned packages. Configure the GPG_PRIVATE_KEY secret, or cut an unsigned trial via workflow_dispatch."
91+
exit 1
8592
else
8693
echo "gpg=false" >> "$GITHUB_OUTPUT"
87-
echo "::notice::No GPG_PRIVATE_KEY secret — packages and checksums will be published unsigned."
94+
echo "::warning::No GPG_PRIVATE_KEY secret — this workflow_dispatch build will be published UNSIGNED. Tag pushes fail closed instead."
8895
fi
8996
if [ -n "$COSIGN_PRIVATE_KEY" ]; then
9097
echo "cosign=true" >> "$GITHUB_OUTPUT"

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,11 @@ Thumbs.db
962962
coverage.html
963963
coverage.txt
964964

965+
# Stray root-level build artifact. `make build` writes to dist/ (ignored
966+
# above); an ad-hoc `go build -o openwatch` at the repo root must never be
967+
# committed (a 34MB ELF slipped in this way once).
968+
/openwatch
969+
965970
# Transient specter run output. CI regenerates these from a real
966971
# go test -json + vitest JUnit run (specter ingest), so a committed copy is
967972
# never consumed by the gate — it only goes stale and produces misleading

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,29 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1010

1111
## [Unreleased]
1212

13+
### Changed
14+
15+
- CI release safety: the release workflow now fails closed on a `v*` tag push
16+
when no GPG signing key is configured, rather than publishing unsigned
17+
packages. Manual `workflow_dispatch` trial builds stay permissive (warn +
18+
publish unsigned).
19+
- CI frontend gate: a failing frontend Vitest suite now hard-fails the build.
20+
Results are still ingested by specter first (so coverage is reported), then a
21+
dedicated enforcement step aborts on a real test failure.
22+
- `make lint` now warns when the locally installed `golangci-lint` does not
23+
match the version pinned in CI, so local runs reproduce CI.
24+
25+
### Fixed
26+
27+
- Documentation version drift: operator guides referenced `0.2.0-rc.5` while
28+
`packaging/version.env` was `0.2.0-rc.10`; all guides now match.
29+
- SPA static-delivery tests are self-contained (in-memory fixture) instead of
30+
depending on a magic staged asset filename, so `go test ./internal/server/`
31+
passes against a real `vite build`, the Makefile stub, or no staged tree.
32+
- Repository hygiene: a stray 34 MB root build artifact was removed and the
33+
root-level `openwatch` binary path is now gitignored; placeholder credentials
34+
were stripped from the prototype login page.
35+
1336
---
1437

1538
## [0.2.0-rc.10] Eyrie — 2026-06-17

Makefile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ DIST_DIR := dist
2020
CMD_DIR := ./cmd/openwatch
2121
SPA_DIR := internal/server/spa
2222

23+
# golangci-lint version. MUST match the version pinned in
24+
# .github/workflows/go-ci.yml (`go install ...golangci-lint@vX.Y.Z`) so a
25+
# local `make lint` reproduces CI exactly — a newer local binary surfaces
26+
# lints CI won't enforce yet (and vice-versa). Bump both together.
27+
GOLANGCI_VERSION := 1.64.8
28+
2329
# Version metadata injected at build time. The Go rebuild has its own
2430
# version track (packaging/version.env) so its milestones decouple from
2531
# the legacy Python project at ../VERSION. Fallback order: local
@@ -132,9 +138,16 @@ vet: internal/server/openapi_embed.yaml $(SPA_DIR)/index.html
132138
.PHONY: lint
133139
lint: internal/server/openapi_embed.yaml $(SPA_DIR)/index.html
134140
@if command -v golangci-lint >/dev/null 2>&1; then \
141+
have=$$(golangci-lint version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \
142+
if [ "$$have" != "$(GOLANGCI_VERSION)" ]; then \
143+
echo "WARNING: golangci-lint $$have installed, but CI pins $(GOLANGCI_VERSION)."; \
144+
echo " Results may differ from CI. Install the pinned version:"; \
145+
echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(GOLANGCI_VERSION)"; \
146+
fi; \
135147
golangci-lint run; \
136148
else \
137-
echo "golangci-lint not installed; skipping (install: https://golangci-lint.run/usage/install/)"; \
149+
echo "golangci-lint not installed; skipping (CI pins v$(GOLANGCI_VERSION)):"; \
150+
echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(GOLANGCI_VERSION)"; \
138151
fi
139152

140153
# vuln: known-CVE scan against deps + stdlib (call-graph aware).

docs/guides/API_GUIDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ contract source of truth is `api/openapi.yaml` in the repository; the running
1111
binary serves the same document, and `GET /api/v1/version` reports the build it
1212
came from.
1313

14-
This guide reflects OpenWatch `0.2.0-rc.5`, a pre-release. The API surface is
14+
This guide reflects OpenWatch `0.2.0-rc.10`, a pre-release. The API surface is
1515
still growing — endpoints that the legacy Python API exposed (scan execution,
1616
remediation, exceptions, posture history, audit exports, the rule-reference
1717
browser) are not yet part of `api/v1`. See [What is not yet in the
@@ -276,7 +276,7 @@ curl -s --cacert /etc/openwatch/tls/ca.crt https://localhost:8443/api/v1/health
276276
```
277277

278278
```json
279-
{"status": "healthy", "db_connected": true, "version": "0.2.0-rc.5"}
279+
{"status": "healthy", "db_connected": true, "version": "0.2.0-rc.10"}
280280
```
281281

282282
`status` is `healthy` or `degraded`; the endpoint returns `503` when the service
@@ -354,7 +354,7 @@ configuration steps, see
354354
## What is not yet in the API
355355

356356
The compliance scanning workflow runs through Kensa and the background worker,
357-
not yet through public REST endpoints. As of `0.2.0-rc.5`, `api/v1` does not
357+
not yet through public REST endpoints. As of `0.2.0-rc.10`, `api/v1` does not
358358
include:
359359

360360
- Scan execution or scan-result endpoints (`/api/v1/scans/…`).

docs/guides/MONITORING_SETUP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ curl -k https://localhost:8443/api/v1/health
5454
A healthy response returns `200 OK`:
5555

5656
```json
57-
{"status": "healthy", "db_connected": true, "version": "0.2.0-rc.5"}
57+
{"status": "healthy", "db_connected": true, "version": "0.2.0-rc.10"}
5858
```
5959

6060
When the database ping fails, the endpoint returns `503 Service Unavailable`
@@ -76,7 +76,7 @@ curl -k https://localhost:8443/api/v1/version
7676

7777
```json
7878
{
79-
"openwatch": "0.2.0-rc.5",
79+
"openwatch": "0.2.0-rc.10",
8080
"kensa": "<embedded engine version>",
8181
"go": "<go toolchain>",
8282
"commit": "<abbrev commit>",

docs/guides/PRODUCTION_DEPLOYMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ touches lightly: process layout, TLS, the background worker, backups, upgrades,
1212
and incident runbooks.
1313

1414
> Verify the version you deploy. The current line is a pre-release
15-
> (`0.2.0-rc.5` per `packaging/version.env`), not a GA build. Treat it
15+
> (`0.2.0-rc.10` per `packaging/version.env`), not a GA build. Treat it
1616
> accordingly until a GA tag ships.
1717
1818
---

docs/guides/QUICKSTART.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ A healthy response looks like this:
4949
{
5050
"status": "healthy",
5151
"db_connected": true,
52-
"version": "0.2.0-rc.5"
52+
"version": "0.2.0-rc.10"
5353
}
5454
```
5555

docs/guides/UPGRADE_PROCEDURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ database backup and restore commands referenced below, see
1313
[`BACKUP_RECOVERY.md`](BACKUP_RECOVERY.md). For migration mechanics, see
1414
[`DATABASE_MIGRATIONS.md`](DATABASE_MIGRATIONS.md).
1515

16-
> Version note: the current release line is a pre-release (`0.2.0-rc.5`). Treat
16+
> Version note: the current release line is a pre-release (`0.2.0-rc.10`). Treat
1717
> upgrades between pre-release builds as potentially breaking and always back up
1818
> first.
1919

0 commit comments

Comments
 (0)