Skip to content

test: add e2e test for repos lifecycle across GitHub and GitLab - #5656

Open
ggallen wants to merge 1 commit into
fullsend-ai:mainfrom
ggallen:worktree-repos-e2e-test
Open

test: add e2e test for repos lifecycle across GitHub and GitLab#5656
ggallen wants to merge 1 commit into
fullsend-ai:mainfrom
ggallen:worktree-repos-e2e-test

Conversation

@ggallen

@ggallen ggallen commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Add TestReposLifecycle e2e test exercising all repos CLI commands (init, add, remove, status, install dry-run, diff, upgrade-mint) against real GitHub and GitLab APIs with ephemeral repos
  • Add RunCLIWithEnv/TryRunCLIWithEnv helpers for multi-token subprocess execution
  • Wire GITLAB_TOKEN into CI workflow and add internal/repos/** to e2e path filters
  • Test skips gracefully when tokens are unavailable — no impact on existing CI until REPOS_E2E_GITLAB_TOKEN secret is provisioned (already done)

Test plan

  • go build -tags e2e ./e2e/admin/ compiles cleanly
  • go vet -tags e2e ./e2e/admin/ passes
  • TestReposLifecycle passes locally with both GITHUB_TOKEN and GITLAB_TOKEN set
  • Test skips when GITLAB_TOKEN is not set
  • Ephemeral repos are cleaned up on both success and failure
  • CI run passes (test will run once REPOS_E2E_GITLAB_TOKEN secret is active)

🤖 Generated with Claude Code

@ggallen
ggallen requested a review from a team as a code owner July 27, 2026 21:59
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add repos lifecycle e2e test spanning GitHub + GitLab

🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add an end-to-end test that exercises all repos CLI commands against real GitHub/GitLab.
• Introduce CLI test helpers to run subprocesses with multiple auth tokens.
• Wire GitLab token into CI and expand e2e path filters to include repos codepaths.
Diagram

sequenceDiagram
  participant T as "Go e2e test"
  participant E as "e2etest helpers"
  participant C as "fullsend CLI"
  participant M as "repos.yaml"
  participant GH as "GitHub API"
  participant GL as "GitLab API"

  T->>E: Build CLI binary
  T->>GH: Create ephemeral repos
  T->>GL: Create ephemeral projects

  T->>C: repos init (github/gitlab)
  C->>M: Write manifests

  T->>C: repos add/remove/status/install --dry-run/diff/upgrade-mint
  C->>M: Read/update manifest
  C->>GH: Probe repo state (variables, discovery)
  C->>GL: Probe repo state (variables, discovery)

  T->>GH: Cleanup ephemeral repos
  T->>GL: Cleanup ephemeral projects
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Split into two independent tests (GitHub-only, GitLab-only)
  • ➕ Reduces blast radius when one forge is flaky or rate-limited
  • ➕ Simplifies debugging and reruns (single-forge focus)
  • ➕ Allows enabling GitLab coverage in CI without coupling to GitHub
  • ➖ More duplicated setup/teardown logic
  • ➖ Less coverage of mixed-forge manifest behavior in one flow
2. Mock forge APIs instead of hitting live GitHub/GitLab
  • ➕ Deterministic and fast; avoids token management and rate limits
  • ➕ Safe for forks/PRs without secrets
  • ➖ Does not validate real API behavior, permissions, or eventual consistency
  • ➖ Higher maintenance to keep mocks aligned with real forges
3. Run against pre-provisioned repos rather than creating ephemeral ones
  • ➕ Less API churn; fewer create/delete operations
  • ➕ Potentially faster test runtime
  • ➖ State leakage between runs is easy (flakes from leftover variables/settings)
  • ➖ Harder to guarantee a clean baseline for status/diff assertions

Recommendation: Keep the current approach (ephemeral repos + real APIs) because it validates the actual repos CLI lifecycle and forge integration end-to-end, and it already mitigates risk by skipping when tokens are absent and cleaning up stale/leftover repos. If CI flakiness appears, the best next step is splitting GitHub and GitLab into separate tests rather than switching to mocks.

Files changed (5) +548 / -2

Enhancement (1) +43 / -0
testutil.goAdd CLI runners that support custom subprocess environment +43/-0

Add CLI runners that support custom subprocess environment

• Adds 'RunCLIWithEnv' and 'TryRunCLIWithEnv' to execute the compiled CLI with arbitrary environment variables (always setting 'CI=true'). Enables tests to supply multiple tokens (GitHub + GitLab) without overloading the single-token helpers.

pkg/e2etest/testutil.go

Tests (2) +497 / -0
repos_helpers.goAdd helpers for ephemeral forge repos and manifest/assert parsing +284/-0

Add helpers for ephemeral forge repos and manifest/assert parsing

• Adds setup utilities to resolve tokens, build a per-test environment, and proactively cleanup stale ephemeral repos in the configured GitHub org and GitLab group. Provides helpers to create/delete ephemeral repos, read/write manifests, and parse 'repos status' / 'repos diff' JSON output.

e2e/admin/repos_helpers.go

repos_test.goAdd 'TestReposLifecycle' covering full repos CLI command flow +213/-0

Add 'TestReposLifecycle' covering full repos CLI command flow

• Implements a multi-phase e2e test that creates ephemeral GitHub/GitLab repos, runs 'repos init/add/status/install --dry-run/diff/remove/upgrade-mint', and validates manifest mutations and basic API probing behavior. Skips when tokens are missing and relies on 't.Cleanup' to remove ephemeral resources.

e2e/admin/repos_test.go

Other (2) +8 / -2
e2e.ymlRun e2e when repos code changes and pass GitLab token secret +4/-1

Run e2e when repos code changes and pass GitLab token secret

• Expands the e2e path/grep filters to include repos-related CLI and internal repos packages. Adds 'GITLAB_TOKEN' env wiring from 'REPOS_E2E_GITLAB_TOKEN' to support GitLab-backed e2e runs.

.github/workflows/e2e.yml

MakefileAdd 'repos-e2e-test' target for focused lifecycle run +4/-1

Add 'repos-e2e-test' target for focused lifecycle run

• Introduces a dedicated make target that runs only 'TestReposLifecycle' under the e2e build tag. Keeps the existing broader 'e2e-test' target unchanged.

Makefile

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 27, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:00 PM UTC · Completed 10:12 PM UTC
Commit: a07a65f · View workflow run →

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.37500% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/e2etest/testutil.go 90.00% 2 Missing and 1 partial ⚠️
pkg/e2etest/lock.go 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@qodo-code-review

qodo-code-review Bot commented Jul 27, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 54 rules

Grey Divider


Action required

1. Deletes concurrent run repos ✓ Resolved 🐞 Bug ☼ Reliability
Description
cleanupStaleEphemeralRepos deletes *all* repos/projects in the shared GitHub org/GitLab group
whose name starts with repos-e2e-, so concurrent E2E runs can delete each other’s active ephemeral
repos and cause flaky failures/unintended deletions.
Code

e2e/admin/repos_helpers.go[R143-175]

+func cleanupStaleEphemeralRepos(t *testing.T, env *reposTestEnv) {
+	t.Helper()
+	ctx := context.Background()
+
+	ghRepos, err := env.ghClient.ListOrgRepos(ctx, env.ghOrg, true)
+	if err != nil {
+		t.Logf("[cleanup] failed to list GitHub repos in %s: %v", env.ghOrg, err)
+	} else {
+		for _, r := range ghRepos {
+			if !strings.HasPrefix(r.Name, ephemeralRepoPrefix) {
+				continue
+			}
+			t.Logf("[cleanup] Deleting leftover GitHub repo %s/%s", env.ghOrg, r.Name)
+			if delErr := env.ghClient.DeleteRepo(ctx, env.ghOrg, r.Name); delErr != nil {
+				t.Logf("[cleanup] failed to delete leftover repo: %v", delErr)
+			}
+		}
+	}
+
+	glRepos, err := env.glClient.ListOrgRepos(ctx, env.glGroup, true)
+	if err != nil {
+		t.Logf("[cleanup] failed to list GitLab projects in %s: %v", env.glGroup, err)
+	} else {
+		for _, r := range glRepos {
+			if !strings.HasPrefix(r.Name, ephemeralRepoPrefix) {
+				continue
+			}
+			t.Logf("[cleanup] Deleting leftover GitLab project %s/%s", env.glGroup, r.Name)
+			if delErr := env.glClient.DeleteRepo(ctx, env.glGroup, r.Name); delErr != nil {
+				t.Logf("[cleanup] failed to delete leftover project: %v", delErr)
+			}
+		}
+	}
Relevance

⭐⭐ Medium

Team has accepted safeguards around destructive deletes, but no direct precedent for cross-run
prefix cleanup races.

PR-#1215
PR-#5614

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The test targets a single shared org/group and deletes all repos/projects matching a global prefix,
while the workflow allows different PRs to run concurrently; this creates a cross-run deletion race.

e2e/admin/repos_helpers.go[21-26]
e2e/admin/repos_helpers.go[69-83]
e2e/admin/repos_helpers.go[143-175]
.github/workflows/e2e.yml[54-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`cleanupStaleEphemeralRepos` deletes every repo/project with the global `repos-e2e-` prefix in shared forge namespaces. Because CI concurrency is only per-PR (not global), multiple PRs can run E2E simultaneously and this setup-time cleanup can delete repos that another run is actively using.

## Issue Context
- The test uses shared constants (`fullsend-repos-e2e-gh` / `fullsend-repos-e2e-gl`) and a shared prefix (`repos-e2e-`).
- The cleanup predicate is only `HasPrefix(name, "repos-e2e-")`, with no run ownership/age check.

## How to Fix
Choose one of:
1) **Remove setup-time cleanup entirely** (safest) and rely on `t.Cleanup` for normal runs + a separate scheduled “janitor” workflow for stale repos.
2) **Make cleanup non-destructive for concurrent runs** by introducing an ownership marker in the repo name (e.g., `repos-e2e-<date>-<runID>-...`) and only deleting repos that are demonstrably stale (parse embedded timestamp) and older than a threshold.
3) **Introduce a shared lock** (GitHub/GitLab-side) around cleanup + creation so only one run can perform global cleanup at a time.

## Fix Focus Areas
- e2e/admin/repos_helpers.go[21-26]
- e2e/admin/repos_helpers.go[69-83]
- e2e/admin/repos_helpers.go[143-175]
- .github/workflows/e2e.yml[54-65]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Unbounded gh auth token ✓ Resolved 🐞 Bug ☼ Reliability
Description
resolveGitHubToken falls back to running gh auth token via e2etest.TryRunCLI, which uses
exec.Command(...).CombinedOutput() with no context/timeout; if gh prompts/stalls, the test can
hang until the outer go test/workflow timeout.
Code

e2e/admin/repos_helpers.go[R86-101]

+func resolveGitHubToken(t *testing.T) string {
+	t.Helper()
+	if token := os.Getenv("GH_TOKEN"); token != "" {
+		return token
+	}
+	if token := os.Getenv("GITHUB_TOKEN"); token != "" {
+		return token
+	}
+	out, err := e2etest.TryRunCLI("gh", "", "auth", "token")
+	if err == nil {
+		token := strings.TrimSpace(out)
+		if token != "" {
+			return token
+		}
+	}
+	t.Skip("no GitHub token available, skipping repos e2e test")
Relevance

⭐⭐⭐ High

Timeout/context for gh auth token was accepted previously to prevent hangs; team likely wants it
here too.

PR-#2277

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new test helper calls TryRunCLI("gh", ...) for token resolution;
TryRunCLI/tryRunCLIFromDir run subprocesses without a context deadline, while existing e2etest
auth code already demonstrates a timeout-based approach for the same gh auth token call.

e2e/admin/repos_helpers.go[86-101]
pkg/e2etest/testutil.go[440-468]
pkg/e2etest/auth.go[15-35]
PR-#2277

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resolveGitHubToken` invokes `gh auth token` through `e2etest.TryRunCLI`, and that helper runs subprocesses without a context deadline. If `gh` blocks (prompting for login, waiting on keychain, etc.), the E2E test can stall until the outer `go test -timeout`/workflow timeout.

## Issue Context
The repo already has a safer pattern in `pkg/e2etest/auth.go` where `gh auth token` is wrapped with `context.WithTimeout` and `exec.CommandContext`.

## How to Fix
- Replace the `e2etest.TryRunCLI("gh", ..., "auth", "token")` fallback with a direct `exec.CommandContext` call using a short timeout (e.g. 30s) and handle `context.DeadlineExceeded` by skipping with a clear message.
- Alternatively, add/extend an e2etest helper that runs arbitrary commands with a context/timeout and reuse it here.

## Fix Focus Areas
- e2e/admin/repos_helpers.go[86-101]
- pkg/e2etest/testutil.go[440-468]
- pkg/e2etest/auth.go[15-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Direct gh.LiveClient usage 📘 Rule violation ⌂ Architecture
Description
The new e2e test code directly constructs and uses forge-specific concrete clients
(*github.LiveClient, *gitlab.LiveClient) from outside internal/forge, bypassing the
forge.Client interface. This undermines the required forge-agnostic routing and makes it easier
for forge-specific behaviors to leak into other packages.
Code

e2e/admin/repos_helpers.go[R28-37]

+type reposTestEnv struct {
+	binary  string
+	ghToken string
+	glToken string
+	ghOrg   string
+	glGroup string
+
+	ghClient *gh.LiveClient
+	glClient *gl.LiveClient
+
Relevance

⭐ Low

Similar e2e/admin forge-abstraction routing suggestion was rejected; tests allowed GitHub-specific
logic.

PR-#4901

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062052 requires forge operations to be routed via forge.Client. The new test
environment stores *gh.LiveClient/*gl.LiveClient and later calls forge-specific methods like
ListOrgRepos/GetRepoVariable directly, which bypasses the forge.Client interface.

Rule 1062052: Route all git forge operations through forge.Client
e2e/admin/repos_helpers.go[28-37]
e2e/admin/repos_helpers.go[143-175]
e2e/admin/repos_test.go[175-183]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new e2e repos lifecycle test stores and calls methods on forge-specific concrete clients (`*gh.LiveClient`, `*gl.LiveClient`) outside `internal/forge`, rather than routing operations through `forge.Client`.

## Issue Context
Compliance requires all forge operations to flow through `forge.Client` to keep the rest of the codebase forge-agnostic.

## Fix Focus Areas
- e2e/admin/repos_helpers.go[28-37]
- e2e/admin/repos_helpers.go[143-175]
- e2e/admin/repos_test.go[175-183]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. GitHub token not env-only ✓ Resolved 📘 Rule violation ⛨ Security
Description
resolveGitHubToken() falls back to obtaining a GitHub token via gh auth token when environment
variables are unset, meaning sensitive configuration is not loaded exclusively from environment
variables. This can cause tests to run with implicit credentials rather than explicitly provided CI
secrets.
Code

e2e/admin/repos_helpers.go[R86-101]

+func resolveGitHubToken(t *testing.T) string {
+	t.Helper()
+	if token := os.Getenv("GH_TOKEN"); token != "" {
+		return token
+	}
+	if token := os.Getenv("GITHUB_TOKEN"); token != "" {
+		return token
+	}
+	out, err := e2etest.TryRunCLI("gh", "", "auth", "token")
+	if err == nil {
+		token := strings.TrimSpace(out)
+		if token != "" {
+			return token
+		}
+	}
+	t.Skip("no GitHub token available, skipping repos e2e test")
Relevance

⭐ Low

Env-only token sourcing not enforced; e2e token resolver kept gh auth token fallback despite
review.

PR-#2277

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062041 requires sensitive configuration (like tokens) to be loaded exclusively
from environment variables without defaults. The new resolveGitHubToken() explicitly falls back to
gh auth token when env vars are missing.

Rule 1062041: Load sensitive configuration exclusively from environment variables without defaults
e2e/admin/repos_helpers.go[86-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Sensitive auth token loading should be exclusively from environment variables with no non-empty defaults or implicit fallbacks.

## Issue Context
The new `resolveGitHubToken()` uses a local `gh auth token` fallback when `GH_TOKEN`/`GITHUB_TOKEN` are unset, which violates the policy and can introduce non-deterministic credential sourcing.

## Fix Focus Areas
- e2e/admin/repos_helpers.go[86-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. e2etest.TryRunCLI("gh") used ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
The new e2e test code shells out to the gh CLI outside internal/forge/github/, which violates
the restriction that GitHub CLI invocation must be confined to that package. This can also
leak/expand token-handling surface area outside the forge abstraction.
Code

e2e/admin/repos_helpers.go[R94-101]

+	out, err := e2etest.TryRunCLI("gh", "", "auth", "token")
+	if err == nil {
+		token := strings.TrimSpace(out)
+		if token != "" {
+			return token
+		}
+	}
+	t.Skip("no GitHub token available, skipping repos e2e test")
Relevance

⭐ Low

Restriction “gh CLI only in internal/forge/github” was explicitly rejected for e2e token resolution.

PR-#2277

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062053 restricts gh CLI execution to internal/forge/github/. The new helper
resolveGitHubToken() invokes e2etest.TryRunCLI("gh", ...), and TryRunCLI uses
exec.Command(binary, ...), meaning this change introduces an indirect gh CLI execution outside
the allowed path.

Rule 1062053: Restrict gh CLI exec.Command usage to internal/forge/github
e2e/admin/repos_helpers.go[94-101]
pkg/e2etest/testutil.go[440-463]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`e2e/admin/repos_helpers.go` calls `e2etest.TryRunCLI("gh", ..., "auth", "token")`, which indirectly executes the GitHub CLI outside the allowed directory (`internal/forge/github/`).

## Issue Context
Compliance requires that any `gh` CLI execution (direct or via wrappers) be restricted to `internal/forge/github/`.

## Fix Focus Areas
- e2e/admin/repos_helpers.go[86-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread e2e/admin/repos_helpers.go Outdated
Comment thread e2e/admin/repos_helpers.go Outdated
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [edge-case] e2e/admin/repos_helpers.go:206extractJSON finds the first { in CLI output and uses json.NewDecoder to extract the first JSON object. If a CLI log line contains a { character before the actual JSON output, it may decode the wrong object or fall back to returning everything from the first {. Unlikely in practice since CLI JSON output mode produces clean output, and this is a test helper.
  • [test-inadequate] pkg/e2etest/testutil_test.go:44 — Unit tests for RunCLIWithEnv and TryRunCLIWithEnv verify happy-path output but do not assert that extra environment variables are actually propagated to the subprocess. The test scripts are simple echo commands that ignore the environment. The real e2e tests would catch a regression (the CLI needs tokens to function), but the unit tests themselves do not exercise the distinguishing feature of these helpers.
    Remediation: Modify the test script to echo a specific env var (e.g., echo $GITLAB_TOKEN) and assert its value appears in the output.
  • [secrets handling] .github/workflows/e2e.yml:177REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN are exposed to all tests under ./e2e/admin/ via make e2e-test, not just TestReposLifecycle. A dedicated make target (repos-e2e-test) exists and could be used to narrow secret exposure. Matches the existing pattern (GCP credentials are similarly exposed), and blast radius is limited by the pull_request_target authorization gate and dedicated e2e orgs.
    Remediation: Consider splitting the CI step so that repos-specific tokens are only passed to make repos-e2e-test, keeping them out of the broader make e2e-test environment.
  • [cleanup-error-handling] e2e/admin/repos_helpers.go:125 — In createEphemeralGitHubRepo and createEphemeralGitLabRepo, cleanup failures are logged with t.Logf. The existing pattern in helpers.go:registerRepoCleanup uses t.Errorf for cleanup failures, which surfaces them as test failures rather than silent log lines.
    Remediation: Use t.Errorf instead of t.Logf for cleanup deletion failures to match registerRepoCleanup.
  • [error-handling-consistency] pkg/e2etest/testutil.goTryRunCLIWithEnv's error return uses double-wrapping: fmt.Errorf("[cli] fullsend %s failed: %w\n%s", ...) which includes both %w and the output string. Consistent with the existing tryRunCLIFromDir pattern but results in redundant output when the error is printed.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [edge-case] e2e/admin/repos_helpers.go:206extractJSON finds the first { in CLI output and uses json.NewDecoder to extract the first JSON object. If a CLI log line contains a { character before the actual JSON output, it may decode the wrong object or fall back to returning everything from the first {. Unlikely in practice since CLI JSON output mode produces clean output, and this is a test helper.
  • [test-inadequate] pkg/e2etest/testutil_test.go:44 — Unit tests for RunCLIWithEnv and TryRunCLIWithEnv verify happy-path output but do not assert that extra environment variables are actually propagated to the subprocess. The test scripts are simple echo commands that ignore the environment. The real e2e tests would catch a regression (the CLI needs tokens to function), but the unit tests themselves do not exercise the distinguishing feature of these helpers.
    Remediation: Modify the test script to echo a specific env var (e.g., echo $GITHUB_TOKEN) and assert its value appears in the output.
  • [secrets handling] .github/workflows/e2e.yml:175REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN are exposed to all tests under ./e2e/admin/ via make e2e-test, not just TestReposLifecycle. A dedicated make target (repos-e2e-test) exists and could be used to narrow secret exposure. Matches the existing pattern (GCP credentials are similarly exposed), and blast radius is limited by the pull_request_target authorization gate and dedicated e2e orgs.
    Remediation: Consider splitting the CI step so that repos-specific tokens are only passed to make repos-e2e-test, keeping them out of the broader make e2e-test environment.
  • [error-handling-consistency] pkg/e2etest/testutil.goTryRunCLIWithEnv's error return uses double-wrapping: fmt.Errorf("[cli] fullsend %s failed: %w\n%s", ...) which includes both %w and the output string. Consistent with the existing tryRunCLIFromDir pattern but results in redundant output when the error is printed.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [error handling] e2e/admin/repos_helpers.go:305getGitLabProjectCreatedAt uses http.DefaultClient with no timeout. The caller cleanupStaleEphemeralRepos passes context.Background() with no deadline, so a hanging GitLab API call could block indefinitely. Consistent with the existing codebase pattern (GetRepoCreatedAt in testutil.go uses the same approach).
    Remediation: Use http.Client{Timeout: 30 * time.Second} or add a timeout to the context passed from cleanupStaleEphemeralRepos.
  • [secrets handling] .github/workflows/e2e.yml:175REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN are exposed to all tests under ./e2e/admin/ via make e2e-test, not just TestReposLifecycle. A dedicated make target (repos-e2e-test) exists and could be used to narrow secret exposure. Matches the existing pattern (GCP credentials are similarly exposed), and blast radius is limited by the pull_request_target authorization gate and dedicated e2e orgs.
    Remediation: Consider splitting the CI step so that repos-specific tokens are only passed to make repos-e2e-test, keeping them out of the broader make e2e-test environment.
  • [edge-case] e2e/admin/repos_helpers.go:206extractJSON finds the first { in CLI output and uses json.NewDecoder to extract the first JSON object. If a CLI log line contains a { character before the actual JSON output, it may decode the wrong object or fall back to returning everything from the first {. Unlikely in practice since CLI JSON output mode produces clean output, and this is a test helper.
  • [test-inadequate] pkg/e2etest/testutil_test.go:44 — Unit tests for RunCLIWithEnv and TryRunCLIWithEnv verify happy-path output but do not assert that extra environment variables are actually propagated to the subprocess. The real e2e tests would catch a regression (the CLI needs tokens to function), but the unit tests themselves do not exercise the distinguishing feature of these helpers.
  • [naming-consistency] pkg/e2etest/testutil.go:392GetRepoCreatedAt was renamed from getRepoCreatedAt (exported) to support cross-package calls from e2e/admin/repos_helpers.go. The export is functionally necessary for the cross-package usage. However, the comment notes it is "only needed for e2e infrastructure," and an alternative would be a thin exported wrapper to keep the original unexported.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [error handling] e2e/admin/repos_helpers.go:318getGitLabProjectCreatedAt uses http.DefaultClient with no timeout. The caller cleanupStaleEphemeralRepos passes context.Background() with no deadline, so a hanging GitLab API call could block indefinitely. Consistent with the existing codebase pattern (GetRepoCreatedAt in testutil.go uses http.DefaultClient identically), so this is a minor hygiene improvement rather than a meaningful bug.
    Remediation: Use http.Client{Timeout: 30 * time.Second} or add a timeout to the context passed from cleanupStaleEphemeralRepos.
  • [secrets handling] .github/workflows/e2e.yml:175REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN are exposed to all tests under ./e2e/admin/ via make e2e-test, not just TestReposLifecycle. A dedicated make target (repos-e2e-test) exists and could be used to narrow secret exposure. Matches the existing pattern (GCP credentials are similarly exposed), and blast radius is limited by the pull_request_target authorization gate and dedicated e2e orgs.
    Remediation: Consider splitting the CI step so that repos-specific tokens are only passed to make repos-e2e-test, keeping them out of the broader make e2e-test environment.
  • [edge-case] e2e/admin/repos_helpers.go:228extractJSON finds the first { in CLI output and uses json.NewDecoder to extract the first JSON object. If a CLI log line contains a { character before the actual JSON output, it may decode the wrong object or fall back to returning everything from the first {. Unlikely in practice since CLI JSON output mode produces clean output, and this is a test helper.
  • [test-inadequate] pkg/e2etest/testutil_test.go:43 — Unit tests for RunCLIWithEnv and TryRunCLIWithEnv verify happy-path output but do not assert that extra environment variables are actually propagated to the subprocess. The real e2e tests would catch a regression (the CLI needs tokens to function), but the unit tests themselves do not exercise the distinguishing feature of these helpers.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (4)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [secrets handling] .github/workflows/e2e.yml:173REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN are exposed to all tests under ./e2e/admin/ via make e2e-test, not just TestReposLifecycle. A dedicated make target (repos-e2e-test) exists and could be used to narrow secret exposure. Matches the existing pattern (GCP credentials are similarly exposed), and blast radius is limited by the pull_request_target authorization gate and dedicated e2e orgs.
    Remediation: Consider splitting the CI step so that repos-specific tokens are only passed to make repos-e2e-test, keeping them out of the broader make e2e-test environment.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (5)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [secrets handling] .github/workflows/e2e.yml:175REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN are exposed to all tests in ./e2e/admin/ via make e2e-test, not just the repos lifecycle test. Matches the existing pattern (GCP credentials are similarly exposed), and the blast radius is limited by the pull_request_target authorization gate.
    Remediation: Consider adding a separate CI step that runs make repos-e2e-test with these tokens only passed there.
  • [edge-case] e2e/admin/repos_helpers.go:228extractJSON finds the first { in CLI output and uses json.NewDecoder to extract the first JSON object. If a CLI log line contains a { character before the actual JSON output, it may decode the wrong object or fall back to returning everything from the first {. Unlikely in practice since CLI JSON output mode produces clean output, and this is a test helper.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (6)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [secrets handling] .github/workflows/e2e.yml:175REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN are exposed to all tests in ./e2e/admin/ via make e2e-test, not just the repos lifecycle test. Matches the existing pattern (GCP credentials are similarly exposed), and the blast radius is limited by the pull_request_target authorization gate.
    Remediation: Consider adding a separate CI step that runs make repos-e2e-test with these tokens only passed there.
  • [api-shape-consistency] pkg/e2etest/testutil.go:496RunCLIWithEnv and TryRunCLIWithEnv accept extraEnv map[string]string, diverging from the existing token-oriented RunCLI/RunCLIFromDir API. The env assembly, command execution, and output logging are duplicated across both styles.
    Remediation: Consider extracting a shared internal execCLI helper.
  • [edge-case] e2e/admin/repos_helpers.go:227extractJSON finds the first { in CLI output and uses json.NewDecoder to extract the first JSON object. If a CLI log line contains a { character before the actual JSON output, it may decode the wrong object or fall back to returning everything from the first {. Unlikely in practice since CLI JSON output mode produces clean output, and this is a test helper.
  • [documentation-style] e2e/admin/repos_helpers.go:47 — Type reposTestEnv has no doc comment. The existing codebase documents similar test env types (e.g., e2eEnv in admin_test.go).
    Remediation: Add a doc comment: // reposTestEnv holds the shared state for a repos lifecycle e2e test run.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (7)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [secrets handling] .github/workflows/e2e.yml:177REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN are exposed to all tests in ./e2e/admin/ via make e2e-test, not just the repos lifecycle test. Matches the existing pattern (GCP credentials are similarly exposed), and the blast radius is limited by the pull_request_target authorization gate.
    Remediation: Consider adding a separate CI step that runs make repos-e2e-test with these tokens only passed there.
  • [api-shape-consistency] pkg/e2etest/testutil.go:493RunCLIWithEnv and TryRunCLIWithEnv accept extraEnv map[string]string, diverging from the existing token-oriented RunCLI/RunCLIFromDir API. The env assembly, command execution, and output logging are duplicated across both styles.
    Remediation: Consider extracting a shared internal execCLI helper.
  • [edge-case] e2e/admin/repos_helpers.go:227extractJSON finds the first { in CLI output and uses json.NewDecoder to extract the first JSON object. If a CLI log line contains a { character before the actual JSON output, it may decode the wrong object or fall back to returning everything from the first {. Unlikely in practice since CLI JSON output mode produces clean output, and this is a test helper.
Previous run (8)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [secrets handling] .github/workflows/e2e.yml:173REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN are exposed to all tests in ./e2e/admin/ via make e2e-test, not just the repos lifecycle test. Matches the existing pattern (GCP credentials are similarly exposed), and the blast radius is limited by the pull_request_target authorization gate.
    Remediation: Consider adding a separate CI step that runs make repos-e2e-test with these tokens only passed there.
  • [api-shape-consistency] pkg/e2etest/testutil.go:493RunCLIWithEnv and TryRunCLIWithEnv accept extraEnv map[string]string, diverging from the existing token-oriented RunCLI/RunCLIFromDir API. The env assembly, command execution, and output logging are duplicated across both styles.
    Remediation: Consider extracting a shared internal execCLI helper.
  • [import-ordering] e2e/admin/repos_test.go:7 — Import groups mix internal packages (fullsend-ai/fullsend/...) with third-party (stretchr/testify) in a single block without blank-line separation. The sibling admin_test.go separates stdlib, third-party, and internal into three distinct groups.
    Remediation: Reorder import groups to match admin_test.go convention: stdlib, third-party, internal — with blank lines between groups.
Previous run (9)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [secrets handling] .github/workflows/e2e.yml:172GITLAB_TOKEN is exposed to all tests in ./e2e/admin/ via make e2e-test, not just the repos lifecycle test. No existing test in e2e/admin/ reads GITLAB_TOKEN (only the new repos_helpers.go does), so the practical blast radius increase is minimal. Matches the existing pattern (GCP credentials are similarly exposed).
    Remediation: Consider adding a separate CI step that runs make repos-e2e-test with GITLAB_TOKEN only passed there.
  • [api-shape-consistency] pkg/e2etest/testutil.go:493RunCLIWithEnv and TryRunCLIWithEnv accept extraEnv map[string]string, diverging from the existing token-oriented RunCLI/RunCLIFromDir API. The env assembly, command execution, and output logging are duplicated across both styles.
    Remediation: Consider extracting a shared internal execCLI helper.
  • [log-format-consistency] pkg/e2etest/testutil.go:520TryRunCLIWithEnv does not log the command invocation before running, unlike RunCLIWithEnv which logs [cli] fullsend ... before execution. Follows the established Try* convention but is inconsistent within the new WithEnv pair.
Previous run (10)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [secrets handling] .github/workflows/e2e.yml:177GITLAB_TOKEN is exposed to all tests in ./e2e/admin/ via make e2e-test, not just the repos lifecycle test. This matches the existing pattern (GCP credentials are similarly exposed) but widens the blast radius for a GitLab token.
    Remediation: Consider running repos-specific tests via make repos-e2e-test (already added to Makefile) with GITLAB_TOKEN only passed to that target.
  • [naming-convention] e2e/admin/repos_helpers.go — Feature-prefixed repos_helpers.go departs from the existing helpers.go naming pattern. Reasonable for a growing package with distinct feature areas.
  • [code-duplication] e2e/admin/repos_helpers.go:107createEphemeralGitHubRepo shares structural similarity with the existing registerRepoCleanup + CreateRepo pattern in helpers.go, though the cleanup behavior intentionally differs (best-effort, non-failing cleanup for ephemeral repos).
    Remediation: Consider refactoring to share the common create-and-register-cleanup pattern.
  • [api-shape-consistency] pkg/e2etest/testutil.go:493RunCLIWithEnv and TryRunCLIWithEnv accept extraEnv map[string]string, diverging from the existing token-oriented RunCLIFromDir API. The env/logging/output plumbing is duplicated.
    Remediation: Consider extracting a shared internal execCLI helper to reduce duplication.
  • [log-format-consistency] pkg/e2etest/testutil.go:520TryRunCLIWithEnv does not log the command invocation before running, unlike RunCLIWithEnv. Consistent with the existing Try* convention but inconsistent across the new WithEnv pair.
  • [missing-doc] docs/guides/dev/e2e-testing.md:29 — The new repos-e2e-test Makefile target is not documented in the Local runs section. The existing make e2e-test command remains correct as it runs the full suite including repos tests.

Labels: PR adds Go e2e test code and helpers; go label matches the established convention for PRs modifying Go files.

Previous run (11)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Low

  • [secrets handling] .github/workflows/e2e.yml:177GITLAB_TOKEN is exposed to all tests in ./e2e/admin/ via make e2e-test, not just the repos lifecycle test. This matches the existing pattern (GCP credentials are similarly exposed) but widens the blast radius for a GitLab token.
  • [edge-case] e2e/admin/repos_helpers.goextractJSON tracks brace depth without accounting for {/} inside JSON string values. If CLI output contains JSON strings with literal braces, the depth tracking could return truncated JSON. Unlikely to trigger in practice with current CLI output.
    Remediation: Consider using json.Decoder to find the boundary of the first JSON object, or skip characters inside quoted strings when tracking brace depth.
Previous run (12)

Review

Findings

High

  • [protected-path] .github/workflows/e2e.yml — This PR modifies a file under .github/, which is a protected governance/infrastructure path. No linked issue provides authorization context for this change. Human approval is always required for protected-path changes, regardless of the nature of the modification.

Medium

  • [import grouping] e2e/admin/repos_helpers.go:5 — Import block does not use the established goimports-style three-group layout (stdlib / third-party / project). All imports are in a single block with no blank-line separators. The existing files in this package (helpers.go, admin_test.go) consistently use three groups separated by blank lines.
    Remediation: Separate into three groups: stdlib (context, encoding/json, fmt, os, strings, testing), third-party (github.com/google/uuid, github.com/stretchr/testify/require), project (github.com/fullsend-ai/fullsend/...).

  • [stale-doc-secret-inventory] docs/guides/dev/e2e-testing.md:58 — The "Required repository secrets" table does not include REPOS_E2E_GITLAB_TOKEN, which this PR wires into the e2e job as GITLAB_TOKEN. Operators provisioning secrets would miss this.
    Remediation: Add a row: REPOS_E2E_GITLAB_TOKEN — GitLab personal access token for repos e2e tests.

Low

  • [API shape consistency] pkg/e2etest/testutil.go:524TryRunCLIWithEnv accepts *testing.T as its first parameter, but the existing TryRunCLI does not. The *testing.T variant is TryRunCLIWithT. The WithEnv functions use a different convention from the established naming pattern.
  • [log format consistency] pkg/e2etest/testutil.go:533TryRunCLIWithEnv logs failures as TryRunCLIWithEnv fullsend %s, while the existing TryRunCLIWithT logs as TryRunCLI fullsend %s — inconsistent prefix conventions.
  • [redundant path filter] .github/workflows/e2e.yml:31 — New push.paths entries (internal/cli/repos.go, internal/repos/**) are already covered by the existing **/*.go glob for Go files. Consistent with existing redundant entries (internal/cli/github.go, internal/cli/run.go).
  • [secrets handling] .github/workflows/e2e.yml:173GITLAB_TOKEN is exposed to all tests in ./e2e/admin/ via make e2e-test, not just the repos lifecycle test. Minor blast-radius note — all tests in this package already receive GCP credentials.
  • [naming convention] e2e/admin/repos_helpers.go:1 — Feature-prefixed repos_helpers.go departs from the existing helpers.go naming pattern. Reasonable for a growing package.
  • [stale-doc-env-vars] docs/guides/dev/e2e-testing.md:34 — Optional environment variables table does not mention GITLAB_TOKEN for local runs. The repos e2e test skips gracefully when unset.
  • [stale-doc-path-trigger] docs/contributing/go-code.md:31 — E2e test guidance lists paths that should trigger make e2e-test but does not include internal/repos/, which the PR adds to CI triggers.

