Skip to content

ci: coverage gates, CI workflow rebuild, release tag-ordering fix - #151

Merged
trick77 merged 2 commits into
masterfrom
feat/coverage-gates
Jul 21, 2026
Merged

ci: coverage gates, CI workflow rebuild, release tag-ordering fix#151
trick77 merged 2 commits into
masterfrom
feat/coverage-gates

Conversation

@trick77

@trick77 trick77 commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Brings relume-tv into the nine-repo CI family whose reference is trick77/peeq.

Coverage gates

hack/coverage-gate.sh, hack/patch-coverage.sh and hack/coverage-gate.test.sh are byte-identical copies of the family reference — verified with diff -q. They auto-detect whether the Go module sits at backend/ or the repo root, so they work here unmodified. hack/coverage-floors sets a hard 75% line-coverage floor (the unused ui=75.0 line is kept so the file matches the rest of the family).

hack/coverage-gate.test.sh passes locally: coverage-gate: all checks passed.

Backend coverage: 71.8% -> 77.5%

This was the only repo in the family below the floor. Measured with the exact command CI runs (-race -covermode=atomic -coverpkg=./..., then a Cobertura conversion, cmd/ excluded). New tests, all asserting real behaviour rather than absence of panics:

file what it covers
internal/bridgepro/resources_test.go CLIP v2 resource layer (was 0%): BridgeInfo name-via-owning-device resolution and its best-effort fallbacks, Lights capability decoding (the pointer sub-objects that distinguish "cannot do colour" from "colour is 0,0"), stable ID sorting, EntertainmentConfigs, and the ErrUnreachable/ErrQueueFull/HTTP-status error taxonomy including a certificate-pin mismatch
internal/bridgepro/pair_test.go Pair (was 0%): app key + DTLS clientkey extraction, generateclientkey in the request body, the "link button not pressed" rejection, empty/malformed/unexpected responses; plus HTTPClientFor pinning — matching pin accepted, wrong pin refused
internal/ssdp/parse_test.go parseHeaders (key uppercasing, value trimming, colons inside values, malformed lines), logDatagram, and handle: one 200 OK per SSDP variant with correct LOCATION/hue-bridgeid/CACHE-CONTROL, NOTIFY datagrams ignored, and bail-on-first-write-error
internal/diag/dnsparse_test.go dnsQuestionNames / readName: compression-pointer following, pointer-loop and self-pointer termination (the decompression bomb this parser guards against on untrusted multicast input), truncated labels/pointers, over-stated QDCOUNT; plus inspect filtering and deadline
internal/config/psk_test.go PSKForUser hex decoding and its rejection paths, save/writeFileSync atomic-write behaviour: 0600 mode, O_TRUNC, parent-dir creation, error surfacing, and no temp file left behind after a failed rename; generateSerial shape and randomness

Network-touching code is tested at its pure boundaries (parsers, encoders, response construction). The only sockets opened are loopback UDP pairs in the SSDP handle tests — no multicast, so nothing here can go flaky in CI.

Gate verification (mandatory step, done)

A scratch exported function was added, coverage regenerated, and patch-coverage.sh run against origin/master:

RED

== backend patch coverage (>= 80%) ==
Failure. Coverage is below 80%.
internal/config/scratch.go (0.0%): Missing lines 7-17
Total:   11 lines
Missing: 11 lines
Coverage: 0%
exit=1

GREEN (after adding a covering test)

== backend patch coverage (>= 80%) ==
internal/config/scratch.go (100%)
Total:   11 lines
Missing: 0 lines
Coverage: 100%
exit=0

The scratch code and its test were then deleted entirely; git ls-files | grep -i scratch returns nothing and the branch history was reset so they never appear in a commit.

test.yaml -> ci.yaml

