Commit ec8b566b11
ec8b566b114c735cb30865164b59c804684098f3
parent: 390e0c9531
Verified · cmc ci/build: success ci/test: success
cmc <hello@cleberg.net> · 2026-09-11 19:41 UTC
control, e2e, wiki: a deploy key closes its own repository's issue by full path
A cross-repository close requires the pushing key's scope to reach the
target, and no deploy scope reaches any repository, so a deploy-key
push saying Closes owner/self#N, naming the repository the key is
bound to, closed nothing while a bare Closes #N in the same commit
did. closeTarget now treats a path that resolves to the source
repository as the bare form; the rule that a deploy key acts nowhere
else is unchanged.
Closes #213
.gitbay/wiki/Users.org
+4 −1
| @@ -349,7 +349,10 @@ branch (direct push, MR merge, =repo commit-file= or the web editor): |
| 349 | 349 | =closes/fixes/resolves #4= closes the |
| 350 | 350 | issue with a linking comment, and a bare =#4= leaves a reference |
| 351 | 351 | comment. Each issue/commit pair acts once, ever. =Closes owner/name#N= closes an issue in another repository when |
| 352 | | you hold write there; otherwise it stays a plain link. The comment left |
| 352 | you hold write there with a key whose scope reaches it; otherwise it |
| 353 | stays a plain link. A deploy key closes nothing outside the repository |
| 354 | it is bound to, but naming that repository by full path works as the |
| 355 | bare form does. The comment left |
| 353 | 356 | on the closed issue links the closing commit by its repository path, so |
| 354 | 357 | closing a public repository's issue from a private one names the private |
| 355 | 358 | repository there. A bare =owner/name#N= links and does nothing. |
e2e/deploykey_test.go
+13 −1
| @@ -77,8 +77,20 @@ func TestDeployKeys(t *testing.T) { |
| 77 | 77 | rwWork := t.TempDir() |
| 78 | 78 | mustGit(t, rwWork, rwEnv, "clone", inst.sshURL("alice/app"), "w") |
| 79 | 79 | rwDir := filepath.Join(rwWork, "w") |
| 80 | | mustGit(t, rwDir, rwEnv, "commit", "-q", "--allow-empty", "-m", "ci push") |
| 80 | // Its push closes the bound repository's issue whether the message |
| 81 | // names it bare or by full path (#213). |
| 82 | for _, title := range []string{"'bare'", "'full path'"} { |
| 83 | if _, errOut, code := inst.ssh(t, aliceKey, "", "issue", "create", "alice/app", "--title", title); code != 0 { |
| 84 | t.Fatalf("issue create: %s", errOut) |
| 85 | } |
| 86 | } |
| 87 | mustGit(t, rwDir, rwEnv, "commit", "-q", "--allow-empty", "-m", "ci push\n\nCloses #1, closes alice/app#2") |
| 81 | 88 | mustGit(t, rwDir, rwEnv, "push", "-q", "origin", "main") |
| 89 | for _, n := range []string{"1", "2"} { |
| 90 | if out, _, _ := inst.ssh(t, aliceKey, "", "issue", "show", "alice/app", n, "--json"); !strings.Contains(out, `"state":"closed"`) { |
| 91 | t.Fatalf("deploy key push did not close issue %s: %s", n, out) |
| 92 | } |
| 93 | } |
| 82 | 94 | |
| 83 | 95 | // The binding survives an owner rename (keys bind to the repo ID). |
| 84 | 96 | if _, errOut, code = inst.ssh(t, aliceKey, "", "org", "create", "moved"); code != 0 { |
internal/control/commitrefs.go
+6
| @@ -163,6 +163,12 @@ func closeTarget(st *store.Store, source store.Repo, actorID int64, scope, path |
| 163 | 163 | if err != nil { |
| 164 | 164 | return store.Repo{}, false |
| 165 | 165 | } |
| 166 | // The source repository named by its full path is the bare form |
| 167 | // spelled out, so a deploy key bound to it closes there as a bare #N |
| 168 | // would (#213). |
| 169 | if target.ID == source.ID { |
| 170 | return source, true |
| 171 | } |
| 166 | 172 | actor, err := st.UserByID(actorID) |
| 167 | 173 | if err != nil { |
| 168 | 174 | return store.Repo{}, false |
internal/control/commitrefs_test.go
+9
| @@ -67,6 +67,15 @@ func TestMRDescriptionClosesAcrossRepos(t *testing.T) { |
| 67 | 67 | if iss, _ := f.st.IssueByNumber(f.priv.ID, 1); iss.State != "open" { |
| 68 | 68 | t.Fatal("a deploy key closed an issue outside its binding") |
| 69 | 69 | } |
| 70 | // The same key naming its own repository by full path is the bare |
| 71 | // form spelled out, not a cross-repo close (#213). |
| 72 | if _, err := f.st.CreateIssue(f.app.ID, f.alice, "in app too", "", "md"); err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | ProcessMRDescription(f.st, f.app, mr(6, "Closes alice/app#2"), f.alice, deploy) |
| 76 | if iss, _ := f.st.IssueByNumber(f.app.ID, 2); iss.State != "closed" { |
| 77 | t.Fatal("a deploy key did not close its own repository's issue by full path") |
| 78 | } |
| 70 | 79 | // An archived target is read-only, cross-repo closes included. |
| 71 | 80 | if _, err := f.st.UpdateRepoSettings(f.priv.ID, func(rs *store.RepoSettings) { rs.Archived = true }); err != nil { |
| 72 | 81 | t.Fatal(err) |