Labels: PR adds e2e test infrastructure for the repos CLI feature and modifies CI workflow

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added component/e2e End-to-end tests component/ci CI pipelines and checks labels Jul 27, 2026
@ggallen
ggallen force-pushed the worktree-repos-e2e-test branch from a07a65f to b8a2d46 Compare July 27, 2026 22:22
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 27, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 10:24 PM UTC · Ended 10:38 PM UTC
Commit: b8a2d46 · View workflow run →

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Site preview

Preview: https://9c8cfc04-site.fullsend-ai.workers.dev

Commit: 5c3848821e17726545cd29f4d58fc9c96609490d

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:24 PM UTC · Completed 10:38 PM UTC
Commit: b8a2d46 · View workflow run →

@ggallen
ggallen force-pushed the worktree-repos-e2e-test branch from b8a2d46 to 31cd0c6 Compare July 27, 2026 22:44
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 27, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:46 PM UTC · Completed 11:01 PM UTC
Commit: 31cd0c6 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the go Pull requests that update go code label Jul 27, 2026
@ggallen
ggallen force-pushed the worktree-repos-e2e-test branch from 31cd0c6 to 3a3dd6f Compare July 27, 2026 23:05
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 27, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:06 PM UTC · Completed 11:21 PM UTC
Commit: 3a3dd6f · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review findings below (inline).

