mirror of
https://github.com/status-im/sourcecred.git
synced 2025-01-13 22:25:47 +00:00
Extract execDependencyGraph
core from CI script (#208)
Summary: We’d like to use the same abstraction for creating multiple cred graphs and then combining them together. This will enable us to do that. Test Plan: Run `yarn travis` to test the success case, and `yarn travis --full` (without setting a `GITHUB_TOKEN`) to test the failure case. wchargin-branch: execdepgraph
This commit is contained in:
parent
a642ed46b9
commit
d3443a3d4c
186
config/travis.js
186
config/travis.js
@ -1,24 +1,8 @@
|
||||
// @flow
|
||||
|
||||
const chalk = require("chalk");
|
||||
const child_process = require("child_process");
|
||||
const execDependencyGraph = require("../src/tools/execDependencyGraph");
|
||||
|
||||
/*::
|
||||
type TaskId = string;
|
||||
type Task = {|
|
||||
+id: TaskId,
|
||||
+cmd: $ReadOnlyArray<string>,
|
||||
+deps: $ReadOnlyArray<TaskId>,
|
||||
|};
|
||||
|
||||
type TaskResult = {|
|
||||
+id: TaskId,
|
||||
+success: boolean,
|
||||
+status: number,
|
||||
+stdout: string,
|
||||
+stderr: string,
|
||||
|};
|
||||
*/
|
||||
main();
|
||||
|
||||
function main() {
|
||||
const mode =
|
||||
@ -26,170 +10,8 @@ function main() {
|
||||
process.argv.includes("--full")
|
||||
? "FULL"
|
||||
: "BASIC";
|
||||
processAll(makeTasks(mode));
|
||||
}
|
||||
main();
|
||||
|
||||
async function processAll(tasks /*: $ReadOnlyArray<Task> */) {
|
||||
const tasksById /*: {[TaskId]: Task} */ = {};
|
||||
tasks.forEach((task) => {
|
||||
if (tasksById[task.id] !== undefined) {
|
||||
throw new Error("Duplicate tasks with ID: " + task.id);
|
||||
}
|
||||
tasksById[task.id] = task;
|
||||
});
|
||||
|
||||
const completedTasks /*: Map<TaskId, TaskResult> */ = new Map();
|
||||
const tasksInProgress /*: Map<TaskId, Promise<TaskResult>> */ = new Map();
|
||||
const remainingTasks /*: Set<TaskId> */ = new Set(Object.keys(tasksById));
|
||||
|
||||
function spawnTasksWhoseDependenciesHaveCompleted() {
|
||||
for (const task of tasks) {
|
||||
if (!remainingTasks.has(task.id)) {
|
||||
continue;
|
||||
}
|
||||
if (incompleteDependencies(task).length > 0) {
|
||||
continue;
|
||||
}
|
||||
// Ready to spawn!
|
||||
remainingTasks.delete(task.id);
|
||||
console.log(chalk.bgBlue.bold.white(" GO ") + " " + task.id);
|
||||
tasksInProgress.set(task.id, processOne(task));
|
||||
}
|
||||
}
|
||||
|
||||
function incompleteDependencies(task /*: Task */) /*: TaskId[] */ {
|
||||
return task.deps.filter((dep) => {
|
||||
const result = completedTasks.get(dep);
|
||||
return !(result && result.success);
|
||||
});
|
||||
}
|
||||
|
||||
async function awaitAnyTask() {
|
||||
if (tasksInProgress.size === 0) {
|
||||
throw new Error("Invariant violation: No tasks to wait for.");
|
||||
}
|
||||
const result /*: TaskResult */ = await Promise.race(
|
||||
Array.from(tasksInProgress.values())
|
||||
);
|
||||
tasksInProgress.delete(result.id);
|
||||
completedTasks.set(result.id, result);
|
||||
displayResult(result.id, result, "OVERVIEW");
|
||||
}
|
||||
|
||||
function displayResult(
|
||||
id /*: TaskId */,
|
||||
result /*: ?TaskResult */,
|
||||
mode /*: "OVERVIEW" | "FULL" */
|
||||
) {
|
||||
const success = result && result.success;
|
||||
const badge = success
|
||||
? chalk.bgGreen.bold.white(" PASS ")
|
||||
: chalk.bgRed.bold.white(" FAIL ");
|
||||
console.log(`${badge} ${id}`);
|
||||
|
||||
if (mode === "OVERVIEW" && success) {
|
||||
return;
|
||||
}
|
||||
|
||||
let loggedAnything = false;
|
||||
function log(...args) {
|
||||
console.log(...args);
|
||||
loggedAnything = true;
|
||||
}
|
||||
if (!result) {
|
||||
log(`Did not run. Missing dependencies:`);
|
||||
incompleteDependencies(tasksById[id]).forEach((dep) => {
|
||||
log(` - ${dep}`);
|
||||
});
|
||||
log();
|
||||
return;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
log("Exit code: " + result.status);
|
||||
}
|
||||
if (result.stdout.length > 0) {
|
||||
log("Contents of stdout:");
|
||||
displayOutputStream(result.stdout);
|
||||
}
|
||||
if (result.stderr.length > 0) {
|
||||
log("Contents of stderr:");
|
||||
displayOutputStream(result.stderr);
|
||||
}
|
||||
if (loggedAnything) {
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
function displayOutputStream(streamContents /*: string */) {
|
||||
streamContents.split("\n").forEach((line, index, array) => {
|
||||
if (line === "" && index === array.length - 1) {
|
||||
return;
|
||||
} else {
|
||||
console.log(" " + line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function printSection(name /*: string */) {
|
||||
console.log("\n" + chalk.bold(name));
|
||||
}
|
||||
|
||||
printSection("Starting tasks");
|
||||
spawnTasksWhoseDependenciesHaveCompleted();
|
||||
while (tasksInProgress.size > 0) {
|
||||
await awaitAnyTask();
|
||||
spawnTasksWhoseDependenciesHaveCompleted();
|
||||
}
|
||||
|
||||
if (remainingTasks.size > 0) {
|
||||
printSection("Unreachable tasks");
|
||||
Array.from(remainingTasks.values()).forEach((line) => {
|
||||
console.log(` - ${line}`);
|
||||
});
|
||||
}
|
||||
|
||||
printSection("Full results");
|
||||
for (const task of tasks) {
|
||||
const result = completedTasks.get(task.id);
|
||||
displayResult(task.id, result, "FULL");
|
||||
}
|
||||
|
||||
printSection("Overview");
|
||||
const failedTasks = tasks.map((t) => t.id).filter((id) => {
|
||||
const result = completedTasks.get(id);
|
||||
return !result || !result.success;
|
||||
});
|
||||
if (failedTasks.length > 0) {
|
||||
console.log("Failed tasks:");
|
||||
failedTasks.forEach((line) => {
|
||||
console.log(` - ${line}`);
|
||||
});
|
||||
}
|
||||
const overallSuccess /*: boolean */ = failedTasks.length === 0;
|
||||
const overallBadge = overallSuccess
|
||||
? chalk.bgGreen.bold.white(" SUCCESS ")
|
||||
: chalk.bgRed.bold.white(" FAILURE ");
|
||||
console.log("Final result: " + overallBadge);
|
||||
process.exitCode = overallSuccess ? 0 : 1;
|
||||
}
|
||||
|
||||
function processOne(task /*: Task */) /*: Promise<TaskResult> */ {
|
||||
if (task.cmd.length === 0) {
|
||||
throw new Error("Empty command for task: " + task.id);
|
||||
}
|
||||
const file = task.cmd[0];
|
||||
const args = task.cmd.slice(1);
|
||||
return new Promise((resolve, _unused_reject) => {
|
||||
child_process.execFile(file, args, (error, stdout, stderr) => {
|
||||
resolve({
|
||||
id: task.id,
|
||||
success: !error,
|
||||
status: error ? error.code : 0,
|
||||
stdout: stdout.toString(),
|
||||
stderr: stderr.toString(),
|
||||
});
|
||||
});
|
||||
execDependencyGraph(makeTasks(mode)).then(({success}) => {
|
||||
process.exitCode = success ? 0 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
|
194
src/tools/execDependencyGraph.js
Normal file
194
src/tools/execDependencyGraph.js
Normal file
@ -0,0 +1,194 @@
|
||||
// @flow
|
||||
|
||||
// NOTE: This module must be written in vanilla ECMAScript that can be
|
||||
// run by Node without a preprocessor. That means that we use
|
||||
// `module.exports` and `require` instead of ECMAScript module keywords,
|
||||
// and we use the Flow comment syntax instead of the inline syntax.
|
||||
|
||||
const chalk = require("chalk");
|
||||
const child_process = require("child_process");
|
||||
|
||||
/*::
|
||||
type TaskId = string;
|
||||
type Task = {|
|
||||
+id: TaskId,
|
||||
+cmd: $ReadOnlyArray<string>,
|
||||
+deps: $ReadOnlyArray<TaskId>,
|
||||
|};
|
||||
|
||||
type TaskResult = {|
|
||||
+id: TaskId,
|
||||
+success: boolean,
|
||||
+status: number,
|
||||
+stdout: string,
|
||||
+stderr: string,
|
||||
|};
|
||||
type OverallResult = {|
|
||||
+success: boolean,
|
||||
|};
|
||||
*/
|
||||
|
||||
module.exports = async function execDepgraph(
|
||||
tasks /*: $ReadOnlyArray<Task> */
|
||||
) /*: Promise<OverallResult> */ {
|
||||
const tasksById /*: {[TaskId]: Task} */ = {};
|
||||
tasks.forEach((task) => {
|
||||
if (tasksById[task.id] !== undefined) {
|
||||
throw new Error("Duplicate tasks with ID: " + task.id);
|
||||
}
|
||||
tasksById[task.id] = task;
|
||||
});
|
||||
|
||||
const completedTasks /*: Map<TaskId, TaskResult> */ = new Map();
|
||||
const tasksInProgress /*: Map<TaskId, Promise<TaskResult>> */ = new Map();
|
||||
const remainingTasks /*: Set<TaskId> */ = new Set(Object.keys(tasksById));
|
||||
|
||||
function spawnTasksWhoseDependenciesHaveCompleted() {
|
||||
for (const task of tasks) {
|
||||
if (!remainingTasks.has(task.id)) {
|
||||
continue;
|
||||
}
|
||||
if (incompleteDependencies(task).length > 0) {
|
||||
continue;
|
||||
}
|
||||
// Ready to spawn!
|
||||
remainingTasks.delete(task.id);
|
||||
console.log(chalk.bgBlue.bold.white(" GO ") + " " + task.id);
|
||||
tasksInProgress.set(task.id, processTask(task));
|
||||
}
|
||||
}
|
||||
|
||||
function incompleteDependencies(task /*: Task */) /*: TaskId[] */ {
|
||||
return task.deps.filter((dep) => {
|
||||
const result = completedTasks.get(dep);
|
||||
return !(result && result.success);
|
||||
});
|
||||
}
|
||||
|
||||
async function awaitAnyTask() {
|
||||
if (tasksInProgress.size === 0) {
|
||||
throw new Error("Invariant violation: No tasks to wait for.");
|
||||
}
|
||||
const result /*: TaskResult */ = await Promise.race(
|
||||
Array.from(tasksInProgress.values())
|
||||
);
|
||||
tasksInProgress.delete(result.id);
|
||||
completedTasks.set(result.id, result);
|
||||
displayResult(result.id, result, "OVERVIEW");
|
||||
}
|
||||
|
||||
function displayResult(
|
||||
id /*: TaskId */,
|
||||
result /*: ?TaskResult */,
|
||||
mode /*: "OVERVIEW" | "FULL" */
|
||||
) {
|
||||
const success = result && result.success;
|
||||
const badge = success
|
||||
? chalk.bgGreen.bold.white(" PASS ")
|
||||
: chalk.bgRed.bold.white(" FAIL ");
|
||||
console.log(`${badge} ${id}`);
|
||||
|
||||
if (mode === "OVERVIEW" && success) {
|
||||
return;
|
||||
}
|
||||
|
||||
let loggedAnything = false;
|
||||
function log(...args) {
|
||||
console.log(...args);
|
||||
loggedAnything = true;
|
||||
}
|
||||
if (!result) {
|
||||
log(`Did not run. Missing dependencies:`);
|
||||
incompleteDependencies(tasksById[id]).forEach((dep) => {
|
||||
log(` - ${dep}`);
|
||||
});
|
||||
log();
|
||||
return;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
log("Exit code: " + result.status);
|
||||
}
|
||||
if (result.stdout.length > 0) {
|
||||
log("Contents of stdout:");
|
||||
displayOutputStream(result.stdout);
|
||||
}
|
||||
if (result.stderr.length > 0) {
|
||||
log("Contents of stderr:");
|
||||
displayOutputStream(result.stderr);
|
||||
}
|
||||
if (loggedAnything) {
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
function displayOutputStream(streamContents /*: string */) {
|
||||
streamContents.split("\n").forEach((line, index, array) => {
|
||||
if (line === "" && index === array.length - 1) {
|
||||
return;
|
||||
} else {
|
||||
console.log(" " + line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function printSection(name /*: string */) {
|
||||
console.log("\n" + chalk.bold(name));
|
||||
}
|
||||
|
||||
printSection("Starting tasks");
|
||||
spawnTasksWhoseDependenciesHaveCompleted();
|
||||
while (tasksInProgress.size > 0) {
|
||||
await awaitAnyTask();
|
||||
spawnTasksWhoseDependenciesHaveCompleted();
|
||||
}
|
||||
|
||||
if (remainingTasks.size > 0) {
|
||||
printSection("Unreachable tasks");
|
||||
Array.from(remainingTasks.values()).forEach((line) => {
|
||||
console.log(` - ${line}`);
|
||||
});
|
||||
}
|
||||
|
||||
printSection("Full results");
|
||||
for (const task of tasks) {
|
||||
const result = completedTasks.get(task.id);
|
||||
displayResult(task.id, result, "FULL");
|
||||
}
|
||||
|
||||
printSection("Overview");
|
||||
const failedTasks = tasks.map((t) => t.id).filter((id) => {
|
||||
const result = completedTasks.get(id);
|
||||
return !result || !result.success;
|
||||
});
|
||||
if (failedTasks.length > 0) {
|
||||
console.log("Failed tasks:");
|
||||
failedTasks.forEach((line) => {
|
||||
console.log(` - ${line}`);
|
||||
});
|
||||
}
|
||||
const overallSuccess /*: boolean */ = failedTasks.length === 0;
|
||||
const overallBadge = overallSuccess
|
||||
? chalk.bgGreen.bold.white(" SUCCESS ")
|
||||
: chalk.bgRed.bold.white(" FAILURE ");
|
||||
console.log("Final result: " + overallBadge);
|
||||
return Promise.resolve({success: overallSuccess});
|
||||
};
|
||||
|
||||
function processTask(task /*: Task */) /*: Promise<TaskResult> */ {
|
||||
if (task.cmd.length === 0) {
|
||||
throw new Error("Empty command for task: " + task.id);
|
||||
}
|
||||
const file = task.cmd[0];
|
||||
const args = task.cmd.slice(1);
|
||||
return new Promise((resolve, _unused_reject) => {
|
||||
child_process.execFile(file, args, (error, stdout, stderr) => {
|
||||
resolve({
|
||||
id: task.id,
|
||||
success: !error,
|
||||
status: error ? error.code : 0,
|
||||
stdout: stdout.toString(),
|
||||
stderr: stderr.toString(),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user