Skip to content

Commit 5089dae

Browse files
release/pick-sha: fall back to master when pre-release staging branch is missing (#170781)
release/pick-sha: fall back to master when pre-release staging branch is missing
2 parents 003aa92 + 68e1527 commit 5089dae

3 files changed

Lines changed: 148 additions & 5 deletions

File tree

pkg/cmd/release/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ go_test(
6464
deps = [
6565
"//pkg/testutils/datapathutils",
6666
"//pkg/testutils/release",
67+
"@com_github_cockroachdb_errors//:errors",
6768
"@com_github_cockroachdb_version//:version",
6869
"@com_github_slack_go_slack//:slack",
6970
"@com_github_stretchr_testify//require",

pkg/cmd/release/pick_sha.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"time"
1717

1818
"github.com/cockroachdb/errors"
19+
"github.com/cockroachdb/version"
1920
"github.com/spf13/cobra"
2021
)
2122

@@ -41,6 +42,11 @@ const pickSHAJQLDryRun = `issueType="CRDB Release" and ` +
4142
// "Pick SHA" subtask on a release ticket.
4243
const pickSHASubtaskMatch = "Pick SHA"
4344

45+
// masterBranch is the fallback branch pick-sha uses when a pre-release
46+
// ticket points at a staging branch that hasn't been cut yet — see
47+
// fetchSHAForPickSHA for the rationale.
48+
const masterBranch = "master"
49+
4450
// pickSHA success notifications go to the release-ops channels — a
4551
// RelEng-only audience. This deliberately differs from the wider
4652
// #db-release-status / #db-release-test channels that branch-cut uses,
@@ -207,7 +213,8 @@ func (r *pickSHARunner) run(ctx context.Context) error {
207213
}
208214

209215
func (r *pickSHARunner) processCandidate(ctx context.Context, c jiraIssue) error {
210-
if _, err := parseReleaseVersion(c.Fields.Summary); err != nil {
216+
v, err := parseReleaseVersion(c.Fields.Summary)
217+
if err != nil {
211218
// Tracking/template tickets that match the JQL but aren't real
212219
// release tickets (e.g. summary "Release: 26.1|25.4|25.2 next
213220
// patch") aren't actionable — log and move on rather than
@@ -272,17 +279,22 @@ func (r *pickSHARunner) processCandidate(ctx context.Context, c jiraIssue) error
272279
if staging == "" {
273280
return errors.Newf("ticket %s has no Staging Branch set", c.Key)
274281
}
275-
sha, err := r.gh.GetBranchSHA(ctx, staging)
282+
sha, fetchBranch, err := r.fetchSHAForPickSHA(ctx, c.Key, staging, v)
276283
if err != nil {
277-
return errors.Wrapf(err, "fetching tip SHA for staging branch %s", staging)
284+
return err
278285
}
279286

287+
// `staging` keeps the ticket's original branch name so the Slack and
288+
// Jira messages name the release series operators care about (e.g.
289+
// release-26.1.0-rc), even when the actual SHA came from master via
290+
// the pre-release fallback. `fetchBranch` is the branch we actually
291+
// fetched and must dispatch on.
280292
details, err := buildPickSHADetails(full, staging, sha, r.repo, r.buildWorkflow)
281293
if err != nil {
282294
return errors.Wrap(err, "building pick-SHA details")
283295
}
284296

285-
ref := "refs/heads/" + staging
297+
ref := "refs/heads/" + fetchBranch
286298
// The workflow_dispatch REST endpoint requires every input value to be
287299
// a JSON string — booleans typed in workflow YAML are coerced from
288300
// "true"/"false" on receipt — so we stringify dry_run rather than
@@ -345,6 +357,39 @@ func (r *pickSHARunner) processCandidate(ctx context.Context, c jiraIssue) error
345357
return nil
346358
}
347359

360+
// fetchSHAForPickSHA fetches the tip SHA for the staging branch named on
361+
// the ticket. If the branch doesn't exist AND the ticket is for a
362+
// pre-release version (vX.Y.0-alpha/beta/rc), it falls back to master:
363+
// branch-cut skips Patch()==0 so the staging branch is created manually
364+
// around beta.1, and in that window the only sensible source for the
365+
// build SHA is the tip of master. The fallback is deliberately scoped to
366+
// pre-releases — a missing staging branch on a patch release means
367+
// branch-cut never ran, which is a real error operators should see.
368+
//
369+
// Returns the SHA and the name of the branch it was actually fetched
370+
// from (which the caller must use to construct the workflow_dispatch
371+
// ref). The original `staging` value is preserved in the caller's
372+
// scope for display in Slack/Jira so operators see the release-series
373+
// branch even when the SHA came from master.
374+
func (r *pickSHARunner) fetchSHAForPickSHA(
375+
ctx context.Context, key, staging string, v version.Version,
376+
) (sha, fetchBranch string, err error) {
377+
sha, err = r.gh.GetBranchSHA(ctx, staging)
378+
if err == nil {
379+
return sha, staging, nil
380+
}
381+
if !errors.Is(err, errBranchNotFound) || !v.IsPrerelease() {
382+
return "", "", errors.Wrapf(err, "fetching tip SHA for staging branch %s", staging)
383+
}
384+
log.Printf("ticket %s: staging branch %s does not exist; falling back to %s for pre-release %s",
385+
key, staging, masterBranch, v)
386+
sha, err = r.gh.GetBranchSHA(ctx, masterBranch)
387+
if err != nil {
388+
return "", "", errors.Wrapf(err, "fetching tip SHA for fallback branch %s", masterBranch)
389+
}
390+
return sha, masterBranch, nil
391+
}
392+
348393
// notifyReleaseNotes builds the release-notes API payload from the Jira
349394
// issue and POSTs it. Returns the generated docs PR URL on success, or
350395
// "" on dry-run, idempotency skip, or any error (after warning to

pkg/cmd/release/pick_sha_test.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)