Comment thread .github/workflows/e2e.yml
Comment thread e2e/admin/repos_helpers.go Outdated
Comment thread e2e/admin/repos_helpers.go
Comment thread e2e/admin/repos_test.go
Comment thread e2e/admin/repos_helpers.go
Comment thread e2e/admin/repos_test.go
@ggallen
ggallen force-pushed the worktree-repos-e2e-test branch from 3a3dd6f to 2c90b34 Compare July 28, 2026 01:50
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:52 AM UTC · Completed 2:07 AM UTC
Commit: 2c90b34 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

fullsend-ai-review[bot]

This comment was marked as outdated.

@ggallen
ggallen force-pushed the worktree-repos-e2e-test branch from be09b2a to b144223 Compare July 28, 2026 15:43
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 3:45 PM UTC · Completed 4:02 PM UTC
Commit: b144223 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@ggallen
ggallen force-pushed the worktree-repos-e2e-test branch from b144223 to 586f808 Compare July 28, 2026 18:18
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 6:20 PM UTC · Completed 6:34 PM UTC
Commit: 586f808 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three additional findings from a review pass, all at the current head commit.

Comment thread e2e/admin/repos_helpers.go Outdated
Comment thread pkg/e2etest/testutil.go
Comment thread e2e/admin/repos_helpers.go
@ggallen
ggallen force-pushed the worktree-repos-e2e-test branch from 586f808 to 31e7827 Compare July 28, 2026 19:40
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 7:41 PM UTC · Ended 7:54 PM UTC
Commit: 31e7827 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:55 PM UTC · Completed 8:12 PM UTC
Commit: 6277a5d · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

