mkdocs2confluence 0.7.38__tar.gz → 0.8.1__tar.gz

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 (84) hide show
  1. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/PKG-INFO +73 -1
  2. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/README.md +72 -0
  3. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/pyproject.toml +1 -1
  4. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs2confluence.egg-info/PKG-INFO +73 -1
  5. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs2confluence.egg-info/SOURCES.txt +12 -0
  6. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/cli.py +123 -0
  7. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/loader/config.py +7 -0
  8. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/publisher/client.py +82 -0
  9. mkdocs2confluence-0.8.1/src/mkdocs_to_confluence/sync/__init__.py +1 -0
  10. mkdocs2confluence-0.8.1/src/mkdocs_to_confluence/sync/anchoring.py +25 -0
  11. mkdocs2confluence-0.8.1/src/mkdocs_to_confluence/sync/command.py +236 -0
  12. mkdocs2confluence-0.8.1/src/mkdocs_to_confluence/sync/comments.py +89 -0
  13. mkdocs2confluence-0.8.1/src/mkdocs_to_confluence/sync/github.py +143 -0
  14. mkdocs2confluence-0.8.1/src/mkdocs_to_confluence/sync/platform.py +50 -0
  15. mkdocs2confluence-0.8.1/src/mkdocs_to_confluence/sync/state.py +45 -0
  16. mkdocs2confluence-0.8.1/tests/test_sync_anchoring.py +56 -0
  17. mkdocs2confluence-0.8.1/tests/test_sync_command.py +637 -0
  18. mkdocs2confluence-0.8.1/tests/test_sync_comments.py +137 -0
  19. mkdocs2confluence-0.8.1/tests/test_sync_github.py +212 -0
  20. mkdocs2confluence-0.8.1/tests/test_sync_state.py +75 -0
  21. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/LICENSE +0 -0
  22. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/setup.cfg +0 -0
  23. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  24. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  25. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  26. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  27. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/__init__.py +0 -0
  28. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  29. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
  30. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  31. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/ir/document.py +0 -0
  32. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  33. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  34. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  35. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  36. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  37. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/loader/page.py +0 -0
  38. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  39. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
  40. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  41. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  42. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  43. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  44. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  45. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  46. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  47. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  48. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  49. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  50. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  51. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preview/render.py +0 -0
  52. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/preview/server.py +0 -0
  53. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  54. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/publisher/pipeline.py +0 -0
  55. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  56. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  57. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  58. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  59. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  60. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  61. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  62. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_abbrevs.py +0 -0
  63. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_cli.py +0 -0
  64. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_editlink.py +0 -0
  65. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_emitter.py +0 -0
  66. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_extra_css.py +0 -0
  67. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_frontmatter.py +0 -0
  68. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_icons.py +0 -0
  69. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_images.py +0 -0
  70. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_internallinks.py +0 -0
  71. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_ir.py +0 -0
  72. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_linkdefs.py +0 -0
  73. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_loader.py +0 -0
  74. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_mermaid.py +0 -0
  75. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_page_loader.py +0 -0
  76. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_parser.py +0 -0
  77. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_pdf.py +0 -0
  78. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_preprocess.py +0 -0
  79. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_preview.py +0 -0
  80. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_publish_client.py +0 -0
  81. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_publish_config.py +0 -0
  82. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_publish_pipeline.py +0 -0
  83. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_server.py +0 -0
  84. {mkdocs2confluence-0.7.38 → mkdocs2confluence-0.8.1}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.7.38
3
+ Version: 0.8.1
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -54,6 +54,8 @@ Dynamic: license-file
54
54
 
55
55
  A Python CLI tool that compiles MkDocs-flavoured Markdown into native Confluence storage XHTML and publishes it directly to Confluence Cloud. It is a **compiler/transpiler**, not an HTML converter — every construct maps to its native Confluence equivalent, so pages look and behave like hand-authored Confluence content.
56
56
 
