From 55e2da741e88332486cd9ddc4b7875ccd2ddf0ab Mon Sep 17 00:00:00 2001 From: David Moruzzi Date: Mon, 10 Aug 2026 23:56:46 -0400 Subject: [PATCH 1/3] feat(cfl): add support for custom version comments in page edits --- tools/cfl/internal/cmd/page/edit.go | 15 +++- tools/cfl/internal/cmd/page/edit_test.go | 99 ++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/tools/cfl/internal/cmd/page/edit.go b/tools/cfl/internal/cmd/page/edit.go index 0165a6f2..c24ed72b 100644 --- a/tools/cfl/internal/cmd/page/edit.go +++ b/tools/cfl/internal/cmd/page/edit.go @@ -26,6 +26,8 @@ type editOptions struct { bodyFormatExplicit bool legacy bool parent string + message string + messageExplicit bool } func newEditCmd(rootOpts *root.Options) *cobra.Command { @@ -71,6 +73,9 @@ without conversion.`, # Move page to a new parent cfl page edit 12345 --parent 67890 + # Update with a custom version comment + cfl page edit 12345 --file content.md -m "Fixed typos" + # Move page and update title cfl page edit 12345 --parent 67890 --title "New Title" @@ -93,6 +98,7 @@ without conversion.`, RunE: func(cmd *cobra.Command, args []string) error { opts.pageID = args[0] opts.bodyFormatExplicit = cmd.Flags().Changed("body-format") + opts.messageExplicit = cmd.Flags().Changed("message") return runEdit(cmd.Context(), opts) }, } @@ -100,6 +106,7 @@ without conversion.`, cmd.Flags().StringVarP(&opts.title, "title", "t", "", "New page title") cmd.Flags().StringVarP(&opts.file, "file", "f", "", "Read content from file") cmd.Flags().StringVarP(&opts.parent, "parent", "p", "", "Move page to new parent page ID") + cmd.Flags().StringVarP(&opts.message, "message", "m", "", "Version comment for the update") cmd.Flags().BoolVar(&opts.editor, "editor", false, "Open editor for content") cmd.Flags().StringVar(&opts.bodyFormat, "body-format", bodyFormatMarkdown, "Input format: markdown, adf, or xhtml") cmd.Flags().BoolVar(&opts.legacy, "legacy", false, "Edit page in legacy editor format (Markdown input only)") @@ -187,13 +194,19 @@ func runEdit(ctx context.Context, opts *editOptions) error { } } + // Backwards compatibility + versionMessage := "Updated via cfl" + if opts.messageExplicit { + versionMessage = opts.message + } + req := &api.UpdatePageRequest{ ID: opts.pageID, Status: "current", Title: newTitle, Version: &api.Version{ Number: existingPage.Version.Number + 1, - Message: "Updated via cfl", + Message: versionMessage, }, } diff --git a/tools/cfl/internal/cmd/page/edit_test.go b/tools/cfl/internal/cmd/page/edit_test.go index 5a2c1ff3..2bc69a58 100644 --- a/tools/cfl/internal/cmd/page/edit_test.go +++ b/tools/cfl/internal/cmd/page/edit_test.go @@ -124,6 +124,105 @@ func TestRunEdit_TitleOnly(t *testing.T) { testutil.Equal(t, "

Keep this

", storage["value"]) } +func TestRunEdit_CustomMessage(t *testing.T) { + t.Parallel() + var receivedBody map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET" && strings.Contains(r.URL.Path, "/pages/12345"): + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "id": "12345", + "title": "Old Title", + "version": {"number": 3}, + "body": {"storage": {"representation": "storage", "value": "

Keep this

"}}, + "_links": {"webui": "/pages/12345"} + }`)) + case r.Method == "PUT" && strings.Contains(r.URL.Path, "/pages/12345"): + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &receivedBody) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "id": "12345", + "title": "Old Title", + "version": {"number": 4}, + "_links": {"webui": "/pages/12345"} + }`)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + rootOpts := newEditTestRootOptions() + client := api.NewClient(server.URL, "test@example.com", "token") + rootOpts.SetAPIClient(client) + rootOpts.Stdin = nil + opts := &editOptions{ + Options: rootOpts, + pageID: "12345", + title: "Old Title", + message: "Fixed typos", + messageExplicit: true, + } + + err := runEdit(context.Background(), opts) + testutil.RequireNoError(t, err) + + version := receivedBody["version"].(map[string]any) + testutil.Equal(t, "Fixed typos", version["message"]) +} + +func TestRunEdit_EmptyMessageOmitsField(t *testing.T) { + t.Parallel() + var receivedBody map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET" && strings.Contains(r.URL.Path, "/pages/12345"): + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "id": "12345", + "title": "Old Title", + "version": {"number": 3}, + "body": {"storage": {"representation": "storage", "value": "

Keep this

"}}, + "_links": {"webui": "/pages/12345"} + }`)) + case r.Method == "PUT" && strings.Contains(r.URL.Path, "/pages/12345"): + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &receivedBody) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "id": "12345", + "title": "Old Title", + "version": {"number": 4}, + "_links": {"webui": "/pages/12345"} + }`)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + rootOpts := newEditTestRootOptions() + client := api.NewClient(server.URL, "test@example.com", "token") + rootOpts.SetAPIClient(client) + rootOpts.Stdin = nil + opts := &editOptions{ + Options: rootOpts, + pageID: "12345", + title: "Old Title", + message: "", + messageExplicit: true, + } + + err := runEdit(context.Background(), opts) + testutil.RequireNoError(t, err) + + version := receivedBody["version"].(map[string]any) + _, hasMessage := version["message"] + testutil.Equal(t, false, hasMessage) +} + func TestRunEdit_PageNotFound(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { From c72b34f5975c36dc6e70d197aa0ebf6fed7dc25f Mon Sep 17 00:00:00 2001 From: Caleb Piekstra Date: Wed, 30 Sep 2026 17:50:14 -0400 Subject: [PATCH 2/3] test(cfl): cover the default page edit version message --- tools/cfl/internal/cmd/page/edit_test.go | 147 +++++++++-------------- 1 file changed, 56 insertions(+), 91 deletions(-) diff --git a/tools/cfl/internal/cmd/page/edit_test.go b/tools/cfl/internal/cmd/page/edit_test.go index cb776771..18b33260 100644 --- a/tools/cfl/internal/cmd/page/edit_test.go +++ b/tools/cfl/internal/cmd/page/edit_test.go @@ -124,103 +124,68 @@ func TestRunEdit_TitleOnly(t *testing.T) { testutil.Equal(t, "

Keep this

", storage["value"]) } -func TestRunEdit_CustomMessage(t *testing.T) { +func TestRunEdit_VersionMessage(t *testing.T) { t.Parallel() - var receivedBody map[string]any - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == "GET" && strings.Contains(r.URL.Path, "/pages/12345"): - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{ - "id": "12345", - "title": "Old Title", - "version": {"number": 3}, - "body": {"storage": {"representation": "storage", "value": "

Keep this

"}}, - "_links": {"webui": "/pages/12345"} - }`)) - case r.Method == "PUT" && strings.Contains(r.URL.Path, "/pages/12345"): - body, _ := io.ReadAll(r.Body) - _ = json.Unmarshal(body, &receivedBody) - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{ - "id": "12345", - "title": "Old Title", - "version": {"number": 4}, - "_links": {"webui": "/pages/12345"} - }`)) - default: - w.WriteHeader(http.StatusNotFound) - } - })) - defer server.Close() - - rootOpts := newEditTestRootOptions() - client := api.NewClient(server.URL, "test@example.com", "token") - rootOpts.SetAPIClient(client) - rootOpts.Stdin = nil - opts := &editOptions{ - Options: rootOpts, - pageID: "12345", - title: "Old Title", - message: "Fixed typos", - messageExplicit: true, + tests := []struct { + name string + message string + messageExplicit bool + wantMessage any + }{ + {name: "default when flag absent", wantMessage: "Updated via cfl"}, + {name: "custom message", message: "Fixed typos", messageExplicit: true, wantMessage: "Fixed typos"}, + {name: "explicit empty omits field", message: "", messageExplicit: true, wantMessage: nil}, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var receivedBody map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET" && strings.Contains(r.URL.Path, "/pages/12345"): + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "id": "12345", + "title": "Old Title", + "version": {"number": 3}, + "body": {"storage": {"representation": "storage", "value": "

Keep this

"}}, + "_links": {"webui": "/pages/12345"} + }`)) + case r.Method == "PUT" && strings.Contains(r.URL.Path, "/pages/12345"): + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &receivedBody) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "id": "12345", + "title": "Old Title", + "version": {"number": 4}, + "_links": {"webui": "/pages/12345"} + }`)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() - err := runEdit(context.Background(), opts) - testutil.RequireNoError(t, err) - - version := receivedBody["version"].(map[string]any) - testutil.Equal(t, "Fixed typos", version["message"]) -} + rootOpts := newEditTestRootOptions() + client := api.NewClient(server.URL, "test@example.com", "token") + rootOpts.SetAPIClient(client) + rootOpts.Stdin = nil + opts := &editOptions{ + Options: rootOpts, + pageID: "12345", + title: "Old Title", + message: tt.message, + messageExplicit: tt.messageExplicit, + } -func TestRunEdit_EmptyMessageOmitsField(t *testing.T) { - t.Parallel() - var receivedBody map[string]any - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == "GET" && strings.Contains(r.URL.Path, "/pages/12345"): - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{ - "id": "12345", - "title": "Old Title", - "version": {"number": 3}, - "body": {"storage": {"representation": "storage", "value": "

Keep this

"}}, - "_links": {"webui": "/pages/12345"} - }`)) - case r.Method == "PUT" && strings.Contains(r.URL.Path, "/pages/12345"): - body, _ := io.ReadAll(r.Body) - _ = json.Unmarshal(body, &receivedBody) - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{ - "id": "12345", - "title": "Old Title", - "version": {"number": 4}, - "_links": {"webui": "/pages/12345"} - }`)) - default: - w.WriteHeader(http.StatusNotFound) - } - })) - defer server.Close() + err := runEdit(context.Background(), opts) + testutil.RequireNoError(t, err) - rootOpts := newEditTestRootOptions() - client := api.NewClient(server.URL, "test@example.com", "token") - rootOpts.SetAPIClient(client) - rootOpts.Stdin = nil - opts := &editOptions{ - Options: rootOpts, - pageID: "12345", - title: "Old Title", - message: "", - messageExplicit: true, + version := receivedBody["version"].(map[string]any) + testutil.Equal(t, version["message"], tt.wantMessage) + }) } - - err := runEdit(context.Background(), opts) - testutil.RequireNoError(t, err) - - version := receivedBody["version"].(map[string]any) - _, hasMessage := version["message"] - testutil.Equal(t, false, hasMessage) } func TestRunEdit_PageNotFound(t *testing.T) { From 8bfe682a74fa62459ba9220a96a8401eecc9875f Mon Sep 17 00:00:00 2001 From: Caleb Piekstra Date: Wed, 30 Sep 2026 17:50:14 -0400 Subject: [PATCH 3/3] docs(cfl): document page edit --message --- tools/cfl/CHANGELOG.md | 1 + tools/cfl/README.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/tools/cfl/CHANGELOG.md b/tools/cfl/CHANGELOG.md index 8db43db2..06f82063 100644 --- a/tools/cfl/CHANGELOG.md +++ b/tools/cfl/CHANGELOG.md @@ -8,6 +8,7 @@ ### Added +- `page edit --message`/`-m` sets the page version comment, which was always "Updated via cfl". Omitting the flag keeps that default; an empty value sends no comment. Thanks to @dmoruzzi for the feature. ([#473](https://github.com/open-cli-collective/atlassian-cli/pull/473)) - Service account support with bearer auth (`--auth-method bearer`) for scoped API tokens ([#171](https://github.com/open-cli-collective/atlassian-cli/pull/171)) - Wiki-link syntax `[[Page Title]]` and `[[SPACE:Page Title]]` for internal Confluence page links ([#129](https://github.com/open-cli-collective/atlassian-cli/pull/129)) - `space view`, `space create`, `space update`, `space delete` commands for full space management ([#151](https://github.com/open-cli-collective/atlassian-cli/issues/151)) diff --git a/tools/cfl/README.md b/tools/cfl/README.md index a8d96cf0..812967aa 100644 --- a/tools/cfl/README.md +++ b/tools/cfl/README.md @@ -426,6 +426,9 @@ cfl page edit 12345 --parent 67890 # Move page and rename in one command cfl page edit 12345 --parent 67890 --title "New Title" +# Update with a custom version comment +cfl page edit 12345 --file content.md -m "Fixed typos" + # Edit using legacy storage format (for pages created in legacy editor) cfl page edit 12345 --file content.md --legacy @@ -442,6 +445,7 @@ cfl page view 12345 --body-format xhtml --content-only | \ | `--title` | `-t` | | New page title (keeps existing if not specified) | | `--parent` | `-p` | | Move page to new parent page ID | | `--file` | `-f` | | Read content from file | +| `--message` | `-m` | `Updated via cfl` | Version comment for the update; an empty value sends none | | `--editor` | | `false` | Force open in $EDITOR | | `--body-format` | | `markdown` | Input/editor format: `markdown`, exact `adf` JSON, or exact storage `xhtml` | | `--legacy` | | `false` | Convert Markdown to storage XHTML instead of ADF; invalid with `adf` or `xhtml` |