@@ -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
@@ -655,35 +655,7 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
655655 printer .StepWarn ("Using provided WIF provider value — skipping inference provider auto-provisioning" )
656656 }
657657
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-
668658 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- })
687659
688660 needsWIFProvision := inferenceWIFProvider == ""
689661
@@ -823,7 +795,14 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
823795 printer .StepInfo (fmt .Sprintf (" Repo restriction: %s/%s" , owner , repo ))
824796 printer .Blank ()
825797 }
826- for _ , f := range files {
798+ dryRunFiles , dryRunErr := repos .BuildScaffoldFiles (repos.InstallConfig {
799+ Owner : owner , Repo : repo , Roles : roles ,
800+ VendorBinary : vendor , UpstreamRef : upstreamRef , UpstreamTag : upstreamTag ,
801+ })
802+ if dryRunErr != nil {
803+ return fmt .Errorf ("generating scaffold files for dry run: %w" , dryRunErr )
804+ }
805+ for _ , f := range dryRunFiles {
827806 printer .StepDone (fmt .Sprintf ("Would write: %s (%d bytes)" , f .Path , len (f .Content )))
828807 }
829808 printer .Blank ()
@@ -970,46 +949,125 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
970949 printer .StepDone ("Mint validated and org registered" )
971950 }
972951
952+ // Delegate WIF provisioning, scaffold commit, and variable/secret
953+ // writes to the reusable repos.Install function. This enables the
954+ // future `fullsend repos install` command to share the same logic.
955+ var wifProvisioner repos.WIFProvisioner
973956 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 )
957+ wifProvisioner = & gcfWIFAdapter {
958+ provisioner : gcf .NewProvisioner (gcf.Config {
959+ ProjectID : inferenceProject ,
960+ GitHubOrgs : []string {owner },
961+ Repo : owner + "/" + repo ,
962+ WIFPoolName : gcf .DefaultInferencePool ,
963+ }, gcf .NewLiveGCFClient (inferenceProject )),
964+ }
965+ }
966+
967+ // Scaffold commit function wrapping layers.CommitScaffoldFiles, which
968+ // provides retry on non-fast-forward errors, branch-protection fallback
969+ // to PR delivery, and fork-based PR support for non-owner users.
970+ scaffoldCommitFn := func (ctx context.Context , owner , repo string , files []forge.TreeFile , direct bool ) error {
971+ targetRepo , repoErr := client .GetRepo (ctx , owner , repo )
972+ if repoErr != nil {
973+ return fmt .Errorf ("getting repo info: %w" , repoErr )
974+ }
975+ commitMsg := fmt .Sprintf ("chore: initialize fullsend-%s per-repo installation" , version )
976+ prTitle := "chore: initialize fullsend per-repo installation"
977+ prBody := "This PR adds the fullsend scaffold files for per-repo installation.\n \n " +
978+ "Merge this PR to activate fullsend workflows."
979+ if direct {
980+ printer .StepStart (fmt .Sprintf ("Committing scaffold files to %s/%s (%s branch)" ,
981+ owner , repo , targetRepo .DefaultBranch ))
982+ } else {
983+ printer .StepStart (fmt .Sprintf ("Creating scaffold PR for %s/%s (target: %s)" ,
984+ owner , repo , targetRepo .DefaultBranch ))
986985 }
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" )
986+ _ , err := layers . CommitScaffoldFiles ( ctx , client , printer , owner , repo ,
987+ targetRepo . DefaultBranch , commitMsg , prTitle , prBody , files , direct , os . Stdin )
988+ return err
990989 }
991990
992- repoVars := map [string ]string {
993- "FULLSEND_MINT_URL" : mintURL ,
994- "FULLSEND_GCP_REGION" : inferenceRegion ,
995- forge .PerRepoGuardVar : "true" ,
991+ installCfg := repos.InstallConfig {
992+ Owner : owner ,
993+ Repo : repo ,
994+ Roles : roles ,
995+ MintURL : mintURL ,
996+ InferenceProject : inferenceProject ,
997+ InferenceRegion : inferenceRegion ,
998+ UpstreamRef : upstreamRef ,
999+ UpstreamTag : upstreamTag ,
1000+ SkipMintCheck : true , // already handled above
1001+ SkipAppSetup : true , // already handled above
1002+ SkipGuardCheck : true , // admin.go handles guard check itself
1003+ SkipWIF : ! needsWIFProvision ,
1004+ WIFProvider : inferenceWIFProvider ,
1005+ VendorBinary : vendor ,
1006+ Direct : c .Direct ,
1007+ SkipScaffoldAndConfig : vendor , // vendor path commits scaffold+vendor atomically below
1008+ }
1009+
1010+ progressFn := func (_ string , phase , msg string ) {
1011+ switch phase {
1012+ case "wif" :
1013+ if strings .Contains (msg , "Provisioning" ) {
1014+ printer .StepStart (msg )
1015+ } else if strings .Contains (msg , "ready" ) {
1016+ printer .StepDone (msg )
1017+ printer .StepInfo ("IAM policy changes may take up to 7 minutes to propagate" )
1018+ printer .StepInfo ("Agent workflows that authenticate via WIF may fail until propagation completes" )
1019+ }
1020+ case "scaffold" :
1021+ if strings .Contains (msg , "Committing" ) || strings .Contains (msg , "Generating" ) {
1022+ printer .StepStart (msg )
1023+ } else {
1024+ printer .StepDone (msg )
1025+ }
1026+ case "vars" :
1027+ if strings .Contains (msg , "Configuring" ) {
1028+ printer .StepStart (msg )
1029+ } else {
1030+ printer .StepDone (msg )
1031+ }
1032+ case "secrets" :
1033+ if strings .Contains (msg , "Configuring" ) {
1034+ printer .StepStart (msg )
1035+ } else {
1036+ printer .StepDone (msg )
1037+ }
1038+ }
9961039 }
9971040
998- repoSecrets := map [string ]string {
999- "FULLSEND_GCP_PROJECT_ID" : inferenceProject ,
1000- "FULLSEND_GCP_WIF_PROVIDER" : inferenceWIFProvider ,
1041+ installResult , installErr := repos .Install (ctx , installCfg , client , wifProvisioner , scaffoldCommitFn , progressFn )
1042+ if installErr != nil {
1043+ return installErr
1044+ }
1045+
1046+ if installResult .WIFProvider != "" {
1047+ inferenceWIFProvider = installResult .WIFProvider
10011048 }
10021049
10031050 if vendor {
1004- var vendorErr error
1005- files , _ , vendorErr = appendVendorTreeFiles (printer , owner , repo , files , vendor , fullsendBinary , fullsendSource )
1051+ scaffoldFiles , buildErr := repos .BuildScaffoldFiles (installCfg )
1052+ if buildErr != nil {
1053+ return fmt .Errorf ("building scaffold files for vendor: %w" , buildErr )
1054+ }
1055+ vendorFiles , _ , vendorErr := appendVendorTreeFiles (printer , owner , repo , scaffoldFiles , vendor , fullsendBinary , fullsendSource )
10061056 if vendorErr != nil {
10071057 return fmt .Errorf ("collecting vendored assets: %w" , vendorErr )
10081058 }
1009- }
1010-
1011- if err := applyPerRepoScaffold (ctx , client , printer , owner , repo , files , repoVars , repoSecrets , c .Direct ); err != nil {
1012- return err
1059+ repoVars := map [string ]string {
1060+ "FULLSEND_MINT_URL" : mintURL ,
1061+ "FULLSEND_GCP_REGION" : inferenceRegion ,
1062+ forge .PerRepoGuardVar : "true" ,
1063+ }
1064+ repoSecrets := map [string ]string {
1065+ "FULLSEND_GCP_PROJECT_ID" : inferenceProject ,
1066+ "FULLSEND_GCP_WIF_PROVIDER" : inferenceWIFProvider ,
1067+ }
1068+ if err := applyPerRepoScaffold (ctx , client , printer , owner , repo , vendorFiles , repoVars , repoSecrets , c .Direct ); err != nil {
1069+ return err
1070+ }
10131071 }
10141072
10151073 if ! vendor {
@@ -1023,6 +1081,58 @@ func runPerRepoInstall(ctx context.Context, c perRepoInstallConfig) error {
10231081 return nil
10241082}
10251083
1084+ // gcfWIFAdapter wraps a gcf.Provisioner to implement repos.WIFProvisioner,
1085+ // bridging the GCF-specific provisioner to the package-agnostic interface.
1086+ type gcfWIFAdapter struct {
1087+ provisioner * gcf.Provisioner
1088+ }
1089+
1090+ func (a * gcfWIFAdapter ) DiscoverMint (ctx context.Context ) (* repos.MintDiscovery , error ) {
1091+ if a .provisioner == nil {
1092+ return nil , repos .ErrMintNotFound
1093+ }
1094+ d , err := a .provisioner .DiscoverMint (ctx )
1095+ if err != nil {
1096+ if errors .Is (err , gcf .ErrFunctionNotFound ) {
1097+ return nil , fmt .Errorf ("%w" , repos .ErrMintNotFound )
1098+ }
1099+ return nil , err
1100+ }
1101+ return & repos.MintDiscovery {
1102+ URL : d .URL ,
1103+ RoleAppIDs : d .RoleAppIDs ,
1104+ PerRepoWIFRepos : d .PerRepoWIFRepos ,
1105+ }, nil
1106+ }
1107+
1108+ func (a * gcfWIFAdapter ) ProvisionWIF (ctx context.Context ) (string , error ) {
1109+ if a .provisioner == nil {
1110+ return "" , fmt .Errorf ("WIF provisioner not configured" )
1111+ }
1112+ return a .provisioner .ProvisionWIF (ctx )
1113+ }
1114+
1115+ func (a * gcfWIFAdapter ) RegisterPerRepoWIF (ctx context.Context , repo string ) error {
1116+ if a .provisioner == nil {
1117+ return fmt .Errorf ("WIF provisioner not configured" )
1118+ }
1119+ return a .provisioner .RegisterPerRepoWIF (ctx , repo )
1120+ }
1121+
1122+ func (a * gcfWIFAdapter ) EnsureOrgInMint (ctx context.Context , expectedURL string , org string ) error {
1123+ if a .provisioner == nil {
1124+ return fmt .Errorf ("WIF provisioner not configured" )
1125+ }
1126+ return a .provisioner .EnsureOrgInMint (ctx , expectedURL , org )
1127+ }
1128+
1129+ func (a * gcfWIFAdapter ) DeletePerRepoWIF (ctx context.Context , repo string ) error {
1130+ if a .provisioner == nil {
1131+ return fmt .Errorf ("WIF provisioner not configured" )
1132+ }
1133+ return a .provisioner .RemoveRepoFromMint (ctx , repo )
1134+ }
1135+
10261136// applyPerRepoScaffold commits scaffold files to the repo's default branch
10271137// and configures the repository variables and secrets needed for fullsend.
10281138func applyPerRepoScaffold (ctx context.Context , client forge.Client , printer * ui.Printer ,
0 commit comments