confluence-md-sync 0.5.2 → 0.7.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.
- package/README.md +35 -0
- package/dist/client/client.d.ts +2 -1
- package/dist/client/client.js +5 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/publish/notice.d.ts +34 -0
- package/dist/publish/notice.js +37 -0
- package/dist/publish/publish.d.ts +8 -0
- package/dist/publish/publish.js +63 -18
- package/dist/publish/runner.d.ts +7 -0
- package/dist/publish/runner.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,9 +18,16 @@ npm install -g confluence-md-sync # as a CLI: `confluence-md-sync …`
|
|
|
18
18
|
|
|
19
19
|
- **No history spam** — rendered content is SHA-256-hashed into a content
|
|
20
20
|
property; identical re-publish skips the update, page version doesn't grow.
|
|
21
|
+
- **Drift-proof** — the content property also stores the page version we last
|
|
22
|
+
wrote. If someone edits or wipes the page in Confluence, the version diverges
|
|
23
|
+
and the next publish detects the drift and restores the page from markdown
|
|
24
|
+
(git stays the source of truth) instead of trusting the stale hash.
|
|
21
25
|
- **Attachment dedup** — uploads are tagged `sha256:<hash>`; unchanged files
|
|
22
26
|
are reused. `<file>.src-sha256` sidecars pin dedup to the *source* of
|
|
23
27
|
non-deterministic artifacts (e.g. PNGs rendered by a headless browser).
|
|
28
|
+
- **Managed-page banner** — optionally stamp a “edit this in <source>” notice
|
|
29
|
+
(info/warning panel with a link) into the page; injected into storage only,
|
|
30
|
+
never into the markdown source. See *Managed-page banner*.
|
|
24
31
|
- **Fail before write** — placeholders, files and macro markers are validated
|
|
25
32
|
up front; Confluence is never touched on a broken input.
|
|
26
33
|
- **Pluggable macros** — built-in `core` + `table-filter` plugins, extend
|
|
@@ -117,6 +124,34 @@ await publishPage({
|
|
|
117
124
|
}, cfg);
|
|
118
125
|
```
|
|
119
126
|
|
|
127
|
+
### Managed-page banner
|
|
128
|
+
|
|
129
|
+
When a page is generated from an external source (e.g. docs-studio) and users
|
|
130
|
+
should not edit it in Confluence, add a `managedNotice`. It is injected into the
|
|
131
|
+
**storage** at publish time — never into the markdown source, so export and
|
|
132
|
+
round-trip stay clean, and git remains the single source of truth. The banner is
|
|
133
|
+
deterministic (fixed macro id), so it doesn't break the no-history-spam hash skip.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
await publishPage({
|
|
137
|
+
pageId,
|
|
138
|
+
markdownPath,
|
|
139
|
+
managedNotice: {
|
|
140
|
+
linkUrl: 'https://studio.example/doc/123', // required
|
|
141
|
+
// everything below is optional:
|
|
142
|
+
linkText: 'постановка в docs-studio', // default: 'docs-studio'
|
|
143
|
+
text: 'Правьте страницу в {link}. Здесь только чтение — правки будут перезаписаны.',
|
|
144
|
+
panel: 'warning', // info | note | warning | tip (default: info)
|
|
145
|
+
position: 'top', // 'top' (default) | 'bottom'
|
|
146
|
+
},
|
|
147
|
+
}, cfg);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`{link}` in `text` is replaced by the link; omit it and the link is appended.
|
|
151
|
+
The presence of `managedNotice` turns the banner on — leave it out and no notice
|
|
152
|
+
is added. To apply one banner to every page of a CI plan, pass `managedNotice`
|
|
153
|
+
to `runPublish(dir, plan, { managedNotice })`; a per-page value overrides it.
|
|
154
|
+
|
|
120
155
|
## BPMN diagrams out of the box
|
|
121
156
|
|
|
122
157
|
Pass a `.bpmn` file as an image (a path or an `http(s)` URL) — it is
|
package/dist/client/client.d.ts
CHANGED
|
@@ -73,12 +73,13 @@ export declare class ConfluenceClient {
|
|
|
73
73
|
/** Finds a page by space key and exact title. Returns null if not found. */
|
|
74
74
|
getPageByTitle(spaceKey: string, title: string): Promise<ConfluencePage | null>;
|
|
75
75
|
createPage(opts: CreatePageOptions): Promise<ConfluencePage>;
|
|
76
|
+
/** Обновляет страницу и возвращает номер версии из ответа Confluence. */
|
|
76
77
|
updatePage(pageId: string, body: {
|
|
77
78
|
title: string;
|
|
78
79
|
version: number;
|
|
79
80
|
storage: string;
|
|
80
81
|
versionMessage?: string;
|
|
81
|
-
}): Promise<
|
|
82
|
+
}): Promise<number>;
|
|
82
83
|
deletePage(pageId: string): Promise<void>;
|
|
83
84
|
/** Direct child pages of a page. */
|
|
84
85
|
getChildPages(pageId: string, limit?: number): Promise<ConfluencePage[]>;
|
package/dist/client/client.js
CHANGED
|
@@ -99,6 +99,7 @@ export class ConfluenceClient {
|
|
|
99
99
|
await this.parseError(res, `createPage(${opts.spaceKey}/${opts.title})`);
|
|
100
100
|
return (await res.json());
|
|
101
101
|
}
|
|
102
|
+
/** Обновляет страницу и возвращает номер версии из ответа Confluence. */
|
|
102
103
|
async updatePage(pageId, body) {
|
|
103
104
|
const version = { number: body.version };
|
|
104
105
|
if (body.versionMessage)
|
|
@@ -118,6 +119,10 @@ export class ConfluenceClient {
|
|
|
118
119
|
});
|
|
119
120
|
if (!res.ok)
|
|
120
121
|
await this.parseError(res, `updatePage(${pageId})`);
|
|
122
|
+
// Confluence возвращает обновлённый content с актуальным version.number.
|
|
123
|
+
// Если тела/поля нет — падаем обратно на посланную версию.
|
|
124
|
+
const data = (await res.json().catch(() => null));
|
|
125
|
+
return data?.version?.number ?? body.version;
|
|
121
126
|
}
|
|
122
127
|
async deletePage(pageId) {
|
|
123
128
|
const res = await fetch(this.url(`/rest/api/content/${pageId}`), {
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { Attachment, AttachmentService, toAttachmentVersion, SRC_SHA_SIDECAR_SUF
|
|
|
10
10
|
export { Page, Table } from './pages/page.js';
|
|
11
11
|
export { readTableFromConfluence, findTable, findTableInMacro, parseHtmlTable, decodeHtmlCell, renderMarkdownTable, escapeMdTableCell, readAndMapTable, type ColumnAlign, type TableColumn, } from './pages/tables.js';
|
|
12
12
|
export { publishPage, computeContentHash, DEFAULT_HASH_PROPERTY_KEY, type PublishPageOptions, type PublishPageResult, type TableData, } from './publish/publish.js';
|
|
13
|
+
export { buildManagedNotice, applyManagedNotice, DEFAULT_MANAGED_NOTICE_TEXT, DEFAULT_MANAGED_NOTICE_LINK_TEXT, type ManagedNoticeOptions, } from './publish/notice.js';
|
|
13
14
|
export { isHttpUrl, remoteFilename, isSameConfluenceOrigin, remoteRequestHeaders, downloadToFile, } from './publish/remote.js';
|
|
14
15
|
export { runPublish, type Here, type Build, type PublishPlan, type RunPublishOptions, } from './publish/runner.js';
|
|
15
16
|
export { parseStorage, serializeStorage, decodeEntities, StorageParseError, type XNode, type XElement, } from './export/xhtml.js';
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { Page, Table } from './pages/page.js';
|
|
|
17
17
|
export { readTableFromConfluence, findTable, findTableInMacro, parseHtmlTable, decodeHtmlCell, renderMarkdownTable, escapeMdTableCell, readAndMapTable, } from './pages/tables.js';
|
|
18
18
|
// Publish
|
|
19
19
|
export { publishPage, computeContentHash, DEFAULT_HASH_PROPERTY_KEY, } from './publish/publish.js';
|
|
20
|
+
export { buildManagedNotice, applyManagedNotice, DEFAULT_MANAGED_NOTICE_TEXT, DEFAULT_MANAGED_NOTICE_LINK_TEXT, } from './publish/notice.js';
|
|
20
21
|
export { isHttpUrl, remoteFilename, isSameConfluenceOrigin, remoteRequestHeaders, downloadToFile, } from './publish/remote.js';
|
|
21
22
|
export { runPublish, } from './publish/runner.js';
|
|
22
23
|
// Export (storage → markdown) & round-trip
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* «Баннер управляемой страницы»: примечание о том, что страница ведётся во
|
|
3
|
+
* внешнем источнике (docs-studio), а правки в самой Confluence будут
|
|
4
|
+
* перезаписаны. Вставляется в storage при публикации, но НЕ в markdown-
|
|
5
|
+
* источник — round-trip/экспорт его не увидят, git остаётся чистым.
|
|
6
|
+
*/
|
|
7
|
+
export interface ManagedNoticeOptions {
|
|
8
|
+
/**
|
|
9
|
+
* URL источника (docs-studio / репозиторий постановки), куда ведёт ссылка.
|
|
10
|
+
* Обязателен — без него примечание не имеет смысла.
|
|
11
|
+
*/
|
|
12
|
+
linkUrl: string;
|
|
13
|
+
/** Текст ссылки. Default: {@link DEFAULT_MANAGED_NOTICE_LINK_TEXT}. */
|
|
14
|
+
linkText?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Текст примечания. Плейсхолдер `{link}` заменяется ссылкой; если его в
|
|
17
|
+
* тексте нет, ссылка добавляется в конец. Default:
|
|
18
|
+
* {@link DEFAULT_MANAGED_NOTICE_TEXT}.
|
|
19
|
+
*/
|
|
20
|
+
text?: string;
|
|
21
|
+
/** Куда вставлять баннер: 'top' (шапка) или 'bottom' (низ). Default: 'top'. */
|
|
22
|
+
position?: 'top' | 'bottom';
|
|
23
|
+
/**
|
|
24
|
+
* Тип панели Confluence (влияет на цвет/иконку). Default: 'info'.
|
|
25
|
+
* 'warning' — красная, самый заметный вариант для «не редактировать».
|
|
26
|
+
*/
|
|
27
|
+
panel?: 'info' | 'note' | 'warning' | 'tip';
|
|
28
|
+
}
|
|
29
|
+
export declare const DEFAULT_MANAGED_NOTICE_TEXT: string;
|
|
30
|
+
export declare const DEFAULT_MANAGED_NOTICE_LINK_TEXT = "docs-studio";
|
|
31
|
+
/** Строит storage-разметку баннера (одиночный `<ac:structured-macro>`). */
|
|
32
|
+
export declare function buildManagedNotice(opts: ManagedNoticeOptions): string;
|
|
33
|
+
/** Дописывает баннер в начало (top) или конец (bottom) storage-контента. */
|
|
34
|
+
export declare function applyManagedNotice(storage: string, opts: ManagedNoticeOptions): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* «Баннер управляемой страницы»: примечание о том, что страница ведётся во
|
|
3
|
+
* внешнем источнике (docs-studio), а правки в самой Confluence будут
|
|
4
|
+
* перезаписаны. Вставляется в storage при публикации, но НЕ в markdown-
|
|
5
|
+
* источник — round-trip/экспорт его не увидят, git остаётся чистым.
|
|
6
|
+
*/
|
|
7
|
+
import { structuredMacro } from '../macros/xml.js';
|
|
8
|
+
import { escapeXmlAttr } from '../macros/xml.js';
|
|
9
|
+
export const DEFAULT_MANAGED_NOTICE_TEXT = 'Страница синхронизируется автоматически. Вносите правки в источнике ({link}) — ' +
|
|
10
|
+
'ручные изменения на этой странице будут перезаписаны при следующей публикации.';
|
|
11
|
+
export const DEFAULT_MANAGED_NOTICE_LINK_TEXT = 'docs-studio';
|
|
12
|
+
// Фиксированный ac:macro-id: баннер обязан давать БАЙТ-В-БАЙТ одинаковый
|
|
13
|
+
// storage при каждой публикации. Со случайным id (generateMacroId) content-
|
|
14
|
+
// hash менялся бы каждый раз → страница вечно считалась бы изменённой.
|
|
15
|
+
const NOTICE_MACRO_ID = '0f0e0d0c-0b0a-4009-8008-000000000001';
|
|
16
|
+
/** Собирает `<a href>`-ссылку на источник. */
|
|
17
|
+
function noticeLink(opts) {
|
|
18
|
+
const text = opts.linkText ?? DEFAULT_MANAGED_NOTICE_LINK_TEXT;
|
|
19
|
+
return `<a href="${escapeXmlAttr(opts.linkUrl)}">${escapeXmlAttr(text)}</a>`;
|
|
20
|
+
}
|
|
21
|
+
/** Строит storage-разметку баннера (одиночный `<ac:structured-macro>`). */
|
|
22
|
+
export function buildManagedNotice(opts) {
|
|
23
|
+
if (!opts.linkUrl)
|
|
24
|
+
throw new Error('managedNotice: linkUrl is required');
|
|
25
|
+
const panel = opts.panel ?? 'info';
|
|
26
|
+
const link = noticeLink(opts);
|
|
27
|
+
const text = opts.text ?? DEFAULT_MANAGED_NOTICE_TEXT;
|
|
28
|
+
const inner = text.includes('{link}')
|
|
29
|
+
? text.split('{link}').map(escapeXmlAttr).join(link)
|
|
30
|
+
: `${escapeXmlAttr(text)} ${link}`;
|
|
31
|
+
return structuredMacro(panel, NOTICE_MACRO_ID, { richBody: `<p>${inner}</p>` });
|
|
32
|
+
}
|
|
33
|
+
/** Дописывает баннер в начало (top) или конец (bottom) storage-контента. */
|
|
34
|
+
export function applyManagedNotice(storage, opts) {
|
|
35
|
+
const notice = buildManagedNotice(opts);
|
|
36
|
+
return (opts.position ?? 'top') === 'bottom' ? `${storage}${notice}` : `${notice}${storage}`;
|
|
37
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ConfluenceConfig } from '../client/config.js';
|
|
2
2
|
import { type RenderStorageOptions } from '../markdown/render.js';
|
|
3
|
+
import { type ManagedNoticeOptions } from './notice.js';
|
|
3
4
|
import type { MacroRegistry } from '../macros/registry.js';
|
|
4
5
|
import { Markdown } from '../markdown/markdown.js';
|
|
5
6
|
export interface TableData {
|
|
@@ -42,6 +43,13 @@ export interface PublishPageOptions {
|
|
|
42
43
|
labels?: string[];
|
|
43
44
|
/** Комментарий к версии страницы. */
|
|
44
45
|
versionMessage?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Примечание «страница управляется извне» (docs-studio). Вставляется в
|
|
48
|
+
* storage при публикации, НО не в markdown-источник — экспорт/round-trip
|
|
49
|
+
* его не увидят. Наличие опции включает баннер; настраиваются текст, ссылка,
|
|
50
|
+
* её текст, тип панели и позиция (шапка/низ). См. {@link ManagedNoticeOptions}.
|
|
51
|
+
*/
|
|
52
|
+
managedNotice?: ManagedNoticeOptions;
|
|
45
53
|
/** Свой реестр макросов (default: встроенные core + table-filter). */
|
|
46
54
|
registry?: MacroRegistry;
|
|
47
55
|
/**
|
package/dist/publish/publish.js
CHANGED
|
@@ -8,6 +8,7 @@ import { bpmnOutputName, convertBpmn, isBpmnFile } from '../bpmn/convert.js';
|
|
|
8
8
|
import { downloadToFile, isHttpUrl, remoteFilename } from './remote.js';
|
|
9
9
|
import { renameImagePlaceholders, renderToStorage, } from '../markdown/render.js';
|
|
10
10
|
import { validateMarkdown } from '../markdown/validate.js';
|
|
11
|
+
import { applyManagedNotice } from './notice.js';
|
|
11
12
|
import { processMacros } from '../macros/registry.js';
|
|
12
13
|
import { defaultMacroRegistry } from '../macros/index.js';
|
|
13
14
|
import { Markdown } from '../markdown/markdown.js';
|
|
@@ -28,15 +29,19 @@ export function computeContentHash(storage) {
|
|
|
28
29
|
const canonical = storage.replace(/(\/download\/attachments\/[^"?\s]+)\?[^"\s]*/g, '$1');
|
|
29
30
|
return createHash('sha256').update(canonical, 'utf-8').digest('hex');
|
|
30
31
|
}
|
|
31
|
-
// Схема
|
|
32
|
+
// Схема content property. Схема 1 подставляла в body download-URL из
|
|
32
33
|
// _links.download как есть — с ?version=N&modificationDate=…; при ребампе
|
|
33
34
|
// аттача без изменения текста страница оставалась UNCHANGED и продолжала
|
|
34
35
|
// отдавать старую, пиненную версию картинки. Схема 2 подставляет
|
|
35
36
|
// канонический URL без query — Confluence по нему отдаёт последнюю версию
|
|
36
|
-
// аттача, и обновление диаграммы видно без переписывания body.
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
37
|
+
// аттача, и обновление диаграммы видно без переписывания body. Схема 3
|
|
38
|
+
// добавляет pageVersion — номер версии страницы после нашей последней
|
|
39
|
+
// записи; по нему детектится «дрейф» (страницу правили мимо публикатора:
|
|
40
|
+
// version.number вырос, а hash-свойство осталось прежним → без этой проверки
|
|
41
|
+
// испорченная страница никогда бы не восстановилась из markdown). Property со
|
|
42
|
+
// схемой ≠ текущей (в т.ч. без поля scheme/pageVersion) считается устаревшей —
|
|
43
|
+
// hash не сверяется, страница один раз переписывается в новом формате.
|
|
44
|
+
const HASH_SCHEME = 3;
|
|
40
45
|
/** Канонический download-URL аттача: без query (?version=N&…). */
|
|
41
46
|
function canonicalDownloadUrl(url) {
|
|
42
47
|
return url.split('?')[0];
|
|
@@ -81,6 +86,10 @@ export async function publishPage(opts, cfg) {
|
|
|
81
86
|
const tables = opts.tables ?? [];
|
|
82
87
|
const registry = opts.registry ?? defaultMacroRegistry;
|
|
83
88
|
const hashKey = opts.hashPropertyKey ?? DEFAULT_HASH_PROPERTY_KEY;
|
|
89
|
+
// Fail fast: баннер без ссылки бессмыслен — падаем до любого сетевого I/O.
|
|
90
|
+
if (opts.managedNotice && !opts.managedNotice.linkUrl) {
|
|
91
|
+
throw new Error('publishPage: managedNotice.linkUrl is required');
|
|
92
|
+
}
|
|
84
93
|
// 0a. Удалённые источники: http(s)://-элементы в images[]/files[]
|
|
85
94
|
// заменяются на локальный путь в downloadDir с именем из URL — дальше
|
|
86
95
|
// конвейер (валидация, BPMN, аплоад) работает с обычными путями.
|
|
@@ -191,6 +200,13 @@ export async function publishPage(opts, cfg) {
|
|
|
191
200
|
let storage = renderToStorage(markdown, urls, opts.render ?? {});
|
|
192
201
|
// 2.5. Преобразование маркеров макросов в XHTML.
|
|
193
202
|
storage = processMacros(storage, registry).toString();
|
|
203
|
+
// 2.6. Баннер «страница управляется извне». Вставляется здесь, в storage —
|
|
204
|
+
// не в markdown — поэтому источник/round-trip его не содержат. Баннер
|
|
205
|
+
// детерминирован (фиксированный macro-id), значит попадает в content-hash
|
|
206
|
+
// стабильно и не ломает идемпотентность.
|
|
207
|
+
if (opts.managedNotice) {
|
|
208
|
+
storage = applyManagedNotice(storage, opts.managedNotice);
|
|
209
|
+
}
|
|
194
210
|
if (opts.dryRun) {
|
|
195
211
|
console.log(`[publish] dry-run: page ${pageId} rendered OK (${storage.length} bytes of storage)`);
|
|
196
212
|
return {
|
|
@@ -203,42 +219,71 @@ export async function publishPage(opts, cfg) {
|
|
|
203
219
|
storage,
|
|
204
220
|
};
|
|
205
221
|
}
|
|
206
|
-
// 3.
|
|
222
|
+
// 3. Решение о публикации. Hash и версия нашей последней записи хранятся в
|
|
223
|
+
// content property (не в body — normalize storage её бы съел). Страница
|
|
224
|
+
// и её версия так и так читаются перед сравнением. Причина публикации
|
|
225
|
+
// (для диагностики инцидентов пишется в лог):
|
|
226
|
+
// no-property — свойства нет (мы эту страницу ещё не публиковали);
|
|
227
|
+
// hash-mismatch — контент изменился (или свойство устаревшей схемы);
|
|
228
|
+
// title-change — сменился заголовок при том же контенте;
|
|
229
|
+
// drift — страницу правили мимо публикатора (version.number
|
|
230
|
+
// разошёлся с записанным нами) → чиним из markdown.
|
|
207
231
|
const newHash = computeContentHash(storage);
|
|
208
232
|
const [existing, hashProp] = await Promise.all([
|
|
209
233
|
client.getPageStorage(pageId),
|
|
210
234
|
client.getContentProperty(pageId, hashKey),
|
|
211
235
|
]);
|
|
212
236
|
const title = opts.title ?? existing.title;
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
237
|
+
// Только свойство текущей схемы несёт доверенные hash + pageVersion.
|
|
238
|
+
// Устаревшая схема (или без scheme) — «версия неизвестна»: hash не сверяем
|
|
239
|
+
// (existingHash = null → одна принудительная публикация с записью нового
|
|
240
|
+
// формата; она же самовосстанавливает ранее испорченные страницы).
|
|
216
241
|
const propValue = hashProp && typeof hashProp.value === 'object' && hashProp.value !== null
|
|
217
242
|
? hashProp.value
|
|
218
243
|
: null;
|
|
219
|
-
const
|
|
244
|
+
const isCurrentScheme = propValue !== null && propValue.scheme === HASH_SCHEME;
|
|
245
|
+
const existingHash = isCurrentScheme
|
|
220
246
|
? (propValue.hash ?? null)
|
|
221
247
|
: null;
|
|
222
|
-
|
|
248
|
+
const publishedVersion = isCurrentScheme && typeof propValue.pageVersion === 'number'
|
|
249
|
+
? propValue.pageVersion
|
|
250
|
+
: null;
|
|
251
|
+
let reason;
|
|
252
|
+
if (propValue === null)
|
|
253
|
+
reason = 'no-property';
|
|
254
|
+
else if (existingHash !== newHash)
|
|
255
|
+
reason = 'hash-mismatch';
|
|
256
|
+
else if (title !== existing.title)
|
|
257
|
+
reason = 'title-change';
|
|
258
|
+
else if (publishedVersion !== existing.version)
|
|
259
|
+
reason = 'drift';
|
|
260
|
+
else
|
|
261
|
+
reason = null;
|
|
262
|
+
if (reason === null) {
|
|
223
263
|
console.log(`[publish] ${pageId} "${title}" → UNCHANGED (hash ${newHash.slice(0, 12)}, v${existing.version})`);
|
|
224
264
|
if (opts.labels?.length)
|
|
225
265
|
await client.addLabels(pageId, opts.labels);
|
|
226
266
|
return { pageId, title, version: existing.version, attachments, updated: false, created, storage };
|
|
227
267
|
}
|
|
228
268
|
// 4. Обновление страницы — только после успешного аплоада всех аттачей.
|
|
229
|
-
|
|
230
|
-
await client.updatePage(pageId, {
|
|
269
|
+
// Версию берём из ответа Confluence (update инкрементирует её сам).
|
|
270
|
+
const nextVersion = await client.updatePage(pageId, {
|
|
231
271
|
title,
|
|
232
|
-
version:
|
|
272
|
+
version: existing.version + 1,
|
|
233
273
|
storage,
|
|
234
274
|
versionMessage: opts.versionMessage,
|
|
235
275
|
});
|
|
236
|
-
// 5. Запись/обновление content property
|
|
237
|
-
// updatePage чтобы при сбое publish
|
|
238
|
-
|
|
276
|
+
// 5. Запись/обновление content property: hash + версия, которая ПОЛУЧИЛАСЬ
|
|
277
|
+
// после нашей записи. Делаем ПОСЛЕ updatePage, чтобы при сбое publish
|
|
278
|
+
// свойство не «опередило» реальное содержимое. pageVersion закрывает
|
|
279
|
+
// слепую зону дрейфа (см. HASH_SCHEME).
|
|
280
|
+
await client.setContentProperty(pageId, hashKey, { hash: newHash, scheme: HASH_SCHEME, pageVersion: nextVersion }, hashProp ? hashProp.version : null);
|
|
239
281
|
if (opts.labels?.length)
|
|
240
282
|
await client.addLabels(pageId, opts.labels);
|
|
241
|
-
|
|
283
|
+
const detail = reason === 'drift'
|
|
284
|
+
? `DRIFT (page v${existing.version} != published v${publishedVersion})`
|
|
285
|
+
: reason;
|
|
286
|
+
console.log(`[publish] ${pageId} "${title}" → ${detail} → v${nextVersion} (hash ${newHash.slice(0, 12)})`);
|
|
242
287
|
return { pageId, title, version: nextVersion, attachments, updated: true, created, storage };
|
|
243
288
|
}
|
|
244
289
|
function escapeRegex(s) {
|
package/dist/publish/runner.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PublishPageOptions, type PublishPageResult } from './publish.js';
|
|
2
|
+
import type { ManagedNoticeOptions } from './notice.js';
|
|
2
3
|
import { type ConfluenceConfig, type LoadConfigOptions } from '../client/config.js';
|
|
3
4
|
export type Here = (relativePath: string) => string;
|
|
4
5
|
export type Build = (relativePath: string) => string;
|
|
@@ -14,6 +15,12 @@ export interface RunPublishOptions {
|
|
|
14
15
|
* соответствует компоновке репо `<repo>/docs/<set>` + `<repo>/build/<set>`.
|
|
15
16
|
*/
|
|
16
17
|
buildDir?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Баннер «страница управляется извне» по умолчанию для ВСЕХ страниц прогона
|
|
20
|
+
* (docs-studio). Значение, заданное у конкретной страницы в `managedNotice`,
|
|
21
|
+
* имеет приоритет. См. {@link ManagedNoticeOptions}.
|
|
22
|
+
*/
|
|
23
|
+
managedNotice?: ManagedNoticeOptions;
|
|
17
24
|
}
|
|
18
25
|
/**
|
|
19
26
|
* Минимальная точка входа для публикующего скрипта (`docs/<set>/publish.ts`).
|
package/dist/publish/runner.js
CHANGED
|
@@ -29,7 +29,11 @@ export async function runPublish(baseDir, plan, opts = {}) {
|
|
|
29
29
|
const pages = typeof plan === 'function' ? await plan(here, build) : plan;
|
|
30
30
|
const results = [];
|
|
31
31
|
for (const page of pages) {
|
|
32
|
-
|
|
32
|
+
// Дефолтный баннер прогона применяется, только если страница его не задала.
|
|
33
|
+
const merged = opts.managedNotice && page.managedNotice === undefined
|
|
34
|
+
? { ...page, managedNotice: opts.managedNotice }
|
|
35
|
+
: page;
|
|
36
|
+
results.push(await publishPage(merged, cfg));
|
|
33
37
|
}
|
|
34
38
|
return results;
|
|
35
39
|
}
|
package/package.json
CHANGED