Skip to content

feat(install): default to PR-based scaffold delivery - #2533

Merged
waynesun09 merged 3 commits into
mainfrom
feat-default-pr-install
Jun 23, 2026
Merged

feat(install): default to PR-based scaffold delivery#2533
waynesun09 merged 3 commits into
mainfrom
feat-default-pr-install

Conversation

@waynesun09

Copy link
Copy Markdown
Member

Summary

  • Invert the default scaffold delivery behavior: fullsend admin install and fullsend github setup now create PRs by default instead of pushing directly to the default branch
  • Add --direct flag to both commands for users who want the old direct-commit behavior (with automatic PR fallback on branch protection)
  • Split CommitScaffoldFiles into commitScaffoldViaPR (new default) and commitScaffoldDirect (--direct path)

Closes #483

Test plan

  • go test ./internal/layers/... ./internal/cli/... -count=1 passes
  • make lint passes
  • New TestWorkflowsLayer_Install_DefaultCreatesPR verifies PR-based default
  • Existing protected-branch fallback tests still pass under --direct mode
  • Manual: fullsend admin install on a test org creates a scaffold PR
  • Manual: fullsend admin install --direct pushes directly to default branch

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

E2E tests are running

Authorization passed for this commit. See the E2E Tests workflow for results.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Default scaffold delivery to PRs for install/setup (add --direct override)
✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

Description

• Switch scaffold delivery to PR-based flow by default for install/setup commands.
• Add --direct flag to push to default branch, with branch-protection PR fallback.
• Refactor scaffold commit logic and update tests/docs for the new default behavior.
Diagram

graph TD
  A["CLI: admin install / github setup"] --> B["CLI orchestration (runInstall/applyPerRepoScaffold)"] --> C["WorkflowsLayer (WithDirect)"] --> D{"direct?"}
  D -->|"false (default)"| E["commitScaffoldViaPR"] --> F{{"Forge/GitHub API"}} --> G["Scaffold branch + PR"]
  D -->|"true (--direct)"| H["commitScaffoldDirect"] --> F --> I["Default branch commit"]
  H -->|"branch protected"| E
  subgraph Legend
    direction LR
    _cli["Component"] ~~~ _dec{"Decision"} ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep direct-push default and add `--pr` flag
  • ➕ Avoids behavioral change for existing automation that relied on direct commits
  • ➕ Keeps the simplest path as the default for unprotected repos
  • ➖ Makes the safer workflow (PR review) opt-in instead of default
  • ➖ Still surprises users on protected branches due to implicit fallback behavior
2. Auto-detect branch protection and choose PR only when protected
  • ➕ Minimizes behavior changes for unprotected repos
  • ➕ PRs only created when required by policy
  • ➖ Detection can be API/provider-specific and sometimes inaccurate
  • ➖ Still produces two different defaults across orgs, reducing predictability and docs simplicity

Recommendation: The PR’s approach (PR-by-default with an explicit --direct override) is the best trade-off: it standardizes a safe, reviewable delivery mechanism across repos while preserving the prior behavior for automation via --direct, and keeps the branch-protection fallback as a safety net.

Files changed (10) +167 / -106

Enhancement (4) +94 / -66
admin.goThread '--direct' through admin install and per-repo scaffold apply +18/-15

Thread '--direct' through admin install and per-repo scaffold apply

• Adds a '--direct' boolean flag to 'fullsend admin install', threads it through 'runInstall', 'buildLayerStack', and 'applyPerRepoScaffold', and configures 'WorkflowsLayer' via 'WithDirect(direct)'. Updates PR body text to be mode-agnostic (no longer assumes protection-triggered PRs only).

internal/cli/admin.go

github.goAdd '--direct' flag to github setup and thread into stack/scaffold apply +5/-3

Add '--direct' flag to github setup and thread into stack/scaffold apply

• Introduces '--direct' for 'fullsend github setup', passes it through per-repo scaffold application and per-org layer stack construction, matching the new PR-default behavior across both commands.

internal/cli/github.go

commit.goSplit scaffold delivery into PR vs direct implementations +61/-44

Split scaffold delivery into PR vs direct implementations

• Refactors 'CommitScaffoldFiles' to accept a 'direct' flag and dispatch to 'commitScaffoldViaPR' (new default) or 'commitScaffoldDirect' (push-first with branch-protection fallback). Makes PR creation idempotent by treating already-existing proposals as success while still reporting whether the branch was updated.

internal/layers/commit.go

workflows.goAdd WorkflowsLayer.WithDirect and use it for scaffold delivery +10/-4

