Commit 7ec87ee7 by Ryan McKinley Committed by Dominik Prokop

grafana/toolkit: improve CircleCI stubs (#17995)

* validate type and id

* copy all svg and png, useful if people don't use the img folder

* update comments

* add stubs for each ci task

* use ci-work folder rather than build

* use axios for basic testing

* Packages: publish packages@6.3.0-alpha.39

* bump version

* add download task

* Packages: publish packages@6.3.0-alpha.40

* merge all dist folders into one

* fix folder paths

* Fix ts error

* Packages: publish packages@6.3.0-beta.0

* Packages: publish packages@6.3.0-beta.1

* bump next to 6.4

* Packages: publish packages@6.4.0-alpha.2

* better build and bundle tasks

* fix lint

* Packages: publish packages@6.4.0-alpha.3

* copy the file to start grafana

* Packages: publish packages@6.4.0-alpha.4

* use sudo for copy

* Packages: publish packages@6.4.0-alpha.5

* add missing service

* add service and homepath

* Packages: publish packages@6.4.0-alpha.6

* make the folder

* Update packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts

* Update packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts
parent 51909499
......@@ -2,5 +2,5 @@
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
"version": "6.3.0-alpha.36"
"version": "6.4.0-alpha.6"
}
......@@ -148,7 +148,7 @@
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
"packages:prepare": "lerna run clean && npm run test && lerna version --tag-version-prefix=\"packages@\" -m \"Packages: publish %s\" --no-push",
"packages:build": "lerna run clean && lerna run build",
"packages:publish": "lerna publish from-package --contents dist --tag-version-prefix=\"packages@\" --dist-tag next"
"packages:publish": "lerna publish from-package --contents dist --dist-tag next --tag-version-prefix=\"packages@\""
},
"husky": {
"hooks": {
......
# Grafana Data Library
The core data components
\ No newline at end of file
This package holds the root data types and functions used within Grafana.
\ No newline at end of file
{
"name": "@grafana/data",
"version": "6.3.0-alpha.36",
"version": "6.4.0-alpha.2",
"description": "Grafana Data Library",
"keywords": [
"typescript"
......
# Grafana Runtime library
Interfaces that let you use the runtime...
\ No newline at end of file
This package allows access to grafana services. It requires Grafana to be running already and the functions to be imported as externals.
\ No newline at end of file
{
"name": "@grafana/runtime",
"version": "6.3.0-alpha.36",
"version": "6.4.0-alpha.2",
"description": "Grafana Runtime Library",
"keywords": [
"typescript",
"react",
"react-component"
"grafana"
],
"main": "src/index.ts",
"scripts": {
......
{
"name": "@grafana/toolkit",
"version": "6.3.0-alpha.36",
"version": "6.4.0-alpha.6",
"description": "Grafana Toolkit",
"keywords": [
"typescript",
"react",
"react-component"
"grafana",
"cli",
"plugins"
],
"bin": {
"grafana-toolkit": "./bin/grafana-toolkit.js"
......@@ -30,6 +30,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",
......
......@@ -13,7 +13,13 @@ 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,
ciSetupPluginTask,
} from './tasks/plugin.ci';
import { buildPackageTask } from './tasks/package.build';
export const run = (includeInternalScripts = false) => {
......@@ -141,15 +147,47 @@ 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 <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-setup')
.option('--installer <installer>', 'Name of installer to download and run')
.description('Install and configure grafana')
.action(async cmd => {
await execTask(ciSetupPluginTask)({
installer: cmd.installer,
});
});
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);
......
......@@ -9,7 +9,8 @@ import path = require('path');
import fs = require('fs');
export interface PluginCIOptions {
dryRun?: boolean;
platform?: string;
installer?: string;
}
const calcJavascriptSize = (base: string, files?: string[]): number => {
......@@ -32,22 +33,164 @@ const calcJavascriptSize = (base: string, files?: string[]): number => {
return size;
};
const pluginCIRunner: TaskRunner<PluginCIOptions> = async ({ dryRun }) => {
const getJobFromProcessArgv = () => {
const arg = process.argv[2];
if (arg && arg.startsWith('plugin:ci-')) {
const task = arg.substring('plugin:ci-'.length);
if ('build' === task) {
if ('--platform' === process.argv[3] && process.argv[4]) {
return task + '_' + process.argv[4];
}
return 'build_nodejs';
}
return task;
}
return 'unknown_job';
};
// /**
// * Like cp -rn... BUT error if an destination file exists
// */
// async function copyDirErrorIfExists(src:string,dest:string) {
// const entries = await fs.readdirSync(src,{withFileTypes:true});
// if(!fs.existsSync(dest)) {
// fs.mkdirSync(dest);
// }
// console.log( 'DIR', src );
// for(let entry of entries) {
// const srcPath = path.join(src,entry.name);
// const destPath = path.join(dest,entry.name);
// if(entry.isDirectory()) {
// await copyDirErrorIfExists(srcPath,destPath);
// } else if(fs.existsSync(destPath)) {
// console.log( 'XXXXXXXXXXXXXXX', destPath );
// console.log( 'XXXXXXXXXXXXXXX', destPath );
// throw new Error('Duplicate entry: '+destPath);
// }
// else {
// // console.log( 'COPY', destPath );
// await fs.copyFileSync(srcPath,destPath);
// }
// }
// }
const job = process.env.CIRCLE_JOB || getJobFromProcessArgv();
const getJobFolder = () => {
const dir = path.resolve(process.cwd(), 'ci', 'jobs', job);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return dir;
};
const getCiFolder = () => {
const dir = path.resolve(process.cwd(), 'ci');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return dir;
};
const writeJobStats = (startTime: number, workDir: string) => {
const stats = {
job,
startTime,
endTime: Date.now(),
};
const f = path.resolve(workDir, 'stats.json');
fs.writeFile(f, JSON.stringify(stats, null, 2), err => {
if (err) {
throw new Error('Unable to stats: ' + f);
}
});
};
/**
* 1. BUILD
*
* when platform exists it is building backend, otherwise frontend
*
* Each build writes data:
* ~/work/build_xxx/
*
* Anything that should be put into the final zip file should be put in:
* ~/work/build_xxx/dist
*/
const buildPluginRunner: TaskRunner<PluginCIOptions> = 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 workDir = getJobFolder();
await execa('rimraf', [workDir]);
fs.mkdirSync(workDir);
// Do regular build process
if (platform) {
console.log('TODO, backend support?');
fs.mkdirSync(path.resolve(process.cwd(), 'dist'));
const file = path.resolve(process.cwd(), 'dist', `README_${platform}.txt`);
fs.writeFile(file, `TODO... build ${platform}!`, err => {
if (err) {
throw new Error('Unable to write: ' + file);
}
});
} else {
// Do regular build process with coverage
await pluginBuildRunner({ coverage: true });
const elapsed = Date.now() - start;
}
// Move local folders to the scoped job folder
for (const name of ['dist', 'coverage']) {
const dir = path.resolve(process.cwd(), name);
if (fs.existsSync(dir)) {
fs.renameSync(dir, path.resolve(workDir, name));
}
}
writeJobStats(start, workDir);
};
export const ciBuildPluginTask = new Task<PluginCIOptions>('Build Plugin', buildPluginRunner);
if (!fs.existsSync(artifactsDir)) {
/**
* 2. BUNDLE
*
* Take everything from `~/ci/job/{any}/dist` and
* 1. merge it into: `~/ci/dist`
* 2. zip it into artifacts in `~/ci/artifacts`
* 3. prepare grafana environment in: `~/ci/grafana-test-env`
*
*/
const bundlePluginRunner: TaskRunner<PluginCIOptions> = async () => {
const start = Date.now();
const ciDir = getCiFolder();
const artifactsDir = path.resolve(ciDir, 'artifacts');
const distDir = path.resolve(ciDir, 'dist');
const grafanaEnvDir = path.resolve(ciDir, 'grafana-test-env');
await execa('rimraf', [artifactsDir, distDir, grafanaEnvDir]);
fs.mkdirSync(artifactsDir);
fs.mkdirSync(distDir);
fs.mkdirSync(grafanaEnvDir);
console.log('Build Dist Folder');
// 1. Check for a local 'dist' folder
const d = path.resolve(process.cwd(), 'dist');
if (fs.existsSync(d)) {
await execa('cp', ['-rn', d + '/.', distDir]);
}
// 2. Look for any 'dist' folders under ci/job/XXX/dist
const dirs = fs.readdirSync(path.resolve(ciDir, 'jobs'));
for (const j of dirs) {
const contents = path.resolve(ciDir, 'jobs', j, 'dist');
if (fs.existsSync(contents)) {
try {
await execa('cp', ['-rn', contents + '/.', distDir]);
} catch (er) {
throw new Error('Duplicate files found in dist folders');
}
}
}
// TODO? can this typed from @grafana/ui?
console.log('Building ZIP');
const pluginInfo = getPluginJson(`${distDir}/plugin.json`);
const zipName = pluginInfo.id + '-' + pluginInfo.info.version + '.zip';
const zipFile = path.resolve(artifactsDir, zipName);
......@@ -55,23 +198,184 @@ const pluginCIRunner: TaskRunner<PluginCIOptions> = async ({ dryRun }) => {
await execa('zip', ['-r', zipFile, '.']);
restoreCwd();
const zipStats = fs.statSync(zipFile);
if (zipStats.size < 100) {
throw new Error('Invalid zip file: ' + zipFile);
}
let sha1 = undefined;
try {
const exe = await execa('shasum', [zipFile]);
const idx = exe.stdout.indexOf(' ');
sha1 = exe.stdout.substring(0, idx);
fs.writeFile(zipFile + '.sha1', sha1, err => {});
} catch {
console.warn('Unable to read SHA1 Checksum');
}
const info = {
name: zipName,
sha1,
size: zipStats.size,
};
let p = path.resolve(artifactsDir, 'info.json');
fs.writeFile(p, JSON.stringify(info, null, 2), err => {
if (err) {
throw new Error('Error writing artifact info: ' + p);
}
});
console.log('Setup Grafan Environment');
p = path.resolve(grafanaEnvDir, 'plugins', pluginInfo.id);
fs.mkdirSync(p, { recursive: true });
await execa('unzip', [zipFile, '-d', p]);
// Write the custom settings
p = path.resolve(grafanaEnvDir, 'custom.ini');
const customIniBody =
`# Autogenerated by @grafana/toolkit \n` +
`[paths] \n` +
`plugins = ${path.resolve(grafanaEnvDir, 'plugins')}\n` +
`\n`; // empty line
fs.writeFile(p, customIniBody, err => {
if (err) {
throw new Error('Unable to write: ' + p);
}
});
writeJobStats(start, getJobFolder());
};
export const ciBundlePluginTask = new Task<PluginCIOptions>('Bundle Plugin', bundlePluginRunner);
/**
* 3. Setup (install grafana and setup provisioning)
*
* deploy the zip to a running grafana instance
*
*/
const setupPluginRunner: TaskRunner<PluginCIOptions> = async ({ installer }) => {
const start = Date.now();
if (!installer) {
throw new Error('Missing installer path');
}
// Download the grafana installer
const installDir = path.resolve(process.cwd(), '.installer');
const installFile = path.resolve(installDir, installer);
if (!fs.existsSync(installFile)) {
if (!fs.existsSync(installDir)) {
fs.mkdirSync(installDir);
}
console.log('download', installer);
const exe = await execa('wget', ['-O', installFile, 'https://dl.grafana.com/oss/release/' + installer]);
console.log(exe.stdout);
}
console.log('Install Grafana');
let exe = await execa('sudo', ['apt-get', 'install', '-y', 'adduser', 'libfontconfig1']);
exe = await execa('sudo', ['dpkg', '-i', installFile]);
console.log(exe.stdout);
const customIniFile = path.resolve(getCiFolder(), 'grafana-test-env', 'custom.ini');
const configDir = '/usr/share/grafana/conf/';
exe = await execa('sudo', ['cp', '-f', customIniFile, configDir]);
console.log(exe.stdout);
// sudo service grafana-server start
console.log('Starting Grafana');
exe = await execa('sudo', ['service', 'grafana-server', 'start']);
console.log(exe.stdout);
// exe = await execa('grafana-cli', ['--version', '--homepath', '/usr/share/grafana']);
// console.log(exe.stdout);
// exe = await execa('grafana-cli', ['plugins', 'ls', '--homepath', '/usr/share/grafana']);
// console.log(exe.stdout);
const dir = getJobFolder() + '_setup';
await execa('rimraf', [dir]);
fs.mkdirSync(dir);
writeJobStats(start, dir);
};
export const ciSetupPluginTask = new Task<PluginCIOptions>('Setup Grafana', setupPluginRunner);
/**
* 4. Test (end-to-end)
*
* deploy the zip to a running grafana instance
*
*/
const testPluginRunner: TaskRunner<PluginCIOptions> = async ({ platform }) => {
const start = Date.now();
const workDir = getJobFolder();
const args = {
withCredentials: true,
baseURL: process.env.GRAFANA_URL || 'http://localhost:3000/',
responseType: 'json',
auth: {
username: 'admin',
password: 'admin',
},
};
const axios = require('axios');
const frontendSettings = await axios.get('api/frontend/settings', args);
console.log('Grafana Version: ' + JSON.stringify(frontendSettings.data.buildInfo, null, 2));
const pluginInfo = getPluginJson(`${process.cwd()}/src/plugin.json`);
const pluginSettings = await axios.get(`api/plugins/${pluginInfo.id}/settings`, args);
console.log('Plugin Info: ' + JSON.stringify(pluginSettings.data, null, 2));
console.log('TODO puppeteer');
const elapsed = Date.now() - start;
const stats = {
job,
sha1: `${process.env.CIRCLE_SHA1}`,
startTime: start,
buildTime: elapsed,
jsSize: calcJavascriptSize(distDir),
zipSize: fs.statSync(zipFile).size,
endTime: Date.now(),
};
fs.writeFile(artifactsDir + '/stats.json', JSON.stringify(stats, null, 2), err => {
if (err) {
throw new Error('Unable to write stats');
}
console.log('Stats', stats);
});
if (!dryRun) {
console.log('TODO send info to github?');
console.log('TODO Puppeteer Tests', stats);
writeJobStats(start, workDir);
};
export const ciTestPluginTask = new Task<PluginCIOptions>('Test Plugin (e2e)', testPluginRunner);
/**
* 4. Deploy
*
* deploy the zip to a running grafana instance
*
*/
const deployPluginRunner: TaskRunner<PluginCIOptions> = 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,
sha1: `${process.env.CIRCLE_SHA1}`,
startTime: start,
buildTime: elapsed,
endTime: Date.now(),
};
console.log('TODO DEPLOY??', stats);
console.log(' if PR => write a comment to github with difference ');
console.log(' if master | vXYZ ==> upload artifacts to some repo ');
};
export const pluginCITask = new Task<PluginCIOptions>('Plugin CI', pluginCIRunner);
export const ciDeployPluginTask = new Task<PluginCIOptions>('Deploy plugin', deployPluginRunner);
......@@ -3,7 +3,7 @@ import { getPluginJson, validatePluginJson } from './pluginValidation';
describe('pluginValdation', () => {
describe('plugin.json', () => {
test('missing plugin.json file', () => {
expect(() => getPluginJson(`${__dirname}/mocks/missing-plugin-json`)).toThrow('plugin.json file is missing!');
expect(() => getPluginJson(`${__dirname}/mocks/missing-plugin.json`)).toThrowError();
});
});
......
import path = require('path');
// See: packages/grafana-ui/src/types/plugin.ts
interface PluginJSONSchema {
id: string;
......@@ -22,15 +20,24 @@ export const validatePluginJson = (pluginJson: any) => {
if (!pluginJson.info.version) {
throw new Error('Plugin info.version is missing in plugin.json');
}
const types = ['panel', 'datasource', 'app'];
const type = pluginJson.type;
if (!types.includes(type)) {
throw new Error('Invalid plugin type in plugin.json: ' + type);
}
if (!pluginJson.id.endsWith('-' + type)) {
throw new Error('[plugin.json] id should end with: -' + type);
}
};
export const getPluginJson = (root: string = process.cwd()): PluginJSONSchema => {
export const getPluginJson = (path: string): PluginJSONSchema => {
let pluginJson;
try {
pluginJson = require(path.resolve(root, 'src/plugin.json'));
pluginJson = require(path);
} catch (e) {
throw new Error('plugin.json file is missing!');
throw new Error('Unable to find: ' + path);
}
validatePluginJson(pluginJson);
......
{
"name": "@grafana/ui",
"version": "6.3.0-alpha.36",
"version": "6.4.0-alpha.2",
"description": "Grafana Components Library",
"keywords": [
"typescript",
"grafana",
"react",
"react-component"
],
......
......@@ -3371,6 +3371,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"
......@@ -17202,6 +17207,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"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment