Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion skills/Jira/Workflows/ManageComments.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ManageComments Workflow

View and add comments on Jira issues.
View, add, update, and delete comments on Jira issues.

## Intent-to-Flag Mapping

Expand All @@ -10,6 +10,7 @@ View and add comments on Jira issues.
|-----------|---------|----------|
| "view comments", "list comments", "show comments" | `jtk comments list PROJ-123` | Issue key |
| "add comment", "comment on", "post comment" | `jtk comments add PROJ-123 --body "TEXT"` | Issue key + `--body` text |
| "edit comment", "update comment", "fix comment" | `jtk comments update PROJ-123 COMMENT_ID --body "TEXT" --force` | Issue key + comment ID + `--body` text |
| "delete comment", "remove comment" | `jtk comments delete PROJ-123 COMMENT_ID` | Issue key + comment ID |

## Execute
Expand Down Expand Up @@ -43,6 +44,16 @@ Line 3"
jtk comments add PROJ-123 --body "Line 1\nLine 2\nLine 3"
```

### Update Comment

**Agent must confirm with user before calling this command.** It replaces the whole body of an existing comment and the previous body cannot be recovered; the comment keeps its ID and position. `--body` is handled exactly like `comments add`. The command prompts unless `--force` is passed, so after the user confirms, pass `--force`:

```bash
jtk comments update PROJ-123 COMMENT_ID --body "Corrected text" --force
```

Prefer this over delete-and-re-add, which changes the comment ID and moves the comment to the end of the thread. If the user refers to the comment by content or author, list comments first to recover the ID.

### Delete Comment

**Agent must confirm with user before calling this command.** `jtk comments delete` executes immediately with no interactive CLI-level prompt. The operation is destructive and cannot be undone; the agent is responsible for the pre-call confirmation.
Expand All @@ -57,6 +68,7 @@ The `COMMENT_ID` is a numeric ID (e.g., `12345`). If the user refers to the comm

- **List comments:** A table with ID, author, timestamp, and body; `--fulltext` preserves these columns
- **Add comment:** Confirm comment was added with issue key and a snippet of the comment text
- **Update comment:** Same detail block as add, with the unchanged comment ID

## Post-Action

Expand Down
1 change: 1 addition & 0 deletions tools/jtk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `comments update <issue-key> <comment-id> --body` replaces the body of an existing comment in place, with the same markdown, escape-sequence, and raw ADF handling as `comments add`. Prompts for confirmation because the previous body is not recoverable; `--force` skips the prompt. ([#498](https://github.com/open-cli-collective/atlassian-cli/issues/498))
- Raw ADF JSON passthrough for `--description` (`issues create`/`update`), `--body` (`comments add`), and rich-text `--field` overrides: a value that parses as `{"type":"doc","version":1,...}` with content is sent as a structured ADF document instead of going through markdown conversion, so documents with node types markdown can't express (e.g. `inlineCard` smart links) can be written directly. ([#486](https://github.com/open-cli-collective/atlassian-cli/pull/486))
- `issues get` now accepts multiple issue keys and renders a summary table for the batch. ([#327](https://github.com/open-cli-collective/atlassian-cli/pull/327))
- `issues check <issue-key>` subcommand to audit an issue for populated/missing field values, with `--require` (hard-fail) and `--warn` (advisory) flags. A curated default warn-list (Summary, Description, Assignee, Priority, Labels, Story Points, Sprint, Components, Fix Version/s) applies when no flags are passed. Useful as a transition guardrail or CI step.
Expand Down
22 changes: 22 additions & 0 deletions tools/jtk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,28 @@ jtk comments add PROJ-123 --body "Line one\nLine two\n\tIndented line"

---

### `jtk comments update <issue-key> <comment-id>`

Replace the body of an existing comment. The comment keeps its ID and position in the thread. The previous body cannot be recovered, so the command prompts for confirmation unless `--force` is passed.

**Aliases:** `jtk comment update`, `jtk c update`

```bash
jtk comments update PROJ-123 10042 --body "Corrected text"
jtk comments update PROJ-123 10042 --body "$(cat comment.adf.json)" --force
```

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--body` | `-b` | | New comment text, handled the same way as `comments add --body` (**required**) |
| `--force` | | `false` | Skip the confirmation prompt (required with `--non-interactive`) |

