Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 56 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,50 @@ concurrency:
cancel-in-progress: true

jobs:
# Full suites produce no path outputs. Run them independently, but require
# their success before production access and the final merge verdict.
validate-go-regressions:
name: 🧪 Validate Complete Go Regressions
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: 📑 Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0

- name: ⚙️ Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false

- name: ⚙️ Initialize the approved kubectl for native regressions
timeout-minutes: 5
shell: bash
env:
KUBECTL_VERSION: "v1.36.2"
KUBECTL_SHA256: "1e9045ec32bea85da43de85f0065358529ea7c7a152eca78154fba5b58c27d82"
run: |
set -euo pipefail
kubectl_path="$RUNNER_TEMP/kubectl"
curl -fsSL \
"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" \
-o "$kubectl_path"
printf '%s %s\n' "$KUBECTL_SHA256" "$kubectl_path" | sha256sum --check
sudo install "$kubectl_path" /usr/local/bin/kubectl
echo /usr/local/bin >> "$GITHUB_PATH"
export PATH="/usr/local/bin:$PATH"
kubectl version --client

- name: 🧪 Validate the complete Go helper regression suite
run: go test ./...

- name: 🧪 Validate native retirement result classification
run: go -C scripts/retire-backstage-stateless/poller-native test -race -mod=readonly -count=1 ./...

changes:
name: 🔍 Detect Changes
runs-on: ubuntu-latest
Expand Down Expand Up @@ -61,12 +105,6 @@ jobs:
export PATH="/usr/local/bin:$PATH"
kubectl version --client

- name: 🧪 Validate the complete Go helper regression suite
run: go test ./...

- name: 🧪 Validate native retirement result classification
run: go -C scripts/retire-backstage-stateless/poller-native test -race -mod=readonly -count=1 ./...

