Commit 7e1d129093

7e1d129093f1ed7a9c96149e9fa4922b6d9a1b7e

parent: 433da19b99

Verified · cmc ci/build: success ci/test: failure

cmc <hello@cleberg.net> · 2026-09-11 16:20 UTC

control, wiki: a duplicate milestone title exits 1

milestone create sent a plain error through failErr, which reads it as a
usage error and exits 2; org milestone create already exits 1 for the
same duplicate. Users gains a sentence on the closing comment naming the
source repository by path, and the e2e comment says the org page checks
are anonymous.

Ref #203
.gitbay/wiki/Users.org +4 −2
@@ -348,8 +348,10 @@ Commit messages act on issues when the commits land on the default
348348branch (direct push or MR merge): =closes/fixes/resolves #4= closes the
349349issue with a linking comment, and a bare =#4= leaves a reference
350350comment. Each issue/commit pair acts once, ever. =Closes owner/name#N= closes an issue in another repository when
351you hold write there; otherwise it stays a plain link. A bare
352=owner/name#N= links and does nothing.
351you hold write there; otherwise it stays a plain link. The comment left
352on the closed issue links the closing commit by its repository path, so
353closing a public repository's issue from a private one names the private
354repository there. A bare =owner/name#N= links and does nothing.
353355
354356Milestones group issues and MRs toward a release (write access to
355357manage, attach with =issue milestone= / =mr milestone=; progress shows
e2e/orglabels_test.go +3 −2
@@ -77,8 +77,9 @@ func TestOrgLabelsMilestonesAndCrossRepoCloses(t *testing.T) {
7777 t.Fatalf("org milestone progress after close: %s", out)
7878 }
7979
80 // carol is outside: she reads the org pages because acme/lib is public,
81 // and the counts stop at it.
80 // carol is outside: her counts stop at acme/lib, the public one. The
81 // page checks below are anonymous, which acme/lib being public allows
82 // just the same.
8283 out = must(carolKey, "org", "milestone", "list", "acme", "--json")
8384 if !strings.Contains(out, `"open":1`) || !strings.Contains(out, `"closed":0`) {
8485 t.Fatalf("outsider progress: %s", out)
internal/control/milestone.go +3 −1
@@ -64,7 +64,9 @@ func runMilestoneCreate(c *Ctx, args []string) int {
6464 if errors.Is(err, store.ErrOrgScoped) {
6565 return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "milestone", title, "create"))
6666 }
67 return c.failErr(err)
67 // A duplicate title is a failure, not a usage error, which is what
68 // failErr would make of it; org milestone create answers the same.
69 return c.fail(protocol.ExitFailure, "%v", err)
6870 }
6971 return c.emit(map[string]string{"milestone": title}, func(w io.Writer) {
7072 fmt.Fprintf(w, "created milestone %q on %s\n", title, repo.Path())
internal/control/orgscope_test.go +22
@@ -152,3 +152,25 @@ func TestRepoMilestoneCommandsRefuseOrgTitles(t *testing.T) {
152152 t.Fatalf("milestone list: exit %d %s", code, out.String())
153153 }
154154}
155
156// A duplicate title fails; it is not a usage error, and the repo-level
157// and org-level commands answer with the same code.
158func TestDuplicateMilestoneTitleFails(t *testing.T) {
159 f := newOrgFixture(t)
160 c, out := f.ctx(f.alice)
161 if code := runMilestoneCreate(c, []string{"alice/app", "v1"}); code != protocol.ExitOK {
162 t.Fatalf("first create: exit %d %s", code, out.String())
163 }
164 out.Reset()
165 if code := runMilestoneCreate(c, []string{"alice/app", "v1"}); code != protocol.ExitFailure {
166 t.Fatalf("duplicate create: exit %d %s", code, out.String())
167 }
168 out.Reset()
169 if code := runOrgMilestoneCreate(c, []string{"acme", "v1"}); code != protocol.ExitOK {
170 t.Fatalf("first org create: exit %d %s", code, out.String())
171 }
172 out.Reset()
173 if code := runOrgMilestoneCreate(c, []string{"acme", "v1"}); code != protocol.ExitFailure {
174 t.Fatalf("duplicate org create: exit %d %s", code, out.String())
175 }
176}