Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 15 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,29 +143,25 @@ account Administrator, in order to publish the contents.

### Publishing policies and procedures to Confluence

You can also publish the policies to a Confluence wiki space. Simply run the
`psp publish` command with the `--confluence` option.
You can also publish the policies to a Confluence Cloud wiki space. Simply run
the `psp publish` command with the `--confluence` option and provide necessary
configuration options for non-interactive publishing:

```bash
psp publish --confluence
psp publish --confluence --site <subdomain> --space <KEY> -u <username/email> -k <key/password> -p <parent page id> --debug
```

You will be prompted to enter your Confluence domain and space key, and
username/password:

```bash
? Confluence domain (the vanity subdomain before '.atlassian.net'):
? Confluence space key:
? Confluence username:
? Confluence password: [hidden]
Published 35 docs to Confluence.
```

Or, provide necessary configuration options for non-interactive publishing:

```bash
psp publish --confluence --site <subdomain> --space <KEY> --docs <path> -u <username/email> -k <key/password>
```
To see all available options for Confluence exports run the `psp publish --help`
command.

| Option | Description | Type | Required | Default |
| -------- | ------------------------------------------------------------------- | ------- | -------- | --------------------- |
| site | the vanity domain for the site `<site>.atlassian.net` | string | yes | |
| space | the site space key to create the pages in, case-sensitive | string | yes | |
| username | username for the upload | string | yes | |
| password | password (or API token) for the upload | string | yes | |
| parent | the parent id for all uploaded pages | string | no | Homepage of the space |
| debug | dump the generated Confluence HTML to a tempdir for troubleshooting | boolean | no | false |

The program will save the page ID for each published policy document locally to
a file in the current directory: `confluence-pages.json`. Make sure this file is
Expand Down
18 changes: 13 additions & 5 deletions src/commands/psp-publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type ProgramInput = {
site?: string;
space?: string;
docs?: string;
parent?: string;
debug?: boolean;
wait?: boolean;
};

Expand Down Expand Up @@ -69,22 +71,28 @@ export async function run() {
)
.option('--space <spaceKey>', 'Space key of the Confluence wiki space')
.option(
'-d, --docs [dir]',
'path to docs; used in conjunction with --confluence option',
'docs'
'-d, --docs <dir>',
'path to docs; used in conjunction with --confluence option'
)
.option('-p, --parent <id>', 'Parent page ID for confluence export')
.option(
'-d, --debug',
'Dump generated confluence html to a tempdir for troubleshooting'
)
.parse(process.argv)
.opts() as ProgramInput;

