From e68d814630f77e942cbf77c3cd97d83503c327e0 Mon Sep 17 00:00:00 2001 From: Remie Bolte Date: Tue, 16 Apr 2024 19:40:44 +0200 Subject: [PATCH] feat: add new start command The start command allows you to run the Atlassian host product within the context of the plugin Maven project with file watchers to (re)-build your project --- package.json | 15 +- src/applications/amps.ts | 178 ++++++++++++++++--- src/applications/bamboo.ts | 10 +- src/applications/base.ts | 25 ++- src/applications/bitbucket.ts | 7 +- src/applications/confluence.ts | 10 +- src/applications/jira.ts | 10 +- src/commands/database-mssql.ts | 1 + src/commands/database-mysql.ts | 1 + src/commands/database-postgres.ts | 1 + src/commands/database.ts | 1 + src/commands/run-bamboo.ts | 1 + src/commands/run-bitbucket.ts | 1 + src/commands/run-confluence.ts | 1 + src/commands/run-jira.ts | 1 + src/commands/run.ts | 3 +- src/commands/start.ts | 102 +++++++++++ src/helpers/getApplication.ts | 15 ++ src/helpers/isRecursiveBuild.ts | 3 + src/helpers/showRecursiveBuildWarning.ts | 13 ++ src/helpers/{assets.ts => toAbsolutePath.ts} | 2 +- src/index.ts | 6 +- yarn.lock | 49 +++-- 23 files changed, 387 insertions(+), 69 deletions(-) create mode 100644 src/commands/start.ts create mode 100644 src/helpers/getApplication.ts create mode 100644 src/helpers/isRecursiveBuild.ts create mode 100644 src/helpers/showRecursiveBuildWarning.ts rename src/helpers/{assets.ts => toAbsolutePath.ts} (82%) diff --git a/package.json b/package.json index eedb5f4..e27fe0c 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@types/js-yaml": "4", "@types/node": "18.16.0", "@types/pg": "8", + "@types/yargs": "17.0.32", "@typescript-eslint/eslint-plugin": "7.6.0", "@typescript-eslint/parser": "7.6.0", "eslint": "9.0.0", @@ -61,6 +62,7 @@ "dependencies": { "@xmldom/xmldom": "0.8.10", "axios": "1.6.8", + "chokidar": "3.6.0", "commander": "12.0.0", "docker-compose": "0.24.8", "exit-hook": "4.0.0", @@ -71,12 +73,19 @@ "sequelize": "6.37.2", "simple-git": "3.24.0", "tedious": "18.1.0", - "xpath": "0.0.34" + "xpath": "0.0.34", + "yargs": "17.7.2" }, "release": { "branches": [ - { "name": "main" }, - { "name": "next", "channel": "next", "prerelease": true } + { + "name": "main" + }, + { + "name": "next", + "channel": "next", + "prerelease": true + } ] } } diff --git a/src/applications/amps.ts b/src/applications/amps.ts index e1f3906..14fc4ef 100644 --- a/src/applications/amps.ts +++ b/src/applications/amps.ts @@ -1,59 +1,179 @@ import { DOMParser, XMLSerializer } from '@xmldom/xmldom'; +import { ChildProcess, spawn } from 'child_process'; import { XMLParser } from 'fast-xml-parser'; import { existsSync, readFileSync } from 'fs' +import { cwd } from 'process'; import xpath from 'xpath'; +import { hideBin } from 'yargs/helpers'; +import yargs from 'yargs/yargs'; import { SupportedApplications } from '../types/SupportedApplications'; +const { P, activeProfiles } = yargs(hideBin(process.argv)).parseSync(); +const profile = P as string || activeProfiles as string || undefined; + export class AMPS { + private static maven: ChildProcess|null; + + // ------------------------------------------------------------------------------------------ Public Static Methods + + public static stop() { + if (AMPS.maven) { + AMPS.maven.kill(0); + } + } + + public static async build(args: Array) { + return new Promise((resolve, reject) => { + if (AMPS.maven) { + const killed = AMPS.maven.kill(0); + if (!killed) { + reject(new Error('Failed to terminate existing Maven process')); + } + } + + AMPS.maven = spawn( + 'mvn', + [ 'package', ...args ], + { cwd: cwd(), stdio: 'inherit' } + ); + + AMPS.maven.on('exit', (code) => { + AMPS.maven = null; + if (code === 0 || code === 130) { + resolve(); + } else { + reject(new Error(`Maven exited with code ${code}`)); + } + }); + }); + } + public static isAtlassianPlugin = (): boolean => { try { - const hasPomFile = existsSync('./pom.xml'); - if (hasPomFile) { - const content = readFileSync('./pom.xml', 'utf8'); - const parser = new XMLParser(); - const pom = parser.parse(content); - return pom?.project?.packaging === 'atlassian-plugin'; - } - return false; + const nodes = AMPS.getNodes('//*[local-name()=\'packaging\']'); + return nodes.some(item => item.textContent === 'atlassian-plugin'); } catch (err) { + console.log(err); return false; } } + public static getApplicationVersion(): string|undefined { + const node = !profile + ? AMPS.getNodes('//*[local-name()=\'groupId\' and text()=\'com.atlassian.maven.plugins\']', true) + : AMPS.getNodes(`//*[local-name()='profile']/*[local-name()='id' and text()='${profile}']/..//*[local-name()='groupId' and text()='com.atlassian.maven.plugins']`, true); + + if (node) { + const parentNode = node.parentNode; + if (parentNode) { + const { plugin } = AMPS.toObject(parentNode); + const version = plugin?.configuration?.productVersion; + return version ? this.doPropertyReplacement(version) : undefined; + } + } + return undefined; + } + public static getApplication(): SupportedApplications|null { const applications = AMPS.getApplications(); - return applications.length === 1 ? applications[0] : null; + if (applications.length === 1) { + return applications[0]; + } else if (profile) { + const profileApplications = AMPS.getApplications(profile); + if (profileApplications.length === 1) { + return profileApplications[0]; + } + } + return null; } - public static getApplications(): Array { + public static getApplications(profile?: string): Array { const result = new Set(); - if (AMPS.isAtlassianPlugin()) { + const nodes = !profile + ? AMPS.getNodes('//*[local-name()=\'groupId\' and text()=\'com.atlassian.maven.plugins\']') + : AMPS.getNodes(`//*[local-name()='profile']/*[local-name()='id' and text()='${profile}']/..//*[local-name()='groupId' and text()='com.atlassian.maven.plugins']`); + + nodes.forEach(node => { + const parentNode = node.parentNode; + if (parentNode) { + const { plugin } = AMPS.toObject(parentNode); + if (plugin?.artifactId?.includes(SupportedApplications.JIRA)) { + result.add(SupportedApplications.JIRA); + } else if (plugin?.artifactId?.includes(SupportedApplications.CONFLUENCE)) { + result.add(SupportedApplications.CONFLUENCE); + } else if (plugin?.artifactId?.includes(SupportedApplications.BAMBOO)) { + result.add(SupportedApplications.BAMBOO); + } else if (plugin?.artifactId?.includes(SupportedApplications.BITBUCKET)) { + result.add(SupportedApplications.BITBUCKET); + } + } + }); + + return Array.from(result); + } + + // ------------------------------------------------------------------------------------------ Private Static Methods + + private static doPropertyReplacement(value: string) { + let result = value; + + // If there is a profile, replace profile properties first as they take precedence + const profileProperties = profile ? AMPS.getProperties(profile) : {}; + Object.entries(profileProperties).forEach(([propertyKey, propertyValue]) => { + result = result.replaceAll(`$\{${propertyKey}}`, propertyValue); + }); + + const properties = AMPS.getProperties(); + Object.entries(properties).forEach(([propertyKey, propertyValue]) => { + result = result.replaceAll(`$\{${propertyKey}}`, propertyValue); + }); + + return result; + } + + private static getProperties(profile?: string): Record { + const result: Record = {}; + + const nodes = !profile + ? AMPS.getNodes('//*[local-name()=\'properties\']') + : AMPS.getNodes(`//*[local-name()='profile']/*[local-name()='id' and text()='${profile}']/..//*[local-name()='properties']`); + + nodes.forEach(node => { + const { properties } = AMPS.toObject(node); + Object.entries(properties as Record).forEach(([ key, value ]) => result[key] = value); + }); + return result; + } + + private static getNodes(expression: string): Array; + private static getNodes(expression: string, single: true): Node|null; + private static getNodes(expression: string, single?: true): Array|Node|null { + const hasPomFile = existsSync('./pom.xml'); + if (hasPomFile) { const xml = readFileSync('./pom.xml', 'utf8'); const doc = new DOMParser().parseFromString(xml, 'text/xml'); - const nodes = xpath.select('//*[local-name()=\'groupId\' and text()=\'com.atlassian.maven.plugins\']', doc); + const nodes = single ? xpath.select(expression, doc, true) : xpath.select(expression, doc, false); if (Array.isArray(nodes)) { - nodes.forEach(node => { - const parentNode = node.parentNode; - if (parentNode) { - const parser = new XMLParser(); - const { plugin } = parser.parse(new XMLSerializer().serializeToString(parentNode)); - if (plugin?.artifactId?.includes(SupportedApplications.JIRA)) { - result.add(SupportedApplications.JIRA); - } else if (plugin?.artifactId?.includes(SupportedApplications.CONFLUENCE)) { - result.add(SupportedApplications.CONFLUENCE); - } else if (plugin?.artifactId?.includes(SupportedApplications.BAMBOO)) { - result.add(SupportedApplications.BAMBOO); - } else if (plugin?.artifactId?.includes(SupportedApplications.BITBUCKET)) { - result.add(SupportedApplications.BITBUCKET); - } - } - }); + return nodes; + } else if (single) { + return nodes as Node; + } else { + return []; } } - return Array.from(result); + return []; + } + + private static toObject(node: Node) { + try { + const parser = new XMLParser(); + return parser.parse(new XMLSerializer().serializeToString(node)); + } catch (err) { + return null; + } } } \ No newline at end of file diff --git a/src/applications/bamboo.ts b/src/applications/bamboo.ts index 8333ed1..cae8174 100644 --- a/src/applications/bamboo.ts +++ b/src/applications/bamboo.ts @@ -1,8 +1,8 @@ import axios from 'axios'; -import { getFullPath } from '../helpers/assets'; import { timebomb } from '../helpers/licences'; +import { toAbsolutePath } from '../helpers/toAbsolutePath'; import { ApplicationOptions } from '../types/ApplicationOptions'; import { DatabaseEngine } from '../types/DatabaseEngine'; import { Service } from '../types/DockerComposeV3'; @@ -31,11 +31,15 @@ export class Bamboo extends Base { return { build: { - context: getFullPath('../../assets'), + context: toAbsolutePath('../../assets'), dockerfile_inline: ` FROM dcdx/${this.name}:${this.options.version} -COPY ./quickreload-5.0.2.jar /var/atlassian/application-data/bamboo/shared/plugins/quickreload-5.0.2.jar COPY ./mysql-connector-j-8.3.0.jar /opt/atlassian/bamboo/lib/mysql-connector-j-8.3.0.jar +COPY ./quickreload-5.0.2.jar /var/atlassian/application-data/bamboo/shared/plugins/quickreload-5.0.2.jar +RUN echo "/opt/quickreload" > /var/atlassian/application-data/bamboo/quickreload.properties; \ + mkdir -p /opt/quickreload; \ + chown -R bamboo:bamboo /opt/quickreload; + RUN chown -R bamboo:bamboo /var/atlassian/application-data/bamboo` }, ports: [ diff --git a/src/applications/base.ts b/src/applications/base.ts index 618b9fa..e279cfc 100644 --- a/src/applications/base.ts +++ b/src/applications/base.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { spawn } from 'child_process'; -import { downAll, ps, upAll } from 'docker-compose/dist/v2.js'; +import { downAll, execCompose, ps, upAll } from 'docker-compose/dist/v2.js'; import EventEmitter from 'events'; import { gracefulExit } from 'exit-hook'; import { existsSync, mkdirSync } from 'fs'; @@ -64,6 +64,20 @@ export abstract class Base extends EventEmitter { await this.down(); } + async cp(filename: string) { + const service = await this.getServiceState(); + const isRunning = service && service.state.toLowerCase().startsWith('up'); + if (isRunning) { + const config = this.getDockerComposeConfig(); + const configAsString = dump(config); + await execCompose('cp', [ filename, `${this.name}:/opt/quickreload/` ], { + cwd: cwd(), + configAsString, + log: false + }); + } + } + // ------------------------------------------------------------------------------------------ Protected Methods protected abstract getService(): Service; @@ -213,10 +227,8 @@ export abstract class Base extends EventEmitter { const docker = spawn( 'docker', [ 'logs', '-f', '-n', '5000', service ], - { cwd: cwd() } + { cwd: cwd(), stdio: 'inherit' } ); - docker.stdout.on('data', (lines: Buffer) => { console.log(lines.toString('utf-8').trim()); }); - docker.stderr.on('data', (lines: Buffer) => { console.log(lines.toString('utf-8').trim()); }); docker.on('exit', (code) => (code === 0) ? resolve() : reject(new Error(`Docker exited with code ${code}`))); }); } @@ -226,11 +238,8 @@ export abstract class Base extends EventEmitter { const docker = spawn( 'docker', [ 'exec', '-i', service, `tail`, `-F`, `-n`, `5000`, this.logFilePath ], - { cwd: cwd() } + { cwd: cwd(), stdio: 'inherit' } ); - docker.stdout.on('data', (lines: Buffer) => { console.log(lines.toString('utf-8').trim()); }); - docker.stderr.on('data', (lines: Buffer) => { console.log(lines.toString('utf-8').trim()); }); - docker.on('SIGINT', () => resolve()); docker.on('exit', (code) => (code === 0) ? resolve() : reject(new Error(`Docker exited with code ${code}`))); }); } diff --git a/src/applications/bitbucket.ts b/src/applications/bitbucket.ts index c4418b2..80452ae 100644 --- a/src/applications/bitbucket.ts +++ b/src/applications/bitbucket.ts @@ -1,6 +1,6 @@ -import { getFullPath } from '../helpers/assets'; import { timebomb } from '../helpers/licences'; +import { toAbsolutePath } from '../helpers/toAbsolutePath'; import { ApplicationOptions } from '../types/ApplicationOptions'; import { DatabaseEngine } from '../types/DatabaseEngine'; import { Service } from '../types/DockerComposeV3'; @@ -29,11 +29,14 @@ export class Bitbucket extends Base { return { build: { - context: getFullPath('../../assets'), + context: toAbsolutePath('../../assets'), dockerfile_inline: ` FROM dcdx/${this.name}:${this.options.version} COPY ./quickreload-5.0.2.jar /var/atlassian/application-data/bitbucket/plugins/installed-plugins/quickreload-5.0.2.jar COPY ./mysql-connector-j-8.3.0.jar /var/atlassian/application-data/bitbucket/lib/mysql-connector-j-8.3.0.jar +RUN echo "/opt/quickreload" > /var/atlassian/application-data/bitbucket/quickreload.properties; \ + mkdir -p /opt/quickreload; \ + chown -R bitbucket:bitbucket /opt/quickreload; RUN mkdir -p /var/atlassian/application-data/bitbucket/shared; \ touch /var/atlassian/application-data/bitbucket/shared/bitbucket.properties; \ diff --git a/src/applications/confluence.ts b/src/applications/confluence.ts index e39f96a..0da16cc 100644 --- a/src/applications/confluence.ts +++ b/src/applications/confluence.ts @@ -1,6 +1,6 @@ -import { getFullPath } from '../helpers/assets'; import { timebomb } from '../helpers/licences'; +import { toAbsolutePath } from '../helpers/toAbsolutePath'; import { ApplicationOptions } from '../types/ApplicationOptions'; import { DatabaseEngine } from '../types/DatabaseEngine'; import { Service } from '../types/DockerComposeV3'; @@ -29,11 +29,15 @@ export class Confluence extends Base { return { build: { - context: getFullPath('../../assets'), + context: toAbsolutePath('../../assets'), dockerfile_inline: ` FROM dcdx/${this.name}:${this.options.version} -COPY ./quickreload-5.0.2.jar /opt/atlassian/confluence/confluence/WEB-INF/atlassian-bundled-plugins/quickreload-5.0.2.jar COPY ./mysql-connector-j-8.3.0.jar /opt/atlassian/confluence/confluence/WEB-INF/lib/mysql-connector-j-8.3.0.jar +COPY ./quickreload-5.0.2.jar /opt/atlassian/confluence/confluence/WEB-INF/atlassian-bundled-plugins/quickreload-5.0.2.jar +RUN echo "/opt/quickreload" > /var/atlassian/application-data/confluence/quickreload.properties; \ + mkdir -p /opt/quickreload; \ + chown -R confluence:confluence /opt/quickreload; + RUN chown -R confluence:confluence /opt/atlassian/confluence` }, ports: [ diff --git a/src/applications/jira.ts b/src/applications/jira.ts index cfbb9f5..2b78d5c 100644 --- a/src/applications/jira.ts +++ b/src/applications/jira.ts @@ -1,6 +1,6 @@ -import { getFullPath } from '../helpers/assets'; import { timebomb } from '../helpers/licences'; +import { toAbsolutePath } from '../helpers/toAbsolutePath'; import { ApplicationOptions } from '../types/ApplicationOptions'; import { DatabaseEngine } from '../types/DatabaseEngine'; import { Service } from '../types/DockerComposeV3'; @@ -28,12 +28,16 @@ export class Jira extends Base { return { build: { - context: getFullPath('../../assets'), + context: toAbsolutePath('../../assets'), dockerfile_inline: ` FROM dcdx/${this.name}:${this.options.version} COPY ./jira-data-generator-5.0.0.jar /var/atlassian/application-data/jira/plugins/installed-plugins/jira-data-generator-5.0.0.jar -COPY ./quickreload-5.0.2.jar /var/atlassian/application-data/jira/plugins/installed-plugins/quickreload-5.0.2.jar COPY ./mysql-connector-j-8.3.0.jar /opt/atlassian/jira/lib/mysql-connector-j-8.3.0.jar +COPY ./quickreload-5.0.2.jar /var/atlassian/application-data/jira/plugins/installed-plugins/quickreload-5.0.2.jar +RUN echo "/opt/quickreload" > /var/atlassian/application-data/jira/quickreload.properties; \ + mkdir -p /opt/quickreload; \ + chown -R jira:jira /opt/quickreload; + RUN chown -R jira:jira /var/atlassian/application-data/jira` }, ports: [ diff --git a/src/commands/database-mssql.ts b/src/commands/database-mssql.ts index ff489b6..cf2c24e 100644 --- a/src/commands/database-mssql.ts +++ b/src/commands/database-mssql.ts @@ -7,6 +7,7 @@ import { MSSQL, MSSQLOptions } from '../databases/mssql'; (async () => { const options = program + .showHelpAfterError(true) .addOption(new Option('-v, --version ', 'The version of Microsoft SQL Server').choices([ '2017', '2019', '2022' ]).default('2022')) .addOption(new Option('-e, --edition ', 'The edition of Microsoft SQL Server').choices([ 'Developer', 'Express', 'Standard', 'Enterprise', 'EnterpriseCore' ]).default('Developer')) .addOption(new Option('-p, --port ', 'The port on which the database will be accessible').default('1433')) diff --git a/src/commands/database-mysql.ts b/src/commands/database-mysql.ts index 3eb12c3..8770127 100644 --- a/src/commands/database-mysql.ts +++ b/src/commands/database-mysql.ts @@ -7,6 +7,7 @@ import { MySQL } from '../databases/mysql'; (async () => { const options = program + .showHelpAfterError(true) .addOption(new Option('-v, --version ', 'The version of Postgres').choices([ '8.0', '8.3' ]).default('8.3')) .addOption(new Option('-d, --database ', 'The value passed to MYSQL_DATABASE environment variable').default('dcdx')) .addOption(new Option('-p, --port ', 'The port on which the database will be accessible').default('3306')) diff --git a/src/commands/database-postgres.ts b/src/commands/database-postgres.ts index 483a029..6800961 100644 --- a/src/commands/database-postgres.ts +++ b/src/commands/database-postgres.ts @@ -7,6 +7,7 @@ import { Postgres } from '../databases/postgres'; (async () => { const options = program + .showHelpAfterError(true) .addOption(new Option('-v, --version ', 'The version of Postgres').choices([ '12', '13', '14', '15']).default('15')) .addOption(new Option('-d, --database ', 'The value passed to POSTGRES_DB environment variable').default('dcdx')) .addOption(new Option('-p, --port ', 'The port on which the database will be accessible').default('5432')) diff --git a/src/commands/database.ts b/src/commands/database.ts index 38ccf73..2c267c1 100644 --- a/src/commands/database.ts +++ b/src/commands/database.ts @@ -7,6 +7,7 @@ program .command('postgresql', 'Start PostgreSQL', { executableFile: './database-postgres.js'}) .command('mysql', 'Start MySQL', { executableFile: './database-mysql.js'}) .command('mssql', 'Start Microsoft SQL Server', { executableFile: './database-mssql.js'}) + .showHelpAfterError(true); program.parse(); diff --git a/src/commands/run-bamboo.ts b/src/commands/run-bamboo.ts index b0c6fce..e1c2cf3 100644 --- a/src/commands/run-bamboo.ts +++ b/src/commands/run-bamboo.ts @@ -7,6 +7,7 @@ import { Bamboo } from '../applications/bamboo'; (async () => { const options = program + .showHelpAfterError(true) .addOption(new Option('-v, --version ', 'The version of the host application').choices([ '9.4.3' ]).default('9.4.3')) .addOption(new Option('-d, --database ', 'The database engine on which the host application will run').choices([ 'postgresql', 'mysql', 'mssql' ]).default('postgresql')) .addOption(new Option('-p, --port ', 'The HTTP port on which the host application will be accessible').default('80')) diff --git a/src/commands/run-bitbucket.ts b/src/commands/run-bitbucket.ts index 000724a..fd0ae9e 100644 --- a/src/commands/run-bitbucket.ts +++ b/src/commands/run-bitbucket.ts @@ -7,6 +7,7 @@ import { Bitbucket } from '../applications/bitbucket'; (async () => { const options = program + .showHelpAfterError(true) .addOption(new Option('-v, --version ', 'The version of the host application').choices([ '8.9.0' ]).default('8.9.0')) .addOption(new Option('-d, --database ', 'The database engine on which the host application will run').choices([ 'postgresql', 'mysql', 'mssql' ]).default('postgresql')) .addOption(new Option('-p, --port ', 'The HTTP port on which the host application will be accessible').default('80')) diff --git a/src/commands/run-confluence.ts b/src/commands/run-confluence.ts index 4e26949..114f7d3 100644 --- a/src/commands/run-confluence.ts +++ b/src/commands/run-confluence.ts @@ -7,6 +7,7 @@ import { Confluence } from '../applications/confluence'; (async () => { const options = program + .showHelpAfterError(true) .addOption(new Option('-v, --version ', 'The version of the host application').choices([ '8.9.0' ]).default('8.9.0')) .addOption(new Option('-d, --database ', 'The database engine on which the host application will run').choices([ 'postgresql', 'mysql', 'mssql' ]).default('postgresql')) .addOption(new Option('-p, --port ', 'The HTTP port on which the host application will be accessible').default('80')) diff --git a/src/commands/run-jira.ts b/src/commands/run-jira.ts index 3e0bc8b..5de033c 100644 --- a/src/commands/run-jira.ts +++ b/src/commands/run-jira.ts @@ -7,6 +7,7 @@ import { Jira } from '../applications/jira'; (async () => { const options = program + .showHelpAfterError(true) .addOption(new Option('-v, --version ', 'The version of the host application').choices([ '9.15.0' ]).default('9.15.0')) .addOption(new Option('-d, --database ', 'The database engine on which the host application will run').choices([ 'postgresql', 'mysql', 'mssql' ]).default('postgresql')) .addOption(new Option('-p, --port ', 'The HTTP port on which the host application will be accessible').default('80')) diff --git a/src/commands/run.ts b/src/commands/run.ts index d3d5c1c..e2fd8ef 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -21,6 +21,7 @@ program .command('bamboo', 'Start Atlassian Bamboo (standalone)', { executableFile: './run-bamboo.js'}) .command('bitbucket', 'Start Atlassian Bitbucket (standalone)', { executableFile: './run-bitbucket.js'}) .command('confluence', 'Start Atlassian Confluence (standalone)', { executableFile: './run-confluence.js'}) - .command('jira', 'Start Atlassian Jira (standalone)', { executableFile: './run-jira.js'}); + .command('jira', 'Start Atlassian Jira (standalone)', { executableFile: './run-jira.js'}) + .showHelpAfterError(true); program.parse(process.argv); \ No newline at end of file diff --git a/src/commands/start.ts b/src/commands/start.ts new file mode 100644 index 0000000..d58c711 --- /dev/null +++ b/src/commands/start.ts @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +import { watch } from 'chokidar'; +import { Option, program } from 'commander'; +import { asyncExitHook, gracefulExit } from 'exit-hook'; +import { cwd } from 'process'; + +import { AMPS } from '../applications/amps'; +import { getApplicationByName } from '../helpers/getApplication'; +import { isRecursiveBuild } from '../helpers/isRecursiveBuild'; +import { showRecursiveBuildWarning } from '../helpers/showRecursiveBuildWarning'; + +if (!AMPS.isAtlassianPlugin()) { + console.log('Unable to find an Atlassian Plugin project in the current directory 🤔'); + gracefulExit(); +} + +const application = AMPS.getApplication(); +if (!application) { + console.log('The Atlassian Plugin project does not contain an AMPS configuration, unable to detect product 😰'); + gracefulExit(); + process.exit(); +} + +const Application = getApplicationByName(application); +if (!Application) { + console.log('The Atlassian Plugin project does not contain an AMPS configuration, unable to detect product 😰'); + process.exit(); +} + +const version = AMPS.getApplicationVersion(); + +(async () => { + const options = program + .name('dcdx start') + .description('Build & install the Atlassian Data Center plugin from the current directory.\nYou can add Maven build arguments after the command options.') + .usage('[options] [...maven_arguments]') + .addOption(new Option('-v, --version ', 'The version of the host application').default(version)) + .addOption(new Option('-d, --database ', 'The database engine on which the host application will run').choices([ 'postgresql', 'mysql', 'mssql' ]).default('postgresql')) + .addOption(new Option('-p, --port ', 'The HTTP port on which the host application will be accessible').default('80')) + .addOption(new Option('-c, --contextPath ', 'The context path on which the host application will be accessible')) + .addOption(new Option('-P, --activate-profiles ', 'Comma-delimited list of profiles to activate')) + .addOption(new Option('-o, --outputDirectory ', 'Output directory where QuickReload will look for generated JAR files').default('target')) + .addOption(new Option('--debug', 'Add support for JVM debugger on port 5005').default(true)) + .allowUnknownOption(true) + .parse(process.argv) + .opts(); + + const instance = new Application({ + version: options.version, + database: options.database, + port: Number(options.port), + contextPath: options.contextPath, + debug: options.debug + }); + + const mavenOpts = program.args.slice(); + if (options.activateProfiles) { + mavenOpts.push(...[ '-P', options.activateProfiles ]); + } + + console.log('Watching filesystem for changes to source files (QuickReload)'); + let lastBuildCompleted = new Date().getTime(); + const quickReload = watch('**/*', { + cwd: cwd(), + usePolling: true, + interval: 2 * 1000, + binaryInterval: 2 * 1000, + awaitWriteFinish: true + }).on('change', async (path) => { + if (path.startsWith(options.outputDirectory) && path.toLowerCase().endsWith('.jar')) { + console.log('Found updated JAR file(s), uploading them to QuickReload'); + await instance.cp(path); + lastBuildCompleted = new Date().getTime(); + } else if (!path.startsWith(options.outputDirectory)) { + if (isRecursiveBuild(lastBuildCompleted)) { + showRecursiveBuildWarning(options.outputDirectory); + } else { + console.log('Detected file change, rebuilding Atlasian Plugin for QuickReload'); + await AMPS.build(mavenOpts).catch(() => Promise.resolve()); + } + } + }); + + asyncExitHook(async () => { + console.log(`Stopping filesystem watcher... ⏳`); + await quickReload.close(); + console.log(`Stopping ${instance.name}... ⏳`); + await instance.stop(); + console.log(`Successfully stopped all running processes 💪`); + }, { wait: 30 * 1000 }); + + console.log('Starting application...'); + await instance.start(); + +})(); + +process.on('SIGINT', () => { + console.log(`Received term signal, trying to stop gracefully 💪`); + gracefulExit(); +}); + diff --git a/src/helpers/getApplication.ts b/src/helpers/getApplication.ts new file mode 100644 index 0000000..2ff88c0 --- /dev/null +++ b/src/helpers/getApplication.ts @@ -0,0 +1,15 @@ +import { Bamboo } from '../applications/bamboo'; +import { Bitbucket } from '../applications/bitbucket'; +import { Confluence } from '../applications/confluence'; +import { Jira } from '../applications/jira'; +import { SupportedApplications } from '../types/SupportedApplications'; + + +export const getApplicationByName = (name: SupportedApplications) => { + switch (name) { + case SupportedApplications.JIRA: return Jira; + case SupportedApplications.CONFLUENCE: return Confluence; + case SupportedApplications.BAMBOO: return Bamboo; + case SupportedApplications.BITBUCKET: return Bitbucket; + } +} \ No newline at end of file diff --git a/src/helpers/isRecursiveBuild.ts b/src/helpers/isRecursiveBuild.ts new file mode 100644 index 0000000..300f898 --- /dev/null +++ b/src/helpers/isRecursiveBuild.ts @@ -0,0 +1,3 @@ + +export const isRecursiveBuild = (lastBuildCompleted: number) => + lastBuildCompleted > (new Date().getTime() - 5 * 1000) diff --git a/src/helpers/showRecursiveBuildWarning.ts b/src/helpers/showRecursiveBuildWarning.ts new file mode 100644 index 0000000..ddf925f --- /dev/null +++ b/src/helpers/showRecursiveBuildWarning.ts @@ -0,0 +1,13 @@ + +export const showRecursiveBuildWarning = (outputDirectory: string) => { + console.log(` +=============================================================================================================== +Recursive build trigger detected. The last build completed last than 5 seconds ago +This may indicate that the build changes files outside of the output directory +Alternatively, Maven is using a different output directory than configured: +'${outputDirectory}' + +Please make sure to check your build process and/or specify a different output directory using the '-o' option +=============================================================================================================== + `); +} diff --git a/src/helpers/assets.ts b/src/helpers/toAbsolutePath.ts similarity index 82% rename from src/helpers/assets.ts rename to src/helpers/toAbsolutePath.ts index dbddc27..7d3353a 100644 --- a/src/helpers/assets.ts +++ b/src/helpers/toAbsolutePath.ts @@ -1,6 +1,6 @@ import { dirname, join } from 'path'; -export const getFullPath = (relativePath: string) => { +export const toAbsolutePath = (relativePath: string) => { const [ , filePath ] = process.argv; const executableFileDir = dirname(filePath); const basedir = executableFileDir.substring(0, executableFileDir.indexOf('dcdx') + 4); diff --git a/src/index.ts b/src/index.ts index ec8b29e..2ae762f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,10 +7,14 @@ import pkg from '../package.json'; program .name('dcdx') .description('The Unofficial Atlassian Data Center Plugin Development CLI') - .version(pkg.version); + .version(pkg.version) + .showHelpAfterError(true); // ------------------------------------------------------------------------------------------ Run +program + .command('start', 'Build & install the Atlassian Data Center plugin from the current directory', { executableFile: './commands/start.js' }); + program .command('run', 'Start the Atlassian host application (standalone)', { executableFile: './commands/run.js' }); diff --git a/yarn.lock b/yarn.lock index 72d3689..9648ccc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1266,6 +1266,22 @@ __metadata: languageName: node linkType: hard +"@types/yargs-parser@npm:*": + version: 21.0.3 + resolution: "@types/yargs-parser@npm:21.0.3" + checksum: 10c0/e71c3bd9d0b73ca82e10bee2064c384ab70f61034bbfb78e74f5206283fc16a6d85267b606b5c22cb2a3338373586786fed595b2009825d6a9115afba36560a0 + languageName: node + linkType: hard + +"@types/yargs@npm:17.0.32": + version: 17.0.32 + resolution: "@types/yargs@npm:17.0.32" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: 10c0/2095e8aad8a4e66b86147415364266b8d607a3b95b4239623423efd7e29df93ba81bb862784a6e08664f645cc1981b25fd598f532019174cd3e5e1e689e1cccf + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:7.6.0": version: 7.6.0 resolution: "@typescript-eslint/eslint-plugin@npm:7.6.0" @@ -1857,7 +1873,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.5.2": +"chokidar@npm:3.6.0, chokidar@npm:^3.5.2": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -2263,10 +2279,12 @@ __metadata: "@types/js-yaml": "npm:4" "@types/node": "npm:18.16.0" "@types/pg": "npm:8" + "@types/yargs": "npm:17.0.32" "@typescript-eslint/eslint-plugin": "npm:7.6.0" "@typescript-eslint/parser": "npm:7.6.0" "@xmldom/xmldom": "npm:0.8.10" axios: "npm:1.6.8" + chokidar: "npm:3.6.0" commander: "npm:12.0.0" docker-compose: "npm:0.24.8" eslint: "npm:9.0.0" @@ -2286,6 +2304,7 @@ __metadata: typescript: "npm:5.4.4" typescript-eslint: "npm:7.6.0" xpath: "npm:0.0.34" + yargs: "npm:17.7.2" bin: dcdx: ./lib/index.js languageName: unknown @@ -7431,33 +7450,33 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^16.0.0": - version: 16.2.0 - resolution: "yargs@npm:16.2.0" +"yargs@npm:17.7.2, yargs@npm:^17.5.1": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" dependencies: - cliui: "npm:^7.0.2" + cliui: "npm:^8.0.1" escalade: "npm:^3.1.1" get-caller-file: "npm:^2.0.5" require-directory: "npm:^2.1.1" - string-width: "npm:^4.2.0" + string-width: "npm:^4.2.3" y18n: "npm:^5.0.5" - yargs-parser: "npm:^20.2.2" - checksum: 10c0/b1dbfefa679848442454b60053a6c95d62f2d2e21dd28def92b647587f415969173c6e99a0f3bab4f1b67ee8283bf735ebe3544013f09491186ba9e8a9a2b651 + yargs-parser: "npm:^21.1.1" + checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 languageName: node linkType: hard -"yargs@npm:^17.5.1": - version: 17.7.2 - resolution: "yargs@npm:17.7.2" +"yargs@npm:^16.0.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" dependencies: - cliui: "npm:^8.0.1" + cliui: "npm:^7.0.2" escalade: "npm:^3.1.1" get-caller-file: "npm:^2.0.5" require-directory: "npm:^2.1.1" - string-width: "npm:^4.2.3" + string-width: "npm:^4.2.0" y18n: "npm:^5.0.5" - yargs-parser: "npm:^21.1.1" - checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + yargs-parser: "npm:^20.2.2" + checksum: 10c0/b1dbfefa679848442454b60053a6c95d62f2d2e21dd28def92b647587f415969173c6e99a0f3bab4f1b67ee8283bf735ebe3544013f09491186ba9e8a9a2b651 languageName: node linkType: hard