confluence-md-sync 0.2.0 → 0.2.1

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.
package/README.md CHANGED
@@ -48,10 +48,11 @@ Markdown + three placeholder kinds:
48
48
  ```markdown
49
49
  # Отчёт за месяц
50
50
 
51
- {{img:flow.png}} <!-- inline image from images[] -->
51
+ {{img:chart.png}} <!-- inline image from images[] -->
52
+ {{img:flow.bpmn}} <!-- BPMN diagram — rendered to PNG automatically -->
52
53
  Исходник: {{file:data.csv}} <!-- attachment link from files[] -->
53
54
 
54
- {{table:summary}} <!-- table injected from tables[] -->
55
+ {{table:summary}} <!-- table injected from tables[] -->
55
56
  ```
56
57
 
57
58
  ```ts
@@ -66,7 +67,10 @@ const summary = renderMarkdownTable(rows, [
66
67
  await publishPage({
67
68
  pageId: '123456789',
68
69
  markdownPath: 'docs/report.md',
69
- images: ['build/flow.png'],
70
+ images: [
71
+ 'build/chart.png',
72
+ 'docs/flow.bpmn', // converted to flow.png on the fly (see BPMN section)
73
+ ],
70
74
  files: ['build/data.csv'],
71
75
  tables: [{
72
76
  name: 'summary',
@@ -99,19 +103,29 @@ await publishPage({ pageId, markdown: '# Generated\n\ntext' }, cfg);
99
103
 
100
104
  Pass a `.bpmn` file as an image — it is rendered to PNG at publish time
101
105
  (headless Chromium via [bpmn-to-image](https://npmjs.com/package/bpmn-to-image),
102
- an optional peer dependency):
106
+ an optional peer dependency).
107
+
108
+ Setup — install the converter **and** force a current puppeteer:
103
109
 
104
110
  ```bash
105
- npm install -D bpmn-to-image
111
+ npm install -D bpmn-to-image puppeteer
106
112
  ```
107
113
 
108
- > bpmn-to-image pins puppeteer 21, whose bundled Chromium fails to launch on
109
- > recent OSes ("socket hang up"). Force a current puppeteer in your
110
- > `package.json`:
111
- >
112
- > ```json
113
- > "overrides": { "puppeteer": "^24.0.0" }
114
- > ```
114
+ ```jsonc
115
+ // package.json required: bpmn-to-image pins puppeteer 21, whose bundled
116
+ // Chromium fails to launch on recent OSes ("socket hang up"). The override
117
+ // must reference the direct dependency ("$puppeteer"), otherwise `npm ci`
118
+ // fails with "Override for puppeteer conflicts with direct dependency".
119
+ {
120
+ "devDependencies": {
121
+ "bpmn-to-image": "^0.7.0",
122
+ "puppeteer": "^24.0.0"
123
+ },
124
+ "overrides": {
125
+ "puppeteer": "$puppeteer"
126
+ }
127
+ }
128
+ ```
115
129
 
116
130
  ```markdown
117
131
  Процесс выпуска релиза:
@@ -20,14 +20,26 @@ import { Markdown } from '../markdown/markdown.js';
20
20
  export const DEFAULT_HASH_PROPERTY_KEY = 'confluence-md-sync-content-hash';
