diff --git a/.changeset/famous-days-kneel.md b/.changeset/famous-days-kneel.md new file mode 100644 index 0000000000..8aef0f4da8 --- /dev/null +++ b/.changeset/famous-days-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +--- + +Added support for Confluence Cloud to the `confluence:transform:markdown` action, view the [README](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-confluence-to-markdown) for more details diff --git a/packages/backend/package.json b/packages/backend/package.json index f225e35d98..dc3d313e8b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -60,6 +60,7 @@ "@backstage/plugin-proxy-backend": "workspace:^", "@backstage/plugin-rollbar-backend": "workspace:^", "@backstage/plugin-scaffolder-backend": "workspace:^", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "workspace:^", "@backstage/plugin-scaffolder-backend-module-rails": "workspace:^", "@backstage/plugin-search-backend": "workspace:^", "@backstage/plugin-search-backend-module-elasticsearch": "workspace:^", diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 821e5c1adf..505a514344 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -15,9 +15,14 @@ */ import { CatalogClient } from '@backstage/catalog-client'; -import { createRouter } from '@backstage/plugin-scaffolder-backend'; +import { + createBuiltinActions, + createRouter, +} from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; +import { ScmIntegrations } from '@backstage/integration'; +import { createConfluenceToMarkdownAction } from '@backstage/plugin-scaffolder-backend-module-confluence-to-markdown'; export default async function createPlugin( env: PluginEnvironment, @@ -26,6 +31,24 @@ export default async function createPlugin( discoveryApi: env.discovery, }); + const integrations = ScmIntegrations.fromConfig(env.config); + + const builtInActions = createBuiltinActions({ + integrations, + config: env.config, + catalogClient, + reader: env.reader, + }); + + const actions = [ + ...builtInActions, + createConfluenceToMarkdownAction({ + integrations, + config: env.config, + reader: env.reader, + }), + ]; + return await createRouter({ logger: env.logger, config: env.config, @@ -35,5 +58,6 @@ export default async function createPlugin( identity: env.identity, scheduler: env.scheduler, permissions: env.permissions, + actions, }); } diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/README.md b/plugins/scaffolder-backend-module-confluence-to-markdown/README.md index d9b235d6c1..ee04f1650e 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/README.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/README.md @@ -4,16 +4,18 @@ Welcome to the `confluence:transform:markdown` action for the `scaffolder-backen ## Getting started -You need to configure the action in your backend: +The following sections will help you getting started -## From your Backstage root directory +### Configure Action in Backend + +From your Backstage root directory run: ```bash # From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-confluence-to-markdown ``` -Configure the action: +Then 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 @@ -56,6 +58,8 @@ export default async function createPlugin( } ``` +### Configuration + 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 @@ -64,7 +68,34 @@ confluence: token: ${CONFLUENCE_TOKEN} ``` -After that you can use the action in your template: +#### Confluence Cloud + +For those using Confluence Cloud you will need to have the following configuration: + +```yaml +confluence: + baseUrl: ${CONFLUENCE_BASE_URL} + token: ${CONFLUENCE_TOKEN} + isCloud: true +``` + +##### baseUrl + +The `baseUrl` for Confluence Cloud should include the product name which is `wiki` by default but can be something else if your Org has changed it. An example `baseUrl` for Confluence Cloud would look like this: `https://example.atlassian.net/wiki` + +##### token + +The `token` for Confluence Cloud needs to be base-64 encoded with your Atlassian account email address. Here's how to do that: + +1. First get your token from: `https://.atlassian.com/manage-profile/security/api-tokens` +2. Next we need to setup a string in this format: `:` +3. For this example we'll use this: `confluence@backstage.io:wDzAzoXWRGLtvbgHvT0W` +4. Now we can run `echo -n "confluence@backstage.io:wDzAzoXWRGLtvbgHvT0W" | base64` +5. This gives us: `Y29uZmx1ZW5jZUBiYWNrc3RhZ2UuaW86d0R6QXpvWFdSR0x0dmJnSHZUMFc=` which we can now use as the value for the `token` in the configuration + +### Template Usage + +Here's an example of how you can use the action in your template: ```yaml apiVersion: scaffolder.backstage.io/v1beta3 @@ -84,7 +115,7 @@ spec: properties: confluenceUrls: type: array - description: Urls for confluence doc to be converted to markdown. In format /display// + description: Urls for confluence doc to be converted to markdown. In format /display// or /spaces//pages// for Confluence cloud items: type: string default: confluence url diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts index 801f8abb28..5e8553e426 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts @@ -57,7 +57,7 @@ export const createConfluenceToMarkdownAction = (options: { type: 'array', title: 'Confluence URL', description: - 'Paste your confluence url. Ensure it follows this format: https://{confluence+base+url}/display/{spacekey}/{page+title}', + 'Paste your confluence url. Ensure it follows this format: https://{confluence+base+url}/display/{spacekey}/{page+title} or https://{confluence+base+url}/spaces/{spacekey}/pages/1234567/{page+title} for Confluence Cloud', items: { type: 'string', default: 'Confluence URL', @@ -98,6 +98,7 @@ export const createConfluenceToMarkdownAction = (options: { const { spacekey, title, titleWithSpaces } = await createConfluenceVariables(url); // This calls confluence to get the page html and page id + ctx.logger.info(`Fetching the Confluence content for ${url}`); const getConfluenceDoc = await fetchConfluence( `/rest/api/content?title=${title}&spaceKey=${spacekey}&expand=body.export_view`, config, diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.test.ts new file mode 100644 index 0000000000..97a69935c8 --- /dev/null +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { createConfluenceVariables } from './helpers'; + +describe('createConfluenceVariables', () => { + it('should return values for Confluence Url', async () => { + const url = 'https://confluence.example.com/display/SPACEKEY/Page+Title'; + const { spacekey, title, titleWithSpaces } = + await createConfluenceVariables(url); + + expect(spacekey).toEqual('SPACEKEY'); + expect(title).toEqual('Page+Title'); + expect(titleWithSpaces).toEqual('Page Title'); + }); + + it('should return values for Confluence Cloud Url', async () => { + const url = + 'https://example.atlassian.net/wiki/spaces/CLOUDSPACEKEY/pages/1234567/Cloud+Page+Title'; + + const { spacekey, title, titleWithSpaces } = + await createConfluenceVariables(url); + + expect(spacekey).toEqual('CLOUDSPACEKEY'); + expect(title).toEqual('Cloud+Page+Title'); + expect(titleWithSpaces).toEqual('Cloud Page Title'); + }); +}); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.ts index c741f6f6c0..0fb7118676 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/helpers.ts @@ -51,10 +51,13 @@ export const readFileAsString = async (fileDir: string) => { 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}`, { + const isCloud = config.getOptionalBoolean('confluence.isCloud') || false; + const authToken = isCloud ? `Basic ${token}` : `Bearer ${token}`; + const url = `${baseUrl}${relativeUrl}`; + const response: Response = await fetch(url, { method: 'GET', headers: { - Authorization: `Bearer ${token}`, + Authorization: authToken, }, }); if (!response.ok) { @@ -73,6 +76,8 @@ export const getAndWriteAttachments = async ( const productArr: string[][] = []; const baseUrl = config.getString('confluence.baseUrl'); const token = config.getString('confluence.token'); + const isCloud = config.getOptionalBoolean('confluence.isCloud') || false; + const authToken = isCloud ? `Basic ${token}` : `Bearer ${token}`; await Promise.all( await arr.results.map(async (result: Result) => { const downloadLink = result._links.download; @@ -80,11 +85,11 @@ export const getAndWriteAttachments = async ( if (result.metadata.mediaType !== 'application/gliffy+json') { productArr.push([result.title.replace(/ /g, '%20'), downloadTitle]); } - - const res = await fetch(`${baseUrl}${downloadLink}`, { + const url = `${baseUrl}${downloadLink}`; + const res = await fetch(url, { method: 'GET', headers: { - Authorization: `Bearer ${token}`, + Authorization: authToken, }, }); if (!res.ok) { @@ -117,12 +122,19 @@ export const createConfluenceVariables = async (url: string) => { let titleWithSpaces: string | undefined = ''; const params = new URL(url); if (params.pathname.split('/')[1] === 'display') { + // https://confluence.example.com/display/SPACEKEY/Page+Title spacekey = params.pathname.split('/')[2]; title = params.pathname.split('/')[3]; titleWithSpaces = title?.replace(/\+/g, ' '); return { spacekey, title, titleWithSpaces }; + } else if (params.pathname.split('/')[2] === 'spaces') { + // https://example.atlassian.net/wiki/spaces/SPACEKEY/pages/1234567/Page+Title + spacekey = params.pathname.split('/')[3]; + title = params.pathname.split('/')[6]; + titleWithSpaces = title?.replace(/\+/g, ' '); + return { spacekey, title, titleWithSpaces }; } throw new InputError( - 'The Url format for Confluence is incorrect. Acceptable format is `/display//`', + 'The Url format for Confluence is incorrect. Acceptable format is `/display//` or `/spaces//pages//` for Confluence cloud', ); }; diff --git a/yarn.lock b/yarn.lock index 07c79358be..f14d695cbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8256,7 +8256,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown@workspace:plugins/scaffolder-backend-module-confluence-to-markdown": +"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown@workspace:^, @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: @@ -24409,6 +24409,7 @@ __metadata: "@backstage/plugin-proxy-backend": "workspace:^" "@backstage/plugin-rollbar-backend": "workspace:^" "@backstage/plugin-scaffolder-backend": "workspace:^" + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "workspace:^" "@backstage/plugin-scaffolder-backend-module-rails": "workspace:^" "@backstage/plugin-search-backend": "workspace:^" "@backstage/plugin-search-backend-module-elasticsearch": "workspace:^"