mirror of
https://github.com/embarklabs/embark.git
synced 2025-01-12 14:54:57 +00:00
feat(plugins/scripts-runner): introduce exec command to run scripts
This commit introduces a new feature that enables users to run (migration) scripts. Similar to deployment hooks, scripts are functions that may perform operations on newly deployed Smart Contracts. Therefore a script needs to export a function that has access to some dependencies: ``` // scripts/001-some-script.js module.exports = async ({contracts, web3, logger}) => { ... }; ``` Where `contracts` is a map of newly deployed Smart Contract instances, `web3` a blockchain connector instance and `logger` Embark's logger instance. Script functions can but don't have to be `async`. To execute such a script users use the newly introduced `exec` command: ``` $ embark exec development scripts/001-some-script.js ``` In the example above, `development` defines the environment in which Smart Contracts are being deployed to as well as where tracking data is stored. Alternativey, users can also provide a directory in which case Embark will try to execute every script living inside of it: ``` $ embark exec development scripts ``` Scripts can fail and therefore emit an error accordingly. When this happens, Embark will abort the script execution (in case multiple are scheduled to run) and informs the user about the original error: ``` .. 001_foo.js running.... Script '001_foo.js' failed to execute. Original error: Error: Some error ``` It's recommended for scripts to emit proper instances of `Error`. (Migration) scripts can be tracked as well but there are a couple of rules to be aware of: - Generally, tracking all scripts that have been executed by default is not a good thing because some scripts might be one-off operations. - OTOH, there might be scripts that should always be tracked by default - Therefore, we introduce a dedicated `migrations` directory in which scripts live that should be tracked by default - Any other scripts that does not live in the specified `migrations` directory will not be tracked **unless** - The new `--track` option was provided For more information see: https://notes.status.im/h8XwB7xkR7GKnfNh6OnPMQ
This commit is contained in:
parent
0f59e0c216
commit
40c3d98217
@ -108,5 +108,6 @@
|
||||
},
|
||||
"environments": {
|
||||
"development": "development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultMigrationsDir": "migrations"
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import { readJsonSync } from 'fs-extra';
|
||||
const cloneDeep = require('lodash.clonedeep');
|
||||
const { replaceZeroAddressShorthand } = AddressUtils;
|
||||
|
||||
import { getBlockchainDefaults, getContractDefaults } from './configDefaults';
|
||||
import { getBlockchainDefaults, getContractDefaults, embarkConfigDefaults } from './configDefaults';
|
||||
|
||||
const constants = readJsonSync(path.join(__dirname, '../constants.json'));
|
||||
|
||||
@ -45,6 +45,7 @@ export interface EmbarkConfig {
|
||||
generationDir?: string;
|
||||
plugins?: any;
|
||||
buildDir?: string;
|
||||
migrations: string;
|
||||
}
|
||||
|
||||
export class Config {
|
||||
@ -83,7 +84,7 @@ export class Config {
|
||||
|
||||
events: Events;
|
||||
|
||||
embarkConfig: any = {};
|
||||
embarkConfig: EmbarkConfig = embarkConfigDefaults;
|
||||
|
||||
context: any;
|
||||
|
||||
@ -629,17 +630,7 @@ export class Config {
|
||||
}
|
||||
|
||||
loadEmbarkConfigFile() {
|
||||
const configObject = {
|
||||
options: {
|
||||
solc: {
|
||||
"optimize": true,
|
||||
"optimize-runs": 200
|
||||
}
|
||||
},
|
||||
generationDir: "embarkArtifacts"
|
||||
};
|
||||
|
||||
this.embarkConfig = recursiveMerge(configObject, this.embarkConfig);
|
||||
this.embarkConfig = recursiveMerge(embarkConfigDefaults, this.embarkConfig);
|
||||
|
||||
const contracts = this.embarkConfig.contracts;
|
||||
// determine contract 'root' directories
|
||||
|
@ -4,6 +4,22 @@ import { join } from "path";
|
||||
|
||||
const constants = readJsonSync(join(__dirname, '../constants.json'));
|
||||
|
||||
export const embarkConfigDefaults = {
|
||||
contracts: [],
|
||||
config: '',
|
||||
migrations: 'migrations',
|
||||
versions: {
|
||||
solc: "0.6.1"
|
||||
},
|
||||
options: {
|
||||
solc: {
|
||||
"optimize": true,
|
||||
"optimize-runs": 200
|
||||
}
|
||||
},
|
||||
generationDir: "embarkArtifacts"
|
||||
};
|
||||
|
||||
export function getBlockchainDefaults(env) {
|
||||
const defaults = {
|
||||
clientConfig: {
|
||||
|
@ -91,6 +91,9 @@ export interface Configuration {
|
||||
cert: string;
|
||||
};
|
||||
};
|
||||
contractsConfig: {
|
||||
tracking?: boolean | string;
|
||||
};
|
||||
plugins: EmbarkPlugins;
|
||||
reloadConfig(): void;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ class Cmd {
|
||||
this.demo();
|
||||
this.build();
|
||||
this.run();
|
||||
this.exec();
|
||||
this.console();
|
||||
this.blockchain();
|
||||
this.simulator();
|
||||
@ -174,6 +175,30 @@ class Cmd {
|
||||
});
|
||||
}
|
||||
|
||||
exec() {
|
||||
program
|
||||
.command('exec [environment] [script|directory]')
|
||||
.option('-t, --track', __('Force tracking of migration script', false))
|
||||
.description(__("Executes specified scripts or all scripts in 'directory'"))
|
||||
.action((env, target, options) => {
|
||||
embark.exec({
|
||||
env,
|
||||
target,
|
||||
forceTracking: options.track
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
console.error(err.message ? err.message : err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Done.');
|
||||
// TODO(pascal): Ideally this shouldn't be needed.
|
||||
// Seems like there's a pending child process at this point that needs
|
||||
// to be stopped.
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console() {
|
||||
program
|
||||
.command('console [environment]')
|
||||
|
@ -352,6 +352,44 @@ class EmbarkController {
|
||||
});
|
||||
}
|
||||
|
||||
exec(options, callback) {
|
||||
|
||||
const engine = new Engine({
|
||||
env: options.env,
|
||||
embarkConfig: options.embarkConfig || 'embark.json'
|
||||
});
|
||||
|
||||
engine.init({}, () => {
|
||||
engine.registerModuleGroup("coreComponents", {
|
||||
disableServiceMonitor: true
|
||||
});
|
||||
engine.registerModuleGroup("stackComponents");
|
||||
engine.registerModuleGroup("blockchain");
|
||||
engine.registerModuleGroup("compiler");
|
||||
engine.registerModuleGroup("contracts");
|
||||
engine.registerModulePackage('embark-deploy-tracker', {
|
||||
plugins: engine.plugins
|
||||
});
|
||||
engine.registerModulePackage('embark-scripts-runner');
|
||||
|
||||
engine.startEngine(async (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
try {
|
||||
await engine.events.request2("blockchain:node:start", engine.config.blockchainConfig);
|
||||
const [contractsList, contractDependencies] = await compileSmartContracts(engine);
|
||||
await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies);
|
||||
await engine.events.request2('scripts-runner:initialize');
|
||||
await engine.events.request2('scripts-runner:execute', options.target, options.forceTracking);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console(options) {
|
||||
this.context = options.context || [constants.contexts.run, constants.contexts.console];
|
||||
const REPL = require('./dashboard/repl.js');
|
||||
|
10
packages/plugins/scripts-runner/README.md
Normal file
10
packages/plugins/scripts-runner/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
embark-scripts-runner
|
||||
==========================
|
||||
|
||||
> Embark Scripts Runner
|
||||
|
||||
Plugin to run migration scripts for Smart Contract Deployment
|
||||
|
||||
Visit [embark.status.im](https://embark.status.im/) to get started with
|
||||
[Embark](https://github.com/embarklabs/embark).
|
||||
|
78
packages/plugins/scripts-runner/package.json
Normal file
78
packages/plugins/scripts-runner/package.json
Normal file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "embark-scripts-runner",
|
||||
"version": "5.1.0",
|
||||
"description": "Embark Scripts Runner",
|
||||
"repository": {
|
||||
"directory": "packages/plugins/scripts-runner",
|
||||
"type": "git",
|
||||
"url": "https://github.com/embarklabs/embark/"
|
||||
},
|
||||
"author": "Iuri Matias <iuri.matias@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": "https://github.com/embarklabs/embark/issues",
|
||||
"keywords": [],
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"embark-collective": {
|
||||
"build:node": true,
|
||||
"typecheck": true
|
||||
},
|
||||
"scripts": {
|
||||
"_build": "npm run solo -- build",
|
||||
"_typecheck": "npm run solo -- typecheck",
|
||||
"ci": "npm run qa",
|
||||
"clean": "npm run reset",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:ts": "tslint -c tslint.json \"src/**/*.ts\"",
|
||||
"qa": "npm-run-all lint _typecheck _build test",
|
||||
"reset": "npx rimraf dist embark-*.tgz package",
|
||||
"solo": "embark-solo",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "2.6.1",
|
||||
"@babel/runtime-corejs3": "7.7.4",
|
||||
"core-js-pure": "3.6.4",
|
||||
"embark-core": "^5.2.0-nightly.1",
|
||||
"embark-i18n": "^5.1.1",
|
||||
"embark-logger": "^5.1.2-nightly.0",
|
||||
"embark-utils": "^5.2.0-nightly.1",
|
||||
"fs-extra": "8.1.0",
|
||||
"web3": "1.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.7.4",
|
||||
"@types/node": "^10.5.3",
|
||||
"babel-jest": "24.9.0",
|
||||
"embark-solo": "^5.1.1-nightly.2",
|
||||
"embark-testing": "^5.1.0",
|
||||
"jest": "24.9.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"rimraf": "3.0.0",
|
||||
"tmp-promise": "1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.17.0",
|
||||
"npm": ">=6.11.3",
|
||||
"yarn": ">=1.19.1"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverage": true,
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"**/test/**/*.js"
|
||||
],
|
||||
"transform": {
|
||||
"\\.(js|ts)$": [
|
||||
"babel-jest",
|
||||
{
|
||||
"rootMode": "upward"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
60
packages/plugins/scripts-runner/src/error.ts
Normal file
60
packages/plugins/scripts-runner/src/error.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import path from 'path';
|
||||
import { Stats } from 'fs';
|
||||
|
||||
enum FileType {
|
||||
SymbolicLink = 'symbolic link',
|
||||
Socket = 'socket',
|
||||
Unknown = 'unknown'
|
||||
}
|
||||
|
||||
export class InitializationError extends Error {
|
||||
|
||||
name = 'InitalizationError';
|
||||
|
||||
constructor(public innerError: Error) {
|
||||
super();
|
||||
this.message = `Failed to initalize tracking file: Orignal Error: ${innerError}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedTargetError extends Error {
|
||||
|
||||
name = 'UnsupportedTargetError';
|
||||
|
||||
constructor(public stats: Stats) {
|
||||
super();
|
||||
// We can't access `this` before `super()` is called so we have to
|
||||
// set `this.message` after that to get a dedicated error message.
|
||||
this.setMessage();
|
||||
}
|
||||
|
||||
private setMessage() {
|
||||
let ftype = FileType.Unknown;
|
||||
if (this.stats.isSymbolicLink()) {
|
||||
ftype = FileType.SymbolicLink;
|
||||
} else if (this.stats.isSocket()) {
|
||||
ftype = FileType.Socket;
|
||||
}
|
||||
this.message = `Script execution target not supported. Expected file path or directory, got ${ftype} type.`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ScriptExecutionError extends Error {
|
||||
|
||||
name = 'ScriptExecutionError';
|
||||
|
||||
constructor(public target: string, public innerError: Error) {
|
||||
super();
|
||||
this.message = `Script '${path.basename(target)}' failed to execute. Original error: ${innerError.stack}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ScriptTrackingError extends Error {
|
||||
|
||||
name = 'ScriptTrackingError';
|
||||
|
||||
constructor(public innerError: Error) {
|
||||
super();
|
||||
this.message = `Couldn't track script due execption. Original error: ${innerError.stack}`;
|
||||
}
|
||||
}
|
202
packages/plugins/scripts-runner/src/index.ts
Normal file
202
packages/plugins/scripts-runner/src/index.ts
Normal file
@ -0,0 +1,202 @@
|
||||
import {
|
||||
InitializationError,
|
||||
UnsupportedTargetError,
|
||||
ScriptExecutionError,
|
||||
ScriptTrackingError
|
||||
} from './error';
|
||||
|
||||
import { FileSystemTracker, ScriptsTracker, TrackingData } from './tracker';
|
||||
|
||||
import AsyncIterator from 'core-js-pure/features/async-iterator';
|
||||
import { Embark, Callback } from 'embark-core';
|
||||
import { __ } from 'embark-i18n';
|
||||
import { Logger } from 'embark-logger';
|
||||
import { dappPath } from 'embark-utils';
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import Web3 from "web3";
|
||||
import { BlockTransactionObject } from 'web3-eth';
|
||||
|
||||
export enum ScriptsRunnerCommand {
|
||||
Initialize = 'scripts-runner:initialize',
|
||||
Execute = 'scripts-runner:execute'
|
||||
}
|
||||
|
||||
export enum ScriptsRunnerEvent {
|
||||
Executed = 'scripts-runner:script:executed'
|
||||
}
|
||||
|
||||
export interface ScriptsRunnerPluginOptions {
|
||||
tracker?: ScriptsTracker;
|
||||
}
|
||||
|
||||
export interface ScriptDependencies {
|
||||
web3: Web3 | null;
|
||||
contracts: any;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface ExecuteOptions {
|
||||
target: string;
|
||||
dependencies: ScriptDependencies;
|
||||
forceTracking: boolean;
|
||||
}
|
||||
|
||||
export default class ScriptsRunnerPlugin {
|
||||
|
||||
private _web3: Web3 | null = null;
|
||||
|
||||
private _block: BlockTransactionObject | null = null;
|
||||
|
||||
private trackingEnabled: boolean;
|
||||
|
||||
private tracker: ScriptsTracker;
|
||||
|
||||
constructor(private embark: Embark, options?: ScriptsRunnerPluginOptions) {
|
||||
this.tracker = options?.tracker ? options.tracker : new FileSystemTracker(embark);
|
||||
this.trackingEnabled = embark.config.contractsConfig.tracking !== false;
|
||||
|
||||
// TODO: it'd be wonderful if Embark called `registerCommandHandlers()` for us
|
||||
this.registerCommandHandlers();
|
||||
}
|
||||
|
||||
private registerCommandHandlers() {
|
||||
this.embark.events.setCommandHandler(ScriptsRunnerCommand.Initialize, this.initialize.bind(this));
|
||||
this.embark.events.setCommandHandler(ScriptsRunnerCommand.Execute, this.execute.bind(this));
|
||||
}
|
||||
|
||||
private async initialize(callback: Callback<any>) {
|
||||
if (!this.trackingEnabled) {
|
||||
return callback();
|
||||
}
|
||||
try {
|
||||
await this.tracker.ensureTrackingFile();
|
||||
callback();
|
||||
} catch (e) {
|
||||
callback(new InitializationError(e));
|
||||
}
|
||||
}
|
||||
|
||||
private get web3() {
|
||||
return (async () => {
|
||||
if (!this._web3) {
|
||||
const provider = await this.embark.events.request2('blockchain:client:provider', 'ethereum');
|
||||
this._web3 = new Web3(provider);
|
||||
}
|
||||
return this._web3;
|
||||
})();
|
||||
}
|
||||
|
||||
private async execute(target: string, forceTracking = false, callback: Callback<any>) {
|
||||
const targetPath = !path.isAbsolute(target) ? dappPath(target) : target;
|
||||
try {
|
||||
const fstat = await fs.stat(targetPath);
|
||||
if (fstat.isDirectory()) {
|
||||
const dependencies = await this.getScriptDependencies();
|
||||
const results = await this.executeAll({ target: targetPath, dependencies, forceTracking });
|
||||
return callback(null, results);
|
||||
}
|
||||
if (fstat.isFile()) {
|
||||
const dependencies = await this.getScriptDependencies();
|
||||
const result = await this.executeSingle({ target: targetPath, dependencies, forceTracking });
|
||||
return callback(null, result);
|
||||
}
|
||||
callback(new UnsupportedTargetError(fstat));
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeSingle(options: ExecuteOptions) {
|
||||
const forceTracking = options.forceTracking;
|
||||
const scriptName = path.basename(options.target);
|
||||
const scriptDirectory = path.basename(path.dirname(options.target));
|
||||
const scriptTracked = await this.tracker.isTracked(scriptName);
|
||||
|
||||
if (scriptTracked && (scriptDirectory === this.embark.config.embarkConfig.migrations || forceTracking)) {
|
||||
this.embark.logger.info(__(' ✓ %s already done', scriptName));
|
||||
return;
|
||||
}
|
||||
|
||||
this.embark.logger.info(__(' %s running....', scriptName));
|
||||
const scriptToRun = require(options.target);
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await scriptToRun(options.dependencies);
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e : new Error(e);
|
||||
throw new ScriptExecutionError(options.target, error);
|
||||
}
|
||||
|
||||
this.embark.logger.info(__(' ✓ finished.'));
|
||||
|
||||
if (!this.trackingEnabled) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.tracker.track({
|
||||
scriptName,
|
||||
scriptDirectory,
|
||||
forceTracking
|
||||
});
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e : new Error(e);
|
||||
throw new ScriptTrackingError(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async executeAll(options: ExecuteOptions) {
|
||||
const target = options.target;
|
||||
const files = await fs.readdir(target);
|
||||
|
||||
const scripts = await Promise.all(
|
||||
files.map(async file => {
|
||||
const targetPath = !path.isAbsolute(target) ? dappPath(target, file) : path.join(target, file);
|
||||
return { target: targetPath, stats: await fs.stat(targetPath) };
|
||||
})
|
||||
);
|
||||
|
||||
return AsyncIterator.from(scripts)
|
||||
.filter(({stats}) => stats.isFile())
|
||||
.map(fstat => ({
|
||||
target: fstat.target,
|
||||
dependencies: options.dependencies,
|
||||
forceTracking: options.forceTracking
|
||||
}))
|
||||
.map(script => this.executeSingle(script))
|
||||
.toArray();
|
||||
}
|
||||
|
||||
private async getScriptDependencies() {
|
||||
const contracts = await this.embark.events.request2('contracts:list');
|
||||
|
||||
const dependencies: ScriptDependencies = {
|
||||
logger: this.embark.logger,
|
||||
web3: null,
|
||||
contracts: {}
|
||||
};
|
||||
|
||||
dependencies.web3 = await this.web3;
|
||||
|
||||
for (const contract of contracts) {
|
||||
const registeredInVM = this.checkContractRegisteredInVM(contract);
|
||||
if (!registeredInVM) {
|
||||
await this.embark.events.request2("embarkjs:contract:runInVm", contract);
|
||||
}
|
||||
const contractInstance = await this.embark.events.request2("runcode:eval", contract.className);
|
||||
dependencies.contracts[contract.className] = contractInstance;
|
||||
}
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
private async checkContractRegisteredInVM(contract) {
|
||||
const checkContract = `
|
||||
return typeof ${contract.className} !== 'undefined';
|
||||
`;
|
||||
return this.embark.events.request2('runcode:eval', checkContract);
|
||||
}
|
||||
}
|
118
packages/plugins/scripts-runner/src/tracker.ts
Normal file
118
packages/plugins/scripts-runner/src/tracker.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { Embark } from 'embark-core';
|
||||
import * as fs from 'fs-extra';
|
||||
import { dappPath } from 'embark-utils';
|
||||
import { BlockTransactionObject } from 'web3-eth';
|
||||
import Web3 from "web3";
|
||||
|
||||
export interface TrackingData {
|
||||
[key: string]: {
|
||||
migrations: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface TrackConfig {
|
||||
scriptName: string;
|
||||
scriptDirectory: string;
|
||||
forceTracking: boolean;
|
||||
migrationsDir?: string;
|
||||
}
|
||||
|
||||
export interface ScriptsTracker {
|
||||
ensureTrackingFile(): Promise<void>;
|
||||
track(trackConfig: TrackConfig): Promise<void>;
|
||||
isTracked(scriptName: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
const DEFAULT_TRACKING_FILE_PATH = '.embark/chains.json';
|
||||
|
||||
export class FileSystemTracker implements ScriptsTracker {
|
||||
|
||||
private _block: BlockTransactionObject | null = null;
|
||||
|
||||
private _web3: Web3 | null = null;
|
||||
|
||||
private migrationsDirectory: string;
|
||||
|
||||
private trackingFilePath: string;
|
||||
|
||||
constructor(private embark: Embark) {
|
||||
this.trackingFilePath = dappPath(embark.config.contractsConfig?.tracking || DEFAULT_TRACKING_FILE_PATH);
|
||||
this.migrationsDirectory = embark.config.embarkConfig.migrations;
|
||||
}
|
||||
|
||||
private get web3() {
|
||||
return (async () => {
|
||||
if (!this._web3) {
|
||||
const provider = await this.embark.events.request2('blockchain:client:provider', 'ethereum');
|
||||
this._web3 = new Web3(provider);
|
||||
}
|
||||
return this._web3;
|
||||
})();
|
||||
}
|
||||
|
||||
get block() {
|
||||
return (async () => {
|
||||
if (this._block) {
|
||||
return this._block;
|
||||
}
|
||||
const web3 = await this.web3;
|
||||
try {
|
||||
this._block = await web3.eth.getBlock(0, true);
|
||||
} catch (err) {
|
||||
// Retry with block 1 (Block 0 fails with Ganache-cli using the --fork option)
|
||||
this._block = await web3.eth.getBlock(1, true);
|
||||
}
|
||||
return this._block;
|
||||
})();
|
||||
}
|
||||
|
||||
async ensureTrackingFile() {
|
||||
const fstat = await fs.stat(this.trackingFilePath);
|
||||
if (!fstat.isFile()) {
|
||||
const block = await this.block;
|
||||
if (block) {
|
||||
await fs.outputJSON(this.trackingFilePath, {
|
||||
[block.hash]: {
|
||||
name: this.embark.env,
|
||||
migrations: []
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async track(trackConfig: TrackConfig) {
|
||||
const {
|
||||
scriptName,
|
||||
scriptDirectory,
|
||||
forceTracking,
|
||||
} = trackConfig;
|
||||
|
||||
if (forceTracking || scriptDirectory === this.migrationsDirectory) {
|
||||
const block = await this.block;
|
||||
|
||||
if (block) {
|
||||
const trackingData = await fs.readJSON(this.trackingFilePath) as TrackingData;
|
||||
|
||||
if (!trackingData[block.hash].migrations) {
|
||||
trackingData[block.hash].migrations = [];
|
||||
}
|
||||
|
||||
if (!trackingData[block.hash].migrations.includes(scriptName)) {
|
||||
trackingData[block.hash].migrations.push(scriptName);
|
||||
await fs.writeJSON(this.trackingFilePath, trackingData, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async isTracked(scriptName: string) {
|
||||
const block = await this.block;
|
||||
const trackingData = await fs.readJSON(this.trackingFilePath) as TrackingData;
|
||||
return (block &&
|
||||
trackingData &&
|
||||
trackingData[block.hash]?.migrations &&
|
||||
trackingData[block.hash]?.migrations.includes(scriptName)) ?? false;
|
||||
}
|
||||
|
||||
}
|
171
packages/plugins/scripts-runner/test/script-runner.spec.js
Normal file
171
packages/plugins/scripts-runner/test/script-runner.spec.js
Normal file
@ -0,0 +1,171 @@
|
||||
import sinon from 'sinon';
|
||||
import assert from 'assert';
|
||||
import path from 'path';
|
||||
import { fakeEmbark } from 'embark-testing';
|
||||
import ScriptsRunnerPlugin, { ScriptsRunnerCommand, ScriptsRunnerEvent } from '../src/';
|
||||
import { file as tmpFile, dir as tmpDir } from 'tmp-promise';
|
||||
import { promises } from 'fs';
|
||||
|
||||
// Due to our `DAPP_PATH` dependency in `embark-utils` `dappPath()`, we need to
|
||||
// ensure that this environment variable is defined.
|
||||
process.env.DAPP_PATH = 'something';
|
||||
|
||||
async function prepareScriptFile(content, dir) {
|
||||
const file = await tmpFile({ postfix: '.js', dir});
|
||||
await promises.writeFile(file.path, content);
|
||||
return file;
|
||||
}
|
||||
|
||||
const web3Mock = {
|
||||
eth: {
|
||||
getBlock: sinon.spy((number, flag) => { hash: 'testhash'})
|
||||
}
|
||||
}
|
||||
|
||||
describe('plugins/scripts-runner', () => {
|
||||
|
||||
let scriptRunner,
|
||||
testTracker,
|
||||
runCodeCommandHandler,
|
||||
blockchainClientProviderCommandHandler,
|
||||
contractsListCommandHandler,
|
||||
embark,
|
||||
plugins;
|
||||
|
||||
beforeEach(async () => {
|
||||
const testBed = fakeEmbark({
|
||||
contractsConfig: {
|
||||
},
|
||||
embarkConfig: {
|
||||
migrations: 'migrations'
|
||||
}
|
||||
});
|
||||
|
||||
testTracker = {
|
||||
web3: web3Mock,
|
||||
ensureTrackingFile: sinon.spy((hash, env) => {}),
|
||||
track: sinon.spy(config => {}),
|
||||
isTracked: sinon.spy(() => false),
|
||||
setWeb3: sinon.spy(web3 => {})
|
||||
};
|
||||
|
||||
|
||||
embark = testBed.embark;
|
||||
scriptRunner = new ScriptsRunnerPlugin(embark, { tracker: testTracker })
|
||||
|
||||
runCodeCommandHandler = sinon.spy((code, cb) => {
|
||||
// `ScriptsRunnerPlugin` requests code evaluation two times.
|
||||
// It expects a boolean for the first one and an object for
|
||||
// the second one.
|
||||
if (code.indexOf('!==') > 0) {
|
||||
cb(null, true);
|
||||
}
|
||||
cb(null, {});
|
||||
});
|
||||
|
||||
blockchainClientProviderCommandHandler = sinon.spy((name, cb) => {
|
||||
cb(null, 'http://localhost:8545');
|
||||
});
|
||||
|
||||
contractsListCommandHandler = sinon.spy(cb => {
|
||||
cb(null, [
|
||||
{ className: 'SimpleStorage' },
|
||||
{ className: 'AnotherOne' },
|
||||
{ className: 'Foo' }
|
||||
])
|
||||
});
|
||||
|
||||
embark.events.setCommandHandler('contracts:list', contractsListCommandHandler);
|
||||
embark.events.setCommandHandler('runcode:eval', runCodeCommandHandler);
|
||||
embark.events.setCommandHandler('blockchain:client:provider', blockchainClientProviderCommandHandler);
|
||||
await embark.events.request2(ScriptsRunnerCommand.Initialize);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
embark.teardown();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('should execute script', async (done) => {
|
||||
const scriptFile = await prepareScriptFile(`module.exports = () => { return 'done'; }`);
|
||||
|
||||
embark.events.request(ScriptsRunnerCommand.Execute, scriptFile.path, false, (err, result) => {
|
||||
assert.equal(result, 'done');
|
||||
scriptFile.cleanup();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute all scripts in a directory', async (done) => {
|
||||
const scriptsDir = await tmpDir();
|
||||
|
||||
const scriptFile1 = await prepareScriptFile(
|
||||
`module.exports = () => { return 'done' }`,
|
||||
scriptsDir.path
|
||||
);
|
||||
|
||||
const scriptFile2 = await prepareScriptFile(
|
||||
`module.exports = () => { return 'done2' }`,
|
||||
scriptsDir.path
|
||||
);
|
||||
|
||||
embark.events.request(ScriptsRunnerCommand.Execute, scriptsDir.path, false, (err, result) => {
|
||||
assert.ok(result.includes('done'));
|
||||
assert.ok(result.includes('done2'));
|
||||
scriptsDir.cleanup();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should force track scripts if --track option is applied', async (done) => {
|
||||
const scriptFile = await prepareScriptFile(`module.exports = () => { return 'done'; }`);
|
||||
|
||||
const expectedResult = {
|
||||
scriptName: path.basename(scriptFile.path),
|
||||
scriptDirectory: path.basename(path.dirname(scriptFile.path)),
|
||||
forceTracking: true,
|
||||
};
|
||||
|
||||
embark.events.request(ScriptsRunnerCommand.Execute, scriptFile.path, true, (err, result) => {
|
||||
assert.equal(result, 'done');
|
||||
assert(testTracker.track.calledOnce);
|
||||
assert(testTracker.track.calledWith(expectedResult));
|
||||
scriptFile.cleanup();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should track automatically if script directory equals migrations directory', async (done) => {
|
||||
const scriptsDir = await tmpDir();
|
||||
const migrationsDir = path.join(scriptsDir.path, embark.config.embarkConfig.migrations)
|
||||
|
||||
await promises.mkdir(migrationsDir);
|
||||
|
||||
const scriptFile = await prepareScriptFile(
|
||||
`module.exports = () => { return 'done' }`,
|
||||
migrationsDir
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
scriptName: path.basename(scriptFile.path),
|
||||
scriptDirectory: path.basename(migrationsDir),
|
||||
forceTracking: false,
|
||||
};
|
||||
|
||||
embark.events.request(ScriptsRunnerCommand.Execute, scriptFile.path, false, (err, result) => {
|
||||
assert.equal(result, 'done');
|
||||
assert(testTracker.track.calledOnce);
|
||||
assert(testTracker.track.calledWith(expectedResult));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not execute script if it was tracked', async(done) => {
|
||||
const scriptFile = await prepareScriptFile(`module.exports = () => { return 'done'; }`);
|
||||
|
||||
embark.events.request(ScriptsRunnerCommand.Execute, scriptFile.path, true, (err, result) => {
|
||||
assert.equal(result, undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
29
packages/plugins/scripts-runner/tsconfig.json
Normal file
29
packages/plugins/scripts-runner/tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declarationDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"tsBuildInfoFile": "./node_modules/.cache/tsc/tsconfig.embark-scripts-runner.tsbuildinfo"
|
||||
},
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../core/core"
|
||||
},
|
||||
{
|
||||
"path": "../../core/i18n"
|
||||
},
|
||||
{
|
||||
"path": "../../core/logger"
|
||||
},
|
||||
{
|
||||
"path": "../../core/utils"
|
||||
},
|
||||
{
|
||||
"path": "../../utils/testing"
|
||||
}
|
||||
]
|
||||
}
|
4
packages/plugins/scripts-runner/tslint.json
Normal file
4
packages/plugins/scripts-runner/tslint.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tslint.json"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ docs:
|
||||
environments: environments.html
|
||||
configuration: configuration.html
|
||||
pipeline_and_webpack: pipeline_and_webpack.html
|
||||
executing_migration_scripts: executing_scripts.html
|
||||
# setting_up_storages: foo.html
|
||||
# uploading_data: foo.html
|
||||
# configuring_whisper: foo.html
|
||||
|
@ -151,6 +151,16 @@ $ embark reset
|
||||
|
||||
Resets embarks state on this dapp including clearing cache.
|
||||
|
||||
## exec
|
||||
|
||||
```
|
||||
$ embark exec [environment] [file|directory]
|
||||
```
|
||||
|
||||
Executes a given (migration) script to perform complex after deployment operations.
|
||||
|
||||
It's required to specifiy the `environment` in which the script(s) will be executed in. In addition it's possible to specificy a directory in which multiple script live in. Embark will execute them one by one.
|
||||
|
||||
## upload
|
||||
|
||||
```
|
||||
|
115
site/source/docs/executing_scripts.md
Normal file
115
site/source/docs/executing_scripts.md
Normal file
@ -0,0 +1,115 @@
|
||||
title: Executing Scripts
|
||||
layout: docs
|
||||
---
|
||||
|
||||
There are various features in Embark that help you making the deployment of your DApps and Smart Contracts as smooth as possible. Next to general [Smart Contract Configurations](contracts_configuration.html), [Deployment Hooks](contracts_configuration.html#Deployment-hooks) and [Cockpit](cockpit_introduction.html), there's the possibility to run (migration) scripts as well.
|
||||
|
||||
In this guide we'll explore why scripts are useful and how they can be run.
|
||||
|
||||
## Why scripts?
|
||||
|
||||
Given that Embark supports [afterDeploy](contracts_configuration.html#afterDeploy-hook) hooks that make it extremely easy to perform custom operations after all of your Smart Contracts have been deployed, you might wonder when and where scripts can be useful.
|
||||
|
||||
It's important to note that `afterDeploy` hooks are executed every time all Smart Contracts have been deployed. Often there are cases where running a (migration) script manually is what you really need.
|
||||
|
||||
Scripts let you do exactly that as they can be run at any time, regardless of what your app's current deployment status is.
|
||||
|
||||
## What's a script?
|
||||
|
||||
A script is really just a file with an exported function that has special dependencies injected into it. Here's what it could look like:
|
||||
|
||||
```
|
||||
modules.exports = async ({ contracts, web3, logger}) => {
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
The injected parameters are:
|
||||
|
||||
- `contracts` - A map object containing all of your Smart Contracts as Embark Smart Contract instances.
|
||||
- `web3` - A web3 instances to give you access to things like accounts.
|
||||
- `logger` - Embark's custom logger.
|
||||
|
||||
Scripts can be located anywhere on your machine, but should most likely live inside your project's file tree in a dedicated folder.
|
||||
|
||||
## Running scripts
|
||||
|
||||
To run a script, use the CLI `exec` command and specify an environment as well as the script to be executed:
|
||||
|
||||
```
|
||||
$ embark exec development scripts/001.js
|
||||
```
|
||||
|
||||
The command above will execute the function in `scripts/001.js` and ensures that Smart Contracts are deployed in the `development` environment.
|
||||
|
||||
If you have multiple scripts that should run in order, it's also possible to specify the directory in which they live in:
|
||||
|
||||
```
|
||||
$ embark exec development scripts
|
||||
```
|
||||
|
||||
Embark will then find all script files inside the specified directory (in this case `scripts`) and then run them one by one. If any of the scripts fails by emitting an error, Embark will abort the execution. Scripts are executed in sequence, which means all following scripts won't be executed in case of an error.
|
||||
|
||||
## Error Handling
|
||||
|
||||
It's possible and recommended for scripts to emit proper errors in case they fail to do their job. There are several ways to emit an error depending on how you write your function. Scripts are executed asyncronously, so one way to emit an error is to reject a promise:
|
||||
|
||||
```
|
||||
modules.exports = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
reject(new Error('Whoops, something went wrong'));
|
||||
});
|
||||
};
|
||||
|
||||
// or
|
||||
modules.exports = () => {
|
||||
return Promise.reject(new Error ('Whoops, something went wrong'));
|
||||
};
|
||||
```
|
||||
|
||||
If your script uses the `async/await` syntax, errors are emitted by default when using other `async` APIs that fail:
|
||||
|
||||
```
|
||||
module.exports = async () => {
|
||||
await someAPIThatFails(); // this will emit an error
|
||||
};
|
||||
```
|
||||
|
||||
If an error is emitted, Embark will do its best to give you information about the original error:
|
||||
|
||||
```
|
||||
001.js running....
|
||||
Script '001.js' failed to execute. Original error: Error: Whoops, something went wrong
|
||||
```
|
||||
|
||||
## Tracking scripts
|
||||
|
||||
Just like Smart Contract deployments are tracked, (migration) scripts can be tracked as well. Since scripts can be one-off operations, Embark will not track whether they have been executed by default. Users are always able to run a script using the `exec` command as discussed in the previous sections.
|
||||
|
||||
To have Embark "remember" that a certain script was already run, you can use the `--track` option of the `exec` command, which will force tracking for this particular script:
|
||||
|
||||
```
|
||||
$ embark exec development scripts/001.js --track
|
||||
```
|
||||
|
||||
If we try to run the script again with the `--track` option, Embark will notice that the script has already been executed and tell us that it's "already done".
|
||||
|
||||
```
|
||||
$ embark exec development scripts/001.js --track
|
||||
.. 001.js already done
|
||||
```
|
||||
|
||||
If however, we don't provide the `--track` flag, Embark will execute the script as usual.
|
||||
|
||||
For cases in which we **do** want to track a set of scripts, especially when the main use case are migration operations, we can put our scripts in a special "migrations" directory. All scripts inside that directory will be tracked by default.
|
||||
|
||||
The directory can be specified using the `migrations` property in your project's embark.json:
|
||||
|
||||
```
|
||||
{
|
||||
...
|
||||
migrations: 'migrations'
|
||||
}
|
||||
```
|
||||
|
||||
If no such property is specified, Embark will default to "migrations". Running any script or set of scripts is then automatically tracked.
|
@ -288,6 +288,7 @@ sidebar:
|
||||
installation: Installation
|
||||
faq: FAQ
|
||||
creating_project: Creating apps
|
||||
executing_migration_scripts: Executing Scripts
|
||||
structure: App structure
|
||||
running_apps: Running apps
|
||||
dashboard: Using the dashboard
|
||||
|
@ -15,4 +15,4 @@
|
||||
"strict": true,
|
||||
"target": "ESNext"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +103,9 @@
|
||||
{
|
||||
"path": "packages/plugins/scaffolding"
|
||||
},
|
||||
{
|
||||
"path": "packages/plugins/scripts-runner"
|
||||
},
|
||||
{
|
||||
"path": "packages/plugins/snark"
|
||||
},
|
||||
|
@ -18,7 +18,8 @@
|
||||
"ordered-imports": false,
|
||||
"quotemark": [false, "single"],
|
||||
"trailing-comma": false,
|
||||
"no-irregular-whitespace": false
|
||||
"no-irregular-whitespace": false,
|
||||
"max-classes-per-file": false
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
||||
|
48
yarn.lock
48
yarn.lock
@ -4512,6 +4512,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.17.tgz#b96d4dd3e427382482848948041d3754d40fd5ce"
|
||||
integrity sha512-p/sGgiPaathCfOtqu2fx5Mu1bcjuP8ALFg4xpGgNkcin7LwRyzUKniEHBKdcE1RPsenq5JVPIpMTJSygLboygQ==
|
||||
|
||||
"@types/node@^10.5.3":
|
||||
version "10.17.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.14.tgz#b6c60ebf2fb5e4229fdd751ff9ddfae0f5f31541"
|
||||
integrity sha512-G0UmX5uKEmW+ZAhmZ6PLTQ5eu/VPaT+d/tdLd5IFsKRPcbe6lPxocBtcYBFSaLaCW8O60AX90e91Nsp8lVHCNw==
|
||||
|
||||
"@types/node@^12.6.1":
|
||||
version "12.12.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.6.tgz#a47240c10d86a9a57bb0c633f0b2e0aea9ce9253"
|
||||
@ -7820,6 +7825,11 @@ core-js-compat@^3.1.1:
|
||||
browserslist "^4.6.6"
|
||||
semver "^6.3.0"
|
||||
|
||||
core-js-pure@3.6.4:
|
||||
version "3.6.4"
|
||||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
|
||||
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
|
||||
|
||||
core-js-pure@^3.0.0:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.3.3.tgz#c6a796e371782394ffb60d82ff67e0e073070093"
|
||||
@ -22279,6 +22289,14 @@ title-case@^2.1.0:
|
||||
no-case "^2.2.0"
|
||||
upper-case "^1.0.3"
|
||||
|
||||
tmp-promise@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-1.1.0.tgz#bb924d239029157b9bc1d506a6aa341f8b13e64c"
|
||||
integrity sha512-8+Ah9aB1IRXCnIOxXZ0uFozV1nMU5xiu7hhFVUSxZ3bYu+psD4TzagCzVbexUCgNNGJnsmNDQlS4nG3mTyoNkw==
|
||||
dependencies:
|
||||
bluebird "^3.5.0"
|
||||
tmp "0.1.0"
|
||||
|
||||
tmp@0.0.33, tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
@ -22286,7 +22304,7 @@ tmp@0.0.33, tmp@^0.0.33:
|
||||
dependencies:
|
||||
os-tmpdir "~1.0.2"
|
||||
|
||||
tmp@^0.1.0:
|
||||
tmp@0.1.0, tmp@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
|
||||
integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==
|
||||
@ -23597,6 +23615,20 @@ web3-utils@1.2.6:
|
||||
underscore "1.9.1"
|
||||
utf8 "3.0.0"
|
||||
|
||||
web3@1.2.4, web3@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/web3/-/web3-1.2.4.tgz#6e7ab799eefc9b4648c2dab63003f704a1d5e7d9"
|
||||
integrity sha512-xPXGe+w0x0t88Wj+s/dmAdASr3O9wmA9mpZRtixGZxmBexAF0MjfqYM+MS4tVl5s11hMTN3AZb8cDD4VLfC57A==
|
||||
dependencies:
|
||||
"@types/node" "^12.6.1"
|
||||
web3-bzz "1.2.4"
|
||||
web3-core "1.2.4"
|
||||
web3-eth "1.2.4"
|
||||
web3-eth-personal "1.2.4"
|
||||
web3-net "1.2.4"
|
||||
web3-shh "1.2.4"
|
||||
web3-utils "1.2.4"
|
||||
|
||||
web3@1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/web3/-/web3-1.2.6.tgz#c497dcb14cdd8d6d9fb6b445b3b68ff83f8ccf68"
|
||||
@ -23611,20 +23643,6 @@ web3@1.2.6:
|
||||
web3-shh "1.2.6"
|
||||
web3-utils "1.2.6"
|
||||
|
||||
web3@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/web3/-/web3-1.2.4.tgz#6e7ab799eefc9b4648c2dab63003f704a1d5e7d9"
|
||||
integrity sha512-xPXGe+w0x0t88Wj+s/dmAdASr3O9wmA9mpZRtixGZxmBexAF0MjfqYM+MS4tVl5s11hMTN3AZb8cDD4VLfC57A==
|
||||
dependencies:
|
||||
"@types/node" "^12.6.1"
|
||||
web3-bzz "1.2.4"
|
||||
web3-core "1.2.4"
|
||||
web3-eth "1.2.4"
|
||||
web3-eth-personal "1.2.4"
|
||||
web3-net "1.2.4"
|
||||
web3-shh "1.2.4"
|
||||
web3-utils "1.2.4"
|
||||
|
||||
webidl-conversions@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||
|
Loading…
x
Reference in New Issue
Block a user