Add TestReposLifecycle in e2e/admin/ that exercises all fullsend repos
CLI commands (init, add, status, install --dry-run, diff, remove,
upgrade-mint) against ephemeral repos on both GitHub and GitLab forges.

The test requires REPOS_E2E_GITHUB_TOKEN and GITLAB_TOKEN to run,
making it fully opt-in. Stale ephemeral repos from crashed runs are
cleaned up at test start using creation timestamp checks.

Signed-off-by: Greg Allen <greg@fullsend.ai>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Greg Allen <gallen@redhat.com>
@ggallen
ggallen force-pushed the worktree-repos-e2e-test branch from 6277a5d to 5c38488 Compare July 28, 2026 23:52
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:54 PM UTC · Completed 12:08 AM UTC
Commit: 5c38488 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread e2e/admin/repos_helpers.go
Comment thread pkg/e2etest/testutil_test.go
Comment thread .github/workflows/e2e.yml
Comment thread e2e/admin/repos_helpers.go

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional review findings (3 medium-severity), verified against the current head and cross-checked against existing review threads to avoid duplicates.

return m
}

type statusJSON struct {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Hand-rolled status/diff JSON structs duplicate exported internal/repos types

statusJSON (here) and diffJSON (line 191) re-declare a strict subset of the fields already exported by internal/repos.RepoStatus/StatusResult and internal/repos.Change/DiffResult, in a file that already imports internal/repos for Manifest/MarshalWithHeader/LoadManifest. Checked against the current internal/repos/status.go and sync.go: RepoStatus has 11 fields (Owner, Repo, Installed, CurrentRef, ExpectedRef, MintURL, ExpectedMintURL, Region, ExpectedRegion, Drifts, Error) but statusJSON decodes only 5 (Owner, Repo, Installed, CurrentRef, Error); Change has 7 fields (Owner, Repo, Field, Type, Action, OldValue, NewValue) but diffJSON decodes only 3 (Owner, Repo, Field).

Because json.Unmarshal silently ignores JSON fields it doesn't recognize rather than failing to compile, a future rename/addition to the real CLI JSON schema won't cause a build break here — it will just silently stop being asserted on, weakening the regression-catching value this lifecycle test exists to provide.

Suggestion: unmarshal directly into repos.StatusResult and repos.DiffResult (already importable from the same package) instead of maintaining parallel, partially-overlapping struct definitions in the test helper.

Comment thread e2e/admin/repos_test.go
"github.com/fullsend-ai/fullsend/pkg/e2etest"
)

