From 2b433bca26c2129173f9ee95bf7ccefa0aff81da Mon Sep 17 00:00:00 2001 From: Jonathon Leight Date: Sat, 8 Aug 2026 19:57:06 -0400 Subject: [PATCH] Fix failing woodpecker pipeline --- .woodpecker/build.yaml | 16 ++++++++--- internal/licenses/reproducible_test.go | 40 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index b16647d..4ebcc32 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -109,10 +109,18 @@ steps: deploy: image: alpine/kubectl:1.35.3 commands: - # No ${...} around shell variables anywhere in this step: Woodpecker does - # its own ${VAR} substitution before the shell runs, so "${IMAGE##*@}" - # would be replaced with an empty string rather than reaching bash. $(…) - # and bare $VAR are untouched, so the digest is cut with a command instead. + # Shell variables stay bare ($IMAGE, never brace-wrapped): Woodpecker runs + # its own substitution over this file before the shell sees it, and it + # replaces a brace-wrapped name with the CI value — empty, for a variable + # only the shell knows. $(…) and bare $VAR are passed through untouched, so + # the digest is cut with a command rather than a brace expansion. + # + # That substitution reads the whole file, comments included, and rejects + # anything brace-wrapped that isn't a valid variable name — which is why + # this comment describes the trap instead of quoting it. A malformed one + # fails the pipeline before any step runs, with "unable to parse variable + # name" and no indication of where. TestWoodpeckerVariablesAreParseable + # catches it locally instead. - IMAGE="$(cat image-ref)" - DIGEST="$(cut -d@ -f2 image-ref)" # Fail loudly rather than deploying with an empty/garbled digest: the app diff --git a/internal/licenses/reproducible_test.go b/internal/licenses/reproducible_test.go index 297522e..5e09c8c 100644 --- a/internal/licenses/reproducible_test.go +++ b/internal/licenses/reproducible_test.go @@ -273,3 +273,43 @@ func TestImageDigestEnvVarMatchesConfig(t *testing.T) { t.Errorf(".woodpecker/build.yaml does not set %s, the variable internal/config reads", name) } } + +// TestWoodpeckerVariablesAreParseable catches a failure with an unusually bad +// signal-to-effort ratio: Woodpecker substitutes variables over the RAW pipeline +// file — comments included — before parsing any YAML. A brace-wrapped token that +// isn't a valid variable name kills the whole pipeline with "unable to parse +// variable name", naming neither the file nor the line, and no step ever runs. A +// comment that merely *describes* the syntax is enough to trigger it. +// +// So: every ${…} in .woodpecker must open with something that could actually be +// a variable name. Bash operators after the name (${VAR##glob}, ${VAR:-default}) +// are fine — only the name itself is checked. +func TestWoodpeckerVariablesAreParseable(t *testing.T) { + root := repoRoot(t) + entries, err := os.ReadDir(filepath.Join(root, ".woodpecker")) + if err != nil { + t.Fatalf("read .woodpecker: %v", err) + } + + braced := regexp.MustCompile(`\$\{([^}]*)\}`) + validName := regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*`) + checked := 0 + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".yaml") { + continue + } + rel := filepath.Join(".woodpecker", e.Name()) + body := readRepoFile(t, rel) + for _, m := range braced.FindAllStringSubmatch(body, -1) { + checked++ + if !validName.MatchString(m[1]) { + t.Errorf("%s: %q is not a parseable variable reference. Woodpecker rejects the "+ + "whole pipeline for this — including in comments, so describe the syntax "+ + "rather than quoting it.", rel, m[0]) + } + } + } + if checked == 0 { + t.Error("found no ${...} references in .woodpecker — has the CI layout changed?") + } +}