Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/publish/confluence/confluence.ts
6446 views
1
import { join } from "../../deno_ral/path.ts";
2
import { Confirm, Input, Secret } from "cliffy/prompt/mod.ts";
3
import { RenderFlags } from "../../command/render/types.ts";
4
import { pathWithForwardSlashes } from "../../core/path.ts";
5
6
import {
7
readAccessTokens,
8
writeAccessToken,
9
writeAccessTokens,
10
} from "../common/account.ts";
11
12
import {
13
AccountToken,
14
AccountTokenType,
15
InputMetadata,
16
PublishFiles,
17
PublishProvider,
18
} from "../provider-types.ts";
19
20
import { PublishOptions, PublishRecord } from "../types.ts";
21
import { ConfluenceClient } from "./api/index.ts";
22
import {
23
AttachmentSummary,
24
ConfluenceParent,
25
ConfluenceSpaceChange,
26
Content,
27
ContentAncestor,
28
ContentBody,
29
ContentBodyRepresentation,
30
ContentChange,
31
ContentChangeType,
32
ContentCreate,
33
ContentProperty,
34
ContentPropertyKey,
35
ContentStatusEnum,
36
ContentSummary,
37
ContentUpdate,
38
LogPrefix,
39
PAGE_TYPE,
40
PublishContentResult,
41
PublishRenderer,
42
PublishType,
43
PublishTypeEnum,
44
SiteFileMetadata,
45
SitePage,
46
SpaceChangeResult,
47
User,
48
WrappedResult,
49
} from "./api/types.ts";
50
import { withSpinner } from "../../core/console.ts";
51
import {
52
buildFileToMetaTable,
53
buildPublishRecordForContent,
54
buildSpaceChanges,
55
confluenceParentFromString,
56
convertForSecondPass,
57
doWithSpinner,
58
filterFilesForUpdate,
59
findAttachments,
60
flattenIndexes,
61
footnoteTransform,
62
getNextVersion,
63
getTitle,
64
isContentCreate,
65
isContentDelete,
66
isContentUpdate,
67
isNotFound,
68
isUnauthorized,
69
mergeSitePages,
70
tokenFilterOut,
71
transformAtlassianDomain,
72
updateImagePaths,
73
updateLinks,
74
validateEmail,
75
validateParentURL,
76
validateServer,
77
validateToken,
78
wrapBodyForConfluence,
79
writeTokenComparator,
80
} from "./confluence-helper.ts";
81
82
import {
83
verifyAccountToken,
84
verifyConfluenceParent,
85
verifyLocation,
86
verifyOrWarnManagePermissions,
87
} from "./confluence-verify.ts";
88
import {
89
DELETE_DISABLED,
90
DELETE_SLEEP_MILLIS,
91
DESCENDANT_PAGE_SIZE,
92
EXIT_ON_ERROR,
93
MAX_PAGES_TO_LOAD,
94
} from "./constants.ts";
95
import { logError, trace } from "./confluence-logger.ts";
96
import { md5HashBytes } from "../../core/hash.ts";
97
import { sleep } from "../../core/async.ts";
98
import { info } from "../../deno_ral/log.ts";
99
100
export const CONFLUENCE_ID = "confluence";
101
102
const getAccountTokens = (): Promise<AccountToken[]> => {
103
const getConfluenceEnvironmentAccount = () => {
104
const server = Deno.env.get("CONFLUENCE_DOMAIN");
105
const name = Deno.env.get("CONFLUENCE_USER_EMAIL");
106
const token = Deno.env.get("CONFLUENCE_AUTH_TOKEN");
107
if (server && name && token) {
108
return {
109
type: AccountTokenType.Environment,
110
name,
111
server: transformAtlassianDomain(server),
112
token,
113
};
114
}
115
};
116
117
const readConfluenceAccessTokens = (): AccountToken[] => {
118
const result = readAccessTokens<AccountToken>(CONFLUENCE_ID) ?? [];
119
return result;
120
};
121
122
let accounts: AccountToken[] = [];
123
124
const envAccount = getConfluenceEnvironmentAccount();
125
if (envAccount) {
126
accounts = [...accounts, envAccount];
127
}
128
129
const tempStoredAccessTokens = readConfluenceAccessTokens();
130
accounts = [...accounts, ...tempStoredAccessTokens];
131
return Promise.resolve(accounts);
132
};
133
134
const removeToken = (token: AccountToken) => {
135
const existingTokens = readAccessTokens<AccountToken>(CONFLUENCE_ID) ?? [];
136
137
const toWrite: Array<AccountToken> = existingTokens.filter((accessToken) =>
138
tokenFilterOut(accessToken, token)
139
);
140
141
writeAccessTokens(CONFLUENCE_ID, toWrite);
142
};
143
144
const promptAndAuthorizeToken = async () => {
145
const server: string = await Input.prompt({
146
indent: "",
147
message: "Confluence Domain:",
148
hint: "e.g. https://mydomain.atlassian.net/",
149
validate: validateServer,
150
transform: transformAtlassianDomain,
151
});
152
153
await verifyLocation(server);
154
155
const name = await Input.prompt({
156
indent: "",
157
message: `Confluence Account Email:`,
158
validate: validateEmail,
159
});
160
161
const token = await Secret.prompt({
162
indent: "",
163
message: "Confluence API Token:",
164
hint: "Create an API token at https://id.atlassian.com/manage/api-tokens",
165
validate: validateToken,
166
});
167
168
const accountToken: AccountToken = {
169
type: AccountTokenType.Authorized,
170
name,
171
server,
172
token,
173
};
174
await withSpinner(
175
{ message: "Verifying account..." },
176
() => verifyAccountToken(accountToken),
177
);
178
writeAccessToken<AccountToken>(
179
CONFLUENCE_ID,
180
accountToken,
181
writeTokenComparator,
182
);
183
184
return Promise.resolve(accountToken);
185
};
186
187
const promptForParentURL = async () => {
188
return await Input.prompt({
189
indent: "",
190
message: `Space or Parent Page URL:`,
191
hint: "Browse in Confluence to the space or parent, then copy the URL",
192
validate: validateParentURL,
193
});
194
};
195
196
const resolveTarget = async (
197
accountToken: AccountToken,
198
target: PublishRecord,
199
): Promise<PublishRecord> => {
200
return Promise.resolve(target);
201
};
202
203
const loadDocument = (baseDirectory: string, rootFile: string): ContentBody => {
204
const documentValue = Deno.readTextFileSync(join(baseDirectory, rootFile));
205
206
const body: ContentBody = wrapBodyForConfluence(documentValue);
207
208
return body;
209
};
210
211
const renderDocument = async (
212
render: PublishRenderer,
213
): Promise<PublishFiles> => {
214
const flags: RenderFlags = {
215
to: "confluence-publish",
216
};
217
218
return await render(flags);
219
};
220
221
const renderSite = async (render: PublishRenderer): Promise<PublishFiles> => {
222
const flags: RenderFlags = {
223
to: "confluence-publish",
224
};
225
226
const renderResult: PublishFiles = await render(flags);
227
return renderResult;
228
};
229
230
async function publish(
231
account: AccountToken,
232
type: PublishType,
233
_input: string,
234
title: string,
235
_slug: string,
236
render: (flags?: RenderFlags) => Promise<PublishFiles>,
237
_options: PublishOptions,
238
publishRecord?: PublishRecord,
239
): Promise<[PublishRecord, URL | undefined]> {
240
trace("publish", {
241
account,
242
type,
243
_input,
244
title,
245
_slug,
246
_options,
247
publishRecord,
248
});
249
250
const client = new ConfluenceClient(account);
251
252
const user: User = await client.getUser();
253
254
let parentUrl: string = publishRecord?.url ?? (await promptForParentURL());
255
256
const parent: ConfluenceParent = confluenceParentFromString(parentUrl);
257
258
const server = account?.server ?? "";
259
260
await verifyConfluenceParent(parentUrl, parent);
261
262
const space = await client.getSpace(parent.space);
263
264
trace("publish", { parent, server, id: space.id, key: space.key });
265
266
await verifyOrWarnManagePermissions(client, space, parent, user);
267
268
const uniquifyTitle = async (title: string, idToIgnore: string = "") => {
269
trace("uniquifyTitle", title);
270
271
const titleIsUnique: boolean = await client.isTitleUniqueInSpace(
272
title,
273
space,
274
idToIgnore,
275
);
276
277
if (titleIsUnique) {
278
return title;
279
}
280
281
const uuid = globalThis.crypto.randomUUID();
282
const shortUUID = uuid.split("-")[0] ?? uuid;
283
const uuidTitle = `${title} ${shortUUID}`;
284
285
return uuidTitle;
286
};
287
288
const fetchExistingSite = async (parentId: string): Promise<SitePage[]> => {
289
let descendants: ContentSummary[] = [];
290
let start = 0;
291
292
for (let i = 0; i < MAX_PAGES_TO_LOAD; i++) {
293
const result: WrappedResult<ContentSummary> = await client
294
.getDescendantsPage(parentId, start);
295
if (result.results.length === 0) {
296
break;
297
}
298
299
descendants = [...descendants, ...result.results];
300
301
start = start + DESCENDANT_PAGE_SIZE;
302
}
303
304
trace("descendants.length", descendants);
305
306
const contentProperties: ContentProperty[][] = await Promise.all(
307
descendants.map((page: ContentSummary) =>
308
client.getContentProperty(page.id ?? "")
309
),
310
);
311
312
const sitePageList: SitePage[] = mergeSitePages(
313
descendants,
314
contentProperties,
315
);
316
317
return sitePageList;
318
};
319
320
const uploadAttachments = (
321
baseDirectory: string,
322
attachmentsToUpload: string[],
323
parentId: string,
324
filePath: string,
325
existingAttachments: AttachmentSummary[] = [],
326
): Promise<AttachmentSummary | null>[] => {
327
const uploadAttachment = async (
328
attachmentPath: string,
329
): Promise<AttachmentSummary | null> => {
330
let fileBuffer: Uint8Array;
331
let fileHash: string;
332
const path = join(baseDirectory, attachmentPath);
333
334
trace(
335
"uploadAttachment",
336
{
337
baseDirectory,
338
attachmentPath,
339
attachmentsToUpload,
340
parentId,
341
existingAttachments,
342
path,
343
},
344
LogPrefix.ATTACHMENT,
345
);
346
347
try {
348
fileBuffer = await Deno.readFile(path);
349
fileHash = await md5HashBytes(fileBuffer);
350
} catch (error) {
351
logError(`${path} not found`, error);
352
return null;
353
}
354
355
const fileName = pathWithForwardSlashes(attachmentPath);
356
357
const existingDuplicateAttachment = existingAttachments.find(
358
(attachment: AttachmentSummary) => {
359
return attachment?.metadata?.comment === fileHash;
360
},
361
);
362
363
if (existingDuplicateAttachment) {
364
trace(
365
"existing duplicate attachment found",
366
existingDuplicateAttachment.title,
367
LogPrefix.ATTACHMENT,
368
);
369
return existingDuplicateAttachment;
370
}
371
372
const file = new File([fileBuffer as BlobPart], fileName);
373
const attachment: AttachmentSummary = await client
374
.createOrUpdateAttachment(parentId, file, fileHash);
375
376
trace("attachment", attachment, LogPrefix.ATTACHMENT);
377
378
return attachment;
379
};
380
381
return attachmentsToUpload.map(uploadAttachment);
382
};
383
384
const updateContent = async (
385
user: User,
386
publishFiles: PublishFiles,
387
id: string,
388
body: ContentBody,
389
titleToUpdate: string = title,
390
fileName: string = "",
391
uploadFileAttachments: boolean = true,
392
): Promise<PublishContentResult> => {
393
const previousPage = await client.getContent(id);
394
395
const attachmentsToUpload: string[] = findAttachments(
396
body.storage.value,
397
publishFiles.files,
398
fileName,
399
);
400
401
let uniqueTitle = titleToUpdate;
402
403
if (previousPage.title !== titleToUpdate) {
404
uniqueTitle = await uniquifyTitle(titleToUpdate, id);
405
}
406
407
trace("attachmentsToUpload", attachmentsToUpload, LogPrefix.ATTACHMENT);
408
409
const updatedBody: ContentBody = updateImagePaths(body);
410
updatedBody.storage.value = footnoteTransform(updatedBody.storage.value);
411
412
const toUpdate: ContentUpdate = {
413
contentChangeType: ContentChangeType.update,
414
id,
415
version: getNextVersion(previousPage),
416
title: uniqueTitle,
417
type: PAGE_TYPE,
418
status: ContentStatusEnum.current,
419
ancestors: null,
420
body: updatedBody,
421
};
422
423
trace("updateContent", toUpdate);
424
trace("updateContent body", toUpdate?.body?.storage?.value);
425
426
const updatedContent: Content = await client.updateContent(user, toUpdate);
427
428
if (toUpdate.id && uploadFileAttachments) {
429
const existingAttachments: AttachmentSummary[] = await client
430
.getAttachments(toUpdate.id);
431
432
trace(
433
"attachments",
434
{ existingAttachments, attachmentsToUpload },
435
LogPrefix.ATTACHMENT,
436
);
437
438
const uploadAttachmentsResult = await Promise.all(
439
uploadAttachments(
440
publishFiles.baseDir,
441
attachmentsToUpload,
442
toUpdate.id,
443
fileName,
444
existingAttachments,
445
),
446
);
447
trace(
448
"uploadAttachmentsResult",
449
uploadAttachmentsResult,
450
LogPrefix.ATTACHMENT,
451
);
452
}
453
454
return {
455
content: updatedContent,
456
hasAttachments: attachmentsToUpload.length > 0,
457
};
458
};
459
460
const createSiteParent = async (
461
title: string,
462
body: ContentBody,
463
): Promise<Content> => {
464
let ancestors: ContentAncestor[] = [];
465
466
if (parent?.parent) {
467
ancestors = [{ id: parent.parent }];
468
} else if (space.homepage?.id) {
469
ancestors = [{ id: space.homepage?.id }];
470
}
471
472
const toCreate: ContentCreate = {
473
contentChangeType: ContentChangeType.create,
474
title,
475
type: PAGE_TYPE,
476
space,
477
status: ContentStatusEnum.current,
478
ancestors,
479
body,
480
};
481
482
const createdContent = await client.createContent(user, toCreate);
483
return createdContent;
484
};
485
486
const checkToCreateSiteParent = async (
487
parentId: string = "",
488
): Promise<string> => {
489
let isQuartoSiteParent = false;
490
491
const existingSiteParent: any = await client.getContent(parentId);
492
493
if (existingSiteParent?.id) {
494
const siteParentContentProperties: ContentProperty[] = await client
495
.getContentProperty(existingSiteParent.id ?? "");
496
497
isQuartoSiteParent = siteParentContentProperties.find(
498
(property: ContentProperty) =>
499
property.key === ContentPropertyKey.isQuartoSiteParent,
500
) !== undefined;
501
}
502
503
if (!isQuartoSiteParent) {
504
const body: ContentBody = {
505
storage: {
506
value: "",
507
representation: ContentBodyRepresentation.storage,
508
},
509
};
510
511
const siteParentTitle = await uniquifyTitle(title);
512
const siteParent: ContentSummary = await createSiteParent(
513
siteParentTitle,
514
body,
515
);
516
517
const newSiteParentId: string = siteParent.id ?? "";
518
519
const contentProperty: Content = await client.createContentProperty(
520
newSiteParentId,
521
{ key: ContentPropertyKey.isQuartoSiteParent, value: true },
522
);
523
524
parentId = newSiteParentId;
525
}
526
return parentId;
527
};
528
529
const createContent = async (
530
publishFiles: PublishFiles,
531
body: ContentBody,
532
titleToCreate: string = title,
533
createParent: ConfluenceParent = parent,
534
fileNameParam: string = "",
535
): Promise<PublishContentResult> => {
536
const createTitle = await uniquifyTitle(titleToCreate);
537
538
const fileName = pathWithForwardSlashes(fileNameParam);
539
540
const attachmentsToUpload: string[] = findAttachments(
541
body.storage.value,
542
publishFiles.files,
543
fileName,
544
);
545
546
trace("attachmentsToUpload", attachmentsToUpload, LogPrefix.ATTACHMENT);
547
const updatedBody: ContentBody = updateImagePaths(body);
548
updatedBody.storage.value = footnoteTransform(updatedBody.storage.value);
549
550
const toCreate: ContentCreate = {
551
contentChangeType: ContentChangeType.create,
552
title: createTitle,
553
type: PAGE_TYPE,
554
space,
555
status: ContentStatusEnum.current,
556
ancestors: createParent?.parent ? [{ id: createParent.parent }] : null,
557
body: updatedBody,
558
};
559
560
trace("createContent", { publishFiles, toCreate });
561
const createdContent = await client.createContent(user, toCreate);
562
563
if (createdContent.id) {
564
const uploadAttachmentsResult = await Promise.all(
565
uploadAttachments(
566
publishFiles.baseDir,
567
attachmentsToUpload,
568
createdContent.id,
569
fileName,
570
),
571
);
572
trace(
573
"uploadAttachmentsResult",
574
uploadAttachmentsResult,
575
LogPrefix.ATTACHMENT,
576
);
577
}
578
579
return {
580
content: createdContent,
581
hasAttachments: attachmentsToUpload.length > 0,
582
};
583
};
584
585
const publishDocument = async (): Promise<
586
[[PublishRecord, URL | undefined], boolean]
587
> => {
588
const publishFiles: PublishFiles = await renderDocument(render);
589
590
const body: ContentBody = loadDocument(
591
publishFiles.baseDir,
592
publishFiles.rootFile,
593
);
594
595
trace("publishDocument", { publishFiles, body }, LogPrefix.RENDER);
596
597
let publishResult: PublishContentResult | undefined;
598
let message: string = "";
599
let doOperation;
600
601
if (publishRecord) {
602
message = `Updating content at ${publishRecord.url}...`;
603
doOperation = async () => {
604
const result = await updateContent(
605
user,
606
publishFiles,
607
publishRecord.id,
608
body,
609
);
610
publishResult = result;
611
};
612
} else {
613
message = `Creating content in space ${parent.space}...`;
614
doOperation = async () => {
615
const result = await createContent(publishFiles, body);
616
publishResult = result;
617
};
618
}
619
try {
620
await doWithSpinner(message, doOperation);
621
return [
622
buildPublishRecordForContent(server, publishResult?.content),
623
!!publishResult?.hasAttachments,
624
];
625
} catch (error: any) {
626
trace("Error Performing Operation", error);
627
trace("Value to Update", body?.storage?.value);
628
throw error;
629
}
630
};
631
632
const publishSite = async (): Promise<
633
[[PublishRecord, URL | undefined], boolean]
634
> => {
635
let parentId: string = parent?.parent ?? space.homepage.id ?? "";
636
637
parentId = await checkToCreateSiteParent(parentId);
638
639
const siteParent: ConfluenceParent = {
640
space: parent.space,
641
parent: parentId,
642
};
643
644
let existingSite: SitePage[] = await fetchExistingSite(parentId);
645
trace("existingSite", existingSite);
646
647
const publishFiles: PublishFiles = await renderSite(render);
648
const metadataByInput: Record<string, InputMetadata> =
649
publishFiles.metadataByInput ?? {};
650
651
trace("metadataByInput", metadataByInput);
652
653
trace("publishSite", {
654
parentId,
655
publishFiles,
656
});
657
658
const filteredFiles: string[] = filterFilesForUpdate(publishFiles.files);
659
660
trace("filteredFiles", filteredFiles);
661
662
const assembleSiteFileMetadata = async (
663
fileName: string,
664
): Promise<SiteFileMetadata> => {
665
const fileToContentBody = async (
666
fileName: string,
667
): Promise<ContentBody> => {
668
return loadDocument(publishFiles.baseDir, fileName);
669
};
670
671
const originalTitle = getTitle(fileName, metadataByInput);
672
const title = originalTitle;
673
674
return await {
675
fileName,
676
title,
677
originalTitle,
678
contentBody: await fileToContentBody(fileName),
679
};
680
};
681
682
const fileMetadata: SiteFileMetadata[] = await Promise.all(
683
filteredFiles.map(assembleSiteFileMetadata),
684
);
685
686
trace("fileMetadata", fileMetadata);
687
688
let metadataByFilename = buildFileToMetaTable(existingSite);
689
690
trace("metadataByFilename", metadataByFilename);
691
692
let changeList: ConfluenceSpaceChange[] = buildSpaceChanges(
693
fileMetadata,
694
siteParent,
695
space,
696
existingSite,
697
);
698
699
changeList = flattenIndexes(changeList, metadataByFilename, parentId);
700
701
const { pass1Changes, pass2Changes } = updateLinks(
702
metadataByFilename,
703
changeList,
704
server,
705
siteParent,
706
);
707
708
changeList = pass1Changes;
709
710
trace("changelist Pass 1", changeList);
711
712
let pathsToId: Record<string, string> = {}; // build from existing site
713
714
const handleChangeError = (
715
label: string,
716
currentChange: ConfluenceSpaceChange,
717
error: any,
718
) => {
719
if (isContentUpdate(currentChange) || isContentCreate(currentChange)) {
720
trace("currentChange.fileName", currentChange.fileName);
721
trace("Value to Update", currentChange.body.storage.value);
722
}
723
if (EXIT_ON_ERROR) {
724
throw error;
725
}
726
};
727
let hasAttachments = false;
728
729
const doChange = async (
730
change: ConfluenceSpaceChange,
731
uploadFileAttachments: boolean = true,
732
) => {
733
if (isContentCreate(change)) {
734
if (change.fileName === "sitemap.xml") {
735
trace("sitemap.xml skipped", change);
736
return;
737
}
738
739
let ancestorId = (change?.ancestors && change?.ancestors[0]?.id) ??
740
null;
741
742
if (ancestorId && pathsToId[ancestorId]) {
743
ancestorId = pathsToId[ancestorId];
744
}
745
746
const ancestorParent: ConfluenceParent = {
747
space: parent.space,
748
parent: ancestorId ?? siteParent.parent,
749
};
750
751
const universalPath = pathWithForwardSlashes(change.fileName ?? "");
752
753
const result = await createContent(
754
publishFiles,
755
change.body,
756
change.title ?? "",
757
ancestorParent,
758
universalPath,
759
);
760
761
if (universalPath) {
762
pathsToId[universalPath] = result.content.id ?? "";
763
}
764
765
const contentPropertyResult: Content = await client
766
.createContentProperty(result.content.id ?? "", {
767
key: ContentPropertyKey.fileName,
768
value: (change as ContentCreate).fileName,
769
});
770
hasAttachments = hasAttachments || result.hasAttachments;
771
return result;
772
} else if (isContentUpdate(change)) {
773
const update = change as ContentUpdate;
774
const result = await updateContent(
775
user,
776
publishFiles,
777
update.id ?? "",
778
update.body,
779
update.title ?? "",
780
update.fileName ?? "",
781
uploadFileAttachments,
782
);
783
hasAttachments = hasAttachments || result.hasAttachments;
784
return result;
785
} else if (isContentDelete(change)) {
786
if (DELETE_DISABLED) {
787
console.warn("DELETE DISABELD");
788
return null;
789
}
790
const result = await client.deleteContent(change);
791
await sleep(DELETE_SLEEP_MILLIS); // TODO replace with polling
792
return { content: result, hasAttachments: false };
793
} else {
794
console.error("Space Change not defined");
795
return null;
796
}
797
};
798
799
let pass1Count = 0;
800
for (let currentChange of changeList) {
801
try {
802
pass1Count = pass1Count + 1;
803
const doOperation = async () => await doChange(currentChange);
804
await doWithSpinner(
805
`Site Updates [${pass1Count}/${changeList.length}]`,
806
doOperation,
807
);
808
} catch (error: any) {
809
handleChangeError(
810
"Error Performing Change Pass 1",
811
currentChange,
812
error,
813
);
814
}
815
}
816
817
if (pass2Changes.length) {
818
//PASS #2 to update links to newly created pages
819
820
trace("changelist Pass 2", pass2Changes);
821
822
existingSite = await fetchExistingSite(parentId);
823
metadataByFilename = buildFileToMetaTable(existingSite);
824
825
const linkUpdateChanges: ConfluenceSpaceChange[] = convertForSecondPass(
826
metadataByFilename,
827
pass2Changes,
828
server,
829
parent,
830
);
831
832
let pass2Count = 0;
833
for (let currentChange of linkUpdateChanges) {
834
try {
835
pass2Count = pass2Count + 1;
836
const doOperation = async () => await doChange(currentChange, false);
837
await doWithSpinner(
838
`Updating Links [${pass2Count}/${linkUpdateChanges.length}]`,
839
doOperation,
840
);
841
} catch (error: any) {
842
handleChangeError(
843
"Error Performing Change Pass 2",
844
currentChange,
845
error,
846
);
847
}
848
}
849
}
850
851
const parentPage: Content = await client.getContent(parentId);
852
return [buildPublishRecordForContent(server, parentPage), hasAttachments];
853
};
854
855
if (type === PublishTypeEnum.document) {
856
const [publishResult, hasAttachments] = await publishDocument();
857
if (hasAttachments) {
858
info(
859
"\nNote: The published content includes attachments or images. You may see a placeholder for a few moments while Confluence processes the image or attachment.\n",
860
);
861
}
862
return publishResult;
863
} else {
864
const [publishResult, hasAttachments] = await publishSite();
865
if (hasAttachments) {
866
info(
867
"\nNote: The published content includes attachments or images. You may see a placeholder for a few moments while Confluence processes the image or attachment.\n",
868
);
869
}
870
return publishResult;
871
}
872
}
873
874
export const confluenceProvider: PublishProvider = {
875
name: CONFLUENCE_ID,
876
description: "Confluence",
877
hidden: false,
878
requiresServer: true,
879
requiresRender: true,
880
accountTokens: getAccountTokens,
881
authorizeToken: promptAndAuthorizeToken,
882
removeToken,
883
resolveTarget,
884
publish,
885
isUnauthorized,
886
isNotFound,
887
};
888
889