func TestReposLifecycle(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] New 13-phase sequential e2e test has no runtime budget against the shared 30-minute CI timeout

TestReposLifecycle runs 14 sequential fullsend CLI subprocess spawns (init x2, add x2, status x1, install x1, diff x1, remove x2, add x2, upgrade-mint x1, remove x1, status x1) plus 4 live forge repo creations/deletions and up to 30s-timeout visibility polling (waitForRepoVisible, repoVisibilityTimeout = 30s), all with no t.Parallel(). This lives in the same e2e/admin package as TestAdminInstallUninstall (admin_test.go:86), which itself calls AcquireOrg with a lock timeout defaulting to 10 minutes (E2E_LOCK_TIMEOUT, documented default 10m in e2e-testing.md). Both tests share one job's timeout-minutes: 30 (.github/workflows/e2e.yml:105). Nothing in this PR measures or reserves budget for the added wall-clock cost.

Suggestion: measure actual added runtime once a real CI run is available; if material, run the repos lifecycle test in its own job/step with an independent timeout rather than folding it into the existing 30-minute admin e2e budget, or mark phases safe for t.Parallel() where they don't share mutable state.

| `FULLSEND_MINT_URL` | Override mint endpoint (default: hosted public mint, same as `fullsend admin --mint-url`) |
| `E2E_LOCK_TIMEOUT` | Max wait for a free pool org (default 10m) |
| `E2E_GCP_PROJECT_ID` | GCP project for inference setup (`github setup --inference-project`) |
| `REPOS_E2E_GITHUB_TOKEN` | GitHub PAT for repos lifecycle e2e tests; overrides `GH_TOKEN`/`GITHUB_TOKEN` for the repos e2e org (test falls back to general auth if unset) |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Docs claim a GitHub token auth fallback that the code does not implement

