Files
minstrel/internal/server/release_gate_test.go
T
bvandeusenandClaude Opus 5.5 d191a28217 docs: server comments say what the code does, not how it came to be (#6277)
Comment blocks in cmd/minstrel and 30 smaller internal packages, rewritten
to rule 32's standard with each citation read in context: task, milestone
and rule ids go, and so do the stories they were attached to ("used to",
"Before #N", measurements from one install, quotes, pointers to Fabled
Librarian's records and to docs/superpowers files that are not in the
repo). Where a story wrapped a real constraint, the constraint stays as a
present-tense fact.

Comments that had gone stale are corrected:
- taste.Config: the tuned values are overlaid by internal/recsettings;
  it is no longer "a later follow-up".
- StorageConfig: the data dir holds art, caches and the stream secret,
  not only playlist covers.
- the Subsonic album list and getUser comments describe what they do
  today rather than a milestone that would change it.

Checked: the non-comment token stream of every changed file matches
HEAD, and gofmt -l is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:15:05 -04:00

121 lines
3.9 KiB
Go

package server
import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
"gopkg.in/yaml.v3"
)
// The publish gate, pinned. release.yml holds every
// verifying lane and every publishing job in one graph so that a publish can
// depend on the lanes; these tests keep that dependency from eroding.
//
// The failure each one guards against is silent. A lane added without being
// named in a publisher's needs runs, goes red, and :dev publishes anyway. A
// condition relaxed to `!failure()` lets a lane that never started count as
// a pass. Neither shows up as a broken build — only as a gate that no longer
// gates.
// gateLanes are the verifying jobs. Every publisher must need each of them and
// require its success by name.
var gateLanes = []string{"go", "integration", "web", "android", "govulncheck"}
// gatePublishers push something other people can pull: image tags, and the
// APK + sidecar attached to a Release.
var gatePublishers = []string{"image-release", "release-assets"}
// gateOthers are the remaining jobs and why they sit outside the gate:
// android-release only builds a workflow artifact (nothing outside the run can
// pull it), and verify-release reports on a finished release.
var gateOthers = []string{"android-release", "verify-release"}
type workflowJob struct {
Needs any `yaml:"needs"`
If string `yaml:"if"`
}
func releaseJobs(t *testing.T) map[string]workflowJob {
t.Helper()
body, err := os.ReadFile(filepath.Join(repoRoot(t), ".gitea", "workflows", "release.yml"))
if err != nil {
t.Fatal(err)
}
var wf struct {
Jobs map[string]workflowJob `yaml:"jobs"`
}
if err := yaml.Unmarshal(body, &wf); err != nil {
t.Fatalf("parse release.yml: %v", err)
}
if len(wf.Jobs) == 0 {
t.Fatal("release.yml has no jobs")
}
return wf.Jobs
}
func jobNeeds(j workflowJob) []string {
switch v := j.Needs.(type) {
case string:
return []string{v}
case []any:
out := make([]string, 0, len(v))
for _, n := range v {
if s, ok := n.(string); ok {
out = append(out, s)
}
}
return out
}
return nil
}
// A job nobody has classified is the case that matters: a new lane that was
// never added to the publishers' needs.
func TestReleaseGate_EveryJobIsClassified(t *testing.T) {
for name := range releaseJobs(t) {
if slices.Contains(gateLanes, name) || slices.Contains(gatePublishers, name) || slices.Contains(gateOthers, name) {
continue
}
t.Errorf("release.yml job %q is not classified. If it verifies, add it to gateLanes and to every publisher's needs and if; "+
"if it publishes, add it to gatePublishers and gate it; otherwise add it to gateOthers with the reason", name)
}
for _, want := range append(append(slices.Clone(gateLanes), gatePublishers...), gateOthers...) {
if _, ok := releaseJobs(t)[want]; !ok {
t.Errorf("release.yml has no %q job, which this test expects", want)
}
}
}
func TestReleaseGate_PublishersNeedEveryLaneToSucceed(t *testing.T) {
jobs := releaseJobs(t)
for _, pub := range gatePublishers {
job, ok := jobs[pub]
if !ok {
t.Errorf("no %q job", pub)
continue
}
needs := jobNeeds(job)
cond := strings.Join(strings.Fields(job.If), " ")
for _, lane := range gateLanes {
if !slices.Contains(needs, lane) {
t.Errorf("%s does not need %q: it can publish without waiting for that lane", pub, lane)
}
if !strings.Contains(cond, "needs."+lane+".result == 'success'") {
t.Errorf("%s's if does not require needs.%s.result == 'success': a skipped or failed %s would not stop it\n if: %s",
pub, lane, lane, cond)
}
}
// always() and failure() both make a job run past a red dependency;
// !cancelled() is fine only because the per-lane success checks above
// carry the gate.
for _, forbidden := range []string{"always()", "failure()"} {
if strings.Contains(cond, forbidden) {
t.Errorf("%s's if uses %s, which runs it past a failed lane\n if: %s", pub, forbidden, cond)
}
}
}
}