if (program.confluence && program.docs) {
if (program.confluence) {
if (program.site && program.space && program.user && program.apiToken) {
const options: PublishToConfluenceOptions = {
domain: program.site,
space: program.space,
username: program.user,
password: program.apiToken,
parent: program.parent ? program.parent : '',
debug: program.debug ? true : false,
};
await publishToConfluence(program.docs, options);
await publishToConfluence(program.docs ? program.docs : '', options);
process.exit(0);
} else {
console.log(chalk.red('Missing required arguments'));
Expand Down
11 changes: 8 additions & 3 deletions src/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,15 @@ function getAdoptedProceduresForPolicy(
// merges additional runtime-calculated values into organization
function mergeAutomaticPSPVars(config: PolicyBuilderConfig) {
const defaultRevision = `${new Date().getFullYear()}.1`;
const mergeValues = {
defaultRevision,

const merged: PolicyBuilderConfig = {
organization: {
defaultRevision,
},
};
Object.assign(config.organization, mergeValues);

Object.assign(merged, config);
return merged;
}

// expects full config object
Expand Down
88 changes: 39 additions & 49 deletions src/publishToConfluence.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { prompt } from 'inquirer';
import path from 'path';
import fs from 'fs-extra';
import fetch from 'node-fetch';
import os from 'os';
import path from 'path';
import showdown from 'showdown';
import * as error from '~/src/error';

const converter = new showdown.Converter({
parseImgDimensions: true,
simplifiedAutoLink: true,
tables: true,
disableForced4SpacesIndentedSublists: true,
ghCompatibleHeaderId: true,
});

const CONFLUENCE_PAGES = './confluence-pages.json';
Expand All @@ -23,51 +25,25 @@ export type PublishToConfluenceOptions = {
space: string;
username: string;
password: string;
parent: string;
debug: boolean;
};

async function gatherCreds() {
const answer = await prompt([
{
type: 'input',
name: 'domain',
message:
"Confluence domain (the vanity subdomain before '.atlassian.net'):",
},
{
type: 'input',
name: 'space',
message: 'Confluence space key:',
},
{
type: 'input',
name: 'username',
message: 'Confluence username:',
},
{
type: 'password',
name: 'password',
message: 'Confluence password:',
},
]);
return {
domain: answer.domain,
space: answer.space,
username: answer.username,
password: answer.password,
};
}

function parseLinks(
pageUrl: string,
html: string,
confluencePages: Record<string, string>
) {
const linkRegex = /href=['"]([\w-]+\.md)(#.*)?['"]/gm;
const match = linkRegex.exec(html);

return match
? html.replace(linkRegex, `href="${pageUrl}/${confluencePages[match[1]]}"`)
: html;
const linkRegex: RegExp = /href=['"]([\w-]+\.md)(#.*)?['"]/gm;
const hasLinks = linkRegex.test(html);

if (hasLinks) {
return html.replace(linkRegex, (match, p1, p2 = '') => {
return `href="${pageUrl}/${confluencePages[p1]}${p2.toLowerCase()}"`;
});
} else {
return html;
}
}

async function getVersion(headers: Record<string, string>, page: string) {
Expand All @@ -83,20 +59,20 @@ export default async function publishToConfluence(
source: string,
options: PublishToConfluenceOptions
) {
const docsPath = source || path.join(__dirname, '../docs');
const docsPath = source || './docs';
if (!fs.existsSync(docsPath)) {
error.fatal('Please run `psp build` first to generate the policy docs.');
}

const { domain, space, username, password } =
options || (await gatherCreds());
const { domain, space, username, password, parent, debug } = options;

const site = `https://${domain || CONFLUENCE_DOMAIN}.atlassian.net`;
const baseUrl = `${site}/wiki/rest/api/content`;
const pageUrl = `${site}/wiki/spaces/${space || CONFLUENCE_SPACE}/pages`;

const headers = {
'Content-Type': 'application/json',

Accept: 'application/json',
Authorization: `Basic ${Buffer.from(
(username || CONFLUENCE_USER) + ':' + (password || CONFLUENCE_PASS)
Expand All @@ -109,6 +85,14 @@ export default async function publishToConfluence(

const worked = [];
const failed = [];
let debugPath = '';

if (debug) {
debugPath = await fs.mkdtemp(path.join(os.tmpdir(), 'confluence-html-'));
console.log(
`Debug enabled, generated confluence html can be found in ${debugPath}`
);
}

const docs = fs.readdirSync(docsPath);

Expand All @@ -121,8 +105,8 @@ export default async function publishToConfluence(
if (doc.endsWith('.md')) {
const data = fs.readFileSync(path.join(docsPath, doc), 'utf8');
const parsedData = data
.replace(/#([\w-]+)/gm, '$&'.toLowerCase())
.replace(/^#(.*)$/m, '') // removes title
.replace(/^ {2}(-|\*)/gm, ' -') // fixes sublist indentation
.replace(/&/gm, '&amp;')
.replace(/[‘’]/gm, `'`) // fixes quote character
.replace(/[“”]/gm, `"`);
Expand All @@ -143,13 +127,14 @@ export default async function publishToConfluence(
}
const title = match[1].trim();

const body = {
const req = {
version,
type: 'page',
title,
space: {
key: space || CONFLUENCE_SPACE,
},
ancestors: parent ? [{ id: parent }] : [],
body: {
storage: {
value: parsedHtml,
Expand All @@ -161,20 +146,25 @@ export default async function publishToConfluence(
const options = {
method: pageId ? 'put' : 'post',
headers,
body: JSON.stringify(body),
body: JSON.stringify(req),
};

const uri = pageId ? `${baseUrl}/${pageId}` : baseUrl;
const response = await fetch(uri, options);
const result = await response.json();
if (response.ok) {
const result = await response.json();
confluencePages[doc] = pageId || result.id;
if (debug) {
fs.writeFileSync(`${debugPath}/${doc}.html`, parsedHtml);
}
worked.push(doc);
} else {
failed.push(doc);
fs.writeFileSync(`./failed-${doc}.html`, parsedHtml);
if (debug) {
fs.writeFileSync(`${debugPath}/failed-${doc}.html`, parsedHtml);
}
console.error(`publish to confluence failed for ${doc}`);
console.error({ response: await response.json() });
console.error(result.message);
continue;
}
}
Expand Down
16 changes: 1 addition & 15 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,6 @@ async function renderTemplateFile(
return outputPath;
}

function mergeAutomaticPSPVars(config: PolicyBuilderConfig) {
const defaultRevision = `${new Date().getFullYear()}.1`;

const merged: PolicyBuilderConfig = {
organization: {
defaultRevision,
},
};

Object.assign(merged, config);
return merged;
}

async function renderPSPDocs(
config: PolicyBuilderConfig,
paths: PolicyBuilderPaths
Expand Down Expand Up @@ -168,7 +155,7 @@ async function renderPartials(
) {
const status: PolicyBuilderStatus = { ok: [], errors: [], type: 'partials' };

config = mergeAutomaticPSPVars(config);
config = configure.mergeAutomaticPSPVars(config);

// TODO: we're ignoring standards for now...
const partialDirs = {
Expand Down Expand Up @@ -303,7 +290,6 @@ const test = {
fillTemplate,
generateIndexTemplate,
generateMkdocsPages,
mergeAutomaticPSPVars,
};

export {
Expand Down