Add WorkflowsLayer.WithDirect and use it for scaffold delivery

• Adds a 'direct' field and 'WithDirect()' builder to 'WorkflowsLayer', and passes the mode into 'CommitScaffoldFiles'. Updates PR body copy to reflect PR-based delivery as the standard path.

internal/layers/workflows.go

Tests (3) +66 / -34
admin_test.goAdjust install tests for PR-default and direct-mode paths +30/-25

Adjust install tests for PR-default and direct-mode paths

• Updates test call sites to include the new 'direct' parameter and changes assertions to expect PR-mode behavior by default (commit-to-branch + PR) while keeping protected-branch fallback coverage in direct mode. Enhances idempotency test setup to account for PR already existing in PR mode.

internal/cli/admin_test.go

github_test.goUpdate github setup/sync tests to assert PR-mode default behavior +9/-7

Update github setup/sync tests to assert PR-mode default behavior

• Changes tests to expect scaffold commits to land on a branch ('CommittedFilesToBranch') and that a PR proposal is created in default mode, instead of asserting direct commits to the default branch.

internal/cli/github_test.go

workflows_test.goAdd PR-default install test and keep direct-mode coverage +27/-2

Add PR-default install test and keep direct-mode coverage

• Updates helper to set direct mode explicitly for tests that expect direct commits, and adds 'TestWorkflowsLayer_Install_DefaultCreatesPR' to verify branch creation, commit-to-branch, and PR creation in the new default mode.

internal/layers/workflows_test.go

Documentation (3) +7 / -6
cli-internals.mdDocument PR-default vs --direct scaffold delivery modes +4/-5

Document PR-default vs --direct scaffold delivery modes

• Updates the internal CLI pipeline diagram/notes to describe two delivery modes: default PR-based delivery and '--direct' push-first behavior with protected-branch PR fallback.

docs/guides/dev/cli-internals.md

github-setup.mdAdd '--direct' flag to github setup reference +1/-0

Add '--direct' flag to github setup reference

• Documents the new '--direct' flag and clarifies that direct pushes fall back to PR creation when blocked by branch protection.

docs/reference/github-setup.md

installation.mdUpdate installation docs for PR-default scaffold delivery +2/-1

Update installation docs for PR-default scaffold delivery

• Replaces the prior “protected branch fallback” wording with a PR-by-default explanation and adds the '--direct' option to the flag reference.

docs/reference/installation.md

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

Site preview

Preview: https://e300b211-site.fullsend-ai.workers.dev

Commit: 3e6efc9f252d6219e3423c35715b641bf4d846d0

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 8:18 PM UTC · Ended 8:30 PM UTC
Commit: 4e21a60 · View workflow run →

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 51 rules

Grey Divider


Action required

1. No-change PR creation fails 🐞 Bug ≡ Correctness
Description
In PR-delivery mode, commitScaffoldViaPR attempts CreateChangeProposal even when CommitFilesToBranch
reports no changes, which can cause GitHub to return a 422 “No commits between …” error and fail
otherwise-idempotent installs/sync-scaffold re-runs.
Code

internal/layers/commit.go[R53-59]

