Merge branch 'master' of github.com:backstage/backstage into fix-plain-file

This commit is contained in:
Brian Fletcher
2023-03-24 14:52:39 +00:00
17 changed files with 984 additions and 44 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Minor visual tweaks to adapt to changes in mkdocs-material v9
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': minor
---
Created `confluence:transform:markdown` action for converting confluence docs to Markdown.
+14 -14
View File
@@ -8,14 +8,14 @@
yarn.lock @backstage/maintainers @backstage-service
*/yarn.lock @backstage/maintainers @backstage-service
/.changeset/*.md
/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core
/docs/assets/search @backstage/techdocs-core
/docs/features/search @backstage/techdocs-core
/docs/features/techdocs @backstage/techdocs-core
/docs/plugins/integrating-search-into-plugins.md @backstage/techdocs-core
/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-maintainers
/docs/assets/search @backstage/discoverability-maintainers
/docs/features/search @backstage/discoverability-maintainers
/docs/features/techdocs @backstage/techdocs-maintainers
/docs/plugins/integrating-search-into-plugins.md @backstage/discoverability-maintainers
/packages/cli/src/commands/onboard @backstage/sharks
/packages/techdocs-cli @backstage/techdocs-core
/packages/techdocs-cli-embedded-app @backstage/techdocs-core
/packages/techdocs-cli @backstage/techdocs-maintainers
/packages/techdocs-cli-embedded-app @backstage/techdocs-maintainers
/plugins/adr @backstage/maintainers @kuangp
/plugins/adr-* @backstage/maintainers @kuangp
/plugins/allure @backstage/maintainers @deepak-bhardwaj-ps
@@ -55,7 +55,7 @@ yarn.lock @backstage/maintainers @back
/plugins/fossa @backstage/maintainers @backstage/sda-se-reviewers
/plugins/gcalendar @backstage/maintainers @szubster @ptychu @kielosz @alexrybch
/plugins/git-release-manager @backstage/maintainers @erikengervall
/plugins/home @backstage/techdocs-core
/plugins/home @backstage/discoverability-maintainers
/plugins/ilert @backstage/maintainers @yacut
/plugins/jenkins @backstage/maintainers @timja
/plugins/jenkins-backend @backstage/maintainers @timja
@@ -71,13 +71,13 @@ yarn.lock @backstage/maintainers @back
/plugins/rollbar-backend @backstage/maintainers @andrewthauer
/plugins/scaffolder-backend-module-rails @backstage/maintainers @angeliski
/plugins/scaffolder-backend-module-yeoman @backstage/maintainers @pawelmitka
/plugins/search @backstage/techdocs-core
/plugins/search-* @backstage/techdocs-core
/plugins/search @backstage/discoverability-maintainers
/plugins/search-* @backstage/discoverability-maintainers
/plugins/sonarqube @backstage/maintainers @backstage/sda-se-reviewers
/plugins/stack-overflow @backstage/techdocs-core
/plugins/stack-overflow-backend @backstage/techdocs-core
/plugins/techdocs @backstage/techdocs-core
/plugins/techdocs-* @backstage/techdocs-core
/plugins/stack-overflow @backstage/discoverability-maintainers
/plugins/stack-overflow-backend @backstage/discoverability-maintainers
/plugins/techdocs @backstage/techdocs-maintainers
/plugins/techdocs-* @backstage/techdocs-maintainers
/plugins/user-settings-backend @backstage/maintainers @backstage/sda-se-reviewers
/tech-insights-backend @backstage/maintainers @xantier @iain-b
/tech-insights-backend-module-jsonfc @backstage/maintainers @xantier @iain-b
+33 -21
View File
@@ -24,6 +24,20 @@ Team: @backstage/catalog-maintainers
| ---- | ------------ | --------- | ---------------------------- | ------- |
| TBD | Spotify | Chipmunks | [TBD](http://github.com/TBD) | - |
### Discoverability
Team: @backstage/discoverability-maintainers
Scope: Discoverability within Backstage, including the home page, information architecture, and search
| Name | Organization | Team | GitHub | Discord |
| ------------------------ | ------------ | ---- | ---------------------------------------- | ------------------ |
| Avantika Iyer | Spotify | BUX | [tikabom](http://github.com/tikabom) | - |
| Camila Belo | Spotify | BUX | [camilaibs](http://github.com/camilaibs) | Camila Loiola#0226 |
| Emma Indal | Spotify | BUX | [emmaindal](http://github.com/emmaindal) | emmaindal#7503 |
| Raghunandan Balachandran | Spotify | BUX | [soapraj](http://github.com/soapraj) | raghunandanb#1114 |
| Renan Mendes Carvalho | Spotify | BUX | [aitherios](http://github.com/aitherios) | aitherios#0593 |
### Kubernetes
Team: @backstage/kubernetes-maintainers
@@ -38,17 +52,12 @@ Scope: The Kubernetes plugin and the base it provides for other plugins to build
Team: @backstage/techdocs-maintainers
| Name | Organization | Team | GitHub | Discord |
| ---- | ------------ | ------------ | ---------------------------- | ------- |
| TBD | Spotify | Pulp Fiction | [TBD](http://github.com/TBD) | - |
Scope: The TechDocs plugin and related tooling
### Search
Team: @backstage/search-maintainers
| Name | Organization | Team | GitHub | Discord |
| ---- | ------------ | ---- | ---------------------------- | ------- |
| TBD | Spotify | BUX | [TBD](http://github.com/TBD) | - |
| Name | Organization | Team | GitHub | Discord |
| ------------------------ | ------------ | ------------ | ------------------------------------------------ | ------------------ |
| Morgan Bentell | Spotify | Pulp Fiction | [agentbellnorm](http://github.com/agentbellnorm) | morganbentell#9030 |
| Raghunandan Balachandran | Spotify | Pulp Fiction | [soapraj](http://github.com/soapraj) | raghunandanb#1114 |
## Sponsors
@@ -60,17 +69,20 @@ Team: @backstage/search-maintainers
## Organization Members
| Name | Organization | GitHub | Discord |
| --------------- | ------------------------ | ----------------------------------------------- | ------------------------------ |
| Adam Harvey | Cisco | [adamdmharvey](https://github.com/adamdmharvey) | `adamharvey#3739` |
| Andre Wanlin | Keyloop | [awanlin](https://github.com/awanlin) | `Ahhhndre#3095` |
| Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` |
| Brian Fletcher | RoadieHQ | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` |
| David Tuite | Roadie | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` |
| Jussi Hallila | Roadie | [Xantier](https://github.com/Xantier) | `Xantier#0086` |
| Mark Avery | Cvent | [webark](https://github.com/webark) | `webark#8471` |
| Phil Kuang | FactSet Research Systems | [kuangp](https://github.com/kuangp) | `pkuang#3202` |
| Taras Mankovski | Frontside | [taras](https://github.com/taras) | `tarasm#1256` |
| Name | Organization | GitHub | Discord |
| ------------------ | ------------------------- | ----------------------------------------------- | ------------------------------ |
| Adam Harvey | Cisco | [adamdmharvey](https://github.com/adamdmharvey) | `adamharvey#3739` |
| Andre Wanlin | Keyloop | [awanlin](https://github.com/awanlin) | `Ahhhndre#3095` |
| Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` |
| Brian Fletcher | RoadieHQ | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` |
| David Tuite | Roadie | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` |
| Himanshu Mishra | Harness.io | [OrkoHunter](https://github.com/OrkoHunter) | `OrkoHunter#1520` |
| Jamie Klassen | VMware | [jamieklassen](https://github.com/jamieklassen) | `jamieklassen#3047` |
| Jussi Hallila | Roadie | [Xantier](https://github.com/Xantier) | `Xantier#0086` |
| Mark Avery | Cvent | [webark](https://github.com/webark) | `webark#8471` |
| Patrick Jungermann | Bonial International GmbH | [pjungermann](https://github.com/pjungermann) | `pjungermann#6933` |
| Phil Kuang | FactSet Research Systems | [kuangp](https://github.com/kuangp) | `pkuang#3202` |
| Taras Mankovski | Frontside | [taras](https://github.com/taras) | `tarasm#1256` |
## Emeritus Core Maintainers
@@ -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"
]
}
@@ -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');
});
});
@@ -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 ![](link to confluence) 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);
},
});
};
@@ -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>`',
);
};
@@ -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';
@@ -23,7 +23,7 @@ import {
SvgIcon,
Tooltip,
} from '@material-ui/core';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import type { Transformer } from './transformer';
import useCopyToClipboard from 'react-use/lib/useCopyToClipboard';
@@ -69,9 +69,13 @@ const CopyToClipboardButton = ({ text }: CopyToClipboardButtonProps) => {
onClose={handleClose}
leaveDelay={1000}
>
<Button className="md-clipboard md-icon" onClick={handleClick}>
<IconButton
style={{ color: 'inherit' }}
className="md-clipboard md-icon"
onClick={handleClick}
>
<CopyToClipboardIcon />
</Button>
</IconButton>
</CopyToClipboardTooltip>
);
};
@@ -21,6 +21,11 @@ const SIDEBAR_WIDTH = '224px';
export default ({ theme, sidebar }: RuleOptions) => `
/*================== Layout ==================*/
/* mkdocs material v9 compat */
.md-nav__title {
color: var(--md-default-fg-color);
}
.md-grid {
max-width: 100%;
margin: 0;
+68 -6
View File
@@ -7609,6 +7609,29 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown@workspace:plugins/scaffolder-backend-module-confluence-to-markdown":
version: 0.0.0-use.local
resolution: "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown@workspace:plugins/scaffolder-backend-module-confluence-to-markdown"
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/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
fs-extra: 10.1.0
git-url-parse: ^13.1.0
mock-fs: ^5.2.0
msw: ^1.0.0
node-fetch: ^2.6.7
node-html-markdown: ^1.3.0
yaml: ^2.0.0
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"
@@ -20408,6 +20431,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"
@@ -20462,6 +20498,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"
@@ -24460,7 +24503,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:
@@ -25144,7 +25187,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:
@@ -30906,6 +30949,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"
@@ -31206,12 +31268,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