@@ -1980,6 +1980,8 @@ func TestApplyPerRepoScaffold_CommitFilesError(t *testing.T) {
19801980 "acme" , "widget" , files , nil , nil )
19811981 require .Error (t , err )
19821982 assert .Contains (t , err .Error (), "committing scaffold files" )
1983+ assert .Empty (t , client .CreatedBranches , "should not attempt fallback for generic error" )
1984+ assert .Empty (t , client .CreatedProposals , "should not attempt fallback for generic error" )
19831985}
19841986
19851987func TestApplyPerRepoScaffold_Idempotent (t * testing.T ) {
@@ -2046,3 +2048,169 @@ func TestApplyPerRepoScaffold_CreateSecretError(t *testing.T) {
20462048 require .Error (t , err )
20472049 assert .Contains (t , err .Error (), "setting repo secret" )
20482050}
2051+
2052+ func TestApplyPerRepoScaffold_ProtectedBranchFallback (t * testing.T ) {
2053+ client := forge .NewFakeClient ()
2054+ client .Repos = []forge.Repository {{FullName : "acme/widget" , DefaultBranch : "main" }}
2055+ client .Errors ["CommitFiles" ] = fmt .Errorf ("%w: github api: 422" , forge .ErrBranchProtected )
2056+ var buf bytes.Buffer
2057+ printer := ui .New (& buf )
2058+
2059+ files := []forge.TreeFile {
2060+ {Path : ".github/workflows/fullsend.yaml" , Content : []byte ("workflow" ), Mode : "100644" },
2061+ }
2062+ repoVars := map [string ]string {"K" : "V" }
2063+ repoSecrets := map [string ]string {"S" : "secret" }
2064+
2065+ err := applyPerRepoScaffold (context .Background (), client , printer ,
2066+ "acme" , "widget" , files , repoVars , repoSecrets )
2067+ require .NoError (t , err )
2068+
2069+ require .Len (t , client .CreatedBranches , 1 )
2070+ assert .Equal (t , "acme/widget/fullsend/scaffold-install" , client .CreatedBranches [0 ])
2071+
2072+ require .Len (t , client .CommittedFilesToBranch , 1 )
2073+ assert .Equal (t , "fullsend/scaffold-install" , client .CommittedFilesToBranch [0 ].Branch )
2074+ assert .Len (t , client .CommittedFilesToBranch [0 ].Files , 1 )
2075+
2076+ require .Len (t , client .CreatedProposals , 1 )
2077+ assert .Contains (t , client .CreatedProposals [0 ].Title , "fullsend" )
2078+
2079+ output := buf .String ()
2080+ assert .Contains (t , output , "protected" )
2081+ assert .Contains (t , output , "PR #1" )
2082+ assert .Contains (t , output , "Merge the PR" )
2083+ }
2084+
2085+ func TestApplyPerRepoScaffold_ProtectedBranch_ExistingBranch (t * testing.T ) {
2086+ client := forge .NewFakeClient ()
2087+ client .Repos = []forge.Repository {{FullName : "acme/widget" , DefaultBranch : "main" }}
2088+ client .Errors ["CommitFiles" ] = fmt .Errorf ("%w: github api: 422" , forge .ErrBranchProtected )
2089+ client .Errors ["CreateBranch" ] = fmt .Errorf ("already exists" )
2090+ var buf bytes.Buffer
2091+ printer := ui .New (& buf )
2092+
2093+ files := []forge.TreeFile {
2094+ {Path : ".fullsend/config.yaml" , Content : []byte ("cfg" ), Mode : "100644" },
2095+ }
2096+
2097+ err := applyPerRepoScaffold (context .Background (), client , printer ,
2098+ "acme" , "widget" , files , nil , nil )
2099+ require .NoError (t , err )
2100+
2101+ require .Len (t , client .CommittedFilesToBranch , 1 , "should proceed despite branch existing" )
2102+ require .Len (t , client .CreatedProposals , 1 )
2103+ }
2104+
2105+ func TestApplyPerRepoScaffold_ProtectedBranch_StillSetsVarsAndSecrets (t * testing.T ) {
2106+ client := forge .NewFakeClient ()
2107+ client .Repos = []forge.Repository {{FullName : "acme/widget" , DefaultBranch : "main" }}
2108+ client .Errors ["CommitFiles" ] = fmt .Errorf ("%w: github api: 422" , forge .ErrBranchProtected )
2109+ printer := ui .New (& bytes.Buffer {})
2110+
2111+ files := []forge.TreeFile {
2112+ {Path : ".fullsend/config.yaml" , Content : []byte ("cfg" ), Mode : "100644" },
2113+ }
2114+ repoVars := map [string ]string {"FULLSEND_MINT_URL" : "https://mint.example.run.app" }
2115+ repoSecrets := map [string ]string {"FULLSEND_GCP_PROJECT_ID" : "my-project" }
2116+
2117+ err := applyPerRepoScaffold (context .Background (), client , printer ,
2118+ "acme" , "widget" , files , repoVars , repoSecrets )
2119+ require .NoError (t , err )
2120+
2121+ assert .Len (t , client .Variables , 1 , "variables should be set even with PR fallback" )
2122+ assert .Len (t , client .CreatedSecrets , 1 , "secrets should be set even with PR fallback" )
2123+ }
2124+
2125+ func TestApplyPerRepoScaffold_ProtectedBranch_CreateBranchFails (t * testing.T ) {
2126+ client := forge .NewFakeClient ()
2127+ client .Repos = []forge.Repository {{FullName : "acme/widget" , DefaultBranch : "main" }}
2128+ client .Errors ["CommitFiles" ] = fmt .Errorf ("%w: github api: 422" , forge .ErrBranchProtected )
2129+ client .Errors ["CreateBranch" ] = fmt .Errorf ("forbidden" )
2130+ printer := ui .New (& bytes.Buffer {})
2131+
2132+ files := []forge.TreeFile {
2133+ {Path : ".fullsend/config.yaml" , Content : []byte ("cfg" ), Mode : "100644" },
2134+ }
2135+
2136+ err := applyPerRepoScaffold (context .Background (), client , printer ,
2137+ "acme" , "widget" , files , nil , nil )
2138+ require .Error (t , err )
2139+ assert .Contains (t , err .Error (), "creating scaffold branch" )
2140+ }
2141+
2142+ func TestApplyPerRepoScaffold_ProtectedBranch_CommitToBranchFails (t * testing.T ) {
2143+ client := forge .NewFakeClient ()
2144+ client .Repos = []forge.Repository {{FullName : "acme/widget" , DefaultBranch : "main" }}
2145+ client .Errors ["CommitFiles" ] = fmt .Errorf ("%w: github api: 422" , forge .ErrBranchProtected )
2146+ client .Errors ["CommitFilesToBranch" ] = fmt .Errorf ("server error" )
2147+ printer := ui .New (& bytes.Buffer {})
2148+
2149+ files := []forge.TreeFile {
2150+ {Path : ".fullsend/config.yaml" , Content : []byte ("cfg" ), Mode : "100644" },
2151+ }
2152+
2153+ err := applyPerRepoScaffold (context .Background (), client , printer ,
2154+ "acme" , "widget" , files , nil , nil )
2155+ require .Error (t , err )
2156+ assert .Contains (t , err .Error (), "committing scaffold files to branch" )
2157+ }
2158+
2159+ func TestApplyPerRepoScaffold_ProtectedBranch_CreatePRFails (t * testing.T ) {
2160+ client := forge .NewFakeClient ()
2161+ client .Repos = []forge.Repository {{FullName : "acme/widget" , DefaultBranch : "main" }}
2162+ client .Errors ["CommitFiles" ] = fmt .Errorf ("%w: github api: 422" , forge .ErrBranchProtected )
2163+ client .Errors ["CreateChangeProposal" ] = fmt .Errorf ("forbidden" )
2164+ printer := ui .New (& bytes.Buffer {})
2165+
2166+ files := []forge.TreeFile {
2167+ {Path : ".fullsend/config.yaml" , Content : []byte ("cfg" ), Mode : "100644" },
2168+ }
2169+
2170+ err := applyPerRepoScaffold (context .Background (), client , printer ,
2171+ "acme" , "widget" , files , nil , nil )
2172+ require .Error (t , err )
2173+ assert .Contains (t , err .Error (), "creating scaffold PR" )
2174+ }
2175+
2176+ func TestApplyPerRepoScaffold_ProtectedBranch_DuplicatePR (t * testing.T ) {
2177+ client := forge .NewFakeClient ()
2178+ client .Repos = []forge.Repository {{FullName : "acme/widget" , DefaultBranch : "main" }}
2179+ client .Errors ["CommitFiles" ] = fmt .Errorf ("%w: github api: 422" , forge .ErrBranchProtected )
2180+ client .Errors ["CreateChangeProposal" ] = fmt .Errorf ("already exists" )
2181+ var buf bytes.Buffer
2182+ printer := ui .New (& buf )
2183+
2184+ files := []forge.TreeFile {
2185+ {Path : ".fullsend/config.yaml" , Content : []byte ("cfg" ), Mode : "100644" },
2186+ }
2187+
2188+ err := applyPerRepoScaffold (context .Background (), client , printer ,
2189+ "acme" , "widget" , files , nil , nil )
2190+ require .NoError (t , err )
2191+
2192+ output := buf .String ()
2193+ assert .Contains (t , output , "already exists" )
2194+ assert .Contains (t , output , "Merge the PR" )
2195+ }
2196+
2197+ func TestApplyPerRepoScaffold_ProtectedBranch_BranchUpToDate (t * testing.T ) {
2198+ client := forge .NewFakeClient ()
2199+ client .Repos = []forge.Repository {{FullName : "acme/widget" , DefaultBranch : "main" }}
2200+ client .Errors ["CommitFiles" ] = fmt .Errorf ("%w: github api: 422" , forge .ErrBranchProtected )
2201+ noChange := false
2202+ client .CommitFilesChanged = & noChange
2203+ var buf bytes.Buffer
2204+ printer := ui .New (& buf )
2205+
2206+ files := []forge.TreeFile {
2207+ {Path : ".fullsend/config.yaml" , Content : []byte ("cfg" ), Mode : "100644" },
2208+ }
2209+
2210+ err := applyPerRepoScaffold (context .Background (), client , printer ,
2211+ "acme" , "widget" , files , nil , nil )
2212+ require .NoError (t , err )
2213+
2214+ assert .Empty (t , client .CreatedProposals , "should not create PR when branch files are unchanged" )
2215+ assert .Contains (t , buf .String (), "up to date" )
2216+ }
0 commit comments