This row states that REPOS_E2E_GITHUB_TOKEN "overrides GH_TOKEN/GITHUB_TOKEN for the repos e2e org (test falls back to general auth if unset)". The current setupReposTest (e2e/admin/repos_helpers.go:79-82) has no fallback at all:

ghToken := os.Getenv("REPOS_E2E_GITHUB_TOKEN")
if ghToken == "" {
    t.Skip("REPOS_E2E_GITHUB_TOKEN not set, skipping repos e2e test")
}

This matches this PR's own review history — an earlier resolveGitHubToken fallback chain (GH_TOKEN -> GITHUB_TOKEN -> gh auth token) was deliberately removed in a later round specifically so the test is fully opt-in via the dedicated secret — but this doc line, added in the same PR, wasn't updated to match. The GITLAB_TOKEN row directly below (line 49) correctly says "test skips if unset"; only the GitHub row overclaims a fallback.

Suggestion: fix the doc line to match the code ("test skips if unset", same wording as the GITLAB_TOKEN row) — a fallback to a contributor's personal token wouldn't be useful anyway since it's unlikely to have admin/delete_repo rights on the dedicated fullsend-repos-e2e-gh org.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review pass: 1 CRITICAL finding (PR no longer compiles against current main — targets a repos CLI/manifest surface removed by three subsequent migrations). See inline comment for details.

