@@ -18,6 +18,7 @@ import (
1818 "testing"
1919 "time"
2020
21+ "github.com/cockroachdb/errors"
2122 "github.com/stretchr/testify/require"
2223)
2324
@@ -465,6 +466,100 @@ func TestPickSHARunnerProcessCandidateIdempotent(t *testing.T) {
465466 "GitHub must not be called when Pick SHA subtask is already Done" )
466467}
467468
469+ // TestFetchSHAForPickSHA exercises the pre-release branch-not-found
470+ // fallback. The critical invariant is the GA-release guard: a missing
471+ // staging branch on a non-pre-release version must surface as an error,
472+ // because it means branch-cut never ran. The fallback must also be
473+ // scoped to errBranchNotFound — other GitHub errors (network, auth, 5xx)
474+ // must propagate unchanged so operators don't silently build against
475+ // master when GitHub is misbehaving.
476+ func TestFetchSHAForPickSHA (t * testing.T ) {
477+ const stagingSHA = "stagingsha111"
478+ const masterSHA = "mastersha222"
479+
480+ tests := []struct {
481+ name string
482+ staging string
483+ summary string
484+ ghRefs map [string ]string // branches that exist on GitHub
485+ expectedSHA string
486+ expectedFetchedOn string
487+ expectErrContain string
488+ }{
489+ {
490+ name : "staging branch exists, no fallback" ,
491+ staging : "release-25.4.3-rc" ,
492+ summary : "Release: v25.4.3" ,
493+ ghRefs : map [string ]string {"release-25.4.3-rc" : stagingSHA },
494+ expectedSHA : stagingSHA ,
495+ expectedFetchedOn : "release-25.4.3-rc" ,
496+ },
497+ {
498+ name : "pre-release with missing staging branch falls back to master" ,
499+ staging : "release-26.1.0-rc" ,
500+ summary : "Release: v26.1.0-rc.1" ,
501+ ghRefs : map [string ]string {masterBranch : masterSHA },
502+ expectedSHA : masterSHA ,
503+ expectedFetchedOn : masterBranch ,
504+ },
505+ {
506+ name : "non-pre-release with missing staging branch surfaces error" ,
507+ staging : "release-25.4.3-rc" ,
508+ summary : "Release: v25.4.3" ,
509+ ghRefs : map [string ]string {masterBranch : masterSHA },
510+ expectErrContain : "fetching tip SHA for staging branch release-25.4.3-rc" ,
511+ },
512+ {
513+ name : "pre-release with master also missing surfaces fallback error" ,
514+ staging : "release-26.1.0-rc" ,
515+ summary : "Release: v26.1.0-rc.1" ,
516+ ghRefs : map [string ]string {}, // neither branch exists
517+ expectErrContain : "fetching tip SHA for fallback branch master" ,
518+ },
519+ }
520+ for _ , tc := range tests {
521+ t .Run (tc .name , func (t * testing.T ) {
522+ ghSrv := httptest .NewServer (mkRefHandler (tc .ghRefs ))
523+ defer ghSrv .Close ()
524+ r := newPickSHARunnerForTest (t , ghSrv , nil , time.Time {})
525+
526+ v , err := parseReleaseVersion (tc .summary )
527+ require .NoError (t , err )
528+
529+ sha , fetchBranch , err := r .fetchSHAForPickSHA (
530+ context .Background (), "REL-1" , tc .staging , v )
531+ if tc .expectErrContain != "" {
532+ require .ErrorContains (t , err , tc .expectErrContain )
533+ return
534+ }
535+ require .NoError (t , err )
536+ require .Equal (t , tc .expectedSHA , sha )
537+ require .Equal (t , tc .expectedFetchedOn , fetchBranch )
538+ })
539+ }
540+ }
541+
542+ // TestFetchSHAForPickSHANonNotFoundErrorPropagates ensures the
543+ // pre-release fallback does not paper over transport/server errors:
544+ // only errBranchNotFound triggers fallback. A 5xx must propagate.
545+ func TestFetchSHAForPickSHANonNotFoundErrorPropagates (t * testing.T ) {
546+ ghSrv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
547+ w .WriteHeader (http .StatusInternalServerError )
548+ }))
549+ defer ghSrv .Close ()
550+ r := newPickSHARunnerForTest (t , ghSrv , nil , time.Time {})
551+
552+ v , err := parseReleaseVersion ("Release: v26.1.0-rc.1" )
553+ require .NoError (t , err )
554+
555+ _ , _ , err = r .fetchSHAForPickSHA (
556+ context .Background (), "REL-1" , "release-26.1.0-rc" , v )
557+ require .Error (t , err )
558+ require .False (t , errors .Is (err , errBranchNotFound ),
559+ "500 must not be wrapped as errBranchNotFound" )
560+ require .ErrorContains (t , err , "fetching tip SHA for staging branch" )
561+ }
562+
468563// mkPickSHACandidate builds a candidate jiraIssue suitable for the JQL search
469564// result: summary parses, pickDate is set, and no further fields are needed
470565// because processCandidate refetches the full issue via Jira.
@@ -508,7 +603,9 @@ func newPickSHARunnerForTest(
508603 gh .client .BaseURL = u
509604
510605 jira := newJiraClient ("bot@example.com" , "test-token" )
511- jira .baseURL = jiraSrv .URL
606+ if jiraSrv != nil {
607+ jira .baseURL = jiraSrv .URL
608+ }
512609
513610 return & pickSHARunner {
514611 jira : jira ,
0 commit comments