+	proposal, prErr := client.CreateChangeProposal(ctx, owner, repo,
+		prTitle, prBody, scaffoldBranch, defaultBranch)
+	if prErr != nil {
+		if !forge.IsAlreadyExists(prErr) {
+			printer.StepFail("Failed to create scaffold PR")
+			return false, fmt.Errorf("creating scaffold PR: %w", prErr)
+		}
Relevance

⭐⭐ Medium

No clear prior review guidance on skipping CreateChangeProposal when no changes; related scaffold/PR
logic discussed in #2201.

PR-#2201

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The GitHub commit implementation returns (false, nil) when there are no changed entries, so
PR-mode can reach CreateChangeProposal with a branch that has no commits ahead of base. The GitHub
API has a known 422 validation error case “No commits between …” (covered in repo tests), and
APIError unwrapping only maps already exists/not found, so this error will not be ignored and
will currently fail the operation.

internal/layers/commit.go[30-69]
internal/forge/github/github.go[641-755]
internal/forge/github/github.go[50-91]
internal/forge/github/github.go[1217-1245]
internal/forge/github/github_test.go[722-732]

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

### Issue description
`commitScaffoldViaPR` always calls `CreateChangeProposal` after `CommitFilesToBranch`, even when `CommitFilesToBranch` returns `false` (no changed entries). On GitHub, creating a PR with no commits/diff can return a 422 validation error like “No commits between …”, which is not treated as `ErrAlreadyExists` and currently bubbles up as a hard failure.

### Issue Context
This is a regression risk introduced by making PR-delivery the default: re-running install/sync on an already-up-to-date repo can now error instead of returning “up to date”.

### Fix Focus Areas
- internal/layers/commit.go[30-69]
- internal/forge/forge.go[19-43]
- internal/forge/github/github.go[50-91]
- internal/forge/github/github.go[1217-1245]
- internal/forge/github/github_test.go[722-732]

### Proposed fix
1. Introduce a forge-level sentinel error for “no diff / nothing to compare” PR creation (e.g., `forge.ErrNoChanges` + `forge.IsNoChanges(err)`).
2. In the GitHub client, detect the 422 PR validation error detail (the repo already has a test fixture for `No commits between ...`) during `CreateChangeProposal` error handling and wrap/unwrap it to `forge.ErrNoChanges`.
3. In `commitScaffoldViaPR`, treat `ErrNoChanges` as a successful idempotent outcome (e.g., `printer.StepDone("Scaffold up to date")`) and return `nil`.
4. Add/adjust tests to cover: PR-mode + no changes + no existing PR should not error.

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



Informational

2. Guide stored under docs/guides/dev/ 📜 Skill insight ⌂ Architecture
Description
A modified guide file lives under docs/guides/dev/, but guides must be placed under either
docs/guides/admin/ or docs/guides/user/. This breaks the required documentation directory
structure and audience targeting policy.
Code

docs/guides/dev/cli-internals.md[R183-186]

+│  │  CommitScaffoldFiles() delivery modes:                      │ │
+│  │    Default (PR):  create feature branch → commit → open PR │ │
+│  │    --direct:      try CommitFiles (default branch)         │ │
+│  │      if ErrBranchProtected → fall back to PR mode          │ │
Relevance

⭐ Low

Repo already maintains dev guides under docs/guides/dev/ (added/edited in PRs #1087, #1252).

PR-#1087
PR-#1252

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires guides to live under docs/guides/admin/ or docs/guides/user/, but this PR
modifies a guide located at docs/guides/dev/cli-internals.md, which is outside the allowed
directories.

docs/guides/dev/cli-internals.md[183-186]
Skill: writing-user-docs

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

## Issue description
`docs/guides/dev/cli-internals.md` is under a non-allowed subdirectory (`dev/`). Per policy, guide files under `docs/guides/` must be placed in either `admin/` or `user/`.

## Issue Context
This PR modifies `docs/guides/dev/cli-internals.md`, which makes the placement rule applicable to this change.

## Fix Focus Areas
- docs/guides/dev/cli-internals.md[183-186]

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


3. --direct enables default-branch push 📎 Requirement gap ⛨ Security
Description
The installer now supports a direct mode that pushes scaffold commits directly to the default
branch, bypassing the PR-only delivery requirement. This allows unreviewed changes to .fullsend
and target repositories.
Code

internal/layers/commit.go[R72-82]

+// commitScaffoldDirect pushes files directly to the default branch, falling
+// back to a PR when branch protection blocks the push.
+func commitScaffoldDirect(ctx context.Context, client forge.Client, printer *ui.Printer,
+	owner, repo, defaultBranch, commitMsg, prTitle, prBody string,
+	files []forge.TreeFile) (bool, error) {
+
+	committed, err := client.CommitFiles(ctx, owner, repo, commitMsg, files)
+	if err != nil && forge.IsBranchProtected(err) {
+		printer.StepWarn("Default branch is protected — creating scaffold PR instead")
+		return commitScaffoldViaPR(ctx, client, printer,
+			owner, repo, defaultBranch, commitMsg, prTitle, prBody, files)
Relevance

⭐ Low

Team previously accepted direct default-branch scaffold commits and only added PR fallback/messaging
(see #2198, #2201).

PR-#2198
PR-#2201

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1062107 prohibits installation workflows from pushing commits directly to default branches;
however, commitScaffoldDirect() calls client.CommitFiles(...) to push scaffold files to the
default branch when direct is enabled, only falling back to PR creation on branch protection
errors.

All changes to .fullsend and target repositories must be made via an approved Pull Request (no direct/merge commits)
internal/layers/commit.go[72-82]
internal/layers/commit.go[18-27]

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

## Issue description
Compliance requires that all changes to `.fullsend` and target repositories be introduced via approved Pull Requests (no direct commits to default branches). The new `commitScaffoldDirect()` path and `direct` flag explicitly perform `client.CommitFiles(...)` to the default branch.

## Issue Context
`CommitScaffoldFiles(..., direct bool)` routes to `commitScaffoldDirect()` when `direct` is true, enabling direct default-branch pushes.

## Fix Focus Areas
- internal/layers/commit.go[72-82]
- internal/layers/commit.go[18-27]

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


Grey Divider

Qodo Logo

Comment thread internal/layers/commit.go
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.21739% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/layers/workflows.go 80.00% 2 Missing and 1 partial ⚠️
internal/cli/admin.go 90.00% 2 Missing ⚠️
internal/cli/github.go 60.00% 1 Missing and 1 partial ⚠️
internal/forge/forge.go 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 8:33 PM UTC · Ended 8:36 PM UTC
Commit: 4e21a60 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:39 PM UTC · Completed 8:52 PM UTC
Commit: 2404039 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

Looks good to me

Findings

Medium

  • [architectural-inconsistency] internal/cli/admin.go:1223 — The dry-run code path passes direct=false to buildLayerStack as a hardcoded value, so --dry-run always previews PR-mode behavior regardless of whether the user also passed --direct. This creates a mismatch between the dry-run preview and actual execution: fullsend admin install --direct --dry-run would show PR-mode output but the real run would use direct mode.
    Remediation: Thread the direct flag value into runDryRun() and pass it to buildLayerStack instead of hardcoding false.

Low

  • [scope-authorization-mismatch] internal/cli/github.go:981 — The sync-scaffold command hardcodes WithDirect(true), bypassing PR-based delivery. While sync-scaffold is a day-2 admin operation (not the initial install path targeted by Require an approved PR for all changes to the .fullsend and target repos #483), this creates an inconsistency where fresh installs require PR approval but scaffold updates do not. Consider exposing --direct as a flag on sync-scaffold rather than hardcoding the choice.

  • [test-adequacy] internal/layers/commit.go — The new commitScaffoldViaPR and commitScaffoldDirect functions lack direct unit tests in a commit_test.go file. Key paths are covered indirectly through workflows_test.go and admin_test.go integration tests, but direct unit tests would improve coverage confidence.

Previous run

Looks good to me

Findings

Low

  • [default-inversion-risk] internal/layers/workflows.go:125 — In default PR mode (direct=false), commitScaffoldViaPR always returns committed=false, so activateRepoMaintenance is never called. The test TestWorkflowsLayer_Install_DefaultCreatesPR explicitly asserts this behavior ("PR mode should not trigger repo-maintenance activation"), confirming it is intentional. This is logically correct: the merge event itself is a push touching workflow files, which triggers GitHub's workflow registration. However, if this assumption does not hold for all GitHub plan tiers, users would need to manually trigger repo-maintenance.yml.
    Remediation: Verify that merging the scaffold PR reliably triggers workflow registration across all GitHub plan tiers. If not, add a post-merge activation step or document the manual workaround.

  • [parameter-explosion] internal/cli/admin.gobuildLayerStack (line 1842, 17 positional parameters) and runInstall (line 1472, 21+ positional parameters) each gain another trailing bool. This is a pre-existing pattern — this PR adds one parameter to already-large signatures — but the incremental growth increases call-site error risk.
    Remediation: Refactor to use config structs (e.g., LayerStackConfig) following the pattern of githubSetupConfig and perRepoInstallConfig.

  • [flag-naming-consistency] internal/cli/admin.go — The --direct flag name could be more descriptive (e.g., --direct-commit) to clarify it controls commit delivery mode rather than some other aspect of installation. Current naming follows the brevity of --vendor but is less self-documenting than --skip-app-setup.
    Remediation: Consider renaming to --direct-commit for clarity.

  • [stale-behavior-description] docs/ADRs/0005-forge-abstraction-layer.md:38 — Describes CommitFilesToBranch as "enabling the protected-branch fallback path." With PR-based delivery now the default, this function serves as the primary delivery mechanism. ADRs are point-in-time records, so the staleness is minor.
    Remediation: Consider adding a brief annotation noting that PR-based delivery is now the default mode (per PR feat(install): default to PR-based scaffold delivery #2533).

  • [test-adequacy] internal/layers/commit.go — The new commitScaffoldViaPR and commitScaffoldDirect functions lack direct unit tests in a commit_test.go file. Key paths (ErrNoChanges handling, IsBranchProtected fallback from direct to PR mode) are covered indirectly through workflows_test.go and admin_test.go integration tests, but direct unit tests would improve coverage confidence.
    Remediation: Add a commit_test.go with unit tests for commitScaffoldViaPR and commitScaffoldDirect.

Previous run

Looks good to me

Findings

Low

  • [default-inversion-risk] internal/cli/admin.go — Inverting the default behavior from direct-commit to PR-based delivery is an intentional breaking change (authorized by Require an approved PR for all changes to the .fullsend and target repos #483) with a --direct escape hatch for backward compatibility. Existing automation calling fullsend admin install or fullsend github setup without --direct will now require merging a scaffold PR to complete setup.
    Remediation: Ensure release notes clearly communicate the default behavior change and the --direct flag for automation users.

  • [stale-behavior-description] docs/ADRs/0005-forge-abstraction-layer.md:38 — Describes CommitFilesToBranch as "enabling the protected-branch fallback path." With PR-based delivery now the default, this characterization is slightly inaccurate. ADRs are point-in-time records, so the staleness is minor.
    Remediation: Consider adding a brief annotation noting that PR-based delivery is now the default mode (per PR feat(install): default to PR-based scaffold delivery #2533).

  • [flag-naming-consistency] internal/cli/admin.go — The --direct flag name could be more descriptive (e.g., --direct-commit) to clarify it controls commit delivery mode rather than some other aspect of installation. Current naming follows the brevity of --vendor but is less self-documenting than --skip-app-setup or --enroll-all.
    Remediation: Consider renaming to --direct-commit for clarity.

Previous run (2)

Review

Findings

Medium

  • [default-inversion-risk] internal/cli/admin.go — Inverting the default behavior from direct-commit to PR-based delivery is a breaking change for users who have CI/CD pipelines or scripts that depend on the install command completing without a manual PR merge step. The --direct flag provides backward compatibility, but existing automation calling fullsend admin install or fullsend github setup without --direct will now require a manual PR merge to complete setup.
    Remediation: Ensure release notes clearly communicate the default behavior change and the --direct escape hatch for automation users.

Low

  • [error-handling] internal/forge/github/github.goisNoChangesError only checks apiErr.Errors[].Message, while the analogous isAlreadyExistsError concatenates apiErr.Message and all apiErr.Errors[].Message before searching. While GitHub's current behavior places the "No commits between" message in the Errors array (not the top-level Message), matching the existing helper pattern would be more defensive against future API changes.
    Remediation: Mirror the pattern used in isAlreadyExistsError by concatenating all message fields before checking.

  • [stale-behavior-description] docs/ADRs/0005-forge-abstraction-layer.md:38 — Describes CommitFilesToBranch as "enabling the protected-branch fallback path." With PR-based delivery becoming the default, this characterization is slightly inaccurate. ADRs are point-in-time records, so the staleness is minor.
    Remediation: Consider adding a brief annotation noting that PR-based delivery is now the default mode (per PR feat(install): default to PR-based scaffold delivery #2533).


Labels: PR modifies CLI install/setup commands and the layers/forge packages that handle scaffold delivery

Previous run

Looks good to me

Findings

Low

  • [default-inversion-risk] internal/layers/workflows.go:125 — In default PR mode (direct=false), commitScaffoldViaPR always returns committed=false, so activateRepoMaintenance is never called. The test TestWorkflowsLayer_Install_DefaultCreatesPR explicitly asserts this behavior ("PR mode should not trigger repo-maintenance activation"), confirming it is intentional. This is logically correct: the merge event itself is a push touching workflow files, which triggers GitHub's workflow registration. However, if this assumption does not hold for all GitHub plan tiers, users would need to manually trigger repo-maintenance.yml.
    Remediation: Verify that merging the scaffold PR reliably triggers workflow registration across all GitHub plan tiers. If not, add a post-merge activation step or document the manual workaround.

  • [parameter-explosion] internal/cli/admin.gobuildLayerStack (line 1842, 17 positional parameters) and runInstall (line 1472, 21+ positional parameters) each gain another trailing bool. This is a pre-existing pattern — this PR adds one parameter to already-large signatures — but the incremental growth increases call-site error risk.
    Remediation: Refactor to use config structs (e.g., LayerStackConfig) following the pattern of githubSetupConfig and perRepoInstallConfig.

  • [flag-naming-consistency] internal/cli/admin.go — The --direct flag name could be more descriptive (e.g., --direct-commit) to clarify it controls commit delivery mode rather than some other aspect of installation. Current naming follows the brevity of --vendor but is less self-documenting than --skip-app-setup.
    Remediation: Consider renaming to --direct-commit for clarity.

  • [stale-behavior-description] docs/ADRs/0005-forge-abstraction-layer.md:38 — Describes CommitFilesToBranch as "enabling the protected-branch fallback path." With PR-based delivery now the default, this function serves as the primary delivery mechanism. ADRs are point-in-time records, so the staleness is minor.
    Remediation: Consider adding a brief annotation noting that PR-based delivery is now the default mode (per PR feat(install): default to PR-based scaffold delivery #2533).

  • [test-adequacy] internal/layers/commit.go — The new commitScaffoldViaPR and commitScaffoldDirect functions lack direct unit tests in a commit_test.go file. Key paths (ErrNoChanges handling, IsBranchProtected fallback from direct to PR mode) are covered indirectly through workflows_test.go and admin_test.go integration tests, but direct unit tests would improve coverage confidence.
    Remediation: Add a commit_test.go with unit tests for commitScaffoldViaPR and commitScaffoldDirect.

Previous run (2)

Looks good to me

Findings

Low

  • [default-inversion-risk] internal/cli/admin.go — Inverting the default behavior from direct-commit to PR-based delivery is an intentional breaking change (authorized by Require an approved PR for all changes to the .fullsend and target repos #483) with a --direct escape hatch for backward compatibility. Existing automation calling fullsend admin install or fullsend github setup without --direct will now require merging a scaffold PR to complete setup.
    Remediation: Ensure release notes clearly communicate the default behavior change and the --direct flag for automation users.

  • [stale-behavior-description] docs/ADRs/0005-forge-abstraction-layer.md:38 — Describes CommitFilesToBranch as "enabling the protected-branch fallback path." With PR-based delivery now the default, this characterization is slightly inaccurate. ADRs are point-in-time records, so the staleness is minor.
    Remediation: Consider adding a brief annotation noting that PR-based delivery is now the default mode (per PR feat(install): default to PR-based scaffold delivery #2533).

  • [flag-naming-consistency] internal/cli/admin.go — The --direct flag name could be more descriptive (e.g., --direct-commit) to clarify it controls commit delivery mode rather than some other aspect of installation. Current naming follows the brevity of --vendor but is less self-documenting than --skip-app-setup or --enroll-all.
    Remediation: Consider renaming to --direct-commit for clarity.

Previous run (3)

Review

Findings

Medium

  • [default-inversion-risk] internal/cli/admin.go — Inverting the default behavior from direct-commit to PR-based delivery is a breaking change for users who have CI/CD pipelines or scripts that depend on the install command completing without a manual PR merge step. The --direct flag provides backward compatibility, but existing automation calling fullsend admin install or fullsend github setup without --direct will now require a manual PR merge to complete setup.
    Remediation: Ensure release notes clearly communicate the default behavior change and the --direct escape hatch for automation users.

Low

  • [error-handling] internal/forge/github/github.goisNoChangesError only checks apiErr.Errors[].Message, while the analogous isAlreadyExistsError concatenates apiErr.Message and all apiErr.Errors[].Message before searching. While GitHub's current behavior places the "No commits between" message in the Errors array (not the top-level Message), matching the existing helper pattern would be more defensive against future API changes.
    Remediation: Mirror the pattern used in isAlreadyExistsError by concatenating all message fields before checking.

  • [stale-behavior-description] docs/ADRs/0005-forge-abstraction-layer.md:38 — Describes CommitFilesToBranch as "enabling the protected-branch fallback path." With PR-based delivery becoming the default, this characterization is slightly inaccurate. ADRs are point-in-time records, so the staleness is minor.
    Remediation: Consider adding a brief annotation noting that PR-based delivery is now the default mode (per PR feat(install): default to PR-based scaffold delivery #2533).


Labels: PR modifies CLI install/setup commands and the layers/forge packages that handle scaffold delivery

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment component/install CLI install and app setup component/docs User-facing documentation labels Jun 22, 2026

@ralphbean ralphbean 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.

The code change itself looks solid — clean split, good idempotency handling with ErrNoChanges, tests cover both modes. Two non-blocking notes inline.

The blocker is e2e coverage. TestAdminInstallUninstall runs admin install without --direct, so after this PR the scaffold files land on a feature branch via PR. But the assertions at lines 158-215 of e2e/admin/admin_test.go call GetFileContent on the default branch — they'd fail because the files aren't there yet.

I think we need a new phase between Phase 1 and Phase 2 that finds and merges the scaffold PR on .fullsend before checking file contents (similar to mergeEnrollmentPR for the test-repo). That way we exercise the new default end-to-end.

Also — e2e is skipping on this PR because of the author authorization gate. @waynesun09, could you check your membership visibility in the fullsend-ai org? If it's set to private, author_association comes back as NONE and the gate skips. Setting it to public should let e2e run automatically for your PRs.

}
return false
}

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.

[moderate] isNoChangesError only iterates apiErr.Errors detail messages, but isAlreadyExistsError (line 962) concatenates the top-level apiErr.Message with all detail messages before searching. If GitHub ever puts "no commits between" in the top-level Message instead of the Errors array, this would miss it. Would it make sense to align with the existing pattern?

Non-blocking — the current GitHub API consistently puts this in the detail array.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — aligned isNoChangesError with the existing concatenation pattern used by isAlreadyExistsError and isBranchProtectionError. Now concatenates apiErr.Message + all apiErr.Errors[].Message before searching. Added a test case for the top-level message scenario too.

Comment thread internal/layers/commit.go
const scaffoldBranch = "fullsend/scaffold-install"
if branchErr := client.CreateBranch(ctx, owner, repo, scaffoldBranch); branchErr != nil {
if !forge.IsAlreadyExists(branchErr) {
printer.StepFail("Failed to create scaffold branch")

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.

[moderate] If a prior scaffold PR was merged and the branch still exists, CreateBranch returns IsAlreadyExists and keeps its old base. If main advanced after the merge, CreateChangeProposal could open a PR with a confusing diff (branch is behind main). Now that PR delivery is the default path rather than a fallback, a stale branch producing a confusing PR on re-run could surprise users.

Non-blocking, but worth thinking about whether to delete-and-recreate the branch when it already exists.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point. The stale-branch scenario is real — if a merged PR's branch lingers and main advances, the re-run would open a PR with a confusing diff. However, the forge interface doesn't currently have a DeleteBranch method, so adding delete-and-recreate would require extending the forge.Client interface across all implementations. I think that's better as a follow-up since it's a larger scope change. I'll file an issue to track it.

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 1:25 AM UTC · Ended 1:38 AM UTC
Commit: 4e21a60 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge and removed requires-manual-review Review requires human judgment labels Jun 23, 2026
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:25 AM UTC · Completed 1:38 AM UTC
Commit: 6ade7ad · View workflow run →

Split CommitScaffoldFiles into commitScaffoldViaPR (new default) and
commitScaffoldDirect (--direct flag). Add WithDirect() builder to
WorkflowsLayer. PR-based delivery creates a fullsend/scaffold-install
branch, commits files, and opens a PR. The --direct flag preserves the
old direct-push-first behavior with branch protection fallback.

Closes #483

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Thread the direct parameter through buildLayerStack, runInstall,
applyPerRepoScaffold, and the github setup config. Both commands
default to PR-based scaffold delivery; --direct restores the old
direct-commit behavior.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 1:43 AM UTC · Ended 1:58 AM UTC
Commit: 4e21a60 · View workflow run →

l.ui.StepStart(fmt.Sprintf("Committing scaffold files to %s/%s (%s branch)",
l.org, forge.ConfigRepoName, cfgRepo.DefaultBranch))
} else {
l.ui.StepStart(fmt.Sprintf("Creating scaffold PR for %s/%s (target: %s)",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] default-inversion-risk

In default PR mode (direct=false), commitScaffoldViaPR always returns committed=false, so activateRepoMaintenance is never called. The test explicitly asserts this behavior, confirming it is intentional. The merge event itself pushes workflow files to the default branch, triggering GitHub workflow registration. If this assumption does not hold for all GitHub plan tiers, users would need to manually trigger repo-maintenance.yml.

Suggested fix: Verify that merging the scaffold PR reliably triggers workflow registration across all GitHub plan tiers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good observation. The e2e tests currently use --direct to keep the existing assertion pattern (verify files on default branch immediately after install). Updating e2e to exercise the default PR flow — create scaffold PR, merge it, then verify files on the default branch — is a larger change that deserves its own issue. I'll file one to track that alongside verifying workflow registration across GitHub plan tiers.

Comment thread internal/cli/admin.go
@@ -1835,6 +1842,7 @@ func buildLayerStack(
analyzeFullsendSource string,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] parameter-explosion

buildLayerStack (17 positional parameters) and runInstall (21+ positional parameters) each gain another trailing bool. Pre-existing pattern made marginally worse by this PR.

Suggested fix: Refactor to use config structs (e.g., LayerStackConfig) following the pattern of githubSetupConfig and perRepoInstallConfig.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed — buildLayerStack and runInstall are overdue for a config-struct refactor. Pre-existing pattern that this PR makes marginally worse. Will file a follow-up.

@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge and removed ready-for-merge All reviewers approved — ready to merge labels Jun 23, 2026
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:43 AM UTC · Completed 1:57 AM UTC
Commit: 6b9f8a7 · View workflow run →

Update existing tests to use WithDirect(true) for direct-mode tests.
Add TestWorkflowsLayer_Install_DefaultCreatesPR to verify the new
default. Update assertions in github_test.go for PR-mode delivery.
Update installation docs and CLI internals to reflect the new default.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 2:42 PM UTC · Completed 2:54 PM UTC
Commit: 3e6efc9 · View workflow run →

@waynesun09
waynesun09 dismissed ralphbean’s stale review June 23, 2026 14:52

Both concerns addressed: (1) e2e tests now pass --direct to preserve existing assertion pattern, e2e CI is green. (2) Full PR-flow e2e coverage tracked in #2558. Stale-branch follow-up in #2560.

@waynesun09
waynesun09 added this pull request to the merge queue Jun 23, 2026

@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 internal/cli/admin.go
@@ -1216,7 +1223,7 @@ func runDryRun(ctx context.Context, client forge.Client, printer *ui.Printer, or
dispatcher = gcf.NewProvisioner(gcf.Config{}, nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] architectural-inconsistency

The dry-run code path passes direct=false to buildLayerStack as a hardcoded value, so --dry-run always previews PR-mode behavior regardless of whether the user also passed --direct. This creates a mismatch between the dry-run preview and actual execution.

Suggested fix: Thread the direct flag value into runDryRun() and pass it to buildLayerStack instead of hardcoding false.

Comment thread internal/cli/github.go
@@ -979,7 +981,7 @@ func runGitHubSyncScaffold(ctx context.Context, client forge.Client, printer *ui
return fmt.Errorf("reading config.yaml: %w", cfgErr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] scope-authorization-mismatch

The sync-scaffold command hardcodes WithDirect(true), bypassing PR-based delivery. While sync-scaffold is a day-2 admin operation, this creates an inconsistency where fresh installs require PR approval but scaffold updates do not.

Suggested fix: Consider exposing --direct as a flag on sync-scaffold rather than hardcoding the choice.

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment and removed ready-for-merge All reviewers approved — ready to merge labels Jun 23, 2026
Merged via the queue into main with commit ce6c786 Jun 23, 2026
22 checks passed
@waynesun09
waynesun09 deleted the feat-default-pr-install branch June 23, 2026 15:03
@fullsend-ai-retro

fullsend-ai-retro Bot commented Jun 23, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 3:08 PM UTC · Completed 3:18 PM UTC
Commit: 3e6efc9 · View workflow run →

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #2533 — feat(install): default to PR-based scaffold delivery

Timeline

PR #2533 was a human-authored PR by waynesun09 closing #483. It switched fullsend admin install from direct-commit to PR-based scaffold delivery by default, adding a --direct flag for the old behavior.

Key events:

  • Jun 22 20:14 UTC — PR created (3 commits)
  • Jun 22 20:18–20:33 — Two review bot runs cancelled on commit 4e21a60 (run 27981041210, run 27981909507)
  • Jun 22 20:39–20:52 — Review bot success (run 27982271590). Posted 1 medium + 2 low findings (dry-run flag inconsistency, scope mismatch, test adequacy)
  • Jun 22 21:22 — ralphbean (human) reviewed with substantive blocking concern: existing e2e test TestAdminInstallUninstall assertions would break because they check files on the default branch, but PR-mode puts them on a feature branch. Also noted e2e auth gate issue and two non-blocking inline suggestions.
  • Jun 23 01:22 — waynesun09 replied to inline comments
  • Jun 23 01:38 & 01:58 — Review bot approved twice in quick succession (run 27995600943, run 27996226306), triggered by pull_request_review events from author comment replies
  • Jun 23 05:47 — rh-hemartin approved (human)
  • Jun 23 14:40 — waynesun09 replied to more review comments, triggering 3 more fullsend.yaml dispatches
  • Jun 23 14:42–14:54 — Another review bot run (run 28034123291)
  • Jun 23 15:04 — PR merged

Total: 9 review bot comments on the PR. 2 cancelled runs + 4 successful review runs.

Review Quality Assessment

The review bot's findings were legitimate but lower-severity (1 medium about dry-run flag, 2 lows). The human reviewer (ralphbean) caught the highest-impact issue: a behavioral default inversion that would break existing e2e test assertions — something the bot missed entirely.

Existing Issue Coverage

Most patterns observed are already covered by open issues:

One gap identified: review agent missing that default-behavior inversions can invalidate unchanged tests.

Proposals filed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/docs User-facing documentation component/install CLI install and app setup requires-manual-review Review requires human judgment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Require an approved PR for all changes to the .fullsend and target repos

3 participants