Skip to content

Commit 52e8e74

Browse files
authored
Merge pull request cli#13541 from cli/feature/discussion
feat: add `discussion` command set
2 parents da68cb8 + 69855b7 commit 52e8e74

29 files changed

Lines changed: 13075 additions & 0 deletions

acceptance/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,19 @@ The following custom commands are defined within [`acceptance_test.go`](./accept
103103
stdout2env PR_URL
104104
```
105105

106+
- `jq-assert`: evaluate a jq expression on a JSON environment variable and assert the result matches a regexp
107+
108+
```txtar
109+
jq-assert ISSUE_JSON '.title' 'Expected Title'
110+
jq-assert DISCUSSION_JSON '.comments | length' '^2$'
111+
```
112+
113+
- `jq2env`: evaluate a jq expression on a JSON environment variable and store the result in another environment variable
114+
115+
```txtar
116+
jq2env ISSUE_JSON '.title' ISSUE_TITLE
117+
```
118+
106119
### Acceptance Test VS Code Support
107120

108121
Due to the `//go:build acceptance` build constraint, some functionality is limited because `gopls` isn't being informed about the tag. To resolve this, set the following in your `settings.json`:

acceptance/acceptance_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
package acceptance_test
44

55
import (
6+
"bytes"
67
"fmt"
78
"os"
89
"path"
910
"path/filepath"
11+
"regexp"
1012
"strconv"
1113
"strings"
1214
"testing"
@@ -16,6 +18,7 @@ import (
1618

1719
"github.com/MakeNowJust/heredoc"
1820
"github.com/cli/cli/v2/internal/ghcmd"
21+
"github.com/cli/go-gh/v2/pkg/jq"
1922
"github.com/cli/go-internal/testscript"
2023
)
2124

@@ -74,6 +77,15 @@ func TestIssues(t *testing.T) {
7477
testscript.Run(t, testScriptParamsFor(tsEnv, "issue"))
7578
}
7679

80+
func TestDiscussions(t *testing.T) {
81+
var tsEnv testScriptEnv
82+
if err := tsEnv.fromEnv(); err != nil {
83+
t.Fatal(err)
84+
}
85+
86+
testscript.Run(t, testScriptParamsFor(tsEnv, "discussion"))
87+
}
88+
7789
func TestIssues2_0(t *testing.T) {
7890
var tsEnv testScriptEnv
7991
if err := tsEnv.fromEnv(); err != nil {
@@ -374,6 +386,57 @@ func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript,
374386
d := time.Duration(seconds) * time.Second
375387
time.Sleep(d)
376388
},
389+
"jq-assert": func(ts *testscript.TestScript, neg bool, args []string) {
390+
if neg {
391+
ts.Fatalf("unsupported: ! jq-assert")
392+
}
393+
if len(args) != 3 {
394+
ts.Fatalf("usage: jq-assert ENV_VAR expression regexp")
395+
}
396+
397+
input := ts.Getenv(args[0])
398+
if input == "" {
399+
ts.Fatalf("jq-assert: environment variable %s is empty or unset", args[0])
400+
}
401+
402+
var buf bytes.Buffer
403+
if err := jq.Evaluate(strings.NewReader(input), &buf, args[1]); err != nil {
404+
ts.Fatalf("jq-assert: %v", err)
405+
}
406+
407+
result := strings.TrimRight(buf.String(), "\n") // jq.Evaluate adds a newline at the end
408+
ts.Logf("jq-assert %s %q => %s", args[0], args[1], result)
409+
410+
re, err := regexp.Compile(args[2])
411+
if err != nil {
412+
ts.Fatalf("jq-assert: invalid regexp %q: %v", args[2], err)
413+
}
414+
if !re.MatchString(result) {
415+
ts.Fatalf("jq-assert: result %q does not match %q", result, args[2])
416+
}
417+
},
418+
"jq2env": func(ts *testscript.TestScript, neg bool, args []string) {
419+
if neg {
420+
ts.Fatalf("unsupported: ! jq2env")
421+
}
422+
if len(args) != 3 {
423+
ts.Fatalf("usage: jq2env SRC_ENV expression DST_ENV")
424+
}
425+
426+
input := ts.Getenv(args[0])
427+
if input == "" {
428+
ts.Fatalf("jq2env: environment variable %s is empty or unset", args[0])
429+
}
430+
431+
var buf bytes.Buffer
432+
if err := jq.Evaluate(strings.NewReader(input), &buf, args[1]); err != nil {
433+
ts.Fatalf("jq2env: %v", err)
434+
}
435+
436+
result := strings.TrimRight(buf.String(), "\n") // jq.Evaluate adds a newline at the end
437+
ts.Logf("jq2env %s %q => %s => %s", args[0], args[1], result, args[2])
438+
ts.Setenv(args[2], result)
439+
},
377440
}
378441
}
379442

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Create a repository with a file so it has a default branch
2+
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
3+
4+
# Defer repo cleanup
5+
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
6+
7+
# Clone the repo
8+
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
9+
cd $SCRIPT_NAME-$RANDOM_STRING
10+
11+
# Enable discussions
12+
exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=true
13+
14+
# Create a discussion to comment on
15+
exec gh discussion create --title 'Comment Test' --body 'Discussion for comment tests' --category 'General'
16+
stdout2env DISCUSSION_URL
17+
18+
# Add a top-level comment
19+
exec gh discussion comment $DISCUSSION_URL --body 'Comment from flag'
20+
stdout 'discussioncomment'
21+
22+
# Add another comment from file
23+
exec gh discussion comment $DISCUSSION_URL --body-file $WORK/comment-body.txt
24+
stdout 'discussioncomment'
25+
26+
# Verify comments appear in view
27+
exec gh discussion view $DISCUSSION_URL --comments --order oldest --json comments
28+
stdout2env COMMENTS_JSON
29+
jq-assert COMMENTS_JSON '.comments.nodes | length' '^2$'
30+
jq-assert COMMENTS_JSON '.comments.nodes[0].body' 'Comment from flag'
31+
jq-assert COMMENTS_JSON '.comments.nodes[1].body' 'Comment from file'
32+
33+
# Get the first comment ID for reply and edit tests
34+
jq2env COMMENTS_JSON '.comments.nodes[0].id' FIRST_COMMENT_ID
35+
jq2env COMMENTS_JSON '.comments.nodes[1].id' SECOND_COMMENT_ID
36+
37+
# Add a reply to the first comment
38+
exec gh discussion comment $FIRST_COMMENT_ID --body 'Reply to first'
39+
stdout 'discussioncomment'
40+
41+
# Add a reply to the second comment from file
42+
exec gh discussion comment $SECOND_COMMENT_ID --body-file $WORK/reply-body.txt
43+
stdout 'discussioncomment'
44+
45+
# Verify the reply appears
46+
exec gh discussion view $FIRST_COMMENT_ID --json comments
47+
stdout2env REPLIES_JSON
48+
jq-assert REPLIES_JSON '.comments.nodes[0].replies.nodes | length' '^1$'
49+
jq-assert REPLIES_JSON '.comments.nodes[0].replies.nodes[0].body' 'Reply to first'
50+
jq2env REPLIES_JSON '.comments.nodes[0].replies.nodes[0].id' FIRST_REPLY_ID
51+
52+
exec gh discussion view $SECOND_COMMENT_ID --json comments
53+
stdout2env REPLIES_JSON
54+
jq-assert REPLIES_JSON '.comments.nodes[0].replies.nodes | length' '^1$'
55+
jq-assert REPLIES_JSON '.comments.nodes[0].replies.nodes[0].body' 'Reply from file'
56+
jq2env REPLIES_JSON '.comments.nodes[0].replies.nodes[0].id' SECOND_REPLY_ID
57+
58+
# Edit the first comment
59+
exec gh discussion comment $FIRST_COMMENT_ID --edit --body 'Edited first comment'
60+
stdout 'discussioncomment'
61+
62+
# Edit the second comment from file
63+
exec gh discussion comment $SECOND_COMMENT_ID --edit --body-file $WORK/edit-comment-body.txt
64+
stdout 'discussioncomment'
65+
66+
# Edit the first reply
67+
exec gh discussion comment $FIRST_REPLY_ID --edit --body 'Edited first reply'
68+
stdout 'discussioncomment'
69+
70+
# Edit the second reply from file
71+
exec gh discussion comment $SECOND_REPLY_ID --edit --body-file $WORK/edit-reply-body.txt
72+
stdout 'discussioncomment'
73+
74+
# Verify edits appear
75+
exec gh discussion view $DISCUSSION_URL --comments --order oldest --json comments
76+
stdout2env EDITED_COMMENTS_JSON
77+
jq-assert EDITED_COMMENTS_JSON '.comments.nodes[0].body' 'Edited first comment'
78+
jq-assert EDITED_COMMENTS_JSON '.comments.nodes[0].replies.nodes[0].body' 'Edited first reply'
79+
jq-assert EDITED_COMMENTS_JSON '.comments.nodes[1].body' 'Edited comment from file'
80+
jq-assert EDITED_COMMENTS_JSON '.comments.nodes[1].replies.nodes[0].body' 'Edited reply from file'
81+
82+
# Delete with --yes should fail when run non-interactively
83+
! exec gh discussion comment $SECOND_COMMENT_ID --delete
84+
stderr '--yes'
85+
86+
# Delete the second comment and its reply
87+
#
88+
# Note that if we only delete the comment, the reply will still exist and the API will keep returning the comment
89+
# (parent), but with an empty body. We delete them both here to avoid confusing assertions later.
90+
exec gh discussion comment $SECOND_COMMENT_ID --delete --yes
91+
exec gh discussion comment $SECOND_REPLY_ID --delete --yes
92+
93+
# Verify deletion
94+
exec gh discussion view $DISCUSSION_URL --comments --order oldest --json comments
95+
stdout2env DELETED_COMMENTS_JSON
96+
jq-assert DELETED_COMMENTS_JSON '.comments.nodes | length' '^1$'
97+
jq-assert DELETED_COMMENTS_JSON '.comments.nodes[0].body' 'Edited first comment'
98+
jq-assert DELETED_COMMENTS_JSON '.comments.nodes[0].replies.nodes | length' '^1$'
99+
jq-assert DELETED_COMMENTS_JSON '.comments.nodes[0].replies.nodes[0].body' 'Edited first reply'
100+
101+
# Test add, edit, and delete using comment URLs instead of node IDs
102+
103+
# Get the URL of the remaining comment
104+
jq2env DELETED_COMMENTS_JSON '.comments.nodes[0].url' FIRST_COMMENT_URL
105+
106+
# Add a reply using the comment URL
107+
exec gh discussion comment $FIRST_COMMENT_URL --body 'Reply via URL'
108+
stdout2env REPLY_URL
109+
stdout 'discussioncomment'
110+
111+
# Verify the reply
112+
exec gh discussion view $FIRST_COMMENT_URL --order oldest --json comments
113+
stdout2env URL_REPLIES_JSON
114+
jq-assert URL_REPLIES_JSON '.comments.nodes[0].replies.nodes | length' '^2$'
115+
116+
# Edit the reply using its URL
117+
exec gh discussion comment $REPLY_URL --edit --body 'Edited reply via URL'
118+
stdout 'discussioncomment'
119+
120+
# Verify the edit
121+
exec gh discussion view $FIRST_COMMENT_URL --order oldest --json comments
122+
stdout2env EDITED_URL_REPLIES_JSON
123+
jq-assert EDITED_URL_REPLIES_JSON '.comments.nodes[0].replies.nodes[0].body' 'Edited first reply'
124+
jq-assert EDITED_URL_REPLIES_JSON '.comments.nodes[0].replies.nodes[1].body' 'Edited reply via URL'
125+
126+
# Delete the reply using its URL
127+
exec gh discussion comment $REPLY_URL --delete --yes
128+
129+
# Verify deletion
130+
exec gh discussion view $FIRST_COMMENT_URL --order oldest --json comments
131+
stdout2env FINAL_URL_REPLIES_JSON
132+
jq-assert FINAL_URL_REPLIES_JSON '.comments.nodes[0].replies.nodes | length' '^1$'
133+
jq-assert FINAL_URL_REPLIES_JSON '.comments.nodes[0].replies.nodes[0].body' 'Edited first reply'
134+
135+
-- comment-body.txt --
136+
Comment from file
137+
-- reply-body.txt --
138+
Reply from file
139+
-- edit-comment-body.txt --
140+
Edited comment from file
141+
-- edit-reply-body.txt --
142+
Edited reply from file
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Create a repository with a file so it has a default branch
2+
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
3+
4+
# Defer repo cleanup
5+
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
6+
7+
# Clone the repo
8+
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
9+
cd $SCRIPT_NAME-$RANDOM_STRING
10+
11+
# Explicitly disable discussions
12+
exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=false
13+
14+
# Creating a discussion should fail when discussions are disabled
15+
! exec gh discussion create --title 'Fail' --body 'Body' --category 'General'
16+
stderr 'has discussions disabled'
17+
18+
# Enable discussions
19+
exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=true
20+
21+
# Create with title + body + category
22+
exec gh discussion create --title 'Basic Discussion' --body 'Basic body' --category 'General'
23+
stdout2env BASIC_URL
24+
exec gh discussion view $BASIC_URL
25+
stdout 'title:\tBasic Discussion'
26+
stdout 'Basic body'
27+
28+
# Create with title + body from file + category
29+
exec gh discussion create --title 'File Body Discussion' --body-file $WORK/body.txt --category 'General'
30+
stdout2env FILE_URL
31+
exec gh discussion view $FILE_URL
32+
stdout 'title:\tFile Body Discussion'
33+
stdout 'Body from file content'
34+
35+
# Create with title + body + category + 2 labels
36+
exec gh discussion create --title 'Labeled Discussion' --body 'Labeled body' --category 'General' --label bug,enhancement
37+
stdout2env LABELED_URL
38+
exec gh discussion view $LABELED_URL
39+
stdout 'title:\tLabeled Discussion'
40+
stdout 'labels:\tbug, enhancement'
41+
42+
# Create with title + body + category + invalid label (label resolution fails before creation)
43+
! exec gh discussion create --title 'Invalid Label Discussion' --body 'Body' --category 'General' --label nonexistent-label-xyz
44+
stderr 'labels not found'
45+
46+
-- body.txt --
47+
Body from file content
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Create a repository with a file so it has a default branch
2+
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
3+
4+
# Defer repo cleanup
5+
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
6+
7+
# Clone the repo
8+
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
9+
cd $SCRIPT_NAME-$RANDOM_STRING
10+
11+
# Enable discussions
12+
exec gh api repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING -X PATCH -F has_discussions=true
13+
14+
# Create a discussion to edit
15+
exec gh discussion create --title 'Original Title' --body 'Original body' --category 'General'
16+
stdout2env DISCUSSION_URL
17+
18+
# Update title only
19+
exec gh discussion edit $DISCUSSION_URL --title 'Updated Title'
20+
exec gh discussion view $DISCUSSION_URL --json title --jq '.title'
21+
stdout 'Updated Title'
22+
23+
# Update body only
24+
exec gh discussion edit $DISCUSSION_URL --body 'Updated body'
25+
exec gh discussion view $DISCUSSION_URL --json body --jq '.body'
26+
stdout 'Updated body'
27+
28+
# Update body from file
29+
exec gh discussion edit $DISCUSSION_URL --body-file $WORK/body.txt
30+
exec gh discussion view $DISCUSSION_URL --json body --jq '.body'
31+
stdout 'Body from file'
32+
33+
# Update category only
34+
exec gh discussion edit $DISCUSSION_URL --category 'Ideas'
35+
exec gh discussion view $DISCUSSION_URL --json category --jq '.category.name'
36+
stdout 'Ideas'
37+
38+
# Update only labels
39+
exec gh discussion edit $DISCUSSION_URL --add-label bug
40+
exec gh discussion view $DISCUSSION_URL --json labels --jq '[.labels[].name] | join(",")'
41+
stdout 'bug'
42+
43+
exec gh discussion edit $DISCUSSION_URL --add-label enhancement --remove-label bug
44+
exec gh discussion view $DISCUSSION_URL --json labels --jq '[.labels[].name] | join(",")'
45+
stdout 'enhancement'
46+
47+
exec gh discussion edit $DISCUSSION_URL --add-label bug,enhancement
48+
exec gh discussion view $DISCUSSION_URL --json labels --jq '[.labels[].name] | join(",")'
49+
stdout 'bug,enhancement'
50+
51+
exec gh discussion edit $DISCUSSION_URL --remove-label bug
52+
exec gh discussion view $DISCUSSION_URL --json labels --jq '[.labels[].name] | join(",")'
53+
stdout 'enhancement'
54+
55+
# Update title, body, and category together
56+
exec gh discussion edit $DISCUSSION_URL --title 'Final Title' --body 'Final body' --category 'General'
57+
exec gh discussion view $DISCUSSION_URL --json title,body,category
58+
stdout2env FINAL_JSON
59+
jq-assert FINAL_JSON '.title' 'Final Title'
60+
jq-assert FINAL_JSON '.body' 'Final body'
61+
jq-assert FINAL_JSON '.category.name' 'General'
62+
63+
# Update title, body, category, and labels together
64+
exec gh discussion edit $DISCUSSION_URL --title 'All Updated' --body 'All body' --category 'Ideas' --add-label bug --remove-label enhancement
65+
exec gh discussion view $DISCUSSION_URL --json title,body,category,labels
66+
stdout2env ALL_JSON
67+
jq-assert ALL_JSON '.title' 'All Updated'
68+
jq-assert ALL_JSON '.body' 'All body'
69+
jq-assert ALL_JSON '.category.name' 'Ideas'
70+
jq-assert ALL_JSON '[.labels[].name] | join(",")' 'bug'
71+
72+
# Failing label update (missing label)
73+
! exec gh discussion edit $DISCUSSION_URL --add-label nonexistent-label-xyz
74+
stderr 'labels not found'
75+
76+
-- body.txt --
77+
Body from file

0 commit comments

Comments
 (0)