From 54831cf92ebe5cac9be82b6b18d4ff26675c8888 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Tue, 16 Jun 2026 23:30:55 -0400 Subject: [PATCH] fix(e2e): anchor localize verify to the action ref it rewrites The post-localize verify grepped for a bare stablekernel/cascade substring, which also matches the verbatim cross-repo callback ref stablekernel/cascade-example-artifact-a/.github/workflows/...@main in the cross-repo scenario. That ref is intentionally left untouched, so verify flagged it as un-localized and failed repo staging after three attempts, failing the e2e suite deterministically. Anchor the verify pattern to the exact action ref the sed rewrites (stablekernel/cascade/.github/actions/) so a sibling-repo callback is no longer mistaken for an un-localized ref. Signed-off-by: Joshua Temple --- e2e/harness/harness.go | 21 ++++++++++- e2e/harness/localize_test.go | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/e2e/harness/harness.go b/e2e/harness/harness.go index 48e991d7..f86b1e76 100644 --- a/e2e/harness/harness.go +++ b/e2e/harness/harness.go @@ -869,6 +869,16 @@ const actionLocalizeSedExpr = `s|stablekernel/cascade/\.github/actions/\([^@]*\) // gaining a second `./` prefix. const usesLocalizeSedExpr = `s|uses: \([^./@][^/@]*\.yaml\)|uses: ./\1|g` +// unlocalizedActionRefPattern is the extended-regex the post-localize verify +// step greps for. It matches only the action ref the sed actually rewrites, +// `stablekernel/cascade/.github/actions/`, so a verbatim cross-repo callback +// such as `stablekernel/cascade-example-artifact-a/.github/workflows/...@ref` +// is not mistaken for an un-localized ref. The trailing `/.github/actions/` +// anchor is what distinguishes the localizable action ref (slash after +// `cascade`) from the distinct `cascade-example-artifact-a` repo (hyphen after +// `cascade`). +const unlocalizedActionRefPattern = `stablekernel/cascade/\.github/actions/` + func (h *Harness) localizeWorkflows(ctx context.Context) error { const maxAttempts = 3 const retryDelay = 200 * time.Millisecond @@ -883,9 +893,18 @@ func (h *Harness) localizeWorkflows(ctx context.Context) error { } // grep -l exits 0 on match (= un-localized ref still present, which we // treat as a failure to localize). Negation gives us 0 = clean. + // + // The pattern is anchored to the exact ref the sed rewrites, + // `stablekernel/cascade/.github/actions/`, rather than a bare + // `stablekernel/cascade` substring. A bare substring also matches a + // cross-repo reusable-workflow callback such as + // `stablekernel/cascade-example-artifact-a/.github/workflows/...@ref` + // (scenario 21), which is an intentional verbatim ref the sed leaves + // untouched. Treating that as un-localized made verify fail deterministically + // for every scenario that wires a cross-repo callback. verify := []string{ "bash", "-c", - "cd /tmp/repo && ! grep -l 'stablekernel/cascade' .github/workflows/*.yaml", + "cd /tmp/repo && ! grep -lE '" + unlocalizedActionRefPattern + "' .github/workflows/*.yaml", } var lastErr error diff --git a/e2e/harness/localize_test.go b/e2e/harness/localize_test.go index a558cbc1..70041958 100644 --- a/e2e/harness/localize_test.go +++ b/e2e/harness/localize_test.go @@ -23,6 +23,74 @@ func runSed(t *testing.T, expr, in string) string { return string(out) } +// grepMatches reports whether `grep -lE ` finds the pattern in in, +// exercising the exact extended-regex the post-localize verify runs inside the +// act container. grep exits 0 on a match (un-localized ref present) and 1 on no +// match (clean); any other exit is a hard failure. +func grepMatches(t *testing.T, pattern, in string) bool { + t.Helper() + grep, err := exec.LookPath("grep") + if err != nil { + t.Skipf("grep not available: %v", err) + } + cmd := exec.Command(grep, "-lE", pattern) + cmd.Stdin = strings.NewReader(in) + err = cmd.Run() + if err == nil { + return true + } + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return false + } + t.Fatalf("grep -lE %q failed: %v", pattern, err) + return false +} + +// TestUnlocalizedActionRefPattern_IgnoresCrossRepoCallback locks in that the +// post-localize verify only flags the action ref the sed rewrites and never the +// distinct cross-repo reusable-workflow callback repo. A bare `stablekernel/cascade` +// substring also matched `stablekernel/cascade-example-artifact-a/...@ref` +// (scenario 21), so every scenario wiring a cross-repo callback failed staging +// with `localize verify found un-localized refs`. +func TestUnlocalizedActionRefPattern_IgnoresCrossRepoCallback(t *testing.T) { + tests := []struct { + name string + in string + wantMatch bool + }{ + { + name: "un-localized action ref is flagged", + in: " uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0\n", + wantMatch: true, + }, + { + name: "localized action ref is clean", + in: " uses: ./.github/actions/setup-cli\n", + wantMatch: false, + }, + { + name: "cross-repo callback to a sibling repo is not flagged", + in: " uses: stablekernel/cascade-example-artifact-a/.github/workflows/build-shared.yaml@main\n", + wantMatch: false, + }, + { + name: "cross-repo reusable workflow in the cascade repo itself is not flagged", + in: " uses: stablekernel/cascade/.github/workflows/x.yaml@v1\n", + wantMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := grepMatches(t, unlocalizedActionRefPattern, tt.in) + if got != tt.wantMatch { + t.Errorf("grep -lE %q over %q: got match=%v, want %v", + unlocalizedActionRefPattern, tt.in, got, tt.wantMatch) + } + }) + } +} + // TestUsesLocalizeSedExpr_Idempotent_LeavesQualifiedPathsUnchanged verifies the // reusable-workflow localizer prefixes a bare ref with `./` but never adds a // second `./` to an already-qualified path or mangles a cross-repo `@ref`.