@repo-toolkit/confluence 0.8.0 → 0.11.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 +140 -17
  2. package/cli.js +1515 -243
  3. package/index.d.ts +308 -22
  4. package/index.js +1298 -206
  5. package/package.json +2 -2
package/README.md CHANGED
@@ -7,10 +7,14 @@ images referenced from the markdown are uploaded as Confluence attachments and
7
7
  inline-rendered as `<ac:image><ri:attachment />` macros; remote images stay as
8
8
  `<ac:image><ri:url />`.
9
9
 
10
- The package is GitHub-Action compatible: when invoked with no CLI flags it reads
11
- `INPUT_*` environment variables (the same shape as the `Bhacaz/docs-as-code-confluence`
12
- action's `action.yml`), so you can drop it into a `node20` action or run it as a
13
- standalone CLI.
10
+ The package is GitHub-Action compatible: when invoked with no CLI flags it
11
+ reads `INPUT_*` environment variables (the same shape as the
12
+ `Bhacaz/docs-as-code-confluence` action's `action.yml`), so you can drop it
13
+ into a `node20` action or run it as a standalone CLI.
14
+
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>.
14
18
 
15
19
  ## Installation
16
20
 
@@ -18,18 +22,42 @@ standalone CLI.
18
22
  pnpm add -D @repo-toolkit/confluence
19
23
  ```
20
24
 
25
+ ## Configuration precedence
26
+
27
+ Every option is resolved independently in this order, with later sources
28
+ overriding earlier ones only for the option they supply:
29
+
30
+ 1. CLI flag for that option
31
+ 2. `--config` file value for that option
32
+ 3. `CONFLUENCE_*` environment variable for that option (higher specificity)
33
+ 4. `INPUT_*` environment variable for that option (GitHub Actions form)
34
+ 5. built-in default
35
+
36
+ Supplying `--api-token` or any other flag does **not** disable environment
37
+ resolution for the options you left unset. This means you can keep credentials
38
+ in the environment and still override, say, `--folder` on the command line.
39
+
40
+ For secret-free required fields (`folder`, `username`, `baseUrl`, `spaceKey`,
41
+ `parentPageId`) the CLI can prompt on a TTY when `--interactive` is set and
42
+ `--dry-run` is not. The API token is **never** prompted interactively (it
43
+ would echo to the terminal); provide it via a secret file or an environment
44
+ variable instead.
45
+
21
46
  ## CLI
22
47
 
23
48
  ```sh
24
49
  repo-toolkit-confluence \
25
50
  --folder docs \
26
51
  --username user@example.com \
27
- --api-token $API_TOKEN \
52
+ --api-token-file /run/secrets/confluence_token \
28
53
  --confluence-base-url https://mydomain.atlassian.net/wiki \
29
54
  --space-key ENG \
30
55
  --parent-page-id 123456789
31
56
  ```
32
57
 
58
+ Prefer `--api-token-file` (or `CONFLUENCE_API_TOKEN_FILE`) over `--api-token`
59
+ to keep the token out of argv / process listings.
60
+
33
61
  Flags:
34
62
 
35
63
  - `--config <path>` — JSON/`.mjs`/`.cjs` config file with any of the options below
@@ -37,13 +65,48 @@ Flags:
37
65
  - `--folder <path>` — documentation folder (required)
38
66
  - `--username <value>` — Confluence username/email (required)
39
67
  - `--api-token <value>` — Confluence API token (required; alias: `--password`)
68
+ - `--api-token-file <path>` — file whose contents become the API token; a
69
+ single trailing newline and surrounding whitespace are stripped (alias:
70
+ `--password-file`). Resolved relative to `--cwd`.
40
71
  - `--confluence-base-url <url>` — URL with `/wiki` (required; alias: `--base-url`)
41
72
  - `--space-key <key>` — Confluence space key (required; resolved to a `spaceId`)
42
73
  - `--parent-page-id <id>` — numeric Confluence page id (required)
43
74
  - `--version-message <text>` — version-message suffix appended to every PUT
44
75
  - `--skip-unchanged` / `--no-skip-unchanged` — skip pages whose body is unchanged (default: `skip`)
45
- - `--dry-run` — walk the doc tree and log the plan, no API calls
46
- - `-i, --interactive` reserved (no interactive prompts yet)
76
+ - `--dry-run` — walk the doc tree and validate every markdown file and local
77
+ image source (same preflight as a real sync) then log the plan. No API
78
+ mutation calls; credentials are not required under `--dry-run`.
79
+ - `--render-html-blocks` — render ` ```html ` fenced blocks as inline HTML via
80
+ the Confluence `html` macro instead of a code box (default: `false`). **Unsafe
81
+ for untrusted Markdown** — see the raw HTML section of the website guide.
82
+ - `-i, --interactive` — prompt interactively (on a real TTY) for missing
83
+ non-secret required fields. The API token is never prompted.
84
+
85
+ ### Environment variables
86
+
87
+ `CONFLUENCE_*` (higher specificity) and the `INPUT_*` GitHub Actions form are
88
+ both read for every option. Boolean env values accept `true|1|yes|on` /
89
+ `false|0|no|off` (empty string is falsy); any other value exits nonzero with
90
+ `Invalid boolean value for <ENV_NAME>: <raw>`.
91
+
92
+ | Option | `CONFLUENCE_*` | `INPUT_*` (Actions form) |
93
+ | ----------------------- | ------------------------------- | --------------------------------------------- |
94
+ | folder | `CONFLUENCE_FOLDER` | `INPUT_FOLDER` |
95
+ | username | `CONFLUENCE_USERNAME` | `INPUT_USERNAME` |
96
+ | apiToken | `CONFLUENCE_API_TOKEN` | `INPUT_API-TOKEN`, `INPUT_PASSWORD` |
97
+ | apiTokenFile | `CONFLUENCE_API_TOKEN_FILE` | `INPUT_API-TOKEN-FILE`, `INPUT_PASSWORD-FILE` |
98
+ | baseUrl | `CONFLUENCE_BASE_URL` | `INPUT_CONFLUENCE-BASE-URL` |
99
+ | spaceKey | `CONFLUENCE_SPACE_KEY` | `INPUT_SPACE-KEY` |
100
+ | parentPageId | `CONFLUENCE_PARENT_PAGE_ID` | `INPUT_PARENT-PAGE-ID` |
101
+ | versionMessage | `CONFLUENCE_VERSION_MESSAGE` | `INPUT_VERSION-MESSAGE` |
102
+ | skipUnchanged (bool) | `CONFLUENCE_SKIP_UNCHANGED` | `INPUT_SKIP-UNCHANGED` |
103
+ | dryRun (bool) | `CONFLUENCE_DRY_RUN` | `INPUT_DRY-RUN` |
104
+ | renderHtmlBlocks (bool) | `CONFLUENCE_RENDER_HTML_BLOCKS` | `INPUT_RENDER-HTML-BLOCKS` |
105
+
106
+ Errors, `--help`, and all log lines never print the supplied token value. The
107
+ secret-file loader wraps fs errors via `Error.cause` (`Failed to read
108
+ apiTokenFile at <path>`, `apiTokenFile at <path> is empty`) without revealing
109
+ the file contents.
47
110
 
48
111
  ## JavaScript API
49
112
 
@@ -53,7 +116,7 @@ import { syncConfluenceToDocs } from '@repo-toolkit/confluence';
53
116
  await syncConfluenceToDocs({
54
117
  folder: 'docs',
55
118
  username: 'user@example.com',
56
- apiToken: process.env.API_TOKEN!,
119
+ apiToken: process.env.CONFLUENCE_API_TOKEN!,
57
120
  baseUrl: 'https://mydomain.atlassian.net/wiki',
58
121
  spaceKey: 'ENG',
59
122
  parentPageId: '123456789',
@@ -61,25 +124,70 @@ await syncConfluenceToDocs({
61
124
  });
62
125
  ```
63
126
 
127
+ `resolveConfluenceSyncPlan(options)` resolves the same plan without starting a
128
+ sync; it is the documented way to validate options up front.
129
+
130
+ `syncConfluenceToDocs` runs a **local preflight** before any remote mutation:
131
+ every markdown file is read and converted to storage HTML, and every local
132
+ image source is validated against the same root-confined, regular-file, and
133
+ size checks the upload path applies. On the first defect it throws an
134
+ `LocalSyncValidationAggregateError` listing every failing entry (zero API
135
+ calls). On a remote failure mid-run it throws a `SyncMutationError` carrying
136
+ `.changes` (pages that succeeded), `.failure` (the failing entry + its error),
137
+ and `.unprocessed` (remaining entries). On success it returns a
138
+ `SyncResult` whose `.changes` lists every created/updated/unchanged page.
139
+ `validateLocalSync(entries, plan)` runs the same local preflight standalone —
140
+ useful for CI gates or `--dry-run`-equivalent validation. Leaf pages with no
141
+ attachment-bearing placeholders are created in a single POST with their final
142
+ rendered body; pages with local images or Mermaid blocks are created first and
143
+ have attachments uploaded against their page id, then their body is PUT.
144
+
145
+ `syncConfluenceToDocs` and the lower-level rewriters depend only on the narrow
146
+ `ConfluenceGateway` and `AttachmentGateway` interfaces (which
147
+ `ConfluenceClient` implements). Supply your own gateway object as `client` and
148
+ the bundled HTTP `username`/`apiToken`/`baseUrl` are no longer required — the
149
+ gateway owns all remote work. Typed fakes implementing the interface are
150
+ accepted by `syncConfluenceToDocs({ client })` without `as unknown` casts.
151
+ `spaceKey` and `parentPageId` remain required even with a custom gateway.
152
+
64
153
  ## GitHub Action usage
65
154
 
66
- The CLI auto-detects the GitHub Actions inputs environment when no flags are
67
- supplied. Build and bundle this package (e.g. with `@vercel/ncc`) and ship the
68
- resulting `dist/index.js` as the action entrypoint. Example `action.yml`:
155
+ The CLI auto-detects the GitHub Actions `INPUT_*` environment when no flags are
156
+ supplied. Bundle the **CLI entrypoint** (`src/cli.ts`) not the library entry
157
+ (`src/index.ts`) — and ship the resulting `dist/cli.js` as the action
158
+ entrypoint. With `tsup` this is the second entry in `tsup.config.ts`, which is
159
+ emitted separately with a `#!/usr/bin/env node` banner.
160
+
161
+ With `@vercel/ncc`:
162
+
163
+ ```sh
164
+ ncc build packages/confluence/src/cli.ts -o action-dist
165
+ # ships action-dist/index.js — reference it as the action main
166
+ ```
167
+
168
+ Example `action.yml`:
69
169
 
70
170
  ```yaml
71
171
  runs:
72
172
  using: 'node20'
73
- main: 'dist/index.js'
173
+ main: 'action-dist/index.js'
74
174
  inputs:
75
175
  folder: { required: true }
76
176
  username: { required: true }
77
177
  password: { required: true }
178
+ password-file: { required: false }
78
179
  confluence-base-url: { required: true }
79
180
  space-key: { required: true }
80
181
  parent-page-id: { required: true }
182
+ version-message: { required: false }
183
+ dry-run: { required: false, default: 'false' }
184
+ skip-unchanged: { required: false, default: 'true' }
185
+ render-html-blocks: { required: false, default: 'false' }
81
186
  ```
82
187
 
188
+ A runnable smoke fixture lives under `packages/confluence/action-fixture/` and
189
+ demonstrates starting a sync with mocked `INPUT_*` inputs (no network).
190
+
83
191
  ## Notes
84
192
 
85
193
  - Authentication uses HTTP Basic with an API token (not your account password).
@@ -87,9 +195,24 @@ inputs:
87
195
  (`/wiki/rest/api/content/{pageId}/child/attachment`) with
88
196
  `X-Atlassian-Token: no-check`, because v2 has no multipart upload contract
89
197
  yet. Page, space, and attachment-list calls use v2.
90
- - Pages are reconciled without race conditions: each PUT supplies
91
- `version.number = current + 1` so the server rejects concurrent writes with
198
+ - Pages are reconciled additively (non-pruning): sync never deletes Confluence
199
+ pages or attachments that are absent locally. Each PUT supplies
200
+ `version.number = current + 1`, so the server rejects concurrent writes with
92
201
  HTTP 409.
93
- - All HTML output is entity-escaped; the only place raw markup lands in the
94
- page body is inside fenced code blocks, where the `]]>` CDATA terminator is
95
- neutralized before publishing.
202
+ - All inline HTML output is entity-escaped; the only place raw markup lands in
203
+ the page body is inside fenced code blocks (where the `]]>` CDATA terminator
204
+ is neutralized) and, when `--render-html-blocks` is on, inside an
205
+ `ac:structured-macro ac:name="html"` body. The latter is an active-content
206
+ boundary — do not feed untrusted Markdown.
207
+ - Inline rendering is a single-pass tokenizer: inline code spans are protected
208
+ first (their contents are never parsed as Markdown or re-escaped), then
209
+ links/images, emphasis, and backslash escapes are resolved against the raw
210
+ text and emitted as escaped XML exactly once. Links/images parse URLs across
211
+ balanced nested parentheses and validate against an **allowlist**
212
+ (`http`/`https`/`mailto`/`tel` + scheme-less relative); other schemes
213
+ (`javascript:`/`data:`/`file:`/`vbscript:`/custom) collapse to their
214
+ label/alt text. C0/DEL control characters and percent-encoded blocked
215
+ schemes are rejected.
216
+ - The Markdown subset (CommonMark flanking emphasis, intraword `*` vs
217
+ non-intraword `_`, nested emphasis/links, backslash escapes) and Mermaid
218
+ rendering requirements/fallback are documented in the website guide.