**Arguments:**
- `<issue-key>` - The issue key (**required**)
- `<comment-id>` - The comment ID (**required**)

---

### `jtk comments delete <issue-key> <comment-id>`

Delete a comment from an issue.
Expand Down
27 changes: 27 additions & 0 deletions tools/jtk/api/comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,33 @@ func (c *Client) AddComment(ctx context.Context, issueKey, commentBody string) (
return &comment, nil
}

// UpdateComment replaces the body of an existing comment
func (c *Client) UpdateComment(ctx context.Context, issueKey, commentID, commentBody string) (*Comment, error) {
if issueKey == "" {
return nil, ErrIssueKeyRequired
}
if commentID == "" {
return nil, ErrCommentIDRequired
}

urlStr := fmt.Sprintf("%s/issue/%s/comment/%s", c.BaseURL, url.PathEscape(issueKey), url.PathEscape(commentID))
req := AddCommentRequest{
Body: NewADFDocument(commentBody),
}

body, err := c.Put(ctx, urlStr, req)
if err != nil {
return nil, fmt.Errorf("updating comment %s: %w", commentID, err)
}

var comment Comment
if err := json.Unmarshal(body, &comment); err != nil {
return nil, fmt.Errorf("parsing comment: %w", err)
}

return &comment, nil
}

// DeleteComment deletes a comment from an issue
func (c *Client) DeleteComment(ctx context.Context, issueKey, commentID string) error {
if issueKey == "" {
Expand Down
52 changes: 52 additions & 0 deletions tools/jtk/api/comments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package api

import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/open-cli-collective/atlassian-go/testutil"
)

func TestUpdateComment(t *testing.T) {
var capturedBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testutil.Equal(t, r.URL.Path, "/rest/api/3/issue/PROJ-123/comment/10001")
testutil.Equal(t, r.Method, http.MethodPut)
capturedBody, _ = io.ReadAll(r.Body)
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "10001",
"author": map[string]any{"displayName": "Alice"},
"created": "2024-06-15T10:00:00.000+0000",
"updated": "2024-06-16T10:00:00.000+0000",
})
}))
defer server.Close()

client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"})
testutil.RequireNoError(t, err)

comment, err := client.UpdateComment(context.Background(), "PROJ-123", "10001", "Corrected text")
testutil.RequireNoError(t, err)
testutil.Equal(t, comment.ID, "10001")
testutil.Equal(t, comment.Author.DisplayName, "Alice")

var req map[string]any
testutil.RequireNoError(t, json.Unmarshal(capturedBody, &req))
body := req["body"].(map[string]any)
testutil.Equal(t, body["type"], "doc")
testutil.Len(t, body["content"].([]any), 1)
}

func TestUpdateComment_EmptyIssueKey(t *testing.T) {
_, err := (&Client{}).UpdateComment(context.Background(), "", "10001", "x")
testutil.Equal(t, err, ErrIssueKeyRequired)
}

func TestUpdateComment_EmptyCommentID(t *testing.T) {
_, err := (&Client{}).UpdateComment(context.Background(), "PROJ-123", "", "x")
testutil.Equal(t, err, ErrCommentIDRequired)
}
6 changes: 6 additions & 0 deletions tools/jtk/integration-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,12 @@ Run these steps in order. Each step depends on the previous.
Expected: Comment detail block (Issue Key, Comment ID, Author, body excerpt)
Capture the comment ID → `$COMMENT_ID`

Update the comment in place and confirm the ID is unchanged:
```bash
jtk comments update $TEST_ISSUE $COMMENT_ID -b "Updated comment text" --force
```
Expected: Comment detail block with the same comment ID and the new body

