Skip to content

Commit 3bbbea0

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

3 files changed

Lines changed: 1167 additions & 58 deletions

File tree

internal/cli/admin.go

Lines changed: 166 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ 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"
33+
3334
"github.com/fullsend-ai/fullsend/internal/ui"
3435
)
3536

@@ -655,35 +656,7 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
655656
printer.StepWarn("Using provided WIF provider value — skipping inference provider auto-provisioning")
656657
}
657658

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-
668659
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-
})
687660

688661
needsWIFProvision := inferenceWIFProvider == ""
689662

@@ -823,7 +796,14 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
823796
printer.StepInfo(fmt.Sprintf(" Repo restriction: %s/%s", owner, repo))
824797
printer.Blank()
825798
}
826-
for _, f := range files {
799+
dryRunFiles, dryRunErr := repos.BuildScaffoldFiles(repos.InstallConfig{
800+
Owner: owner, Repo: repo, Roles: roles,
801+
VendorBinary: vendor, UpstreamRef: upstreamRef, UpstreamTag: upstreamTag,
802+
})
803+
if dryRunErr != nil {
804+
return fmt.Errorf("generating scaffold files for dry run: %w", dryRunErr)
805+
}
806+
for _, f := range dryRunFiles {
827807
printer.StepDone(fmt.Sprintf("Would write: %s (%d bytes)", f.Path, len(f.Content)))
828808
}
829809
printer.Blank()
@@ -970,46 +950,125 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
970950
printer.StepDone("Mint validated and org registered")
971951
}
972952

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

992-
repoVars := map[string]string{
993-
"FULLSEND_MINT_URL": mintURL,
994-
"FULLSEND_GCP_REGION": inferenceRegion,
995-
forge.PerRepoGuardVar: "true",
1042+
installResult, installErr := repos.Install(ctx, installCfg, client, wifProv, scaffoldCommitFn, progressFn)
1043+
if installErr != nil {
1044+
return installErr
9961045
}
9971046

998-
repoSecrets := map[string]string{
999-
"FULLSEND_GCP_PROJECT_ID": inferenceProject,
1000-
"FULLSEND_GCP_WIF_PROVIDER": inferenceWIFProvider,
1047+
if installResult.WIFProvider != "" {
1048+
inferenceWIFProvider = installResult.WIFProvider
10011049
}
10021050

10031051
if vendor {
1004-
var vendorErr error
1005-
files, _, vendorErr = appendVendorTreeFiles(printer, owner, repo, files, vendor, fullsendBinary, fullsendSource)
1052+
scaffoldFiles, buildErr := repos.BuildScaffoldFiles(installCfg)
1053+
if buildErr != nil {
1054+
return fmt.Errorf("building scaffold files for vendor: %w", buildErr)
1055+
}
1056+
vendorFiles, _, vendorErr := appendVendorTreeFiles(printer, owner, repo, scaffoldFiles, vendor, fullsendBinary, fullsendSource)
10061057
if vendorErr != nil {
10071058
return fmt.Errorf("collecting vendored assets: %w", vendorErr)
10081059
}
1009-
}
1010-
1011-
if err := applyPerRepoScaffold(ctx, client, printer, owner, repo, files, repoVars, repoSecrets, c.Direct); err != nil {
1012-
return err
1060+
repoVars := map[string]string{
1061+
"FULLSEND_MINT_URL": mintURL,
1062+
"FULLSEND_GCP_REGION": inferenceRegion,
1063+
forge.PerRepoGuardVar: "true",
1064+
}
1065+
repoSecrets := map[string]string{
1066+
"FULLSEND_GCP_PROJECT_ID": inferenceProject,
1067+
"FULLSEND_GCP_WIF_PROVIDER": inferenceWIFProvider,
1068+
}
1069+
if err := applyPerRepoScaffold(ctx, client, printer, owner, repo, vendorFiles, repoVars, repoSecrets, c.Direct); err != nil {
1070+
return err
1071+
}
10131072
}
10141073

10151074
if !vendor {
@@ -1023,6 +1082,55 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
10231082
return nil
10241083
}
10251084

1085+
// gcfWIFAdapter wraps a gcf.Provisioner to implement repos.WIFProvisioner,
1086+
// bridging the GCF-specific provisioner to the package-agnostic interface.
1087+
type gcfWIFAdapter struct {
1088+
provisioner *gcf.Provisioner
1089+
}
1090+
1091+
func (a *gcfWIFAdapter) DiscoverMint(ctx context.Context) (*repos.MintDiscovery, error) {
1092+
if a.provisioner == nil {
1093+
return nil, repos.ErrMintNotFound
1094+
}
1095+
d, err := a.provisioner.DiscoverMint(ctx)
1096+
if err != nil {
1097+
return nil, err
1098+
}
1099+
return &repos.MintDiscovery{
1100+
URL: d.URL,
1101+
RoleAppIDs: d.RoleAppIDs,
1102+
PerRepoWIFRepos: d.PerRepoWIFRepos,
1103+
}, nil
1104+
}
1105+
1106+
func (a *gcfWIFAdapter) ProvisionWIF(ctx context.Context) (string, error) {
1107+
if a.provisioner == nil {
1108+
return "", fmt.Errorf("WIF provisioner not configured")
1109+
}
1110+
return a.provisioner.ProvisionWIF(ctx)
1111+
}
1112+
1113+
func (a *gcfWIFAdapter) RegisterPerRepoWIF(ctx context.Context, repo string) error {
1114+
if a.provisioner == nil {
1115+
return fmt.Errorf("WIF provisioner not configured")
1116+
}
1117+
return a.provisioner.RegisterPerRepoWIF(ctx, repo)
1118+
}
1119+
1120+
func (a *gcfWIFAdapter) EnsureOrgInMint(ctx context.Context, expectedURL string, org string) error {
1121+
if a.provisioner == nil {
1122+
return fmt.Errorf("WIF provisioner not configured")
1123+
}
1124+
return a.provisioner.EnsureOrgInMint(ctx, expectedURL, org)
1125+
}
1126+
1127+
func (a *gcfWIFAdapter) DeletePerRepoWIF(ctx context.Context, repo string) error {
1128+
if a.provisioner == nil {
1129+
return fmt.Errorf("WIF provisioner not configured")
1130+
}
1131+
return a.provisioner.RemoveRepoFromMint(ctx, repo)
1132+
}
1133+
10261134
// applyPerRepoScaffold commits scaffold files to the repo's default branch
10271135
// and configures the repository variables and secrets needed for fullsend.
10281136
func applyPerRepoScaffold(ctx context.Context, client forge.Client, printer *ui.Printer,

0 commit comments

Comments
 (0)