- name: 💾 Validate storage audit regressions
run: |
go test ./scripts/audit-storage-retention
Expand Down Expand Up @@ -3598,6 +3636,7 @@ jobs:
needs:
[
changes,
validate-go-regressions,
validate-consumer-discovery,
validate-floating-image-tags,
validate-helm-post-renderers,
Expand Down Expand Up @@ -3956,6 +3995,7 @@ jobs:
needs:
[
changes,
validate-go-regressions,
validate-consumer-discovery,
validate-publication-contract,
validate-matcher-efficacy,
Expand All @@ -3980,6 +4020,7 @@ jobs:
with:
job-results: >-
${{ needs.changes.result }}
${{ needs.validate-go-regressions.result }}
${{ needs.validate-consumer-discovery.result }}
${{ needs.validate-publication-contract.result }}
${{ needs.validate-matcher-efficacy.result }}
Expand All @@ -3996,6 +4037,15 @@ jobs:
${{ needs.validate-ghcr-fanout-merge-group.result }}
${{ needs.deploy-prod.result }}

- name: 🧪 Require complete Go regressions
env:
REGRESSION_RESULT: ${{ needs.validate-go-regressions.result }}
run: |
if [[ "$REGRESSION_RESULT" != success ]]; then
echo "The unconditional Go regression job must succeed, got: $REGRESSION_RESULT" >&2
exit 1
fi

- name: ⚖️ Require selected policy validation
env:
EVENT_NAME: ${{ github.event_name }}
Expand Down
2 changes: 1 addition & 1 deletion scripts/guard-arc-recovery/ci_dependency_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestNativeSuiteInstallsTheApprovedKubectlFirst(t *testing.T) {
if err := yaml.Unmarshal(data, &workflow); err != nil {
t.Fatal(err)
}
steps := workflow.Jobs["changes"].Steps
steps := workflow.Jobs["validate-go-regressions"].Steps
authorization := workflow.Jobs["validate-eks-authorization"].Steps
if err := nativeKubectlDependency(steps, authorization); err != nil {
t.Fatal(err)
Expand Down
2 changes: 2 additions & 0 deletions scripts/validate-dr-signing/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,7 @@ func TestMergeQueueContractGateIsEnforced(t *testing.T) {
deployNeeds = ` needs:
[
changes,
validate-go-regressions,
validate-consumer-discovery,
validate-floating-image-tags,
validate-helm-post-renderers,
Expand All @@ -1613,6 +1614,7 @@ func TestMergeQueueContractGateIsEnforced(t *testing.T) {
deployNeedsWithoutGate = ` needs:
[
changes,
validate-go-regressions,
validate-consumer-discovery,
validate-floating-image-tags,
validate-helm-post-renderers,
Expand Down
137 changes: 137 additions & 0 deletions scripts/validate-merge-group-heal/go_regressions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"os"
"os/exec"
"slices"
"strings"
"testing"

"gopkg.in/yaml.v3"
)

type regressionRunDefaults struct {
Run struct {
WorkingDirectory string `yaml:"working-directory"`
} `yaml:"run"`
}

type regressionWorkflow struct {
Defaults regressionRunDefaults `yaml:"defaults"`
Jobs map[string]struct {
Defaults regressionRunDefaults `yaml:"defaults"`
If string `yaml:"if"`
Needs []string `yaml:"needs"`
Continue any `yaml:"continue-on-error"`
Permissions map[string]string `yaml:"permissions"`
Steps []struct {
Name string `yaml:"name"`
If string `yaml:"if"`
Run string `yaml:"run"`
Continue any `yaml:"continue-on-error"`
Env map[string]string `yaml:"env"`
WorkingDirectory string `yaml:"working-directory"`
} `yaml:"steps"`
} `yaml:"jobs"`
}

// readRegressionWorkflow loads the actual CI workflow rather than a separate fixture.
func readRegressionWorkflow(t *testing.T) regressionWorkflow {
t.Helper()
data, err := os.ReadFile("../../.github/workflows/ci.yaml")
if err != nil {
t.Fatal(err)
}
var workflow regressionWorkflow
if err := yaml.Unmarshal(data, &workflow); err != nil {
t.Fatal(err)
}
return workflow
}

// Full-suite work is independent of path classification. Serializing it ahead
// of changes delays every selected validator without adding a safety barrier.
func TestFullGoRegressionsRunIndependently(t *testing.T) {
workflow := readRegressionWorkflow(t)
job, found := workflow.Jobs["validate-go-regressions"]
if !found {
t.Fatal("full Go regressions need an independent validation job")
}
if len(job.Needs) != 0 || job.If != "" || job.Continue != nil {
t.Fatal("full regressions must start independently and run unconditionally")
}
if len(job.Permissions) != 1 || job.Permissions["contents"] != "read" {
t.Fatal("the regression runner needs only read-only checkout permission")
}
for _, command := range []string{
"go test ./...",
"go -C scripts/retire-backstage-stateless/poller-native test -race -mod=readonly -count=1 ./...",
} {
found := false
for name, candidate := range workflow.Jobs {
for _, step := range candidate.Steps {
if strings.TrimSpace(step.Run) != command {
continue
}
if name != "validate-go-regressions" || step.If != "" || step.Continue != nil {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
t.Fatalf("complete suite must run once, unconditionally in its independent job: %s", command)
}
for _, directory := range []string{
workflow.Defaults.Run.WorkingDirectory,
candidate.Defaults.Run.WorkingDirectory,
step.WorkingDirectory,
} {
if directory != "" && directory != "." && directory != "./" {
t.Fatalf("complete suite must run from repository root, not %q: %s", directory, command)
}
}
if found {
t.Fatalf("duplicate complete-suite invocation: %s", command)
}
found = true
}
}
if !found {
t.Fatalf("complete suite was removed or narrowed: %s", command)
}
}
for _, name := range []string{"deploy-prod", "ci-required-checks"} {
if !slices.Contains(workflow.Jobs[name].Needs, "validate-go-regressions") {
t.Fatalf("%s must wait for full Go regressions", name)
}
}
deploy := workflow.Jobs["deploy-prod"]
if deploy.Continue != nil || strings.Contains(deploy.If, "always(") || strings.Contains(deploy.If, "cancelled(") || strings.Contains(deploy.If, "failure(") {
t.Fatal("deployment must not bypass failed or skipped dependencies")
}
}

// Execute the actual aggregate guard: GitHub considers some skipped checks
// successful, but an unconditional full suite has no legitimate skipped result.
func TestFullGoRegressionsRejectEveryNonSuccessResult(t *testing.T) {
workflow := readRegressionWorkflow(t)
gate := workflow.Jobs["ci-required-checks"]
if gate.If != "always()" {
t.Fatal("the aggregate must still run after a failed dependency")
}
for _, step := range gate.Steps {
if step.Name != "🧪 Require complete Go regressions" {
continue
}
if step.If != "" || step.Continue != nil || step.Env["REGRESSION_RESULT"] != "${{ needs.validate-go-regressions.result }}" {
t.Fatal("full-suite result enforcement must be unconditional and bind the real job")
}
for _, result := range []string{"success", "failure", "cancelled", "skipped", "", "unknown"} {
t.Run(result, func(t *testing.T) {
command := exec.Command("bash", "-e", "-o", "pipefail", "-c", step.Run)
command.Env = append(os.Environ(), "REGRESSION_RESULT="+result)
output, err := command.CombinedOutput()
if (err == nil) != (result == "success") {
t.Fatalf("result %q: error=%v, output=%s", result, err, output)
}
})
}
return
}
t.Fatal("required aggregate has no full-suite result enforcement")
}
Loading