Skip to content

Commit 2b48644

Browse files
committed
refactor(repos): extract per-repo install logic into internal/repos package
Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: Greg Allen <gallen@redhat.com>
1 parent 6cfeae9 commit 2b48644

4 files changed

Lines changed: 1636 additions & 65 deletions

File tree

internal/cli/admin.go

Lines changed: 189 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929
"github.com/fullsend-ai/fullsend/internal/inference/vertex"
3030
"github.com/fullsend-ai/fullsend/internal/layers"
3131
"github.com/fullsend-ai/fullsend/internal/mintcore"
32-
"github.com/fullsend-ai/fullsend/internal/scaffold"
32+
"github.com/fullsend-ai/fullsend/internal/repos"
3333
"github.com/fullsend-ai/fullsend/internal/ui"
3434
)
3535

@@ -156,6 +156,12 @@ type perRepoInstallConfig struct {
156156
FullsendBinary string
157157
FullsendSource string
158158
Direct bool
159+
160+
// Testing overrides — when non-nil, used instead of resolving from
161+
// the environment. Not set by CLI flag parsing.
162+
testClient forge.Client
163+
testPrinter *ui.Printer
164+
testWIFProvisioner repos.WIFProvisioner
159165
}
160166

161167
// wifProviderPattern validates the full WIF provider resource name format
@@ -638,14 +644,20 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
638644
return err
639645
}
640646

641-
token, err := resolveToken()
642-
if err != nil {
643-
return err
647+
var client forge.Client
648+
var printer *ui.Printer
649+
if c.testClient != nil {
650+
client = c.testClient
651+
printer = c.testPrinter
652+
} else {
653+
token, tokenErr := resolveToken()
654+
if tokenErr != nil {
655+
return tokenErr
656+
}
657+
client = gh.New(token)
658+
printer = ui.New(os.Stdout)
644659
}
645660

646-
client := gh.New(token)
647-
printer := ui.New(os.Stdout)
648-
649661
printer.Banner(Version())
650662
printer.Blank()
651663
printer.Header("Installing per-repo fullsend for " + repoFullName)
@@ -655,35 +667,7 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
655667
printer.StepWarn("Using provided WIF provider value — skipping inference provider auto-provisioning")
656668
}
657669

658-
cfg := config.NewPerRepoConfig(roles, repoFullName)
659-
if err := cfg.Validate(); err != nil {
660-
return fmt.Errorf("invalid config: %w", err)
661-
}
662-
663-
cfgYAML, err := cfg.Marshal()
664-
if err != nil {
665-
return fmt.Errorf("marshaling per-repo config: %w", err)
666-
}
667-
668670
upstreamRef, upstreamTag := resolveUpstreamRef()
669-
installFiles, err := scaffold.CollectPerRepoInstallFiles(vendor, upstreamRef, upstreamTag)
670-
if err != nil {
671-
return fmt.Errorf("collecting per-repo scaffold files: %w", err)
672-
}
673-
674-
var files []forge.TreeFile
675-
for _, f := range installFiles {
676-
files = append(files, forge.TreeFile{
677-
Path: f.Path,
678-
Content: f.Content,
679-
Mode: f.Mode,
680-
})
681-
}
682-
files = append(files, forge.TreeFile{
683-
Path: ".fullsend/config.yaml",
684-
Content: cfgYAML,
685-
Mode: "100644",
686-
})
687671

688672
needsWIFProvision := inferenceWIFProvider == ""
689673

@@ -823,7 +807,14 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
823807
printer.StepInfo(fmt.Sprintf(" Repo restriction: %s/%s", owner, repo))
824808
printer.Blank()
825809
}
826-
for _, f := range files {
810+
dryRunFiles, dryRunErr := repos.BuildScaffoldFiles(repos.InstallConfig{
811+
Owner: owner, Repo: repo, Roles: roles,
812+
VendorBinary: vendor, UpstreamRef: upstreamRef, UpstreamTag: upstreamTag,
813+
})
814+
if dryRunErr != nil {
815+
return fmt.Errorf("generating scaffold files for dry run: %w", dryRunErr)
816+
}
817+
for _, f := range dryRunFiles {
827818
printer.StepDone(fmt.Sprintf("Would write: %s (%d bytes)", f.Path, len(f.Content)))
828819
}
829820
printer.Blank()
@@ -970,46 +961,127 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
970961
printer.StepDone("Mint validated and org registered")
971962
}
972963

973-
if needsWIFProvision {
974-
printer.StepStart("Provisioning WIF infrastructure")
975-
provisioner := gcf.NewProvisioner(gcf.Config{
976-
ProjectID: inferenceProject,
977-
GitHubOrgs: []string{owner},
978-
Repo: owner + "/" + repo,
979-
WIFPoolName: gcf.DefaultInferencePool,
980-
}, gcf.NewLiveGCFClient(inferenceProject))
981-
var provErr error
982-
inferenceWIFProvider, provErr = provisioner.ProvisionWIF(ctx)
983-
if provErr != nil {
984-
printer.StepFail("WIF provisioning failed")
985-
return fmt.Errorf("provisioning WIF: %w", provErr)
964+
// Delegate WIF provisioning, scaffold commit, and variable/secret
965+
// writes to the reusable repos.Install function. This enables the
966+
// future `fullsend repos install` command to share the same logic.
967+
var wifProvisioner repos.WIFProvisioner
968+
if c.testWIFProvisioner != nil {
969+
wifProvisioner = c.testWIFProvisioner
970+
} else if needsWIFProvision {
971+
wifProvisioner = &gcfWIFAdapter{
972+
provisioner: gcf.NewProvisioner(gcf.Config{
973+
ProjectID: inferenceProject,
974+
GitHubOrgs: []string{owner},
975+
Repo: owner + "/" + repo,
976+
WIFPoolName: gcf.DefaultInferencePool,
977+
}, gcf.NewLiveGCFClient(inferenceProject)),
978+
}
979+
}
980+
981+
// Scaffold commit function wrapping layers.CommitScaffoldFiles, which
982+
// provides retry on non-fast-forward errors, branch-protection fallback
983+
// to PR delivery, and fork-based PR support for non-owner users.
984+
scaffoldCommitFn := func(ctx context.Context, owner, repo string, files []forge.TreeFile, direct bool) error {
985+
targetRepo, repoErr := client.GetRepo(ctx, owner, repo)
986+
if repoErr != nil {
987+
return fmt.Errorf("getting repo info: %w", repoErr)
988+
}
989+
commitMsg := fmt.Sprintf("chore: initialize fullsend-%s per-repo installation", version)
990+
prTitle := "chore: initialize fullsend per-repo installation"
991+
prBody := "This PR adds the fullsend scaffold files for per-repo installation.\n\n" +
992+
"Merge this PR to activate fullsend workflows."
993+
if direct {
994+
printer.StepStart(fmt.Sprintf("Committing scaffold files to %s/%s (%s branch)",
995+
owner, repo, targetRepo.DefaultBranch))
996+
} else {
997+
printer.StepStart(fmt.Sprintf("Creating scaffold PR for %s/%s (target: %s)",
998+
owner, repo, targetRepo.DefaultBranch))
999+
}
1000+
_, err := layers.CommitScaffoldFiles(ctx, client, printer, owner, repo,
1001+
targetRepo.DefaultBranch, commitMsg, prTitle, prBody, files, direct, os.Stdin)
1002+
return err
1003+
}
1004+
1005+
installCfg := repos.InstallConfig{
1006+
Owner: owner,
1007+
Repo: repo,
1008+
Roles: roles,
1009+
MintURL: mintURL,
1010+
InferenceProject: inferenceProject,
1011+
InferenceRegion: inferenceRegion,
1012+
UpstreamRef: upstreamRef,
1013+
UpstreamTag: upstreamTag,
1014+
SkipMintCheck: true, // already handled above
1015+
SkipAppSetup: true, // already handled above
1016+
SkipGuardCheck: true, // admin.go handles guard check itself
1017+
SkipWIF: !needsWIFProvision,
1018+
WIFProvider: inferenceWIFProvider,
1019+
VendorBinary: vendor,
1020+
Direct: c.Direct,
1021+
SkipScaffoldAndConfig: vendor, // vendor path commits scaffold+vendor atomically below
1022+
}
1023+
1024+
progressFn := func(_ string, phase, msg string) {
1025+
switch phase {
1026+
case "wif":
1027+
if strings.Contains(msg, "Provisioning") {
1028+
printer.StepStart(msg)
1029+
} else if strings.Contains(msg, "ready") {
1030+
printer.StepDone(msg)
1031+
printer.StepInfo("IAM policy changes may take up to 7 minutes to propagate")
1032+
printer.StepInfo("Agent workflows that authenticate via WIF may fail until propagation completes")
1033+
}
1034+
case "scaffold":
1035+
if strings.Contains(msg, "Committing") || strings.Contains(msg, "Generating") {
1036+
printer.StepStart(msg)
1037+
} else {
1038+
printer.StepDone(msg)
1039+
}
1040+
case "vars":
1041+
if strings.Contains(msg, "Configuring") {
1042+
printer.StepStart(msg)
1043+
} else {
1044+
printer.StepDone(msg)
1045+
}
1046+
case "secrets":
1047+
if strings.Contains(msg, "Configuring") {
1048+
printer.StepStart(msg)
1049+
} else {
1050+
printer.StepDone(msg)
1051+
}
9861052
}
987-
printer.StepDone("WIF infrastructure ready")
988-
printer.StepInfo("IAM policy changes may take up to 7 minutes to propagate")
989-
printer.StepInfo("Agent workflows that authenticate via WIF may fail until propagation completes")
9901053
}
9911054

992-
repoVars := map[string]string{
993-
"FULLSEND_MINT_URL": mintURL,
994-
"FULLSEND_GCP_REGION": inferenceRegion,
995-
forge.PerRepoGuardVar: "true",
1055+
installResult, installErr := repos.Install(ctx, installCfg, client, wifProvisioner, scaffoldCommitFn, progressFn)
1056+
if installErr != nil {
1057+
return installErr
9961058
}
9971059

998-
repoSecrets := map[string]string{
999-
"FULLSEND_GCP_PROJECT_ID": inferenceProject,
1000-
"FULLSEND_GCP_WIF_PROVIDER": inferenceWIFProvider,
1060+
if installResult.WIFProvider != "" {
1061+
inferenceWIFProvider = installResult.WIFProvider
10011062
}
10021063

10031064
if vendor {
1004-
var vendorErr error
1005-
files, _, vendorErr = appendVendorTreeFiles(printer, owner, repo, files, vendor, fullsendBinary, fullsendSource)
1065+
scaffoldFiles, buildErr := repos.BuildScaffoldFiles(installCfg)
1066+
if buildErr != nil {
1067+
return fmt.Errorf("building scaffold files for vendor: %w", buildErr)
1068+
}
1069+
vendorFiles, _, vendorErr := appendVendorTreeFiles(printer, owner, repo, scaffoldFiles, vendor, fullsendBinary, fullsendSource)
10061070
if vendorErr != nil {
10071071
return fmt.Errorf("collecting vendored assets: %w", vendorErr)
10081072
}
1009-
}
1010-
1011-
if err := applyPerRepoScaffold(ctx, client, printer, owner, repo, files, repoVars, repoSecrets, c.Direct); err != nil {
1012-
return err
1073+
repoVars := map[string]string{
1074+
"FULLSEND_MINT_URL": mintURL,
1075+
"FULLSEND_GCP_REGION": inferenceRegion,
1076+
forge.PerRepoGuardVar: "true",
1077+
}
1078+
repoSecrets := map[string]string{
1079+
"FULLSEND_GCP_PROJECT_ID": inferenceProject,
1080+
"FULLSEND_GCP_WIF_PROVIDER": inferenceWIFProvider,
1081+
}
1082+
if err := applyPerRepoScaffold(ctx, client, printer, owner, repo, vendorFiles, repoVars, repoSecrets, c.Direct); err != nil {
1083+
return err
1084+
}
10131085
}
10141086

10151087
if !vendor {
@@ -1023,6 +1095,58 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
10231095
return nil
10241096
}
10251097

1098+
// gcfWIFAdapter wraps a gcf.Provisioner to implement repos.WIFProvisioner,
1099+
// bridging the GCF-specific provisioner to the package-agnostic interface.
1100+
type gcfWIFAdapter struct {
1101+
provisioner *gcf.Provisioner
1102+
}
1103+
1104+
func (a *gcfWIFAdapter) DiscoverMint(ctx context.Context) (*repos.MintDiscovery, error) {
1105+
if a.provisioner == nil {
1106+
return nil, repos.ErrMintNotFound
1107+
}
1108+
d, err := a.provisioner.DiscoverMint(ctx)
1109+
if err != nil {
1110+
if errors.Is(err, gcf.ErrFunctionNotFound) {
1111+
return nil, fmt.Errorf("%w", repos.ErrMintNotFound)
1112+
}
1113+
return nil, err
1114+
}
1115+
return &repos.MintDiscovery{
1116+
URL: d.URL,
1117+
RoleAppIDs: d.RoleAppIDs,
1118+
PerRepoWIFRepos: d.PerRepoWIFRepos,
1119+
}, nil
1120+
}
1121+
1122+
func (a *gcfWIFAdapter) ProvisionWIF(ctx context.Context) (string, error) {
1123+
if a.provisioner == nil {
1124+
return "", fmt.Errorf("WIF provisioner not configured")
1125+
}
1126+
return a.provisioner.ProvisionWIF(ctx)
1127+
}
1128+
1129+
func (a *gcfWIFAdapter) RegisterPerRepoWIF(ctx context.Context, repo string) error {
1130+
if a.provisioner == nil {
1131+
return fmt.Errorf("WIF provisioner not configured")
1132+
}
1133+
return a.provisioner.RegisterPerRepoWIF(ctx, repo)
1134+
}
1135+
1136+
func (a *gcfWIFAdapter) EnsureOrgInMint(ctx context.Context, expectedURL string, org string) error {
1137+
if a.provisioner == nil {
1138+
return fmt.Errorf("WIF provisioner not configured")
1139+
}
1140+
return a.provisioner.EnsureOrgInMint(ctx, expectedURL, org)
1141+
}
1142+
1143+
func (a *gcfWIFAdapter) DeletePerRepoWIF(ctx context.Context, repo string) error {
1144+
if a.provisioner == nil {
1145+
return fmt.Errorf("WIF provisioner not configured")
1146+
}
1147+
return a.provisioner.RemoveRepoFromMint(ctx, repo)
1148+
}
1149+
10261150
// applyPerRepoScaffold commits scaffold files to the repo's default branch
10271151
// and configures the repository variables and secrets needed for fullsend.
10281152
func applyPerRepoScaffold(ctx context.Context, client forge.Client, printer *ui.Printer,

0 commit comments

Comments
 (0)