func buildCombinedManifest(ghOrg, glGroup string, ghRepos, glRepos []string) *repos.Manifest {
m := &repos.Manifest{
Version: 1,
Mint: repos.MintConfig{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] PR does not compile against current main — tests a repos CLI/manifest surface that no longer exists

This PR branched at merge-base eeb97439 (2026-07-27 or earlier) and was never rebased, while three schema/CLI migrations landed on main since then: 2477b780 (move mint config into per-forge section, removing top-level Manifest.Mint/MintConfig), 464747b6 (consolidate repos commands from 9 to 4), and b526e287 (add repos migrate, remove repos init).

Verified directly against origin/main (7999832c):

  1. internal/repos/manifest.goManifest is now {Version, Forge, Defaults, Repos} with no Mint field, and MintConfig does not exist anywhere in the repo (git grep confirms); DefaultsConfig is now {Forge, AllowedRemoteResources} — none of InferenceProject/InferenceRegion/FullsendRef exist on it.
  2. internal/cli/repos.go on main registers exactly 5 subcommands: migrate, install, uninstall, status, set-defaultinit, add, remove, diff, and upgrade-mint (all used by this PR's test, and named explicitly in its own PR description) do not exist.

This PR's buildCombinedManifest() (this file, lines 331-345) constructs exactly the now-removed repos.Manifest{Mint: repos.MintConfig{...}} / repos.DefaultsConfig{InferenceProject, InferenceRegion, FullsendRef} literal, and repos_test.go's TestReposLifecycle invokes exactly the five removed subcommands (repos init, add, remove, diff, upgrade-mint) via 14 sequential CLI subprocess calls. A git merge --no-ff of the PR branch into origin/main produces a real conflict (confirmed in docs/guides/dev/e2e-testing.md, internal/cli/lock.go, internal/cli/lock_test.go, internal/harnessdispatch/enumerate_test.go).

None of the review rounds on this PR so far — including the round completed at this exact head commit — raise this specific schema/CLI-surface break; existing findings are unrelated (duplicate JSON structs, runtime budget, docs wording). The PR's own test-plan checkboxes ("go build -tags e2e ./e2e/admin/ compiles cleanly", "TestReposLifecycle passes locally") were only true against the PR's stale base and are no longer accurate against main.

Suggestion: This needs more than a rebase — the new test file targets a CLI/manifest schema that has since been redesigned twice. Rewrite TestReposLifecycle and buildCombinedManifest/writeTestManifest against the current migrate / install / uninstall / status / set-default command set and the current Manifest{Version, Forge, Defaults, Repos} / per-forge mint_url schema before this can land. Do not merge as-is; re-verify go build -tags e2e ./e2e/admin/... against current main before requesting re-review.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review sweep: 2 additional findings not previously flagged.

t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), repoVisibilityTimeout)
defer cancel()
for {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] waitForRepoVisible retries on every GetRepo error, not just not-found

waitForRepoVisible polls client.GetRepo(ctx, owner, repo) in a loop and treats any non-nil error identically to "not yet visible", retrying every 2s for the full 30s repoVisibilityTimeout before t.Fatalf. Unlike the codebase's own retryOnNotFound helper (pkg/e2etest/testutil.go:536-554), which explicitly checks forge.IsNotFound(err) before retrying and returns immediately on other error classes, this new helper has no such check.

Suggestion: only retry when the error is a not-found condition (forge.IsNotFound(err)); propagate/fail fast on other error classes such as auth failures, rate limiting, or malformed org/group names, instead of masking them behind a generic 30s timeout message.

if !strings.HasPrefix(r.Name, ephemeralRepoPrefix) {
continue
}
createdAt, ageErr := getGitLabProjectCreatedAt(ctx, glToken, glGroup, r.Name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Stale GitLab cleanup uses display Name where a URL path/slug is required

ListOrgRepos for GitLab (internal/forge/gitlab/repo.go:107-123) populates forge.Repository.Name from the JSON name field (display name) and FullName from path_with_namespace (the actual URL slug). cleanupStaleEphemeralRepos iterates glRepos and passes r.Name directly into getGitLabProjectCreatedAt(ctx, glToken, glGroup, r.Name) and glClient.DeleteRepo(ctx, glGroup, r.Name) — both of which build the API path from that value as if it were the project's path/slug. createEphemeralGitLabRepo only ever sets the name field on creation (internal/forge/gitlab/repo.go:182-188), so GitLab currently auto-derives an identical path from the already slug-safe ephemeral repo names, masking the bug today. Any future change to naming (spaces, non-ASCII, or an explicit differing path) would silently break stale-repo age lookups and deletion targeting.

Suggestion: derive the path from the last segment of r.FullName (path_with_namespace) rather than relying on r.Name coinciding with the URL slug.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review-only sweep: 3 additional findings not covered by existing review comments (verified against origin/main and current PR diff).

Comment thread e2e/admin/repos_test.go
installOut := e2etest.RunCLIWithEnv(t, env.binary, env.cliEnv(),
"repos", "install",
"--dry-run",
"--skip-mint-check",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] --skip-mint-check flag does not exist on repos install

Phase 6 of TestReposLifecycle invokes repos install --dry-run --skip-mint-check -f manifestPath. Verified directly against internal/cli/repos.go on origin/main: newReposInstallCmd()'s flag set is exactly --manifest/-f, --dry-run, --concurrency, --roles, --direct, --force, --forge, --inference-project, --inference-project-number, --inference-region, --fullsend-ref, --mint-url, --allowed-remote-resources, --gitlab-bot-token. --skip-mint-check only exists on the unrelated admin install command (internal/cli/admin.go:603), not on repos install. Unlike the already-flagged CRITICAL issue on this PR covering the removed init/add/remove/diff/upgrade-mint subcommands and Manifest/MintConfig schema drift, repos install itself does exist on main today — but this specific flag on it does not, so Phase 6 will fail immediately with an "unknown flag" cobra parse error even after the schema/subcommand issues elsewhere in the test are fixed.

Suggestion: Drop --skip-mint-check from the repos install invocation, or determine what current flag (if any) suppresses mint verification during a dry run and use that instead.

Comment thread .github/workflows/e2e.yml
env:
E2E_SCREENSHOT_DIR: ${{ runner.temp }}/e2e-screenshots
E2E_GCP_PROJECT_ID: ${{ secrets.E2E_GCP_PROJECT_ID }}
REPOS_E2E_GITHUB_TOKEN: ${{ secrets.REPOS_E2E_GITHUB_TOKEN }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] New GitHub secret uses a static long-lived credential pattern inconsistent with sibling jobs' short-lived tokens

This PR wires REPOS_E2E_GITHUB_TOKEN: ${{ secrets.REPOS_E2E_GITHUB_TOKEN }} into the e2e job's "Run e2e tests" step, which runs under pull_request_target with an allow-unsafe-pr-checkout PR-head checkout (gated by a separate gate job's authorization check). Elsewhere in this same workflow file, equivalent-power credentials are minted short-lived: GCP access uses google-github-actions/auth with E2E_GCP_WIF_PROVIDER/E2E_GCP_SERVICE_ACCOUNT (Workload Identity Federation), and the behaviour job mints GitHub App installation tokens from TEST_FULLSEND_PEM/TEST_TRIAGE_PEM/etc. rather than static PATs. REPOS_E2E_GITHUB_TOKEN (needed to create/delete repos in the external fullsend-repos-e2e-gh org) breaks that pattern by being a plain, presumably long-lived static secret. The gate job's authorization check mitigates but does not eliminate exposure risk for this new credential type.

Suggestion: If feasible, mint a short-lived GitHub token for the fullsend-repos-e2e-gh org via the same OIDC/App-installation-token pattern used for GCP and the behaviour job's GitHub Apps, instead of a static classic PAT; if that's not practical, at minimum scope the PAT as narrowly as possible (repo-only, no org-admin) and document its blast radius alongside the other secrets in docs/guides/dev/e2e-testing.md.


binary := e2etest.BuildCLIBinary(t)

ghToken := os.Getenv("REPOS_E2E_GITHUB_TOKEN")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Test silently skips (rather than failing) when its required secrets are unset, masking lack of coverage in CI

setupReposTest does if ghToken == "" { t.Skip(...) } and the same for GITLAB_TOKEN. If REPOS_E2E_GITHUB_TOKEN/GITLAB_TOKEN are not yet provisioned as repo secrets (a real possibility immediately after this PR merges, before someone sets them up) or are otherwise unavailable, make e2e-test reports green with zero coverage of the internal/repos/** and internal/cli/repos.go code paths this test was specifically added to protect — even though the CI path filter added by this same PR explicitly triggers the e2e job on changes to those paths. This is distinct from the already-fixed issue about the CI step not wiring a GitHub token at all — this finding is about the ongoing risk of the test degrading to a silent no-op whenever either secret is transiently absent.

Suggestion: In CI (e.g. when CI=true), fail loudly (t.Fatal) instead of skipping when the required secrets are absent, or route this test through a separate required status check so a missing secret is visibly surfaced rather than silently passing.

@github-actions
github-actions Bot deleted the worktree-repos-e2e-test branch August 30, 2026 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/ci CI pipelines and checks component/e2e End-to-end tests go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants