adding new action confluence to markdown for GitHub repos onboarded onto techdocs
Signed-off-by: Tracey <tradcliffe@expediagroup.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,126 @@
|
||||
# @backstage/plugin-scaffolder-backend-module-confluence-to-markdowwn
|
||||
|
||||
Welcome to the `transform:confluence-to-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-markdowwn
|
||||
```
|
||||
|
||||
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-markdowwn';
|
||||
|
||||
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`
|
||||
|
||||
```
|
||||
confluence:
|
||||
baseUrl: ${CONFLUENCE_BASE_URL}
|
||||
token: ${CONFLUENCE_TOKEN}
|
||||
|
||||
```
|
||||
|
||||
After that you can use the action in your template:
|
||||
|
||||
```
|
||||
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> or <CONFLUENCE_BASE_URL>/pages/viewpage.action?spacekey=<SPACEKEY>&title=<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: transform:confluence-to-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
|
||||
- id: merge
|
||||
name: Merge PR
|
||||
action: publish:github:merge-pull-request
|
||||
input:
|
||||
prUrl: ${{ steps['publish'].output.remoteUrl }}
|
||||
forceAdmin: true
|
||||
|
||||
```
|
||||
|
||||
Replace `<GITHUB_BASE_URL>` with your GitHub url without `https://`.
|
||||
|
||||
You can find a list of all registred actions including their parameters at the /create/actions route in your Backstage application.
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdowwn",
|
||||
"description": "The confluence-to-markdowwn 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": "^0.0.1-security",
|
||||
"fs-extra": "^11.1.0",
|
||||
"git-url-parse": "^13.1.0",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"yaml": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"mock-fs": "^5.2.0",
|
||||
"os": "^0.1.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* 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 mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import fetch from 'node-fetch';
|
||||
import type { ActionContext } from '@backstage/plugin-scaffolder-node';
|
||||
import { readFile, writeFile } from 'fs-extra';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
jest.mock('node-fetch');
|
||||
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()),
|
||||
writeStream: jest.fn(),
|
||||
}));
|
||||
const { Response } = jest.requireActual('node-fetch');
|
||||
|
||||
describe('transform:confluence-to-markdown', () => {
|
||||
const config = new ConfigReader({
|
||||
confluence: {
|
||||
baseUrl: 'https://nodomain.confluence.com/',
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
(fetch as jest.MockedFunction<typeof fetch>)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(responseBody), {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(responseBodyTwo), {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(Readable.from('Hello world'), {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
}),
|
||||
);
|
||||
|
||||
const mockCreateWriteStream = jest.fn().mockReturnValue({
|
||||
on: jest.fn(),
|
||||
once: jest.fn((_event, callback) => {
|
||||
callback();
|
||||
}),
|
||||
});
|
||||
jest.mock('fs', () => ({
|
||||
openSync: jest.fn(),
|
||||
createWriteStream: jest.fn().mockReturnValue(mockCreateWriteStream),
|
||||
}));
|
||||
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(fetch).toHaveBeenCalledTimes(3);
|
||||
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: [],
|
||||
};
|
||||
|
||||
(fetch as jest.MockedFunction<typeof fetch>)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(responseBody), {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(responseBodyTwo), {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('fs-extra', () => ({
|
||||
readFile: jest.fn().mockResolvedValue('File contents'),
|
||||
}));
|
||||
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(fetch).toHaveBeenCalledTimes(2);
|
||||
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 = {};
|
||||
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(responseBody), {
|
||||
status: 401,
|
||||
statusText: 'npot',
|
||||
ok: false,
|
||||
}),
|
||||
);
|
||||
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: [],
|
||||
};
|
||||
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(responseBody), {
|
||||
status: 200,
|
||||
statusText: 'ok',
|
||||
}),
|
||||
);
|
||||
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 = {};
|
||||
|
||||
(fetch as jest.MockedFunction<typeof fetch>)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(responseBody), {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(responseBodyTwo), {
|
||||
status: 404,
|
||||
statusText: 'notok',
|
||||
}),
|
||||
);
|
||||
const action = createConfluenceToMarkdownAction(options);
|
||||
await expect(async () => {
|
||||
await action.handler(mockContext);
|
||||
}).rejects.toThrow('Request failed with 404 Error');
|
||||
});
|
||||
});
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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 {
|
||||
parseFromYaml,
|
||||
readFileAsString,
|
||||
transformToYaml,
|
||||
updateFile,
|
||||
fetchConfluence,
|
||||
getAndWriteAttachments,
|
||||
createConfluenceVariables,
|
||||
} from './helpers';
|
||||
|
||||
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: 'transform:confluence-to-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}`);
|
||||
ctx.logger.info(confluenceUrls);
|
||||
const params = new URL(confluenceUrls[0]);
|
||||
ctx.logger.info(params.searchParams);
|
||||
// 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 (let p = 0; p < confluenceUrls.length; p++) {
|
||||
const confluenceUrl = confluenceUrls[p];
|
||||
|
||||
const { spacekey, title, titleWithSpaces } =
|
||||
await createConfluenceVariables(confluenceUrl);
|
||||
ctx.logger.info(spacekey);
|
||||
ctx.logger.info(title);
|
||||
ctx.logger.info(titleWithSpaces);
|
||||
|
||||
// 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 ${confluenceUrl}. 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 = parseFromYaml(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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
const newTemplateFileContent = transformToYaml(mkdocsFile);
|
||||
await updateFile(newTemplateFileContent, repoFileDir);
|
||||
|
||||
// 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.map((product: string[]) => {
|
||||
const regex = product[1].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);
|
||||
},
|
||||
});
|
||||
};
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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 YAML from 'yaml';
|
||||
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 parseFromYaml = (content: string) => {
|
||||
return YAML.parse(content);
|
||||
};
|
||||
|
||||
export const transformToYaml = (content: any) => {
|
||||
return YAML.stringify(content);
|
||||
};
|
||||
|
||||
export const updateFile = async (content: string, file: string) => {
|
||||
await fs.writeFile(file, content);
|
||||
};
|
||||
|
||||
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() as Promise<any>;
|
||||
};
|
||||
|
||||
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}`,
|
||||
},
|
||||
});
|
||||
// console.log(`${workspace}/${mkdocsDir}docs/img/${downloadTitle}`)
|
||||
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 | null = null;
|
||||
let title: string | null = null;
|
||||
let titleWithSpaces: string | null = '';
|
||||
const params = new URL(url);
|
||||
if (url.includes('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-markdowwn module for @backstage/plugin-scaffolder-backend.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './actions';
|
||||
@@ -7651,6 +7651,29 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdowwn@workspace:plugins/scaffolder-backend-module-confluence-to-markdowwn":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-scaffolder-backend-module-confluence-to-markdowwn@workspace:plugins/scaffolder-backend-module-confluence-to-markdowwn"
|
||||
dependencies:
|
||||
"@backstage/backend-common": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/integration": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-backend": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
fs: ^0.0.1-security
|
||||
fs-extra: ^11.1.0
|
||||
git-url-parse: ^13.1.0
|
||||
mock-fs: ^5.2.0
|
||||
node-fetch: 2.6.7
|
||||
node-html-markdown: ^1.3.0
|
||||
os: ^0.1.2
|
||||
yaml: ^2.2.1
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-scaffolder-backend-module-cookiecutter@workspace:plugins/scaffolder-backend-module-cookiecutter":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-scaffolder-backend-module-cookiecutter@workspace:plugins/scaffolder-backend-module-cookiecutter"
|
||||
@@ -20175,6 +20198,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-select@npm:^5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "css-select@npm:5.1.0"
|
||||
dependencies:
|
||||
boolbase: ^1.0.0
|
||||
css-what: ^6.1.0
|
||||
domhandler: ^5.0.2
|
||||
domutils: ^3.0.1
|
||||
nth-check: ^2.0.1
|
||||
checksum: 2772c049b188d3b8a8159907192e926e11824aea525b8282981f72ba3f349cf9ecd523fdf7734875ee2cb772246c22117fc062da105b6d59afe8dcd5c99c9bda
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-to-react-native@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "css-to-react-native@npm:3.0.0"
|
||||
@@ -20229,6 +20265,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-what@npm:^6.1.0":
|
||||
version: 6.1.0
|
||||
resolution: "css-what@npm:6.1.0"
|
||||
checksum: b975e547e1e90b79625918f84e67db5d33d896e6de846c9b584094e529f0c63e2ab85ee33b9daffd05bff3a146a1916bec664e18bb76dd5f66cbff9fc13b2bbe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css.escape@npm:1.5.1, css.escape@npm:^1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "css.escape@npm:1.5.1"
|
||||
@@ -23898,6 +23941,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-extra@npm:^11.1.0":
|
||||
version: 11.1.0
|
||||
resolution: "fs-extra@npm:11.1.0"
|
||||
dependencies:
|
||||
graceful-fs: ^4.2.0
|
||||
jsonfile: ^6.0.1
|
||||
universalify: ^2.0.0
|
||||
checksum: 5ca476103fa1f5ff4a9b3c4f331548f8a3c1881edaae323a4415d3153b5dc11dc6a981c8d1dd93eec8367ceee27b53f8bd27eecbbf66ffcdd04927510c171e7f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-extra@npm:^7.0.1, fs-extra@npm:~7.0.1":
|
||||
version: 7.0.1
|
||||
resolution: "fs-extra@npm:7.0.1"
|
||||
@@ -23955,6 +24009,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs@npm:^0.0.1-security":
|
||||
version: 0.0.1-security
|
||||
resolution: "fs@npm:0.0.1-security"
|
||||
checksum: 53c6230e1faae9fa32c1df82c16a84b51b1243d20f3da2b64bd110bb472b73b9185169b703e008356e3cdc92d155088b617d9d39a63b5227a30b3621baad7f5d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2":
|
||||
version: 2.3.2
|
||||
resolution: "fsevents@npm:2.3.2"
|
||||
@@ -24228,7 +24289,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"git-url-parse@npm:^13.0.0":
|
||||
"git-url-parse@npm:^13.0.0, git-url-parse@npm:^13.1.0":
|
||||
version: 13.1.0
|
||||
resolution: "git-url-parse@npm:13.1.0"
|
||||
dependencies:
|
||||
@@ -24900,7 +24961,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"he@npm:^1.2.0":
|
||||
"he@npm:1.2.0, he@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "he@npm:1.2.0"
|
||||
bin:
|
||||
@@ -30598,6 +30659,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-html-markdown@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "node-html-markdown@npm:1.3.0"
|
||||
dependencies:
|
||||
node-html-parser: ^6.1.1
|
||||
checksum: 303affd75bf11e92f004047e283aa05da5c8ae8696c1f28f811e9f61762c55b4480e0e5986a9a4b399791485b835380af8ab0554ebf867d6cf9be362abedcf5d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-html-parser@npm:^6.1.1":
|
||||
version: 6.1.5
|
||||
resolution: "node-html-parser@npm:6.1.5"
|
||||
dependencies:
|
||||
css-select: ^5.1.0
|
||||
he: 1.2.0
|
||||
checksum: b54257b31954be17d473c86a2fede53d5e238708f3d88577c98881c28ab66013fd37044903f6f3cd662b56d882681fb511bca75a80a80b62f7111b32b39eebae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-int64@npm:^0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "node-int64@npm:0.4.0"
|
||||
@@ -30898,12 +30978,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nth-check@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "nth-check@npm:2.0.1"
|
||||
"nth-check@npm:^2.0.0, nth-check@npm:^2.0.1":
|
||||
version: 2.1.1
|
||||
resolution: "nth-check@npm:2.1.1"
|
||||
dependencies:
|
||||
boolbase: ^1.0.0
|
||||
checksum: 5386d035c48438ff304fe687704d93886397349d1bed136de97aeae464caba10e8ffac55a04b215b86b3bc8897f33e0a5aa1045a9d8b2f251ae61b2a3ad3e450
|
||||
checksum: 5afc3dafcd1573b08877ca8e6148c52abd565f1d06b1eb08caf982e3fa289a82f2cae697ffb55b5021e146d60443f1590a5d6b944844e944714a5b549675bcd3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -31278,6 +31358,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"os@npm:^0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "os@npm:0.1.2"
|
||||
checksum: dc2d99759eef13f5dc47ddb12c67b9760a7196fd83a35a7aec2d75b82f91163ca1d4e8872238f8c2a35f4cddd5adf5ce6638a234c0563c748d3cd1d69a9f7153
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ospath@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "ospath@npm:1.2.2"
|
||||
@@ -39460,10 +39547,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:^2.0.0, yaml@npm:^2.1.1, yaml@npm:^2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "yaml@npm:2.1.3"
|
||||
checksum: 91316062324a93f9cb547469092392e7d004ff8f70c40fecb420f042a4870b2181557350da56c92f07bd44b8f7a252b0be26e6ade1f548e1f4351bdd01c9d3c7
|
||||
"yaml@npm:^2.0.0, yaml@npm:^2.1.1, yaml@npm:^2.1.3, yaml@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "yaml@npm:2.2.1"
|
||||
checksum: 84f68cbe462d5da4e7ded4a8bded949ffa912bc264472e5a684c3d45b22d8f73a3019963a32164023bdf3d83cfb6f5b58ff7b2b10ef5b717c630f40bd6369a23
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user