Gate deployment on license checks

This commit is contained in:
Jonathon Leight committed 2026-08-06 21:05:04 -04:00
1 parent 71d9daff9d
commit 8238dbd35e
2 files changed
+71

No files matched your search

+9
View File
@@ -23,10 +23,19 @@ when:
branch: main
- event: tag
# The licensing audit gates the build for the same reason the vuln scan does:
# a non-permissive dependency is a legal defect in the artifact, so it must not
# reach the registry. e2e is deliberately absent — it is non-gating.
#
# These four all declare `event: [push, pull_request]`, so none of them run on a
# tag; Woodpecker treats a filtered-out dependency as satisfied, which is why a
# tag build publishes without re-running them. It is riding on the checks that
# already passed for that commit on main.
depends_on:
- test
- lint
- vuln
- licenses
variables:
# Keep the builder image, GOTOOLCHAIN, and the ko version in sync with the
+62
View File
@@ -142,3 +142,65 @@ func TestReleasePinsAreConsistent(t *testing.T) {
t.Errorf("ko version differs: mise %q, .woodpecker/build.yaml %q", koPin, koCI)
}
}
// nonGatingWorkflows are the .woodpecker workflows that are deliberately not
// prerequisites of the build: build and deploy are the thing being gated, and
// e2e is non-gating on purpose (it skips when no browser container is up, so
// requiring it would make releases depend on an advisory check).
var nonGatingWorkflows = map[string]bool{
"build": true,
"deploy": true,
"e2e": true,
}
// TestBuildDependsOnEveryGatingWorkflow catches a whole class of quiet failure:
// a check that runs, goes red, and is ignored because nothing depends on it.
// The licensing audit sat in exactly that state — failing on every push while
// images kept building and deploying, because build.yaml listed only test, lint
// and vuln. Adding a new check is easy to do without wiring it in, and the
// symptom is invisible (a red workflow next to a green deploy), so assert the
// wiring instead of trusting it.
func TestBuildDependsOnEveryGatingWorkflow(t *testing.T) {
root := repoRoot(t)
entries, err := os.ReadDir(filepath.Join(root, ".woodpecker"))
if err != nil {
t.Fatalf("read .woodpecker: %v", err)
}
build := readRepoFile(t, ".woodpecker/build.yaml")
// The depends_on block runs to the next top-level key.
dependsRE := regexp.MustCompile(`(?ms)^depends_on:\s*\n((?:\s*-\s*\S+\s*\n)+)`)
m := dependsRE.FindStringSubmatch(build)
if m == nil {
t.Fatal(".woodpecker/build.yaml: found no depends_on list")
}
declared := map[string]bool{}
for _, line := range strings.Split(strings.TrimSpace(m[1]), "\n") {
declared[strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "-"))] = true
}
found := 0
for _, e := range entries {
name, ok := strings.CutSuffix(e.Name(), ".yaml")
if e.IsDir() || !ok || nonGatingWorkflows[name] {
continue
}
found++
if !declared[name] {
t.Errorf(".woodpecker/%s.yaml is a check but build.yaml does not depend on it, "+
"so it can fail while the image still builds and deploys. Add it to depends_on, "+
"or to nonGatingWorkflows with a reason.", name)
}
}
if found == 0 {
t.Error("found no gating workflows in .woodpecker — has the CI layout changed?")
}
// A name in depends_on that matches no workflow file is silently ignored by
// Woodpecker, which looks like a gate but is not one.
for name := range declared {
if _, err := os.Stat(filepath.Join(root, ".woodpecker", name+".yaml")); err != nil {
t.Errorf("build.yaml depends on %q, which is not a .woodpecker workflow", name)
}
}
}