Renamed via git mv, name: CI, job display name Backend (build + test).

  • Trigger is now pull_request: branches: [master] + workflow_dispatch. Master no longer runs tests — it builds and pushes the image.
  • Added permissions: contents: read and a ci-${{ github.ref }} concurrency group with cancel-in-progress.
  • actions/setup-go@v6 -> @v7; the pinned go-version: "1.26" is replaced by go-version-file: go.mod, so the toolchain can never drift from the module.
  • fetch-depth: 0 on the checkout plus an explicit "Fetch base ref" step — patch-coverage.sh diffs against the PR base, which is not resolvable otherwise.
  • Added actions/setup-python@v6 + diff-cover==10.3.0.
  • Added a gofmt check after go vet (go vet does not cover formatting). The tree is gofmt-clean today; this keeps it that way.
  • Coverage now runs with -race (hence CGO_ENABLED: "1") and -coverpkg=./..., then converts to Cobertura — Go's own tooling reports statements only and exposes no line metric.
  • Dropped the Coverage summary and upload-artifact steps: they only reported a number nobody enforced. The two gates replace them.

No golangci-lint in this PR — separate pass.

release.yaml

Tag-ordering bug (the real fix here). The tag was created and pushed at roughly line 57, before setup-buildx and build-push. A failed image build therefore left an orphan tag behind — and since the next run computes its version from git tag --list ... --sort=-v:refname | head -n1, that version number was permanently burned: the release that failed still consumed it, and no later run could ever reuse it. Tag creation now happens only after the image push succeeds.

Mechanism. Switched from git tag + git push origin to the family's REST create-ref, and added a GitHub Release. Both approaches work; create-ref needs no git credentials configured in the checkout and is a single atomic call, and every repo in the family uses it so the release workflows stay directly comparable.

paths-ignore. Added to the push trigger so doc-only commits stop cutting full versions, multi-arch builds, GHCR pushes, tags and releases. Deliberately an ignore-list, not an allow-list: a new top-level directory starts out releasing, which is the safe default — forgetting to add a path costs one redundant release, whereas forgetting to add one to an allow-list would silently skip a needed release. dist/** is deliberately not ignored, since it holds shipped assets the image serves. workflow_dispatch stays unfiltered, so a release can always be forced by hand.

permissions already had contents: write and packages: write; unchanged.

Deliberate: Dependabot auto-merge removed

.github/workflows/dependabot-auto-merge.yaml is deleted on purpose, not by accident. This is a decision taken across all nine repos: dependency PRs get reviewed and merged by hand.

Deliberate: cleanup-images.yaml untouched

Zero changes to that file, also on purpose. It uses dataaxiom/ghcr-cleanup-action@v1 rather than the snok/container-retention-policy the rest of the family uses, because this repo builds multi-arch (qemu + buildx, amd64/arm64) and that action is manifest-list aware, with keep-n-tagged: 10 and delete-untagged: true. Switching it would risk breaking multi-arch cleanup for no benefit.

Local verification

go build ./...          OK
go vet ./...            OK
gofmt -l .              (no output)
go test -race ./...     all packages ok
hack/coverage-gate.sh backend   -> backend ok — 77.5% of lines (floor 75.0%)
hack/coverage-gate.test.sh      -> all checks passed

trick77 added 2 commits July 21, 2026 08:48
Joins the nine-repo CI family: hack/coverage-gate.sh, hack/patch-coverage.sh
and hack/coverage-gate.test.sh are byte-identical copies of the family
reference, with a 75% hard line-coverage floor in hack/coverage-floors.

Backend line coverage 71.8% -> 77.5% via new tests for the Hue Bridge Pro
CLIP v2 resource layer, the SSDP header parser and M-SEARCH responder, the
mDNS/DNS name parser and the config persistence path.

Release: the git tag was created BEFORE the image build, so a failed build
left an orphan tag and permanently burned that version number. The tag now
follows a successful image push, is created via the REST create-ref call the
rest of the family uses, and is accompanied by a GitHub Release. A
paths-ignore block stops doc-only commits from cutting a release.

Removes the Dependabot auto-merge workflow: dependency PRs are merged by
hand across this family. cleanup-images.yaml is deliberately untouched.
dist/ is gitignored and has no tracked files, so it can never appear in a
push diff and paths-ignore has no bearing on it either way.
@trick77
trick77 merged commit 439157f into master Jul 21, 2026
2 checks passed
@trick77
trick77 deleted the feat/coverage-gates branch July 21, 2026 21:28
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