Also test `--id` variant. Capture the ID so it can be cleaned up:
```bash
COMMENT_ID_2=$(jtk comments add $TEST_ISSUE -b "ID flag test comment" --id)
Expand Down
10 changes: 9 additions & 1 deletion tools/jtk/internal/cmd/OUTPUT_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ Deleted MON-4820

Multi-delete: one line per deleted issue.

### `comments add / delete`
### `comments add / update / delete`

```
$ jtk comments add MON-4810 --body "Noting that this needs QA review on Safari 16."
Expand All @@ -533,6 +533,14 @@ $ jtk comments add MON-4810 --body "..." --id
21276
```

`comments update` renders the same detail block as `comments add`, and `--id` emits the comment ID.

```
$ jtk comments update PROJ-123 21276 --body "Corrected text" --force
PROJ-123 #21276 — Jane Doe, 2026-04-16
Corrected text
```

```
$ jtk comments delete MON-4810 21276
Deleted comment 21276 from MON-4810
Expand Down
60 changes: 60 additions & 0 deletions tools/jtk/internal/cmd/comments/comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package comments

import (
"context"
"fmt"
"strconv"

"github.com/spf13/cobra"

"github.com/open-cli-collective/atlassian-go/prompt"

"github.com/open-cli-collective/jira-ticket-cli/api"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/root"
jtkpresent "github.com/open-cli-collective/jira-ticket-cli/internal/present"
Expand All @@ -32,6 +35,7 @@ func Register(parent *cobra.Command, opts *root.Options) {

cmd.AddCommand(newListCmd(opts))
cmd.AddCommand(newAddCmd(opts))
cmd.AddCommand(newUpdateCmd(opts))
cmd.AddCommand(newDeleteCmd(opts))

parent.AddCommand(cmd)
Expand Down Expand Up @@ -195,6 +199,62 @@ func runAdd(ctx context.Context, opts *root.Options, issueKey, body string) erro
return jtkpresent.Emit(opts, jtkpresent.CommentPresenter{}.PresentAddedDetail(issueKey, comment))
}

func newUpdateCmd(opts *root.Options) *cobra.Command {
var body string
var force bool

cmd := &cobra.Command{
Use: "update <issue-key> <comment-id>",
Short: "Update a comment on an issue",
Long: "Replace the body of an existing comment. The comment keeps its ID and position in the thread. The previous body cannot be recovered.",
Example: ` # Update a comment (will prompt for confirmation)
jtk comments update PROJ-123 12345 --body "Corrected text"

# Update without confirmation
jtk comments update PROJ-123 12345 --body "Corrected text" --force`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return runUpdate(cmd.Context(), opts, args[0], args[1], body, force)
},
}

cmd.Flags().StringVarP(&body, "body", "b", "", "New comment text (raw ADF JSON is sent as structured ADF) (required)")
_ = cmd.MarkFlagRequired("body")
cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt")

return cmd
}

func runUpdate(ctx context.Context, opts *root.Options, issueKey, commentID, body string, force bool) error {
if !force && !opts.NonInteractive {
fmt.Fprintf(opts.Stderr, "This will replace the body of comment %s on %s. The previous body cannot be recovered.\n", commentID, issueKey)
fmt.Fprint(opts.Stderr, "Are you sure? [y/N]: ")
}
confirmed, err := prompt.ConfirmOrFail(force, opts.NonInteractive, opts.Stdin)
if err != nil {
return err
}
if !confirmed {
return jtkpresent.Emit(opts, jtkpresent.CommentPresenter{}.PresentUpdateCancelled())
}

client, err := opts.APIClient()
if err != nil {
return err
}

Comment thread
monit-reviewer marked this conversation as resolved.
comment, err := client.UpdateComment(ctx, issueKey, commentID, text.InterpretEscapesUnlessRawADF(body))
if err != nil {
return err
}

if opts.EmitIDOnly() {
return jtkpresent.EmitIDs(opts, []string{comment.ID})
}

return jtkpresent.Emit(opts, jtkpresent.CommentPresenter{}.PresentAddedDetail(issueKey, comment))
}

func newDeleteCmd(opts *root.Options) *cobra.Command {
cmd := &cobra.Command{
Use: "delete <issue-key> <comment-id>",
Expand Down
Loading
Loading