Skip to content

Commit 2fbc047

Browse files
committed
fix: validate superseded pr membership
1 parent 02909ed commit 2fbc047

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

internal/cmd/root.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,6 +2093,10 @@ func buildSupersedePlan(runtime *stackruntime.Runtime, state store.RepoState, la
20932093
LandingPR: landingPRs[0],
20942094
SupersededPRs: make([]store.PullRequest, 0, len(prNumbers)),
20952095
}
2096+
sourceBranches := map[string]bool{}
2097+
for _, branch := range landing.SourceBranches {
2098+
sourceBranches[branch] = true
2099+
}
20962100

20972101
for _, number := range prNumbers {
20982102
if number == plan.LandingPR.Number {
@@ -2102,6 +2106,9 @@ func buildSupersedePlan(runtime *stackruntime.Runtime, state store.RepoState, la
21022106
if err != nil {
21032107
return supersedePlan{}, err
21042108
}
2109+
if !sourceBranches[pr.HeadRefName] {
2110+
return supersedePlan{}, fmt.Errorf("pull request #%d head %q is not part of landing batch %q; expected one of: %s", pr.Number, pr.HeadRefName, landingBranch, strings.Join(landing.SourceBranches, ", "))
2111+
}
21052112
plan.SupersededPRs = append(plan.SupersededPRs, pr)
21062113
}
21072114

internal/cmd/root_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2171,6 +2171,77 @@ func TestSupersedeRecordsMetadataAndCommentsOriginalPRs(t *testing.T) {
21712171
}
21722172
}
21732173

2174+
func TestSupersedeRejectsPRsOutsideLandingSourceBranches(t *testing.T) {
2175+
repo := testutil.SetupGitRepo(t)
2176+
testutil.Run(t, repo, "git", "switch", "-c", "stack/discovery-core")
2177+
testutil.WriteFile(t, filepath.Join(repo, "landing.txt"), "landing\n")
2178+
testutil.Run(t, repo, "git", "add", "landing.txt")
2179+
testutil.Run(t, repo, "git", "commit", "-m", "landing")
2180+
landingHead := strings.TrimSpace(testutil.Run(t, repo, "git", "rev-parse", "HEAD"))
2181+
2182+
runtime := newTestRuntime(repo)
2183+
state := store.RepoState{
2184+
Version: 1,
2185+
Repo: "hack-dance/stack",
2186+
DefaultRemote: "origin",
2187+
Trunk: "main",
2188+
Branches: map[string]store.BranchRecord{},
2189+
Landings: map[string]store.LandingRecord{
2190+
"stack/discovery-core": {
2191+
BaseBranch: "main",
2192+
SourceBranches: []string{"hack-agent/lnhack-66-feature-a", "hack-agent/lnhack-67-feature-b"},
2193+
CreatedAt: "2026-03-26T19:00:00Z",
2194+
},
2195+
},
2196+
}
2197+
if err := runtime.Store.WriteState(runtime.Context, state); err != nil {
2198+
t.Fatalf("write state: %v", err)
2199+
}
2200+
2201+
ghStub := testutil.SetupGHStub(t, "hack-dance/stack", "main")
2202+
t.Setenv("STACK_TEST_GH_STATE", ghStub.StatePath)
2203+
t.Setenv("STACK_TEST_GH_LOG", ghStub.LogPath)
2204+
t.Setenv("PATH", ghStub.Dir+string(os.PathListSeparator)+os.Getenv("PATH"))
2205+
2206+
writeGHState(t, ghStub.StatePath, `{
2207+
"repo": {
2208+
"nameWithOwner": "hack-dance/stack",
2209+
"url": "https://github.com/hack-dance/stack",
2210+
"defaultBranchRef": { "name": "main" }
2211+
},
2212+
"prs": {
2213+
"3": {
2214+
"id": "PR_3",
2215+
"number": 3,
2216+
"url": "https://example.com/hack-dance/stack/pull/3",
2217+
"repo": "hack-dance/stack",
2218+
"headRefName": "hack-agent/unrelated-follow-up",
2219+
"baseRefName": "main",
2220+
"headRefOid": "",
2221+
"state": "OPEN",
2222+
"isDraft": false
2223+
},
2224+
"9": {
2225+
"id": "PR_9",
2226+
"number": 9,
2227+
"url": "https://example.com/hack-dance/stack/pull/9",
2228+
"repo": "hack-dance/stack",
2229+
"headRefName": "stack/discovery-core",
2230+
"baseRefName": "main",
2231+
"headRefOid": "`+landingHead+`",
2232+
"state": "OPEN",
2233+
"isDraft": false
2234+
}
2235+
},
2236+
"next_number": 10
2237+
}`)
2238+
2239+
err := executeCommandExpectError(runtime, "supersede", "--landing", "stack/discovery-core", "--prs", "3", "--no-comment", "--yes")
2240+
if err == nil || !strings.Contains(err.Error(), `pull request #3 head "hack-agent/unrelated-follow-up" is not part of landing batch "stack/discovery-core"`) {
2241+
t.Fatalf("expected supersede source-branch validation error, got %v", err)
2242+
}
2243+
}
2244+
21742245
func TestVerifyAddAndListForTrackedBranch(t *testing.T) {
21752246
repo := testutil.SetupGitRepo(t)
21762247

0 commit comments

Comments
 (0)