From e682eaa21d437093d4bd84f92db2165d07dcca81 Mon Sep 17 00:00:00 2001 From: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:03:50 +0000 Subject: [PATCH] fix(#1960): capture repo-maintenance workflow logs in e2e unenrollment test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add logRepoMaintenanceRun helper to the e2e unenrollment phase that finds the repo-maintenance workflow run dispatched during repo disablement, waits for it to complete, and logs its full output (status, conclusion, workflow logs, and artifacts). This runs unconditionally — not just on failure — so that when the removal PR never appears, the e2e test output contains the workflow's decision-making context: whether it tried to create the PR and failed, or decided not to create one at all. The helper follows the same patterns used by runTriageDispatchSmokeTest for its workflow log capture: polling for the run by CreatedAt, fetching logs via GetWorkflowRunLogs, and downloading artifacts via downloadRunArtifacts. Note: go test and make lint could not run in the sandbox (requires Go 1.26, sandbox has Go 1.24). pre-commit also failed due to Go toolchain download permissions. Manual verification required. Closes #1960 --- e2e/admin/admin_test.go | 104 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/e2e/admin/admin_test.go b/e2e/admin/admin_test.go index 4fb0916a3e..fbc24b386f 100644 --- a/e2e/admin/admin_test.go +++ b/e2e/admin/admin_test.go @@ -577,6 +577,9 @@ func runUnenrollmentTest(t *testing.T, env *e2eEnv) { t.Helper() ctx := context.Background() + // Record the time before disabling so we can find the workflow run. + disableTime := time.Now() + // Disable the test repo via CLI (updates config.yaml). The CLI now // watches the repo-maintenance workflow to completion before returning, // so the removal PR should already exist when this returns. @@ -584,6 +587,11 @@ func runUnenrollmentTest(t *testing.T, env *e2eEnv) { "admin", "disable", "repos", env.org, testRepo, "--yolo") t.Logf("Disable repos output:\n%s", output) + // Fetch and log repo-maintenance workflow output for debugging (#1960). + // The CLI dispatches repo-maintenance when disabling repos — capture its + // logs regardless of outcome so we can see what it did. + logRepoMaintenanceRun(t, env, disableTime) + // The CLI waited for repo-maintenance, so the removal PR should exist. // A few retries handle GitHub eventual consistency. var removalPR *forge.ChangeProposal @@ -617,3 +625,99 @@ func runUnenrollmentTest(t *testing.T, env *e2eEnv) { require.True(t, forge.IsNotFound(err), "shim should be removed from %s after unenrollment", testRepo) t.Log("Verified shim is gone") } + +// logRepoMaintenanceRun finds the most recent repo-maintenance workflow run +// created after the given time, waits for it to complete if still in progress, +// and logs its output (status, conclusion, logs, artifacts). This provides +// visibility into what the workflow did so that failures in the unenrollment +// flow can be diagnosed from e2e test output alone. +func logRepoMaintenanceRun(t *testing.T, env *e2eEnv, afterTime time.Time) { + t.Helper() + ctx := context.Background() + + // Find the repo-maintenance run dispatched after afterTime. + var maintenanceRun *forge.WorkflowRun + for attempt := 0; attempt < 6; attempt++ { + if attempt > 0 { + time.Sleep(5 * time.Second) + } + runs, err := env.client.ListWorkflowRuns(ctx, env.org, forge.ConfigRepoName, "repo-maintenance.yml") + if err != nil { + t.Logf("[repo-maintenance] Attempt %d: error listing runs: %v", attempt+1, err) + continue + } + for _, run := range runs { + runTime, parseErr := time.Parse(time.RFC3339, run.CreatedAt) + if parseErr != nil { + continue + } + if runTime.Before(afterTime) { + continue + } + r := run + maintenanceRun = &r + break + } + if maintenanceRun != nil { + break + } + t.Logf("[repo-maintenance] Attempt %d: no run found after %s", attempt+1, afterTime.Format(time.RFC3339)) + } + + if maintenanceRun == nil { + t.Log("[repo-maintenance] WARNING: no repo-maintenance workflow run found — cannot capture logs") + return + } + + t.Logf("[repo-maintenance] Found run %d (status: %s, conclusion: %s, created: %s)", + maintenanceRun.ID, maintenanceRun.Status, maintenanceRun.Conclusion, maintenanceRun.CreatedAt) + + // Wait for completion if still in progress (up to 3 minutes). + if maintenanceRun.Status != "completed" { + t.Logf("[repo-maintenance] Run %d still in progress, waiting for completion...", maintenanceRun.ID) + deadline := time.Now().Add(3 * time.Minute) + for time.Now().Before(deadline) { + time.Sleep(10 * time.Second) + run, err := env.client.GetWorkflowRun(ctx, env.org, forge.ConfigRepoName, maintenanceRun.ID) + if err != nil { + t.Logf("[repo-maintenance] Error polling run: %v", err) + continue + } + t.Logf("[repo-maintenance] Run %d: status=%s conclusion=%s", run.ID, run.Status, run.Conclusion) + if run.Status == "completed" { + maintenanceRun = run + break + } + } + if maintenanceRun.Status != "completed" { + t.Log("[repo-maintenance] WARNING: run did not complete within 3 minutes") + } + } + + runURL := fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d", + env.org, forge.ConfigRepoName, maintenanceRun.ID) + t.Logf("[repo-maintenance] Run URL: %s", runURL) + fmt.Fprintf(os.Stderr, "::notice::repo-maintenance run %d: conclusion=%s url=%s\n", + maintenanceRun.ID, maintenanceRun.Conclusion, runURL) + + // Download and log workflow logs. + logs, err := env.client.GetWorkflowRunLogs(ctx, env.org, forge.ConfigRepoName, maintenanceRun.ID) + if err != nil { + t.Logf("[repo-maintenance] Could not fetch run logs: %v", err) + } else { + debugDir := filepath.Join(env.screenshotDir, fmt.Sprintf("repo-maintenance-run-%d", maintenanceRun.ID)) + _ = os.MkdirAll(debugDir, 0o755) + logPath := filepath.Join(debugDir, "workflow-logs.txt") + if writeErr := os.WriteFile(logPath, []byte(logs), 0o644); writeErr != nil { + t.Logf("[repo-maintenance] Could not write logs to %s: %v", logPath, writeErr) + } else { + fmt.Fprintf(os.Stderr, "::notice file=%s::repo-maintenance run %d workflow logs saved\n", logPath, maintenanceRun.ID) + } + t.Logf("[repo-maintenance] Workflow run logs:\n%s", logs) + } + + // Download run artifacts. + debugDir := filepath.Join(env.screenshotDir, fmt.Sprintf("repo-maintenance-run-%d", maintenanceRun.ID)) + _ = os.MkdirAll(debugDir, 0o755) + downloadRunArtifacts(ctx, env.token, env.org, forge.ConfigRepoName, maintenanceRun.ID, debugDir, t) +}