diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index dbc667de0db..ba583537c20 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -28,6 +28,7 @@ "@types/node": "^12.0.4", "@types/react-dev-utils": "^9.0.1", "@types/semver": "^6.0.0", + "@types/tmp": "^0.1.0", "@types/webpack": "4.4.34", "axios": "0.19.0", "babel-loader": "8.0.6", diff --git a/packages/grafana-toolkit/src/cli/index.ts b/packages/grafana-toolkit/src/cli/index.ts index 423a14b4a58..a136ca804cd 100644 --- a/packages/grafana-toolkit/src/cli/index.ts +++ b/packages/grafana-toolkit/src/cli/index.ts @@ -15,7 +15,7 @@ import { pluginTestTask } from './tasks/plugin.tests'; import { searchTestDataSetupTask } from './tasks/searchTestDataSetup'; import { closeMilestoneTask } from './tasks/closeMilestone'; import { pluginDevTask } from './tasks/plugin.dev'; -import { pluginCITask } from './tasks/plugin.ci'; +import { ciBuildPluginTask, ciBundlePluginTask, ciTestPluginTask, ciDeployPluginTask } from './tasks/plugin.ci'; export const run = (includeInternalScripts = false) => { if (includeInternalScripts) { @@ -154,15 +154,38 @@ export const run = (includeInternalScripts = false) => { }); program - .command('plugin:ci') - .option('--dryRun', "Dry run (don't post results)") - .description('Run Plugin CI task') + .command('plugin:ci-build') + .option('--platform', 'For backend task, which backend to run') + .description('Build the plugin, leaving artifacts in /dist') .action(async cmd => { - await execTask(pluginCITask)({ - dryRun: cmd.dryRun, + await execTask(ciBuildPluginTask)({ + platform: cmd.platform, }); }); + program + .command('plugin:ci-bundle') + .description('Create a zip artifact for the plugin') + .action(async cmd => { + await execTask(ciBundlePluginTask)({}); + }); + + program + .command('plugin:ci-test') + .description('end-to-end test using bundle in /artifacts') + .action(async cmd => { + await execTask(ciTestPluginTask)({ + platform: cmd.platform, + }); + }); + + program + .command('plugin:ci-deploy') + .description('Publish plugin CI results') + .action(async cmd => { + await execTask(ciDeployPluginTask)({}); + }); + program.on('command:*', () => { console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' ')); process.exit(1); diff --git a/packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts b/packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts index 9a5834de9dc..bf5b67d1267 100644 --- a/packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts +++ b/packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts @@ -8,9 +8,10 @@ import { getPluginJson } from '../../config/utils/pluginValidation'; import execa = require('execa'); import path = require('path'); import fs = require('fs'); +import tmp = require('tmp'); export interface PluginCIOptions { - dryRun?: boolean; + platform?: string; } const calcJavascriptSize = (base: string, files?: string[]): number => { @@ -33,22 +34,74 @@ const calcJavascriptSize = (base: string, files?: string[]): number => { return size; }; -const pluginCIRunner: TaskRunner = async ({ dryRun }) => { +/** + * 1. BUILD + * + * when platform exists it is building backend, otherwise frontend + * + * Everything in /build folder + * + */ +const buildPluginRunner: TaskRunner = async ({ platform }) => { const start = Date.now(); const distDir = `${process.cwd()}/dist`; - const artifactsDir = `${process.cwd()}/artifacts`; - await execa('rimraf', [`${process.cwd()}/coverage`]); - await execa('rimraf', [artifactsDir]); + const buildDir = `${process.cwd()}/build`; + const coverageDir = `${process.cwd()}/coverage`; + if (!fs.existsSync(buildDir)) { + fs.mkdirSync(buildDir); + } + + if (platform) { + console.log('TODO, backend support?'); + const stub = distDir + `/bin_${platform}`; + if (!fs.existsSync(stub)) { + fs.mkdirSync(stub); + } + fs.writeFile(stub + '/README.txt', 'TODO... build it!', err => { + if (err) { + throw new Error('Unable to write: ' + stub); + } + }); + } else { + // Do regular build process + await pluginBuildRunner({ coverage: true }); + } + + // Move dist & coverage into 'build' + fs.renameSync(distDir, path.resolve(buildDir, 'dist')); + fs.renameSync(coverageDir, path.resolve(buildDir, 'coverage')); - // Do regular build process - await pluginBuildRunner({ coverage: true }); const elapsed = Date.now() - start; + const stats = { + job: `${process.env.CIRCLE_JOB}`, + sha1: `${process.env.CIRCLE_SHA1}`, + startTime: start, + buildTime: elapsed, + endTime: Date.now(), + }; + console.log('BUILD Info', stats); +}; +export const ciBuildPluginTask = new Task('Build Plugin', buildPluginRunner); + +/** + * 2. BUNDLE + * + * Take everything from /build/dist and zip it up + * + */ +const bundlePluginRunner: TaskRunner = async () => { + const start = Date.now(); + const distDir = `${process.cwd()}/build/dist`; + if (!fs.existsSync(distDir)) { + throw new Error('Dist folder does not exist: ' + distDir); + } + + const artifactsDir = `${process.cwd()}/build/artifacts`; if (!fs.existsSync(artifactsDir)) { fs.mkdirSync(artifactsDir); } - // TODO? can this typed from @grafana/ui? const pluginInfo = getPluginJson(`${distDir}/plugin.json`); const zipName = pluginInfo.id + '-' + pluginInfo.info.version + '.zip'; const zipFile = path.resolve(artifactsDir, zipName); @@ -56,23 +109,95 @@ const pluginCIRunner: TaskRunner = async ({ dryRun }) => { await execa('zip', ['-r', zipFile, '.']); restoreCwd(); + const zipStats = fs.statSync(zipFile); + if (zipStats.size < 100) { + throw new Error('Invalid zip file: ' + zipFile); + } + const stats = { - startTime: start, - buildTime: elapsed, - jsSize: calcJavascriptSize(distDir), - zipSize: fs.statSync(zipFile).size, - endTime: Date.now(), + name: zipName, + size: zipStats.size, }; - fs.writeFile(artifactsDir + '/stats.json', JSON.stringify(stats, null, 2), err => { + + fs.writeFile(artifactsDir + '/info.json', JSON.stringify(stats, null, 2), err => { if (err) { throw new Error('Unable to write stats'); } - console.log('Stats', stats); + console.log('Created', stats); }); - - if (!dryRun) { - console.log('TODO send info to github?'); - } }; -export const pluginCITask = new Task('Plugin CI', pluginCIRunner); +export const ciBundlePluginTask = new Task('Bundle Plugin', bundlePluginRunner); + +/** + * 3. Test (end-to-end) + * + * deploy the zip to a running grafana instance + * + */ +const testPluginRunner: TaskRunner = async ({ platform }) => { + const start = Date.now(); + + const artifactsDir = `${process.cwd()}/build/artifacts`; + const infoFile = path.resolve(artifactsDir, 'info.json'); + const zipInfo = require(infoFile); + const zipPath = path.resolve(artifactsDir, zipInfo.name); + + const tmpobj = tmp.dirSync(); + const pluginFolder = tmpobj.name; + console.log('Temp Folder', pluginFolder); + + await execa('unzip', [zipPath, '-d', pluginFolder]); + + const { stdout } = await execa('ls', ['-Rl', pluginFolder]); + console.log(stdout); + + // Manual cleanup + tmpobj.removeCallback(); + + fs.mkdirSync(pluginFolder, { recursive: true }); + + const elapsed = Date.now() - start; + const stats = { + job: `${process.env.CIRCLE_JOB}`, + sha1: `${process.env.CIRCLE_SHA1}`, + startTime: start, + buildTime: elapsed, + endTime: Date.now(), + }; + console.log('TODO Test', stats); +}; + +export const ciTestPluginTask = new Task('Test Plugin (e2e)', testPluginRunner); + +/** + * 4. Deploy + * + * deploy the zip to a running grafana instance + * + */ +const deployPluginRunner: TaskRunner = async () => { + const start = Date.now(); + + // TASK Time + if (process.env.CIRCLE_INTERNAL_TASK_DATA) { + const timingInfo = fs.readdirSync(`${process.env.CIRCLE_INTERNAL_TASK_DATA}`); + if (timingInfo) { + timingInfo.forEach(file => { + console.log('TIMING INFO: ', file); + }); + } + } + + const elapsed = Date.now() - start; + const stats = { + job: `${process.env.CIRCLE_JOB}`, + sha1: `${process.env.CIRCLE_SHA1}`, + startTime: start, + buildTime: elapsed, + endTime: Date.now(), + }; + console.log('TODO DEPLOY', stats); +}; + +export const ciDeployPluginTask = new Task('Deploy plugin', deployPluginRunner); diff --git a/yarn.lock b/yarn.lock index 113e9a0e954..72a85838247 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2568,6 +2568,11 @@ resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf" integrity sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw== +"@types/tmp@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.1.0.tgz#19cf73a7bcf641965485119726397a096f0049bd" + integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA== + "@types/uglify-js@*": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" @@ -15752,6 +15757,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" + integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== + dependencies: + rimraf "^2.6.3" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"