21
21
  export function computeContentHash(storage) {
22
22
  // Перед хешированием вычищаем query (?version=N&modificationDate=…) из
23
- // attachment download-URL'ов. Иначе любой ребамп аттача (новая версия
24
- // от не-детерминированной генерации картинок новый SHA новый
25
- // version N в URL) ломал бы hash страницы, хотя тело логически
26
- // не изменилось — Confluence сам отдаст последнюю версию аттача по
27
- // base-URL без query.
23
+ // attachment download-URL'ов защита для storage, писанного схемой 1
24
+ // (см. HASH_SCHEME): там в body лежали URL с пином версии. Начиная со
25
+ // схемы 2 в body подставляются канонические URL без query, и replace —
26
+ // no-op.
28
27
  const canonical = storage.replace(/(\/download\/attachments\/[^"?\s]+)\?[^"\s]*/g, '$1');
29
28
  return createHash('sha256').update(canonical, 'utf-8').digest('hex');
30
29
  }
30
+ // Схема записи download-URL в body. Схема 1 подставляла URL из
31
+ // _links.download как есть — с ?version=N&modificationDate=…; при ребампе
32
+ // аттача без изменения текста страница оставалась UNCHANGED и продолжала
33
+ // отдавать старую, пиненную версию картинки. Схема 2 подставляет
34
+ // канонический URL без query — Confluence по нему отдаёт последнюю версию
35
+ // аттача, и обновление диаграммы видно без переписывания body. Property со
36
+ // схемой ≠ текущей (в т.ч. без поля scheme) считается устаревшей — страница
37
+ // один раз переписывается каноническими URL.
38
+ const HASH_SCHEME = 2;
39
+ /** Канонический download-URL аттача: без query (?version=N&…). */
40
+ function canonicalDownloadUrl(url) {
41
+ return url.split('?')[0];
42
+ }
31
43
  async function resolvePage(client, opts) {
32
44
  if (opts.pageId)
33
45
  return { pageId: opts.pageId, created: false };
@@ -120,15 +132,18 @@ export async function publishPage(opts, cfg) {
120
132
  urls.files.set(basename(p), `dry-run://file/${basename(p)}`);
121
133
  }
122
134
  else {
135
+ // В body уходит канонический URL без query (см. HASH_SCHEME) — страница
136
+ // всегда отдаёт последнюю версию аттача. Полный URL с версией остаётся
137
+ // в результате для caller'а.
123
138
  for (const p of images) {
124
139
  const r = await attachmentSvc.ensure(pageId, p);
125
- urls.images.set(r.filename, r.downloadUrl);
140
+ urls.images.set(r.filename, canonicalDownloadUrl(r.downloadUrl));
126
141
  attachments.push({ filename: r.filename, id: r.id, reused: r.reused, url: r.downloadUrl });
127
142
  console.log(`[attachment] ${r.filename}: ${r.reused ? 'reused' : 'uploaded'} (id=${r.id})`);
128
143
  }
129
144
  for (const p of files) {
130
145
  const r = await attachmentSvc.ensure(pageId, p);
131
- urls.files.set(r.filename, r.downloadUrl);
146
+ urls.files.set(r.filename, canonicalDownloadUrl(r.downloadUrl));
132
147
  attachments.push({ filename: r.filename, id: r.id, reused: r.reused, url: r.downloadUrl });
133
148
  console.log(`[attachment] ${r.filename}: ${r.reused ? 'reused' : 'uploaded'} (id=${r.id})`);
134
149
  }
@@ -156,8 +171,14 @@ export async function publishPage(opts, cfg) {
156
171
  client.getContentProperty(pageId, hashKey),
157
172
  ]);
158
173
  const title = opts.title ?? existing.title;
159
- const existingHash = hashProp && typeof hashProp.value === 'object' && hashProp.value !== null
160
- ? hashProp.value.hash ?? null
174
+ // Property, писанная другой схемой (или до появления scheme), не считается
175
+ // совпадением: body мог быть записан с пином версий аттачей — его нужно
176
+ // один раз переписать каноническими URL.
177
+ const propValue = hashProp && typeof hashProp.value === 'object' && hashProp.value !== null
178
+ ? hashProp.value
179
+ : null;
180
+ const existingHash = propValue && propValue.scheme === HASH_SCHEME
181
+ ? (propValue.hash ?? null)
161
182
  : null;
162
183
  if (existingHash === newHash && title === existing.title) {
163
184
  console.log(`[publish] ${pageId} "${title}" → UNCHANGED (hash ${newHash.slice(0, 12)}, v${existing.version})`);
@@ -175,7 +196,7 @@ export async function publishPage(opts, cfg) {
175
196
  });
176
197
  // 5. Запись/обновление content property с новым hash. Делаем ПОСЛЕ
177
198
  // updatePage чтобы при сбое publish hash не «опередил» реальное содержимое.
178
- await client.setContentProperty(pageId, hashKey, { hash: newHash }, hashProp ? hashProp.version : null);
199
+ await client.setContentProperty(pageId, hashKey, { hash: newHash, scheme: HASH_SCHEME }, hashProp ? hashProp.version : null);
179
200
  if (opts.labels?.length)
180
201
  await client.addLabels(pageId, opts.labels);
181
202
  console.log(`[publish] ${pageId} "${title}" → v${nextVersion} (hash ${newHash.slice(0, 12)})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-md-sync",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Publish Markdown to Confluence (Data Center & Cloud): idempotent page sync, attachment dedup, tables and a pluggable macro system",
5
5
  "keywords": [
6
6
  "confluence",