diff --git a/components/markdown-confluence-sync/CHANGELOG.md b/components/markdown-confluence-sync/CHANGELOG.md index 018d8609..45f1eb50 100644 --- a/components/markdown-confluence-sync/CHANGELOG.md +++ b/components/markdown-confluence-sync/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Deprecated #### Removed +## [2.1.0] - 2025-06-10 + +### Added + +* feat: Add `dryRun` option, enabling to run the sync process without actually sending the content to Confluence. This is useful for testing the configuration and markdown files without making changes in Confluence. + ## [2.0.1] - 2025-04-15 ### Changed diff --git a/components/markdown-confluence-sync/README.md b/components/markdown-confluence-sync/README.md index 2d0578c7..0f753e4a 100644 --- a/components/markdown-confluence-sync/README.md +++ b/components/markdown-confluence-sync/README.md @@ -283,6 +283,7 @@ The namespace for the configuration of this library is `markdown-confluence-sync | `confluence.noticeMessage` | `string` | Notice message to add at the beginning of the Confluence pages. | | | `confluence.noticeTemplate` | `string` | Template string to use for the notice message. | | | `confluence.dryRun` | `boolean` | Log create, update or delete requests to Confluence instead of really making them | `false` | +| `dryRun` | `boolean` | Process markdown files without sending them to `confluence-sync`. Useful to early detection of possible errors in configuration, etc. Note that, requests that would be made to Confluence won't be logged, use `confluence.dryRun` for that, which also connects to Confluence to calculate the requests to do | `false` | | `config.readArguments` | `boolean` | Read configuration from arguments or not | `false` | | `config.readFile` | `boolean` | Read configuration from file or not | `false` | | `config.readEnvironment` | `boolean` | Read configuration from environment or not | `false` | diff --git a/components/markdown-confluence-sync/package.json b/components/markdown-confluence-sync/package.json index e12e6d62..ebb7df51 100644 --- a/components/markdown-confluence-sync/package.json +++ b/components/markdown-confluence-sync/package.json @@ -1,7 +1,7 @@ { "name": "@telefonica/markdown-confluence-sync", "description": "Creates/updates/deletes Confluence pages based on markdown files in a directory. Supports Mermaid diagrams and per-page configuration using frontmatter metadata. Works great with Docusaurus", - "version": "2.0.1", + "version": "2.1.0", "license": "Apache-2.0", "author": "Telefónica Innovación Digital", "repository": { diff --git a/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.ts b/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.ts index af2b2789..2b979fae 100644 --- a/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.ts +++ b/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.ts @@ -34,6 +34,8 @@ import type { FilesMetadataOption, ContentPreprocessorOptionDefinition, ContentPreprocessorOption, + MainDryRunOptionDefinition, + MainDryRunOption, } from "./MarkdownConfluenceSync.types.js"; const MODULE_NAME = "markdown-confluence-sync"; @@ -78,6 +80,13 @@ const contentPreprocessorOption: ContentPreprocessorOptionDefinition = { type: "unknown", }; +const dryRunOption: MainDryRunOptionDefinition = { + name: "dryRun", + type: "boolean", + default: false, + description: "Process markdown files without sending them to confluence-sync", +}; + export const MarkdownConfluenceSync: MarkdownConfluenceSyncConstructor = class MarkdownConfluenceSync implements MarkdownConfluenceSyncInterface { @@ -93,6 +102,7 @@ export const MarkdownConfluenceSync: MarkdownConfluenceSyncConstructor = class M private _filesMetadataOption: FilesMetadataOption; private _filesIgnoreOption: FilesIgnoreOption; private _contentPreprocessorOption: ContentPreprocessorOption; + private _dryRunOption: MainDryRunOption; private _cwd: string; constructor(config: Configuration) { @@ -128,6 +138,10 @@ export const MarkdownConfluenceSync: MarkdownConfluenceSyncConstructor = class M contentPreprocessorOption as ContentPreprocessorOptionDefinition, ) as unknown as ContentPreprocessorOption; + this._dryRunOption = this._configuration.addOption( + dryRunOption as MainDryRunOptionDefinition, + ); + const markdownLogger = this._logger.namespace(MARKDOWN_NAMESPACE); const confluenceConfig = @@ -154,9 +168,16 @@ export const MarkdownConfluenceSync: MarkdownConfluenceSyncConstructor = class M public async sync(): Promise { await this._init(); const pages = await this._markdownDocuments.read(); - await this._confluenceSync.sync( - this._markdownPagesToConfluencePages(pages), - ); + const convertedPages = this._markdownPagesToConfluencePages(pages); + const dryRun = this._dryRunOption.value; + if (dryRun) { + this._logger.info( + "Dry run mode is enabled. No changes will be made to Confluence.", + ); + return; + } + + await this._confluenceSync.sync(convertedPages); } private async _init() { diff --git a/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.types.ts b/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.types.ts index 53485d4c..95df82e3 100644 --- a/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.types.ts +++ b/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.types.ts @@ -27,6 +27,8 @@ export type FilesMetadata = FileMetadata[]; export type ContentPreprocessor = (content: string, path: string) => string; +export type DryRun = boolean; + declare global { //eslint-disable-next-line @typescript-eslint/no-namespace namespace MarkdownConfluenceSync { @@ -61,6 +63,9 @@ declare global { /** Hook enabling to modify the content of files before processing them */ preprocessor?: ContentPreprocessor; + + /** Process markdown files without sending them to confluence-sync */ + dryRun?: DryRun; } } } @@ -103,6 +108,16 @@ export type ContentPreprocessorOptionDefinition = export type ContentPreprocessorOption = OptionInterfaceOfType; +export type MainDryRunOptionDefinition = OptionDefinition< + boolean, + { hasDefault: true } +>; + +export type MainDryRunOption = OptionInterfaceOfType< + boolean, + { hasDefault: true } +>; + /** Creates a MarkdownConfluenceSync interface */ export interface MarkdownConfluenceSyncConstructor { /** Returns MarkdownConfluenceSync interface diff --git a/components/markdown-confluence-sync/test/component/fixtures/dry-run/docs/foo.md b/components/markdown-confluence-sync/test/component/fixtures/dry-run/docs/foo.md new file mode 100644 index 00000000..6e9ffa0d --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/dry-run/docs/foo.md @@ -0,0 +1,6 @@ +id: foo-ignored-index +title: foo-ignored-index-title +sync_to_confluence: true +--- + +# Hello World diff --git a/components/markdown-confluence-sync/test/component/fixtures/dry-run/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/dry-run/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..dd73bd96 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/dry-run/markdown-confluence-sync.config.cjs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Telefónica Innovación Digital +// SPDX-License-Identifier: MIT + +const path = require("node:path"); + +module.exports = { + confluence: { + url: "https://my-confluence.com", + spaceKey: "FOO", + }, + docsDir: path.join(__dirname, "./docs"), + dryRun: true, +}; diff --git a/components/markdown-confluence-sync/test/component/specs/dryRun.spec.ts b/components/markdown-confluence-sync/test/component/specs/dryRun.spec.ts new file mode 100644 index 00000000..459ab4dd --- /dev/null +++ b/components/markdown-confluence-sync/test/component/specs/dryRun.spec.ts @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2025 Telefónica Innovación Digital +// SPDX-License-Identifier: Apache-2.0 + +import { ChildProcessManager } from "@telefonica/child-process-manager"; +import type { ChildProcessManagerInterface } from "@telefonica/child-process-manager"; + +import { cleanLogs } from "../support/Logs"; +import { + getFixtureFolder, + getBinaryPathFromFixtureFolder, +} from "../support/Paths"; + +describe("dryRun mode", () => { + let cli: ChildProcessManagerInterface; + + beforeEach(() => { + process.env.MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL = "debug"; + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("dry-run"), + silent: true, + }); + }); + + afterEach(async () => { + await cli.kill(); + }); + + describe("when dryRun option is true", () => { + it("should exit with code 1 when there are errors in markdown", async () => { + const { exitCode, logs } = await cli.run(); + + const allLogs = cleanLogs(logs).join("\n"); + + expect(allLogs).toContain(`Title is required:`); + expect(allLogs).toContain(`dry-run/docs/foo.md`); + expect(allLogs).toContain( + `Please provide it using frontmatter or filesMetadata option`, + ); + expect(exitCode).toBe(1); + }); + + it("should not call to synchronize", async () => { + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-confluence-title"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_DRY_RUN: "true", + }, + }); + const { logs } = await cli.run(); + + const allLogs = cleanLogs(logs).join("\n"); + + expect(allLogs).toContain( + `Dry run mode is enabled. No changes will be made to Confluence.`, + ); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/MarkdownConfluenceSync.test.ts b/components/markdown-confluence-sync/test/unit/specs/MarkdownConfluenceSync.test.ts index a1edf2f5..9402fc6e 100644 --- a/components/markdown-confluence-sync/test/unit/specs/MarkdownConfluenceSync.test.ts +++ b/components/markdown-confluence-sync/test/unit/specs/MarkdownConfluenceSync.test.ts @@ -31,6 +31,21 @@ describe("markdownConfluenceSync", () => { ); }); + it("should not call to synchronize if dryRun is true", async () => { + // Arrange + const markdownConfluenceSync = new MarkdownConfluenceSync({ + ...CONFIG, + dryRun: true, + }); + customDocusaurusPages.read.mockResolvedValue([]); + + // Act + await markdownConfluenceSync.sync(); + + // Assert + expect(customConfluenceSync.sync).not.toHaveBeenCalled(); + }); + it("when called twice, it should send to synchronize the pages to confluence twice", async () => { // Arrange const markdownConfluenceSync = new MarkdownConfluenceSync(CONFIG);