@repo-toolkit/confluence 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +129 -25
  2. package/cli.js +647 -14
  3. package/index.d.ts +150 -1
  4. package/index.js +622 -12
  5. package/package.json +2 -2
package/README.md CHANGED
@@ -13,8 +13,8 @@ reads `INPUT_*` environment variables (the same shape as the
13
13
  into a `node20` action or run it as a standalone CLI.
14
14
 
15
15
  The long-form guide (credentials, Mermaid, Markdown subset, optimistic
16
- concurrency, non-pruning behavior, raw-HTML safety) lives at
17
- <https://repo-toolkit.pages.dev/docs/packages/confluence>.
16
+ concurrency, managed ownership/pruning/clean, parent summary, dry-run, raw-HTML
17
+ safety) lives at <https://repo-toolkit.pages.dev/docs/packages/confluence>.
18
18
 
19
19
  ## Installation
20
20
 
@@ -76,13 +76,19 @@ Flags:
76
76
  italic source notice. The `--folder` path is added to the notice link when it
77
77
  is relative to `--cwd`. When omitted, GitHub Actions runs infer this from
78
78
  `GITHUB_SERVER_URL` and `GITHUB_REPOSITORY`.
79
+ - `--page-title-strategy <value>` — leaf page title strategy (default: `filename-stem`). One of `filename-stem`, `filename`, `sentence-case-parent`, `sentence-case-parents`, `sentence-case-path`. Applied only to Markdown leaf pages; folder-generated parent pages keep their raw directory-segment titles. See [leaf page title strategies](#leaf-page-title-strategies).
79
80
  - `--skip-unchanged` / `--no-skip-unchanged` — skip pages whose body is unchanged (default: `skip`)
80
81
  - `--dry-run` — walk the doc tree and validate every markdown file and local
81
82
  image source (same preflight as a real sync) then log the plan. No API
82
- mutation calls; credentials are not required under `--dry-run`.
83
+ mutation calls; credentials are not required under `--dry-run`. Dry-run can
84
+ show clean/prune and parent-summary intent but cannot list/count remote
85
+ deletion candidates, fetch parent content, or provide mapped remote links
86
+ because it makes zero API calls.
83
87
  - `--render-html-blocks` — render ` ```html ` fenced blocks as inline HTML via
84
88
  the Confluence `html` macro instead of a code box (default: `false`). **Unsafe
85
89
  for untrusted Markdown** — see the raw HTML section of the website guide.
90
+ - `--clean` — move all page descendants to trash before recreation (default: `false`). **WARNING: destructive — all page descendants, including manual/unlabeled pages, are moved to trash before recreation; `parentPageId` itself is retained and never deleted.** Pages are moved to trash (recoverable), never purged.
91
+ - `--update-parent-page` / `--no-update-parent-page` — update the parent page summary region (default: `true`). Use `--no-update-parent-page` to opt out.
86
92
  - `-i, --interactive` — prompt interactively (on a real TTY) for missing
87
93
  non-secret required fields. The API token is never prompted.
88
94
 
@@ -93,26 +99,96 @@ both read for every option. Boolean env values accept `true|1|yes|on` /
93
99
  `false|0|no|off` (empty string is falsy); any other value exits nonzero with
94
100
  `Invalid boolean value for <ENV_NAME>: <raw>`.
95
101
 
96
- | Option | `CONFLUENCE_*` | `INPUT_*` (Actions form) |
97
- | ----------------------- | ------------------------------- | --------------------------------------------- |
98
- | folder | `CONFLUENCE_FOLDER` | `INPUT_FOLDER` |
99
- | username | `CONFLUENCE_USERNAME` | `INPUT_USERNAME` |
100
- | apiToken | `CONFLUENCE_API_TOKEN` | `INPUT_API-TOKEN`, `INPUT_PASSWORD` |
101
- | apiTokenFile | `CONFLUENCE_API_TOKEN_FILE` | `INPUT_API-TOKEN-FILE`, `INPUT_PASSWORD-FILE` |
102
- | baseUrl | `CONFLUENCE_BASE_URL` | `INPUT_CONFLUENCE-BASE-URL` |
103
- | spaceKey | `CONFLUENCE_SPACE_KEY` | `INPUT_SPACE-KEY` |
104
- | parentPageId | `CONFLUENCE_PARENT_PAGE_ID` | `INPUT_PARENT-PAGE-ID` |
105
- | versionMessage | `CONFLUENCE_VERSION_MESSAGE` | `INPUT_VERSION-MESSAGE` |
106
- | repositoryUrl | `CONFLUENCE_REPOSITORY_URL` | `INPUT_REPOSITORY-URL` |
107
- | skipUnchanged (bool) | `CONFLUENCE_SKIP_UNCHANGED` | `INPUT_SKIP-UNCHANGED` |
108
- | dryRun (bool) | `CONFLUENCE_DRY_RUN` | `INPUT_DRY-RUN` |
109
- | renderHtmlBlocks (bool) | `CONFLUENCE_RENDER_HTML_BLOCKS` | `INPUT_RENDER-HTML-BLOCKS` |
102
+ | Option | `CONFLUENCE_*` | `INPUT_*` (Actions form) |
103
+ | ----------------------- | -------------------------------- | --------------------------------------------- |
104
+ | folder | `CONFLUENCE_FOLDER` | `INPUT_FOLDER` |
105
+ | username | `CONFLUENCE_USERNAME` | `INPUT_USERNAME` |
106
+ | apiToken | `CONFLUENCE_API_TOKEN` | `INPUT_API-TOKEN`, `INPUT_PASSWORD` |
107
+ | apiTokenFile | `CONFLUENCE_API_TOKEN_FILE` | `INPUT_API-TOKEN-FILE`, `INPUT_PASSWORD-FILE` |
108
+ | baseUrl | `CONFLUENCE_BASE_URL` | `INPUT_CONFLUENCE-BASE-URL` |
109
+ | spaceKey | `CONFLUENCE_SPACE_KEY` | `INPUT_SPACE-KEY` |
110
+ | parentPageId | `CONFLUENCE_PARENT_PAGE_ID` | `INPUT_PARENT-PAGE-ID` |
111
+ | versionMessage | `CONFLUENCE_VERSION_MESSAGE` | `INPUT_VERSION-MESSAGE` |
112
+ | repositoryUrl | `CONFLUENCE_REPOSITORY_URL` | `INPUT_REPOSITORY-URL` |
113
+ | pageTitleStrategy | `CONFLUENCE_PAGE_TITLE_STRATEGY` | `INPUT_PAGE-TITLE-STRATEGY` |
114
+ | skipUnchanged (bool) | `CONFLUENCE_SKIP_UNCHANGED` | `INPUT_SKIP-UNCHANGED` |
115
+ | dryRun (bool) | `CONFLUENCE_DRY_RUN` | `INPUT_DRY-RUN` |
116
+ | renderHtmlBlocks (bool) | `CONFLUENCE_RENDER_HTML_BLOCKS` | `INPUT_RENDER-HTML-BLOCKS` |
117
+ | clean (bool) | `CONFLUENCE_CLEAN` | `INPUT_CLEAN` |
118
+ | updateParentPage (bool) | `CONFLUENCE_UPDATE_PARENT_PAGE` | `INPUT_UPDATE-PARENT-PAGE` |
110
119
 
111
120
  Errors, `--help`, and all log lines never print the supplied token value. The
112
121
  secret-file loader wraps fs errors via `Error.cause` (`Failed to read
113
122
  apiTokenFile at <path>`, `apiTokenFile at <path> is empty`) without revealing
114
123
  the file contents.
115
124
 
125
+ ## Leaf page title strategies
126
+
127
+ Leaf Confluence page titles are derived from the Markdown file's relative path segments. Directory segments become synthetic parent-page titles and retain their raw segment text for every strategy — only the Markdown leaf file uses `pageTitleStrategy`.
128
+
129
+ | Strategy | Behavior | Example for `community-nodes/cdogs-document-generator/credentials.md` |
130
+ | ------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
131
+ | `filename-stem` (default) | Original filename without the final `.md` extension | `credentials` |
132
+ | `filename` | Original filename including extension | `credentials.md` |
133
+ | `sentence-case-parent` | Sentence-case filename stem plus immediate parent folder | `Credentials (cdogs-document-generator)` |
134
+ | `sentence-case-parents` | Sentence-case filename stem plus all parent folders | `Credentials (community-nodes/cdogs-document-generator)` |
135
+ | `sentence-case-path` | Sentence-case filename stem plus all parent folders and original filename including extension | `Credentials (community-nodes/cdogs-document-generator/credentials.md)` |
136
+
137
+ Sentence case is deterministic and dependency-free: remove only the final case-insensitive `.md`; replace each run of `-` and `_` with one space; trim; lowercase ASCII letters; uppercase the first ASCII letter. Digits and other punctuation are preserved. Examples: `failed-deployment.md` → `Failed deployment`, `n8n_setup.md` → `N8n setup`, `README.md` → `Readme`. Parent folders are the segments before the filename, joined with `/` exactly as stored. Preserve the original filename and extension casing in `filename` and inside `sentence-case-path` (for example, `Guide.MD` remains `Guide.MD` where the original filename is included).
138
+
139
+ Root-file behavior: for `sentence-case-parent` and `sentence-case-parents` the parentheses are omitted when a file is directly under the documentation root (`overview.md` → `Overview`). For `sentence-case-path`, the root parenthesized path is the filename itself (`overview.md` → `Overview (overview.md)`).
140
+
141
+ ### Migration and uniqueness notes
142
+
143
+ > **Changing the strategy changes title-based identity.** Sync looks up Confluence pages by title under parent. Switching the strategy therefore seeks a new title and may **create a new page while leaving the old page untouched** — it does not rename or move existing pages. Previously labeled pages created under the old strategy carry the `repo-toolkit-confluence` ownership label and are **pruned (moved to trash) after a successful sync** under the new strategy because they become stale labeled descendants. Pages created before the labeling feature or otherwise unlabeled remain unlabeled and are **never pruned automatically** — they require manual cleanup in Confluence. Use `--dry-run` to preview generated titles (`would sync <path> as "<title>"`) before switching strategies in production.
144
+
145
+ Path-based strategies reduce predictable local collisions (for example, repeated basenames such as `credentials.md` or `README.md` in separate subtrees) but cannot guarantee uniqueness against unrelated or manually created pages already present in the target Confluence space. Existing spaces may still cause Confluence API conflicts if a generated title collides with an unrelated page under the same parent. Do not truncate or hash titles locally; if Confluence imposes a remote title-length limit, the existing API error behavior applies.
146
+
147
+ ## Managed ownership and reconciliation
148
+
149
+ Every generated folder page and Markdown leaf page created, updated, or otherwise mapped by a successful sync carries the fixed global Confluence label `repo-toolkit-confluence` (`CONFLUENCE_MANAGED_LABEL`, prefix `global`). The label is additive — existing labels are preserved — and is added only when absent, so repeated unchanged syncs issue no redundant label POST. Adoption is explicit: an existing unlabeled page found by title-under-parent becomes managed once the sync maps it and successfully adds the marker; removing that page's local source later makes it eligible for pruning because it now carries the marker.
150
+
151
+ Default pruning (`clean: false`, the default) runs only after every local page has synced and its ownership label has been verified or added successfully. It enumerates all descendants of `parentPageId` (never the parent itself), and deletes stale pages deepest-first (children before parents). A page is stale only when it has the exact global `repo-toolkit-confluence` marker and its id is absent from the current mapped-id set. Pruning is **label-gated and target-subtree-scoped** — only labeled descendants of the configured `parentPageId` are candidates; space-wide label queries are not used. Unlabeled/manual pages are never deleted by default pruning. Removing the marker from a managed page opts it out of pruning until a future sync maps/adopts it again and re-applies the label. If a stale labeled ancestor contains an unlabeled, non-page, inaccessible, or otherwise retained descendant, that ancestor is blocked and reported as `blocked` rather than risking collateral deletion; safe stale siblings are still deleted. Pruning deletes by page id, moves pages to trash (recoverable), and never purges (`purge=true` is never sent). The target `parentPageId` itself is always an external anchor — it is never labeled and never deleted by either pruning mode.
152
+
153
+ Explicit clean (`clean: true`, default `false` via API/config/CLI/`CONFLUENCE_CLEAN`/`INPUT_CLEAN`) runs **before** creation after successful local preflight and moves **every page descendant** of `parentPageId` to trash, regardless of label, deepest-first, then recreates and labels the local hierarchy without a second prune pass. No destructive reset occurs when `clean` is omitted. An empty local tree with `clean: true` still performs the clean and leaves no safely deletable page descendants. Label lookup is not required for clean; it is deliberately stronger than managed pruning. A stale stale-ancestor protection still applies: pages with unsupported types or incomplete inventories fail closed before any deletion. Partial clean or prune failures abort and return structured evidence (`ReconciliationError` with phase `clean`/`prune`, `completed`, `failure`, `unprocessed`).
154
+
155
+ If sync or labeling fails before pruning, no stale-page deletions occur.
156
+
157
+ ## Parent page summary
158
+
159
+ `updateParentPage` defaults to `true` via API/config, `--update-parent-page` (positive CLI), `CONFLUENCE_UPDATE_PARENT_PAGE`, and `INPUT_UPDATE-PARENT-PAGE` (GitHub Action). Use `--no-update-parent-page` or set `updateParentPage: false` / `CONFLUENCE_UPDATE_PARENT_PAGE=false` / `INPUT_UPDATE-PARENT-PAGE=false` to opt out.
160
+
161
+ After child-page sync, labeling, and clean/prune reconciliation succeed, the tool fetches the current target parent page, preserves its title and every byte outside one tool-managed region bounded by `<!-- repo-toolkit-confluence:parent-summary:start -->` / `<!-- repo-toolkit-confluence:parent-summary:end -->`, and updates that region with `version.number = current + 1`. The target parent itself remains unlabeled and is never deleted. On first run the region is appended; later runs replace it in place. Setting opt-out leaves both existing managed and manual parent content untouched — it does not remove a previously generated region. The PUT is skipped when the reconstructed body is byte-equal to the current body; no wall-clock timestamp, version, or per-run counters are included so identical deployments are idempotent. Malformed or duplicate markers fail closed without rewriting the parent. If any earlier phase fails, the parent summary is left unchanged; a parent GET/PUT failure after successful child reconciliation surfaces as `ParentSummaryError` (phase `parent-summary`) with child evidence and the failed parent id.
162
+
163
+ The generated region contains:
164
+
165
+ - `Synced documentation` heading with source provenance — when `repositoryUrl` resolves, the link is included; otherwise a generic `maintained by repo-toolkit-confluence` statement is rendered without exposing runner paths.
166
+ - Deterministic statistics derived from the validated local plan and final mapping: Markdown page count, generated directory-page count, total managed child-page count, maximum documentation depth, local attachment-reference count, and Mermaid-block count (zero values render explicitly for an empty tree).
167
+ - A nested deterministic directory-style tree for every generated directory and Markdown page, in local relative-path order, displaying the resolved Confluence title, distinguishing directory pages from Markdown leaves, and linking each item to its mapped Confluence page via id-backed storage links.
168
+ - Stable guidance that generated descendants carry the `repo-toolkit-confluence` label, missing labeled pages are pruned after successful sync, unlabeled pages are preserved by default, and explicit clean moves all safely deletable page descendants to trash.
169
+
170
+ With an empty local tree, provenance/guidance, zero statistics, and an explicit `No managed child pages` tree state are rendered. No child-page bodies, headings, excerpts, attachment names, secrets, credentials, local absolute paths, or unbounded remote metadata are copied into the parent.
171
+
172
+ Dry-run with `updateParentPage: true` prints locally known parent-summary statistics and title tree but cannot fetch parent content or provide mapped remote links because it makes zero API calls.
173
+
174
+ ## Dry run
175
+
176
+ `--dry-run` walks the documentation tree, runs the same local preflight (read and convert every Markdown file, validate every local image source), and prints the plan without any API mutation calls. Credentials are not required under `--dry-run`. Dry-run preserves zero API calls — `dryRun + clean` logs that a remote clean would be requested and `dry-run` states that managed stale-page pruning would run during a real sync, but it cannot enumerate or count remote deletion candidates. With `updateParentPage` enabled, dry-run can show local parent-summary statistics and title tree, but cannot claim the parent would be unchanged, fetch parent content, or emit remote id-backed links.
177
+
178
+ ## Permissions and concurrency
179
+
180
+ Required Confluence permissions and API scopes for a real sync:
181
+
182
+ - Read descendants of the configured `parentPageId` (`GET /wiki/api/v2/pages/{id}/descendants`, paginated, same-origin cursor).
183
+ - Read and add labels on generated pages (`GET /wiki/api/v2/pages/{id}/labels`, `POST /wiki/rest/api/content/{id}/label` with global `repo-toolkit-confluence`).
184
+ - Create and update pages and attachments under the target (`POST`/`PUT` pages, `GET`/`PUT` page bodies, attachment upload via v1 multipart `POST /wiki/rest/api/content/{pageId}/child/attachment` with `X-Atlassian-Token: no-check`).
185
+ - Read and update the target parent page body (`GET /wiki/api/v2/pages/{id}`, `PUT` with `version.number = current + 1`).
186
+ - Delete (move to trash) pages when pruning or cleaning (`DELETE /wiki/api/v2/pages/{id}` without `purge=true`). Pages are recoverable from Confluence trash.
187
+
188
+ Concurrent sync/clean jobs against the same `parentPageId` are unsupported and dangerous. Callers must serialize deployments for the same target; the tool does not add distributed locking. Two syncs racing on the same page are detected via optimistic concurrency (server returns HTTP 409 on version conflict) and the loser exits nonzero — rerun to reconcile.
189
+
190
+ Downstream `egose/actions/confluence` wiring for `clean` and `update-parent-page` is a named release follow-up if that repository requires separately declared inputs (this package already accepts them via CLI/config/`CONFLUENCE_*`/`INPUT_*`; the Action example below lists them).
191
+
116
192
  ## JavaScript API
117
193
 
118
194
  ```ts
@@ -126,11 +202,16 @@ await syncConfluenceToDocs({
126
202
  spaceKey: 'ENG',
127
203
  parentPageId: '123456789',
128
204
  versionMessage: 'chore(docs): sync',
205
+ pageTitleStrategy: 'sentence-case-parents', // optional; default: 'filename-stem'
206
+ clean: false, // optional; default: false — when true, trash every page descendant before recreation
207
+ updateParentPage: true, // optional; default: true — set false to skip parent summary update
129
208
  });
130
209
  ```
131
210
 
132
211
  `resolveConfluenceSyncPlan(options)` resolves the same plan without starting a
133
- sync; it is the documented way to validate options up front.
212
+ sync; it validates `pageTitleStrategy`, fills `clean` (default `false`) and
213
+ `updateParentPage` (default `true`), and is the documented way to validate
214
+ options up front.
134
215
 
135
216
  `syncConfluenceToDocs` runs a **local preflight** before any remote mutation:
136
217
  every markdown file is read and converted to storage HTML, and every local
@@ -139,8 +220,16 @@ size checks the upload path applies. On the first defect it throws an
139
220
  `LocalSyncValidationAggregateError` listing every failing entry (zero API
140
221
  calls). On a remote failure mid-run it throws a `SyncMutationError` carrying
141
222
  `.changes` (pages that succeeded), `.failure` (the failing entry + its error),
142
- and `.unprocessed` (remaining entries). On success it returns a
143
- `SyncResult` whose `.changes` lists every created/updated/unchanged page.
223
+ and `.unprocessed` (remaining entries). Pruning/clean failures throw
224
+ `ReconciliationError` with `.phase` (`clean` or `prune`), `.completed`,
225
+ `.failure` (failed `pageId` + error), and `.unprocessed`. Parent summary
226
+ failures throw `ParentSummaryError` (phase `parent-summary`) after child
227
+ reconciliation succeeded, carrying `.changes`, `.labelsAdded`,
228
+ `.cleanDeletions`, `.pruneDeletions`, `.blocked`, and `.failure`. On success it
229
+ returns a `SyncResult` with `.changes` (created/updated/unchanged pages),
230
+ `.labelsAdded` (ids that received the ownership marker), `.cleanDeletions`
231
+ (pre-sync trash ids), `.pruneDeletions` (post-sync stale ids), `.blocked`
232
+ (retained stale ancestors), and `.parentStatus` (`updated`|`unchanged`|`skipped`).
144
233
  `validateLocalSync(entries, plan)` runs the same local preflight standalone —
145
234
  useful for CI gates or `--dry-run`-equivalent validation. Leaf pages with no
146
235
  attachment-bearing placeholders are created in a single POST with their final
@@ -155,6 +244,11 @@ gateway owns all remote work. Typed fakes implementing the interface are
155
244
  accepted by `syncConfluenceToDocs({ client })` without `as unknown` casts.
156
245
  `spaceKey` and `parentPageId` remain required even with a custom gateway.
157
246
 
247
+ Exported constants and helpers: `CONFLUENCE_MANAGED_LABEL`
248
+ (`repo-toolkit-confluence`), `planStalePruning` / `planCleanDeletions` (pure
249
+ deepest-first planners), plus `ReconciliationError`, `ParentSummaryError`,
250
+ `SyncMutationError`, `LocalSyncValidationAggregateError`.
251
+
158
252
  ## GitHub Action usage
159
253
 
160
254
  The CLI auto-detects the GitHub Actions `INPUT_*` environment when no flags are
@@ -185,13 +279,24 @@ inputs:
185
279
  space-key: { required: true }
186
280
  parent-page-id: { required: true }
187
281
  version-message: { required: false }
282
+ page-title-strategy: { required: false, default: 'filename-stem' }
188
283
  dry-run: { required: false, default: 'false' }
189
284
  skip-unchanged: { required: false, default: 'true' }
190
285
  render-html-blocks: { required: false, default: 'false' }
286
+ clean: { required: false, default: 'false' }
287
+ update-parent-page: { required: false, default: 'true' }
191
288
  ```
192
289
 
290
+ The `clean` input is destructive — when `true`, every page descendant of
291
+ `parentPageId`, including manual/unlabeled pages, is moved to trash before
292
+ recreation. The `update-parent-page` input controls the parent summary region
293
+ (default on); set `false` to skip parent updates and leave existing managed and
294
+ manual parent content untouched. If `egose/actions/confluence` requires
295
+ separately declared inputs, wire both there as a release follow-up.
296
+
193
297
  A runnable smoke fixture lives under `packages/confluence/action-fixture/` and
194
- demonstrates starting a sync with mocked `INPUT_*` inputs (no network).
298
+ demonstrates starting a sync with mocked `INPUT_*` inputs (no network), proving
299
+ `INPUT_CLEAN` and `INPUT_UPDATE-PARENT-PAGE` reach planning via dry-run.
195
300
 
196
301
  ## Notes
197
302
 
@@ -200,10 +305,9 @@ demonstrates starting a sync with mocked `INPUT_*` inputs (no network).
200
305
  (`/wiki/rest/api/content/{pageId}/child/attachment`) with
201
306
  `X-Atlassian-Token: no-check`, because v2 has no multipart upload contract
202
307
  yet. Page, space, and attachment-list calls use v2.
203
- - Pages are reconciled additively (non-pruning): sync never deletes Confluence
204
- pages or attachments that are absent locally. Each PUT supplies
205
- `version.number = current + 1`, so the server rejects concurrent writes with
206
- HTTP 409.
308
+ - Each PUT supplies `version.number = current + 1`, so the server rejects
309
+ concurrent writes with HTTP 409. See optimistic concurrency in the website
310
+ guide.
207
311
  - All inline HTML output is entity-escaped; the only place raw markup lands in
208
312
  the page body is inside fenced code blocks (where the `]]>` CDATA terminator
209
313
  is neutralized) and, when `--render-html-blocks` is on, inside an