57
+ It also bridges the gap between Confluence reviewers and developers: the `sync-comments` command turns open Confluence page comments into GitHub pull request review threads, and auto-resolves them in Confluence when the PR is merged.
58
+
57
59
  > **Zensical compatible** — [Zensical](https://zensical.org/) is the modern successor to MkDocs + Material for MkDocs. Since it uses the same `mkdocs.yml` format and Python Markdown extensions, your Zensical project works with mk2conf today with no changes required.
58
60
 
59
61
  ---
@@ -109,6 +111,12 @@ mk2conf publish
109
111
 
110
112
  # Export a nav section to a stand-alone PDF document
111
113
  mk2conf pdf --section Guide --out guide.pdf
114
+
115
+ # Sync open Confluence comments to GitHub review PRs
116
+ mk2conf sync-comments
117
+
118
+ # Resolve Confluence comments when their review PRs are merged
119
+ mk2conf sync-comments --check-merges
112
120
  ```
113
121
 
114
122
  ---
@@ -128,12 +136,32 @@ confluence:
128
136
  full_width: true # default: true
129
137
  ```
130
138
 
139
+ The `confluence:` block is also accepted under `extra:` (MkDocs strict-mode compatible):
140
+
141
+ ```yaml
142
+ extra:
143
+ confluence:
144
+ base_url: https://yourorg.atlassian.net
145
+ space_key: TECH
146
+ ...
147
+ ```
148
+
131
149
  The API token is read from (in priority order):
132
150
 
133
151
  1. `token:` in `mkdocs.yml` — typically via `!ENV CONFLUENCE_API_TOKEN`
134
152
  2. `CONFLUENCE_API_TOKEN` environment variable
135
153
  3. `MK2CONF_TOKEN` environment variable
136
154
 
155
+ ### Additional fields for `sync-comments`
156
+
157
+ ```yaml
158
+ confluence:
159
+ # ... base fields above ...
160
+ github_repo: owner/repo # required for sync-comments
161
+ github_token: !ENV GITHUB_TOKEN # falls back to GITHUB_TOKEN env var
162
+ github_base_branch: main # default: main
163
+ ```
164
+
137
165
  ---
138
166
 
139
167
  ## Your first publish
@@ -270,6 +298,48 @@ The PDF includes a **cover page**, **table of contents** with page numbers, and
270
298
 
271
299
  ---
272
300
 
301
+ ### `mk2conf sync-comments`
302
+
303
+ Bridge Confluence page/inline comments to GitHub pull request review threads. Non-technical reviewers comment in Confluence; developers address feedback on a GitHub feature branch; Confluence comments are auto-resolved when the PR is merged.
304
+
305
+ ```
306
+ mk2conf sync-comments [--config PATH] [--check-merges] [--force] [--dry-run] [--quiet]
307
+ ```
308
+
309
+ | Flag | Default | Description |
310
+ |---|---|---|
311
+ | `--config PATH` | `./mkdocs.yml` | Path to `mkdocs.yml` |
312
+ | `--check-merges` | off | Check tracked PRs for merges and auto-resolve Confluence comments |
313
+ | `--force` | off | Re-sync pages that already have an open review PR |
314
+ | `--dry-run` | off | Print what would be synced without making any API calls |
315
+ | `--quiet` | off | Suppress progress output |
316
+
317
+ **Required config** (add to the `confluence:` block in `mkdocs.yml`):
318
+
319
+ ```yaml
320
+ confluence:
321
+ # ... base fields ...
322
+ github_repo: owner/repo # required for sync-comments
323
+ github_token: !ENV GITHUB_TOKEN # falls back to GITHUB_TOKEN env var
324
+ github_base_branch: main # default: main
325
+ ```
326
+
327
+ **Workflow:**
328
+
329
+ 1. Run `mk2conf publish` first — generates `.mk2conf-pages.json` mapping source files to Confluence page IDs.
330
+ 2. Run `mk2conf sync-comments` — for each page with open Confluence comments, creates a `mk2conf/review/{slug}` branch and PR, then posts each comment as a GitHub review thread. Inline comments with a text selection are anchored to the matching source line; page-level comments fall back to file-level review threads. Every thread body includes a **View in Confluence** deep-link that opens Confluence focused on the exact comment.
331
+ 3. Developer addresses feedback on the branch, pushes changes, and merges the PR.
332
+ 4. Run `mk2conf sync-comments --check-merges` — detects merged PRs, adds a resolution reply to each Confluence comment with the commit info, and marks the comments as resolved.
333
+
334
+ **State files** (add to `.gitignore`):
335
+
336
+ | File | Purpose |
337
+ |---|---|
338
+ | `.mk2conf-pages.json` | Source path → Confluence page ID map, written after each `publish` |
339
+ | `.mk2conf-sync-state.json` | Tracks open/merged review PRs and their associated comment IDs |
340
+
341
+ ---
342
+
273
343
  ## Supported Markdown features
274
344
 
275
345
  ### Block elements
@@ -379,6 +449,8 @@ MkDocs abbreviation definitions (`*[ABBR]: Full term`) are rendered as inline su
379
449
 
380
450
  Each stage is a separate Python module under `src/mkdocs_to_confluence/`. The **plan** phase makes all API read calls (find existing pages); the **execute** phase makes all write calls, ensuring parent pages always exist before their children.
381
451
 
452
+ The `sync/` package is a self-contained pipeline for the `sync-comments` command: it fetches Confluence comments, anchors them to source lines, posts them as GitHub review threads via GraphQL, and resolves them on PR merge. The `ReviewPlatformClient` Protocol keeps GitHub-specific code isolated so future GitLab or Azure DevOps adapters slot in without touching the core.
453
+
382
454
  ---
383
455
 
384
456
  ## Development
@@ -14,6 +14,8 @@
14
14
 
15
15
  A Python CLI tool that compiles MkDocs-flavoured Markdown into native Confluence storage XHTML and publishes it directly to Confluence Cloud. It is a **compiler/transpiler**, not an HTML converter — every construct maps to its native Confluence equivalent, so pages look and behave like hand-authored Confluence content.
16
16
 
17
+ It also bridges the gap between Confluence reviewers and developers: the `sync-comments` command turns open Confluence page comments into GitHub pull request review threads, and auto-resolves them in Confluence when the PR is merged.
18
+
17
19
  > **Zensical compatible** — [Zensical](https://zensical.org/) is the modern successor to MkDocs + Material for MkDocs. Since it uses the same `mkdocs.yml` format and Python Markdown extensions, your Zensical project works with mk2conf today with no changes required.
18
20
 
19
21
  ---
@@ -69,6 +71,12 @@ mk2conf publish
69
71
 
70
72
  # Export a nav section to a stand-alone PDF document
71
73
  mk2conf pdf --section Guide --out guide.pdf
74
+
75
+ # Sync open Confluence comments to GitHub review PRs
76
+ mk2conf sync-comments
77
+
78
+ # Resolve Confluence comments when their review PRs are merged
79
+ mk2conf sync-comments --check-merges
72
80
  ```
73
81
 
74
82
  ---
@@ -88,12 +96,32 @@ confluence:
88
96
  full_width: true # default: true
89
97
  ```
90
98
 
99
+ The `confluence:` block is also accepted under `extra:` (MkDocs strict-mode compatible):
100
+
101
+ ```yaml
102
+ extra:
103
+ confluence:
104
+ base_url: https://yourorg.atlassian.net
105
+ space_key: TECH
106
+ ...
107
+ ```
108
+
91
109
  The API token is read from (in priority order):
92
110
 
93
111
  1. `token:` in `mkdocs.yml` — typically via `!ENV CONFLUENCE_API_TOKEN`
94
112
  2. `CONFLUENCE_API_TOKEN` environment variable
95
113
  3. `MK2CONF_TOKEN` environment variable
96
114
 
115
+ ### Additional fields for `sync-comments`
116
+
117
+ ```yaml
118
+ confluence:
119
+ # ... base fields above ...
120
+ github_repo: owner/repo # required for sync-comments
121
+ github_token: !ENV GITHUB_TOKEN # falls back to GITHUB_TOKEN env var
122
+ github_base_branch: main # default: main
123
+ ```
124
+
97
125
  ---
98
126
 
99
127
  ## Your first publish
@@ -230,6 +258,48 @@ The PDF includes a **cover page**, **table of contents** with page numbers, and
230
258
 
231
259
  ---
232
260
 
261
+ ### `mk2conf sync-comments`
262
+
263
+ Bridge Confluence page/inline comments to GitHub pull request review threads. Non-technical reviewers comment in Confluence; developers address feedback on a GitHub feature branch; Confluence comments are auto-resolved when the PR is merged.
264
+
265
+ ```
266
+ mk2conf sync-comments [--config PATH] [--check-merges] [--force] [--dry-run] [--quiet]
267
+ ```
268
+
269
+ | Flag | Default | Description |
270
+ |---|---|---|
271
+ | `--config PATH` | `./mkdocs.yml` | Path to `mkdocs.yml` |
272
+ | `--check-merges` | off | Check tracked PRs for merges and auto-resolve Confluence comments |
273
+ | `--force` | off | Re-sync pages that already have an open review PR |
274
+ | `--dry-run` | off | Print what would be synced without making any API calls |
275
+ | `--quiet` | off | Suppress progress output |
276
+
277
+ **Required config** (add to the `confluence:` block in `mkdocs.yml`):
278
+
279
+ ```yaml
280
+ confluence:
281
+ # ... base fields ...
282
+ github_repo: owner/repo # required for sync-comments
283
+ github_token: !ENV GITHUB_TOKEN # falls back to GITHUB_TOKEN env var
284
+ github_base_branch: main # default: main
285
+ ```
286
+
287
+ **Workflow:**
288
+
289
+ 1. Run `mk2conf publish` first — generates `.mk2conf-pages.json` mapping source files to Confluence page IDs.
290
+ 2. Run `mk2conf sync-comments` — for each page with open Confluence comments, creates a `mk2conf/review/{slug}` branch and PR, then posts each comment as a GitHub review thread. Inline comments with a text selection are anchored to the matching source line; page-level comments fall back to file-level review threads. Every thread body includes a **View in Confluence** deep-link that opens Confluence focused on the exact comment.
291
+ 3. Developer addresses feedback on the branch, pushes changes, and merges the PR.
292
+ 4. Run `mk2conf sync-comments --check-merges` — detects merged PRs, adds a resolution reply to each Confluence comment with the commit info, and marks the comments as resolved.
293
+
294
+ **State files** (add to `.gitignore`):
295
+
296
+ | File | Purpose |
297
+ |---|---|
298
+ | `.mk2conf-pages.json` | Source path → Confluence page ID map, written after each `publish` |
299
+ | `.mk2conf-sync-state.json` | Tracks open/merged review PRs and their associated comment IDs |
300
+
301
+ ---
302
+
233
303
  ## Supported Markdown features
234
304
 
235
305
  ### Block elements
@@ -339,6 +409,8 @@ MkDocs abbreviation definitions (`*[ABBR]: Full term`) are rendered as inline su
339
409
 
340
410
  Each stage is a separate Python module under `src/mkdocs_to_confluence/`. The **plan** phase makes all API read calls (find existing pages); the **execute** phase makes all write calls, ensuring parent pages always exist before their children.
341
411
 
412
+ The `sync/` package is a self-contained pipeline for the `sync-comments` command: it fetches Confluence comments, anchors them to source lines, posts them as GitHub review threads via GraphQL, and resolves them on PR merge. The `ReviewPlatformClient` Protocol keeps GitHub-specific code isolated so future GitLab or Azure DevOps adapters slot in without touching the core.
413
+
342
414
  ---
343
415
 
344
416
  ## Development
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.7.38"
3
+ version = "0.8.1"
4
4
  description = "Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more"
5
5
  readme = "README.md"
6
6
  license = { text = "GPL-3.0-or-later" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.7.38
3
+ Version: 0.8.1
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -54,6 +54,8 @@ Dynamic: license-file
54
54
 
55
55
  A Python CLI tool that compiles MkDocs-flavoured Markdown into native Confluence storage XHTML and publishes it directly to Confluence Cloud. It is a **compiler/transpiler**, not an HTML converter — every construct maps to its native Confluence equivalent, so pages look and behave like hand-authored Confluence content.
56
56
 
57
+ It also bridges the gap between Confluence reviewers and developers: the `sync-comments` command turns open Confluence page comments into GitHub pull request review threads, and auto-resolves them in Confluence when the PR is merged.
58
+
57
59
  > **Zensical compatible** — [Zensical](https://zensical.org/) is the modern successor to MkDocs + Material for MkDocs. Since it uses the same `mkdocs.yml` format and Python Markdown extensions, your Zensical project works with mk2conf today with no changes required.
58
60
 
59
61
  ---
@@ -109,6 +111,12 @@ mk2conf publish
109
111
 
110
112
  # Export a nav section to a stand-alone PDF document
111
113
  mk2conf pdf --section Guide --out guide.pdf
114
+
115
+ # Sync open Confluence comments to GitHub review PRs
116
+ mk2conf sync-comments
117
+
118
+ # Resolve Confluence comments when their review PRs are merged
119
+ mk2conf sync-comments --check-merges
112
120
  ```
113
121
 
114
122
  ---
@@ -128,12 +136,32 @@ confluence:
128
136
  full_width: true # default: true
129
137
  ```
130
138
 
139
+ The `confluence:` block is also accepted under `extra:` (MkDocs strict-mode compatible):
140
+
141
+ ```yaml
142
+ extra:
143
+ confluence:
144
+ base_url: https://yourorg.atlassian.net
145
+ space_key: TECH
146
+ ...
147
+ ```
148
+
131
149
  The API token is read from (in priority order):
132
150
 
133
151
  1. `token:` in `mkdocs.yml` — typically via `!ENV CONFLUENCE_API_TOKEN`
134
152
  2. `CONFLUENCE_API_TOKEN` environment variable
135
153
  3. `MK2CONF_TOKEN` environment variable
136
154
 
155
+ ### Additional fields for `sync-comments`
156
+
157
+ ```yaml
158
+ confluence:
159
+ # ... base fields above ...
160
+ github_repo: owner/repo # required for sync-comments
161
+ github_token: !ENV GITHUB_TOKEN # falls back to GITHUB_TOKEN env var
162
+ github_base_branch: main # default: main
163
+ ```
164
+
137
165
  ---
138
166
 
139
167
  ## Your first publish
@@ -270,6 +298,48 @@ The PDF includes a **cover page**, **table of contents** with page numbers, and
270
298
 
271
299
  ---
272
300
 
301
+ ### `mk2conf sync-comments`
302
+
303
+ Bridge Confluence page/inline comments to GitHub pull request review threads. Non-technical reviewers comment in Confluence; developers address feedback on a GitHub feature branch; Confluence comments are auto-resolved when the PR is merged.
304
+
305
+ ```
306
+ mk2conf sync-comments [--config PATH] [--check-merges] [--force] [--dry-run] [--quiet]
307
+ ```
308
+
309
+ | Flag | Default | Description |
310
+ |---|---|---|
311
+ | `--config PATH` | `./mkdocs.yml` | Path to `mkdocs.yml` |
312
+ | `--check-merges` | off | Check tracked PRs for merges and auto-resolve Confluence comments |
313
+ | `--force` | off | Re-sync pages that already have an open review PR |
314
+ | `--dry-run` | off | Print what would be synced without making any API calls |
315
+ | `--quiet` | off | Suppress progress output |
316
+
317
+ **Required config** (add to the `confluence:` block in `mkdocs.yml`):
318
+
319
+ ```yaml
320
+ confluence:
321
+ # ... base fields ...
322
+ github_repo: owner/repo # required for sync-comments
323
+ github_token: !ENV GITHUB_TOKEN # falls back to GITHUB_TOKEN env var
324
+ github_base_branch: main # default: main
325
+ ```
326
+
327
+ **Workflow:**
328
+
329
+ 1. Run `mk2conf publish` first — generates `.mk2conf-pages.json` mapping source files to Confluence page IDs.
330
+ 2. Run `mk2conf sync-comments` — for each page with open Confluence comments, creates a `mk2conf/review/{slug}` branch and PR, then posts each comment as a GitHub review thread. Inline comments with a text selection are anchored to the matching source line; page-level comments fall back to file-level review threads. Every thread body includes a **View in Confluence** deep-link that opens Confluence focused on the exact comment.
331
+ 3. Developer addresses feedback on the branch, pushes changes, and merges the PR.
332
+ 4. Run `mk2conf sync-comments --check-merges` — detects merged PRs, adds a resolution reply to each Confluence comment with the commit info, and marks the comments as resolved.
333
+
334
+ **State files** (add to `.gitignore`):
335
+
336
+ | File | Purpose |
337
+ |---|---|
338
+ | `.mk2conf-pages.json` | Source path → Confluence page ID map, written after each `publish` |
339
+ | `.mk2conf-sync-state.json` | Tracks open/merged review PRs and their associated comment IDs |
340
+
341
+ ---
342
+
273
343
  ## Supported Markdown features
274
344
 
275
345
  ### Block elements
@@ -379,6 +449,8 @@ MkDocs abbreviation definitions (`*[ABBR]: Full term`) are rendered as inline su
379
449
 
380
450
  Each stage is a separate Python module under `src/mkdocs_to_confluence/`. The **plan** phase makes all API read calls (find existing pages); the **execute** phase makes all write calls, ensuring parent pages always exist before their children.
381
451
 
452
+ The `sync/` package is a self-contained pipeline for the `sync-comments` command: it fetches Confluence comments, anchors them to source lines, posts them as GitHub review threads via GraphQL, and resolves them on PR merge. The `ReviewPlatformClient` Protocol keeps GitHub-specific code isolated so future GitLab or Azure DevOps adapters slot in without touching the core.
453
+
382
454
  ---
383
455
 
384
456
  ## Development
@@ -38,6 +38,13 @@ src/mkdocs_to_confluence/preview/server.py
38
38
  src/mkdocs_to_confluence/publisher/__init__.py
39
39
  src/mkdocs_to_confluence/publisher/client.py
40
40
  src/mkdocs_to_confluence/publisher/pipeline.py
41
+ src/mkdocs_to_confluence/sync/__init__.py
42
+ src/mkdocs_to_confluence/sync/anchoring.py
43
+ src/mkdocs_to_confluence/sync/command.py
44
+ src/mkdocs_to_confluence/sync/comments.py
45
+ src/mkdocs_to_confluence/sync/github.py
46
+ src/mkdocs_to_confluence/sync/platform.py
47
+ src/mkdocs_to_confluence/sync/state.py
41
48
  src/mkdocs_to_confluence/transforms/__init__.py
42
49
  src/mkdocs_to_confluence/transforms/abbrevs.py
43
50
  src/mkdocs_to_confluence/transforms/assets.py
@@ -67,4 +74,9 @@ tests/test_publish_client.py
67
74
  tests/test_publish_config.py
68
75
  tests/test_publish_pipeline.py
69
76
  tests/test_server.py
77
+ tests/test_sync_anchoring.py
78
+ tests/test_sync_command.py
79
+ tests/test_sync_comments.py
80
+ tests/test_sync_github.py
81
+ tests/test_sync_state.py
70
82
  tests/test_treeutil.py
@@ -223,6 +223,48 @@ def _build_parser() -> argparse.ArgumentParser:
223
223
  help="Suppress per-item progress output.",
224
224
  )
225
225
 
226
+ # --- sync-comments ---
227
+ sc = sub.add_parser(
228
+ "sync-comments",
229
+ help="Sync open Confluence comments to GitHub review PRs.",
230
+ formatter_class=argparse.RawDescriptionHelpFormatter,
231
+ epilog=(
232
+ "Requires github_repo and github_token (or GITHUB_TOKEN env var) in mkdocs.yml.\n"
233
+ "\n"
234
+ "Examples:\n"
235
+ " mk2conf sync-comments # sync new comments → PRs\n"
236
+ " mk2conf sync-comments --check-merges # resolve merged PRs in Confluence\n"
237
+ " mk2conf sync-comments --dry-run # preview without making changes\n"
238
+ ),
239
+ )
240
+ sc.add_argument(
241
+ "--config",
242
+ metavar="PATH",
243
+ default="mkdocs.yml",
244
+ help="Path to mkdocs.yml (default: ./mkdocs.yml).",
245
+ )
246
+ sc.add_argument(
247
+ "--check-merges",
248
+ action="store_true",
249
+ help="Check tracked PRs for merges and resolve their Confluence comments.",
250
+ )
251
+ sc.add_argument(
252
+ "--force",
253
+ action="store_true",
254
+ help="Re-sync pages that already have an open review PR.",
255
+ )
256
+ sc.add_argument(
257
+ "--dry-run",
258
+ action="store_true",
259
+ help="Print what would be synced without creating branches, PRs, or review threads.",
260
+ )
261
+ sc.add_argument(
262
+ "--quiet",
263
+ "-q",
264
+ action="store_true",
265
+ help="Suppress progress output.",
266
+ )
267
+
226
268
  return parser
227
269
 
228
270
 
@@ -244,6 +286,8 @@ def main(argv: list[str] | None = None) -> None:
244
286
  _cmd_publish(args)
245
287
  elif args.command == "pdf":
246
288
  _cmd_pdf(args)
289
+ elif args.command == "sync-comments":
290
+ _cmd_sync_comments(args)
247
291
  except (ValueError, FileNotFoundError) as exc:
248
292
  print(f"error: {exc}", file=sys.stderr)
249
293
  sys.exit(1)
@@ -503,6 +547,27 @@ def _cmd_publish(args: argparse.Namespace) -> None:
503
547
 
504
548
  print(str(report))
505
549
 
550
+ # Write page map so sync-comments can match source files to Confluence pages.
551
+ if not (getattr(args, "page", None) or getattr(args, "section", None)):
552
+ import json as _json_pm
553
+ try:
554
+ repo_root = config_path.parent
555
+ try:
556
+ docs_rel = config.docs_dir.relative_to(repo_root)
557
+ except ValueError:
558
+ docs_rel = Path("docs")
559
+ page_map = {
560
+ str(docs_rel / action.node.docs_path): action.page_id
561
+ for action in plan
562
+ if action.node.docs_path and action.page_id and not action.is_folder
563
+ }
564
+ pm_path = repo_root / ".mk2conf-pages.json"
565
+ pm_path.write_text(_json_pm.dumps(page_map, indent=2), encoding="utf-8")
566
+ if not getattr(args, "quiet", False):
567
+ print(f"Page map: {len(page_map)} page(s) → {pm_path.name}")
568
+ except Exception as exc:
569
+ print(f" [warn] could not write page map: {exc}", file=sys.stderr)
570
+
506
571
  if getattr(args, "report", None):
507
572
  import json as _json
508
573
 
@@ -612,3 +677,61 @@ def _cmd_pdf(args: argparse.Namespace) -> None:
612
677
  sys.exit(1)
613
678
 
614
679
  print(f"PDF written to {out_path}")
680
+
681
+
682
+ def _cmd_sync_comments(args: argparse.Namespace) -> None:
683
+ from mkdocs_to_confluence.publisher.client import ConfluenceClient, ConfluenceError
684
+ from mkdocs_to_confluence.sync.command import check_and_resolve_merges, run_sync_comments
685
+ from mkdocs_to_confluence.sync.github import GitHubReviewClient
686
+
687
+ config_path = Path(args.config).resolve()
688
+ config = load_config(config_path)
689
+
690
+ conf = config.confluence
691
+ if conf is None:
692
+ print("error: no 'confluence:' section in mkdocs.yml", file=sys.stderr)
693
+ sys.exit(1)
694
+
695
+ if not conf.token:
696
+ print("error: Confluence API token not set. Set CONFLUENCE_API_TOKEN env var.", file=sys.stderr)
697
+ sys.exit(1)
698
+
699
+ if not conf.github_repo:
700
+ print(
701
+ "error: 'confluence.github_repo' is required for sync-comments (e.g. 'owner/repo').",
702
+ file=sys.stderr,
703
+ )
704
+ sys.exit(1)
705
+
706
+ if not conf.github_token:
707
+ print(
708
+ "error: GitHub token not set. Set GITHUB_TOKEN env var or 'confluence.github_token' in mkdocs.yml.",
709
+ file=sys.stderr,
710
+ )
711
+ sys.exit(1)
712
+
713
+ review_client = GitHubReviewClient(conf.github_repo, conf.github_token)
714
+ config_dir = config_path.parent
715
+
716
+ try:
717
+ with ConfluenceClient(conf) as confluence_client:
718
+ if args.check_merges:
719
+ check_and_resolve_merges(
720
+ config_dir=config_dir,
721
+ confluence_client=confluence_client,
722
+ review_client=review_client,
723
+ quiet=args.quiet,
724
+ )
725
+ else:
726
+ run_sync_comments(
727
+ config=config,
728
+ config_dir=config_dir,
729
+ confluence_client=confluence_client,
730
+ review_client=review_client,
731
+ force=args.force,
732
+ dry_run=args.dry_run,
733
+ quiet=args.quiet,
734
+ )
735
+ except ConfluenceError as exc:
736
+ print(f"error: {exc}", file=sys.stderr)
737
+ sys.exit(1)
@@ -30,6 +30,9 @@ class ConfluenceConfig:
30
30
  full_width: bool = True # set full-width layout on every published page
31
31
  nav_file: str = ".pages" # awesome-pages filename (default: .pages, can be .nav etc.)
32
32
  mermaid_render: str = "kroki" # "kroki", "kroki:<url>", or "none"
33
+ github_repo: str | None = None # "owner/repo" — required for sync-comments
34
+ github_token: str | None = None # GitHub PAT (falls back to GITHUB_TOKEN env var)
35
+ github_base_branch: str = "main" # base branch for review PRs
33
36
 
34
37
 
35
38
  @dataclass(frozen=True)
@@ -241,6 +244,10 @@ def load_config(mkdocs_yml: Path) -> MkDocsConfig:
241
244
  full_width=bool(raw_conf.get("full_width", True)),
242
245
  nav_file=str(raw_conf.get("nav_file", ".pages")),
243
246
  mermaid_render=str(raw_conf.get("mermaid_render", "kroki")),
247
+ github_repo=str(raw_conf["github_repo"]) if raw_conf.get("github_repo") else None,
248
+ github_token=(str(raw_conf["github_token"]) if raw_conf.get("github_token")
249
+ else os.environ.get("GITHUB_TOKEN") or None),
250
+ github_base_branch=str(raw_conf.get("github_base_branch", "main")),
244
251
  )
245
252
 
246
253
  # --- extra_css (optional) ---
@@ -538,3 +538,85 @@ class ConfluenceClient:
538
538
  """Permanently delete *page_id* from Confluence."""
539
539
  resp = self._http.delete(self._v2(f"/pages/{page_id}"))
540
540
  self._raise_for_status(resp, f"delete_page({page_id!r})")
541
+
542
+ # ── Comments ───────────────────────────────────────────────────────────────
543
+
544
+ def get_page_inline_comments(self, page_id: str) -> list[dict[str, Any]]:
545
+ """Return all open inline comments for *page_id* (paginated)."""
546
+ results: list[dict[str, Any]] = []
547
+ url = self._v2(f"/pages/{page_id}/inline-comments")
548
+ params: dict[str, Any] = {"resolution-status": "open", "body-format": "storage", "limit": 250}
549
+ while True:
550
+ resp = self._http.get(url, params=params)
551
+ self._raise_for_status(resp, f"get_page_inline_comments({page_id!r})")
552
+ data = resp.json()
553
+ results.extend(data.get("results", []))
554
+ next_url = data.get("_links", {}).get("next")
555
+ if not next_url:
556
+ break
557
+ url = self._v2(f"/pages/{page_id}/inline-comments")
558
+ params = {"resolution-status": "open", "body-format": "storage",
559
+ "limit": 250, "cursor": _extract_cursor(next_url)}
560
+ return results
561
+
562
+ def get_page_footer_comments(self, page_id: str) -> list[dict[str, Any]]:
563
+ """Return all open footer comments for *page_id* (paginated)."""
564
+ results: list[dict[str, Any]] = []
565
+ url = self._v2(f"/pages/{page_id}/footer-comments")
566
+ params: dict[str, Any] = {"resolution-status": "open", "body-format": "storage", "limit": 250}
567
+ while True:
568
+ resp = self._http.get(url, params=params)
569
+ self._raise_for_status(resp, f"get_page_footer_comments({page_id!r})")
570
+ data = resp.json()
571
+ results.extend(data.get("results", []))
572
+ next_url = data.get("_links", {}).get("next")
573
+ if not next_url:
574
+ break
575
+ url = self._v2(f"/pages/{page_id}/footer-comments")
576
+ params = {"resolution-status": "open", "body-format": "storage",
577
+ "limit": 250, "cursor": _extract_cursor(next_url)}
578
+ return results
579
+
580
+ def add_comment_reply(self, comment_id: str, reply_text: str) -> None:
581
+ """Post a reply to *comment_id* using the v1 content API."""
582
+ url = self._v1(f"/content/{comment_id}/child/comment")
583
+ resp = self._http.post(url, json={
584
+ "type": "comment",
585
+ "body": {
586
+ "storage": {
587
+ "value": f"<p>{reply_text}</p>",
588
+ "representation": "storage",
589
+ }
590
+ },
591
+ })
592
+ self._raise_for_status(resp, f"add_comment_reply({comment_id!r})")
593
+
594
+ def resolve_inline_comment(self, comment_id: str) -> None:
595
+ """Resolve an inline comment by setting *resolved=true*."""
596
+ url = self._v2(f"/inline-comments/{comment_id}")
597
+ get_resp = self._http.get(url, params={"body-format": "storage"})
598
+ self._raise_for_status(get_resp, f"get_inline_comment({comment_id!r})")
599
+ data = get_resp.json()
600
+ version = data.get("version", {}).get("number", 1)
601
+ body_value = data.get("body", {}).get("storage", {}).get("value", "")
602
+ resp = self._http.put(url, json={
603
+ "version": {"number": version + 1},
604
+ "resolved": True,
605
+ "body": {"representation": "storage", "value": body_value},
606
+ })
607
+ self._raise_for_status(resp, f"resolve_inline_comment({comment_id!r})")
608
+
609
+ def resolve_footer_comment(self, comment_id: str) -> None:
610
+ """Resolve a footer comment by setting *resolved=true*."""
611
+ url = self._v2(f"/footer-comments/{comment_id}")
612
+ get_resp = self._http.get(url, params={"body-format": "storage"})
613
+ self._raise_for_status(get_resp, f"get_footer_comment({comment_id!r})")
614
+ data = get_resp.json()
615
+ version = data.get("version", {}).get("number", 1)
616
+ body_value = data.get("body", {}).get("storage", {}).get("value", "")
617
+ resp = self._http.put(url, json={
618
+ "version": {"number": version + 1},
619
+ "resolved": True,
620
+ "body": {"representation": "storage", "value": body_value},
621
+ })
622
+ self._raise_for_status(resp, f"resolve_footer_comment({comment_id!r})")
@@ -0,0 +1 @@
1
+ """Confluence ↔ GitHub comment synchronisation."""
@@ -0,0 +1,25 @@
1
+ """Map Confluence inlineOriginalSelection text to a source file line number."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def find_anchor_line(source_path: Path, selection_text: str) -> int | None:
9
+ """Return the 1-based line number of the first line containing *selection_text*.
10
+
11
+ Returns ``None`` when:
12
+ - *selection_text* is empty
13
+ - the file cannot be read
14
+ - no line contains the text
15
+ """
16
+ if not selection_text:
17
+ return None
18
+ try:
19
+ lines = source_path.read_text(encoding="utf-8").splitlines()
20
+ except OSError:
21
+ return None
22
+ for i, line in enumerate(lines, start=1):
23
+ if selection_text in line:
24
+ return i
25
+ return None