Merge pull request #16820 from tradcliffe-expediagroup/tradcliffe-expediagroup/confluence-to-markdown-action
Tradcliffe expediagroup/confluence to markdown action
This commit is contained in:
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,118 @@
|
||||
# @backstage/plugin-scaffolder-backend-module-confluence-to-markdown
|
||||
|
||||
Welcome to the `confluence:transform:markdown` action for the `scaffolder-backend`.
|
||||
|
||||
## Getting started
|
||||
|
||||
You need to configure the action in your backend:
|
||||
|
||||
## From your Backstage root directory
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-confluence-to-markdown
|
||||
```
|
||||
|
||||
Configure the action:
|
||||
(you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options):
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/plugins/scaffolder.ts
|
||||
|
||||
import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { createConfluenceToMarkdownAction } from '@backstage/plugin-scaffolder-backend-module-confluence-to-markdown';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const catalogClient = new CatalogClient({ discoveryApi: env.discovery });
|
||||
const integrations = ScmIntegrations.fromConfig(env.config);
|
||||
|
||||
const builtInActions = createBuiltinActions({
|
||||
integrations,
|
||||
catalogClient,
|
||||
config: env.config,
|
||||
reader: env.reader,
|
||||
});
|
||||
|
||||
const actions = [
|
||||
...builtInActions,
|
||||
createConfluenceToMarkdownAction({
|
||||
integrations,
|
||||
config: env.config,
|
||||
reader: env.reader,
|
||||
}),
|
||||
];
|
||||
|
||||
return createRouter({
|
||||
actions,
|
||||
catalogClient: catalogClient,
|
||||
logger: env.logger,
|
||||
config: env.config,
|
||||
database: env.database,
|
||||
reader: env.reader,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
You will also need an access token for authorization with `Read` permissions. You can create a Personal Access Token (PAT) in confluence and add the PAT to your `app-config.yaml`
|
||||
|
||||
```yaml
|
||||
confluence:
|
||||
baseUrl: ${CONFLUENCE_BASE_URL}
|
||||
token: ${CONFLUENCE_TOKEN}
|
||||
```
|
||||
|
||||
After that you can use the action in your template:
|
||||
|
||||
```yaml
|
||||
apiVersion: scaffolder.backstage.io/v1beta3
|
||||
kind: Template
|
||||
metadata:
|
||||
name: confluence-to-markdown
|
||||
title: Confluence to Markdown
|
||||
description: This template converts a single confluence document to Markdown for Techdocs and adds it to a given GitHub repo.
|
||||
tags:
|
||||
- do-not-use
|
||||
- poc
|
||||
spec:
|
||||
owner: <YOUR_EMAIL>
|
||||
type: service
|
||||
parameters:
|
||||
- title: Confluence and Github Repo Information
|
||||
properties:
|
||||
confluenceUrls:
|
||||
type: array
|
||||
description: Urls for confluence doc to be converted to markdown. In format <CONFLUENCE_BASE_URL>/display/<SPACEKEY>/<PAGE+TITLE>
|
||||
items:
|
||||
type: string
|
||||
default: confluence url
|
||||
ui:options:
|
||||
addable: true
|
||||
minItems: 1
|
||||
maxItems: 5
|
||||
repoUrl:
|
||||
type: string
|
||||
title: GitHub URL mkdocs.yaml link
|
||||
description: The GitHub repo URL to your mkdocs.yaml file. Example <https://github.com/blob/master/mkdocs.yml>
|
||||
steps:
|
||||
- id: create-docs
|
||||
name: Get markdown file created and update markdown.yaml file
|
||||
action: confluence:transform:markdown
|
||||
input:
|
||||
confluenceUrls: ${{ parameters.confluenceUrls }}
|
||||
repoUrl: ${{ parameters.repoUrl }}
|
||||
- id: publish
|
||||
name: Publish PR to GitHub
|
||||
action: publish:github:pull-request
|
||||
input:
|
||||
repoUrl: <GITHUB_BASE_URL>?repo=${{ steps['create-docs'].output.repo }}&owner=${{ steps['create-docs'].output.owner }}
|
||||
branchName: confluence-to-markdown
|
||||
title: Confluence to Markdown
|
||||
description: PR for converting confluence page to mkdocs
|
||||
```
|
||||
|
||||
Replace `<GITHUB_BASE_URL>` with your GitHub URL without `https://`.
|
||||
|
||||
You can find a list of all registered actions including their parameters at the /create/actions route in your Backstage application.
|
||||
@@ -0,0 +1,20 @@
|
||||
## API Report File for "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { Config } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
|
||||
// @public (undocumented)
|
||||
export const createConfluenceToMarkdownAction: (options: {
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrations;
|
||||
config: Config;
|
||||
}) => TemplateAction<{
|
||||
confluenceUrls: string[];
|
||||
repoUrl: string;
|
||||
}>;
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown",
|
||||
"description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"help": "backstage-cli help"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-backend": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"fs-extra": "10.1.0",
|
||||
"git-url-parse": "^13.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"yaml": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"mock-fs": "^5.2.0",
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { PassThrough } from 'stream';
|
||||
import { createConfluenceToMarkdownAction } from './confluenceToMarkdown';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import type { ActionContext } from '@backstage/plugin-scaffolder-node';
|
||||
import { readFile, writeFile, createWriteStream } from 'fs-extra';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
jest.mock('fs-extra', () => ({
|
||||
mkdirSync: jest.fn(),
|
||||
readFile: jest.fn().mockResolvedValue('File contents'),
|
||||
writeFile: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve();
|
||||
}),
|
||||
outputFile: jest.fn(),
|
||||
openSync: jest.fn(),
|
||||
createWriteStream: jest.fn().mockReturnValue(new PassThrough()),
|
||||
ensureDir: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('confluence:transform:markdown', () => {
|
||||
const baseUrl = `https://nodomain.confluence.com`;
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
const config = new ConfigReader({
|
||||
confluence: {
|
||||
baseUrl: baseUrl,
|
||||
token: 'fake_token',
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [{ host: 'github.com', token: 'token' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let reader: UrlReader;
|
||||
let mockContext: ActionContext<{
|
||||
confluenceUrls: string[];
|
||||
repoUrl: string;
|
||||
}>;
|
||||
|
||||
const logger = getVoidLogger();
|
||||
jest.spyOn(logger, 'info');
|
||||
|
||||
const mockTmpDir = os.tmpdir();
|
||||
|
||||
beforeEach(() => {
|
||||
reader = {
|
||||
readUrl: jest.fn(),
|
||||
readTree: jest.fn().mockResolvedValue({
|
||||
dir: jest.fn(),
|
||||
}),
|
||||
search: jest.fn(),
|
||||
};
|
||||
mockContext = {
|
||||
input: {
|
||||
confluenceUrls: [
|
||||
'https://nodomain.confluence.com/display/testing/mkdocs',
|
||||
],
|
||||
repoUrl: 'https://notreal.github.com/space/backstage/mkdocs.yml',
|
||||
},
|
||||
workspacePath: '/tmp',
|
||||
logger,
|
||||
logStream: new PassThrough(),
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir),
|
||||
};
|
||||
mockFs({ [`${mockTmpDir}/src/docs`]: {} });
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should call confluence to markdown action successfully with results array', async () => {
|
||||
const options = {
|
||||
reader,
|
||||
integrations,
|
||||
config,
|
||||
};
|
||||
const responseBody = {
|
||||
results: [
|
||||
{
|
||||
id: '4444444',
|
||||
type: 'page',
|
||||
title: 'Testing',
|
||||
body: {
|
||||
export_view: {
|
||||
value: '<p>hello world</p>',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const responseBodyTwo = {
|
||||
results: [
|
||||
{
|
||||
id: '4444444',
|
||||
type: 'attachment',
|
||||
title: 'testing.pdf',
|
||||
metadata: {
|
||||
mediaType: 'application/pdf',
|
||||
},
|
||||
_links: {
|
||||
download: '/download/attachments/4444444/testing.pdf',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
worker.use(
|
||||
rest.get(`${baseUrl}/rest/api/content`, (_, res, ctx) =>
|
||||
res(ctx.status(200, 'OK'), ctx.json(responseBody)),
|
||||
),
|
||||
rest.get(
|
||||
`${baseUrl}/rest/api/content/4444444/child/attachment`,
|
||||
(_, res, ctx) => res(ctx.status(200, 'OK'), ctx.json(responseBodyTwo)),
|
||||
),
|
||||
rest.get(
|
||||
`${baseUrl}/download/attachments/4444444/testing.pdf`,
|
||||
(_, res, ctx) => res(ctx.status(200, 'OK'), ctx.body('hello')),
|
||||
),
|
||||
);
|
||||
|
||||
const action = createConfluenceToMarkdownAction(options);
|
||||
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
`Fetching the mkdocs.yml catalog from https://notreal.github.com/space/backstage/mkdocs.yml`,
|
||||
);
|
||||
expect(logger.info).toHaveBeenCalledTimes(5);
|
||||
expect(createWriteStream).toHaveBeenCalledTimes(1);
|
||||
expect(readFile).toHaveBeenCalledTimes(1);
|
||||
expect(writeFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call confluence to markdown action successfully with empty results array', async () => {
|
||||
const options = {
|
||||
reader,
|
||||
integrations,
|
||||
config,
|
||||
};
|
||||
const responseBody = {
|
||||
results: [
|
||||
{
|
||||
id: '4444444',
|
||||
type: 'page',
|
||||
title: 'Testing',
|
||||
body: {
|
||||
export_view: {
|
||||
value: '<p>hello world</p>',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const responseBodyTwo = {
|
||||
results: [],
|
||||
};
|
||||
|
||||
worker.use(
|
||||
rest.get(`${baseUrl}/rest/api/content`, (_, res, ctx) =>
|
||||
res(ctx.status(200, 'OK'), ctx.json(responseBody)),
|
||||
),
|
||||
rest.get(
|
||||
`${baseUrl}/rest/api/content/4444444/child/attachment`,
|
||||
(_, res, ctx) => res(ctx.status(200, 'OK'), ctx.json(responseBodyTwo)),
|
||||
),
|
||||
);
|
||||
|
||||
const action = createConfluenceToMarkdownAction(options);
|
||||
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
`Fetching the mkdocs.yml catalog from https://notreal.github.com/space/backstage/mkdocs.yml`,
|
||||
);
|
||||
expect(logger.info).toHaveBeenCalledTimes(5);
|
||||
|
||||
expect(createWriteStream).not.toHaveBeenCalled();
|
||||
expect(readFile).toHaveBeenCalledTimes(1);
|
||||
expect(writeFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shoud fail on the first fetch call with response.ok set to false', async () => {
|
||||
const options = {
|
||||
reader,
|
||||
integrations,
|
||||
config,
|
||||
};
|
||||
const responseBody = {};
|
||||
|
||||
worker.use(
|
||||
rest.get(`${baseUrl}/rest/api/content`, (_, res, ctx) =>
|
||||
res(ctx.status(401, 'nope'), ctx.json(responseBody)),
|
||||
),
|
||||
);
|
||||
|
||||
const action = createConfluenceToMarkdownAction(options);
|
||||
await expect(async () => {
|
||||
await action.handler(mockContext);
|
||||
}).rejects.toThrow('Request failed with 401 Error');
|
||||
});
|
||||
|
||||
it('should return nothing in results from the first api call and fail', async () => {
|
||||
const options = {
|
||||
reader,
|
||||
integrations,
|
||||
config,
|
||||
};
|
||||
const responseBody = {
|
||||
results: [],
|
||||
};
|
||||
|
||||
worker.use(
|
||||
rest.get(`${baseUrl}/rest/api/content`, (_, res, ctx) =>
|
||||
res(ctx.status(200, 'OK'), ctx.json(responseBody)),
|
||||
),
|
||||
);
|
||||
|
||||
const action = createConfluenceToMarkdownAction(options);
|
||||
await expect(async () => {
|
||||
await action.handler(mockContext);
|
||||
}).rejects.toThrow(
|
||||
'Could not find document https://nodomain.confluence.com/display/testing/mkdocs. Please check your input.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail on the second fetch call to confluence', async () => {
|
||||
const options = {
|
||||
reader,
|
||||
integrations,
|
||||
config,
|
||||
};
|
||||
const responseBody = {
|
||||
results: [
|
||||
{
|
||||
id: '4444444',
|
||||
type: 'page',
|
||||
title: 'Testing',
|
||||
body: {
|
||||
export_view: {
|
||||
value: '<p>hello world</p>',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const responseBodyTwo = {};
|
||||
|
||||
worker.use(
|
||||
rest.get(`${baseUrl}/rest/api/content`, (_, res, ctx) =>
|
||||
res(ctx.status(200, 'OK'), ctx.json(responseBody)),
|
||||
),
|
||||
rest.get(
|
||||
`${baseUrl}/rest/api/content/4444444/child/attachment`,
|
||||
(_, res, ctx) =>
|
||||
res(ctx.status(404, 'nope'), ctx.json(responseBodyTwo)),
|
||||
),
|
||||
);
|
||||
|
||||
const action = createConfluenceToMarkdownAction(options);
|
||||
await expect(async () => {
|
||||
await action.handler(mockContext);
|
||||
}).rejects.toThrow('Request failed with 404 Error');
|
||||
});
|
||||
});
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Config } from '@backstage/config';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { createFetchPlainAction } from '@backstage/plugin-scaffolder-backend';
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { InputError, ConflictError } from '@backstage/errors';
|
||||
import { NodeHtmlMarkdown } from 'node-html-markdown';
|
||||
import fs from 'fs-extra';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import YAML from 'yaml';
|
||||
import {
|
||||
readFileAsString,
|
||||
fetchConfluence,
|
||||
getAndWriteAttachments,
|
||||
createConfluenceVariables,
|
||||
} from './helpers';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
export const createConfluenceToMarkdownAction = (options: {
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrations;
|
||||
config: Config;
|
||||
}) => {
|
||||
const { config, reader, integrations } = options;
|
||||
const fetchPlainAction = createFetchPlainAction({ reader, integrations });
|
||||
type Obj = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
return createTemplateAction<{
|
||||
confluenceUrls: string[];
|
||||
repoUrl: string;
|
||||
}>({
|
||||
id: 'confluence:transform:markdown',
|
||||
schema: {
|
||||
input: {
|
||||
properties: {
|
||||
confluenceUrls: {
|
||||
type: 'array',
|
||||
title: 'Confluence URL',
|
||||
description:
|
||||
'Paste your confluence url. Ensure it follows this format: https://{confluence+base+url}/display/{spacekey}/{page+title}',
|
||||
items: {
|
||||
type: 'string',
|
||||
default: 'Confluence URL',
|
||||
},
|
||||
},
|
||||
repoUrl: {
|
||||
type: 'string',
|
||||
title: 'GitHub Repo Url',
|
||||
description:
|
||||
'mkdocs.yml file location inside the github repo you want to store the document',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { confluenceUrls, repoUrl } = ctx.input;
|
||||
const parsedRepoUrl = parseGitUrl(repoUrl);
|
||||
const filePathToMkdocs = parsedRepoUrl.filepath.substring(
|
||||
0,
|
||||
parsedRepoUrl.filepath.lastIndexOf('/') + 1,
|
||||
);
|
||||
const dirPath = ctx.workspacePath;
|
||||
let productArray: string[][] = [];
|
||||
|
||||
ctx.logger.info(`Fetching the mkdocs.yml catalog from ${repoUrl}`);
|
||||
|
||||
// This grabs the files from Github
|
||||
const repoFileDir = `${dirPath}/${parsedRepoUrl.filepath}`;
|
||||
await fetchPlainAction.handler({
|
||||
...ctx,
|
||||
input: {
|
||||
url: `https://${parsedRepoUrl.resource}/${parsedRepoUrl.owner}/${parsedRepoUrl.name}`,
|
||||
targetPath: dirPath,
|
||||
},
|
||||
});
|
||||
|
||||
for (const url of confluenceUrls) {
|
||||
const { spacekey, title, titleWithSpaces } =
|
||||
await createConfluenceVariables(url);
|
||||
// This calls confluence to get the page html and page id
|
||||
const getConfluenceDoc = await fetchConfluence(
|
||||
`/rest/api/content?title=${title}&spaceKey=${spacekey}&expand=body.export_view`,
|
||||
config,
|
||||
);
|
||||
if (getConfluenceDoc.results.length === 0) {
|
||||
throw new InputError(
|
||||
`Could not find document ${url}. Please check your input.`,
|
||||
);
|
||||
}
|
||||
// This gets attachements for the confluence page if they exist
|
||||
const getDocAttachments = await fetchConfluence(
|
||||
`/rest/api/content/${getConfluenceDoc.results[0].id}/child/attachment`,
|
||||
config,
|
||||
);
|
||||
|
||||
if (getDocAttachments.results.length) {
|
||||
fs.mkdirSync(`${dirPath}/${filePathToMkdocs}docs/img`, {
|
||||
recursive: true,
|
||||
});
|
||||
productArray = await getAndWriteAttachments(
|
||||
getDocAttachments,
|
||||
dirPath,
|
||||
config,
|
||||
filePathToMkdocs,
|
||||
);
|
||||
}
|
||||
|
||||
ctx.logger.info(
|
||||
`starting action for converting ${titleWithSpaces} from Confluence To Markdown`,
|
||||
);
|
||||
|
||||
// This reads mkdocs.yml file
|
||||
const mkdocsFileContent = await readFileAsString(repoFileDir);
|
||||
const mkdocsFile = await YAML.parse(mkdocsFileContent);
|
||||
ctx.logger.info(
|
||||
`Adding new file - ${titleWithSpaces} to the current mkdocs.yml file`,
|
||||
);
|
||||
|
||||
// This modifies the mkdocs.yml file
|
||||
if (mkdocsFile !== undefined && mkdocsFile.hasOwnProperty('nav')) {
|
||||
const { nav } = mkdocsFile;
|
||||
if (!nav.some((i: Obj) => i.hasOwnProperty(titleWithSpaces))) {
|
||||
nav.push({
|
||||
[titleWithSpaces]: `${titleWithSpaces.replace(/\s+/g, '-')}.md`,
|
||||
});
|
||||
mkdocsFile.nav = nav;
|
||||
} else {
|
||||
throw new ConflictError(
|
||||
'This document looks to exist inside the GitHub repo. Will end the action.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(repoFileDir, YAML.stringify(mkdocsFile));
|
||||
|
||||
// This grabs the confluence html and converts it to markdown and adds attachments
|
||||
const html = getConfluenceDoc.results[0].body.export_view.value;
|
||||
const markdownToPublish = NodeHtmlMarkdown.translate(html);
|
||||
let newString: string = markdownToPublish;
|
||||
productArray.forEach((product: string[]) => {
|
||||
// This regex is looking for either [](link to confluence) or  in the newly created markdown doc and updating it to point to the versions saved(in ./docs/img) in the local version of GitHub Repo during getAndWriteAttachments
|
||||
const regex = product[0].includes('.pdf')
|
||||
? new RegExp(`(\\[.*?\\]\\()(.*?${product[0]}.*?)(\\))`, 'gi')
|
||||
: new RegExp(`(\\!\\[.*?\\]\\()(.*?${product[0]}.*?)(\\))`, 'gi');
|
||||
newString = newString.replace(regex, `$1./img/${product[1]}$3`);
|
||||
});
|
||||
|
||||
ctx.logger.info(`Adding new file to repo.`);
|
||||
await fs.outputFile(
|
||||
`${dirPath}/${filePathToMkdocs}docs/${titleWithSpaces.replace(
|
||||
/\s+/g,
|
||||
'-',
|
||||
)}.md`,
|
||||
newString,
|
||||
);
|
||||
}
|
||||
|
||||
ctx.output('repo', parsedRepoUrl.name);
|
||||
ctx.output('owner', parsedRepoUrl.owner);
|
||||
},
|
||||
});
|
||||
};
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { ResponseError, ConflictError, InputError } from '@backstage/errors';
|
||||
import fs from 'fs-extra';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
|
||||
interface Links {
|
||||
webui: string;
|
||||
download: string;
|
||||
thumbnail: string;
|
||||
self: string;
|
||||
}
|
||||
|
||||
interface Metadata {
|
||||
mediaType: string;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
id: string;
|
||||
type: string;
|
||||
status: string;
|
||||
title: string;
|
||||
metadata: Metadata;
|
||||
_links: Links;
|
||||
}
|
||||
|
||||
export interface Results {
|
||||
results: Result[];
|
||||
}
|
||||
|
||||
export const readFileAsString = async (fileDir: string) => {
|
||||
const content = await fs.readFile(fileDir, 'utf-8');
|
||||
return content.toString();
|
||||
};
|
||||
|
||||
export const fetchConfluence = async (relativeUrl: string, config: Config) => {
|
||||
const baseUrl = config.getString('confluence.baseUrl');
|
||||
const token = config.getString('confluence.token');
|
||||
const response: Response = await fetch(`${baseUrl}${relativeUrl}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getAndWriteAttachments = async (
|
||||
arr: Results,
|
||||
workspace: string,
|
||||
config: Config,
|
||||
mkdocsDir: string,
|
||||
) => {
|
||||
const productArr: string[][] = [];
|
||||
const baseUrl = config.getString('confluence.baseUrl');
|
||||
const token = config.getString('confluence.token');
|
||||
await Promise.all(
|
||||
await arr.results.map(async (result: Result) => {
|
||||
const downloadLink = result._links.download;
|
||||
const downloadTitle = result.title.replace(/ /g, '-');
|
||||
if (result.metadata.mediaType !== 'application/gliffy+json') {
|
||||
productArr.push([result.title.replace(/ /g, '%20'), downloadTitle]);
|
||||
}
|
||||
|
||||
const res = await fetch(`${baseUrl}${downloadLink}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw ResponseError.fromResponse(res);
|
||||
} else if (res.body !== null) {
|
||||
fs.openSync(`${workspace}/${mkdocsDir}docs/img/${downloadTitle}`, 'w');
|
||||
const writeStream = fs.createWriteStream(
|
||||
`${workspace}/${mkdocsDir}docs/img/${downloadTitle}`,
|
||||
);
|
||||
res.body.pipe(writeStream);
|
||||
await new Promise((resolve, reject) => {
|
||||
writeStream.on('finish', () => {
|
||||
resolve(`${workspace}/${mkdocsDir}docs/img/${downloadTitle}`);
|
||||
});
|
||||
writeStream.on('error', reject);
|
||||
});
|
||||
} else {
|
||||
throw new ConflictError(
|
||||
'No Body on the response. Can not save images from Confluence Doc',
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return productArr;
|
||||
};
|
||||
|
||||
export const createConfluenceVariables = async (url: string) => {
|
||||
let spacekey: string | undefined = undefined;
|
||||
let title: string | undefined = undefined;
|
||||
let titleWithSpaces: string | undefined = '';
|
||||
const params = new URL(url);
|
||||
if (params.pathname.split('/')[1] === 'display') {
|
||||
spacekey = params.pathname.split('/')[2];
|
||||
title = params.pathname.split('/')[3];
|
||||
titleWithSpaces = title?.replace(/\+/g, ' ');
|
||||
return { spacekey, title, titleWithSpaces };
|
||||
}
|
||||
throw new InputError(
|
||||
'The Url format for Confluence is incorrect. Acceptable format is `<CONFLUENCE_BASE_URL>/display/<SPACEKEY>/<PAGE+TITLE>`',
|
||||
);
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { createConfluenceToMarkdownAction } from './confluenceToMarkdown';
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './confluence';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The confluence-to-markdown module for \@backstage/plugin-scaffolder-backend.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './actions';
|
||||
Reference in New Issue
Block a user