Plan and apply
This commit is contained in:
parent
5fc14b1403
commit
d5c9e97e10
|
@ -1,6 +1,7 @@
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
|
import readline from "readline";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
|
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
|
@ -287,7 +288,7 @@ async function getDepGraph() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTransitiveDependencies({ artifacts, targets }) {
|
function getTransitiveDependencies({ artifacts, targets }) {
|
||||||
let queue = targets;
|
let queue = [...targets];
|
||||||
let found = new Set();
|
let found = new Set();
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const name = queue.pop();
|
const name = queue.pop();
|
||||||
|
@ -303,26 +304,56 @@ function getTransitiveDependencies({ artifacts, targets }) {
|
||||||
return _.sortBy([...found], (name) => Object.keys(artifacts).indexOf(name));
|
return _.sortBy([...found], (name) => Object.keys(artifacts).indexOf(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printTable(data, headers) {
|
||||||
|
const widths = headers.map(({ key, title }) =>
|
||||||
|
Math.max(title.length, ...data.map((datum) => datum[key].length))
|
||||||
|
);
|
||||||
|
[
|
||||||
|
headers.map(({ title }) => title.toUpperCase()),
|
||||||
|
widths.map((width) => "-".repeat(width)),
|
||||||
|
...data.map((datum) => headers.map(({ key }) => datum[key])),
|
||||||
|
].map((values) =>
|
||||||
|
console.log(
|
||||||
|
values.map((value, idx) => value.padEnd(widths[idx])).join(" ")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserInput(prompt) {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
prompt: prompt,
|
||||||
|
});
|
||||||
|
rl.prompt();
|
||||||
|
const resp = await new Promise((resolve) => {
|
||||||
|
rl.on("line", (line) => {
|
||||||
|
resolve(line);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
rl.close();
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
async function executeDepGraph({
|
async function executeDepGraph({
|
||||||
depgraph,
|
depgraph,
|
||||||
manual,
|
manual,
|
||||||
holdManual,
|
holdManual,
|
||||||
|
all,
|
||||||
publish,
|
publish,
|
||||||
yes,
|
yes,
|
||||||
targets,
|
targets,
|
||||||
}) {
|
}) {
|
||||||
const artifacts = {};
|
const artifacts = {};
|
||||||
for (const artifact of depgraph.artifacts) {
|
for (const artifact of depgraph.artifacts) {
|
||||||
artifacts[artifact.name] = artifact;
|
for (const dep of artifact.dependencies) {
|
||||||
}
|
if (!artifacts[dep]) {
|
||||||
if (manual) {
|
|
||||||
for (const target of targets) {
|
|
||||||
if (artifacts[target].dependencies.length > 0) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`cannot build target ${target} with --manual as it is not a leaf artifact`
|
`artifact ${artifact.name} appears before dependency ${dep} in depgraph`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
artifacts[artifact.name] = artifact;
|
||||||
}
|
}
|
||||||
const transitiveTargets = getTransitiveDependencies({ artifacts, targets });
|
const transitiveTargets = getTransitiveDependencies({ artifacts, targets });
|
||||||
const requiredInfo = new Set();
|
const requiredInfo = new Set();
|
||||||
|
@ -363,7 +394,7 @@ async function executeDepGraph({
|
||||||
dependencyHashes[dependency] = await promises.desired[dependency];
|
dependencyHashes[dependency] = await promises.desired[dependency];
|
||||||
if (!dependencyHashes[dependency]) {
|
if (!dependencyHashes[dependency]) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`manual dependency must be built explicitly: dep ${target} --manual [--publish]`
|
`manual dependency must be built explicitly: dep ${dependency} --manual [--publish]`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -389,12 +420,9 @@ async function executeDepGraph({
|
||||||
}
|
}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
transitiveTargets.map(async (target) => {
|
transitiveTargets.map(async (target) => {
|
||||||
const {
|
const { getLocalHash, getPublishedHash, getDesiredHash } = artifacts[
|
||||||
publishOnly,
|
target
|
||||||
getLocalHash,
|
];
|
||||||
getPublishedHash,
|
|
||||||
getDesiredHash,
|
|
||||||
} = artifacts[target];
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
promises.local[target].then((hash) => {
|
promises.local[target].then((hash) => {
|
||||||
hashes.local[target] = hash;
|
hashes.local[target] = hash;
|
||||||
|
@ -408,7 +436,156 @@ async function executeDepGraph({
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
console.log(hashes);
|
const statuses = {};
|
||||||
|
for (const name in artifacts) {
|
||||||
|
if (!hashes.desired[name]) {
|
||||||
|
statuses[name] = "buildLocally";
|
||||||
|
} else if (
|
||||||
|
hashes.published[name] === hashes.desired[name] &&
|
||||||
|
hashes.local[name] === hashes.desired[name]
|
||||||
|
) {
|
||||||
|
statuses[name] = null;
|
||||||
|
} else if (
|
||||||
|
hashes.local[name] === hashes.desired[name] &&
|
||||||
|
hashes.published[name] !== hashes.desired[name]
|
||||||
|
) {
|
||||||
|
statuses[name] = "publishToRegistry";
|
||||||
|
} else if (
|
||||||
|
hashes.published[name] === hashes.desired[name] &&
|
||||||
|
hashes.local[name] !== hashes.desired[name]
|
||||||
|
) {
|
||||||
|
statuses[name] = "downloadFromRegistry";
|
||||||
|
} else {
|
||||||
|
statuses[name] = "buildLocally";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let priorityTargets;
|
||||||
|
if (all) {
|
||||||
|
priorityTargets = transitiveTargets;
|
||||||
|
} else {
|
||||||
|
const queue = [...targets];
|
||||||
|
const found = new Set();
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const name = queue.pop();
|
||||||
|
if (found.has(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const dep of artifacts[name].dependencies) {
|
||||||
|
if (statuses[dep] === "buildLocally") {
|
||||||
|
queue.push(dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found.add(name);
|
||||||
|
}
|
||||||
|
priorityTargets = [...found];
|
||||||
|
}
|
||||||
|
priorityTargets = _.sortBy(priorityTargets, (name) =>
|
||||||
|
Object.keys(artifacts).indexOf(name)
|
||||||
|
);
|
||||||
|
const plan = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const target of priorityTargets) {
|
||||||
|
for (const dep of artifacts[target].dependencies) {
|
||||||
|
if (artifacts[target].publishTarget) {
|
||||||
|
if (statuses === "publishToRegistry") {
|
||||||
|
plan.push({
|
||||||
|
artifact: dep,
|
||||||
|
action: "publishToRegistry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (statuses === "downloadFromRegistry") {
|
||||||
|
plan.push({
|
||||||
|
artifact: dep,
|
||||||
|
action: "downloadFromRegistry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seen.has(dep)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(dep);
|
||||||
|
}
|
||||||
|
if (statuses[target]) {
|
||||||
|
if (!artifacts[target].publishTarget) {
|
||||||
|
plan.push({
|
||||||
|
artifact: target,
|
||||||
|
action: statuses[target],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (statuses[target] === "buildLocally" && publish) {
|
||||||
|
plan.push({
|
||||||
|
artifact: target,
|
||||||
|
action: "publishToRegistry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seen.add(target);
|
||||||
|
}
|
||||||
|
const shortnames = {
|
||||||
|
buildLocally: "rebuild",
|
||||||
|
downloadFromRegistry: "download",
|
||||||
|
publishToRegistry: "publish",
|
||||||
|
};
|
||||||
|
const totals = {};
|
||||||
|
for (let { action } of plan) {
|
||||||
|
action = shortnames[action];
|
||||||
|
totals[action] = (totals[action] || 0) + 1;
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
if (plan.length === 0) {
|
||||||
|
console.log("No updates needed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printTable(
|
||||||
|
plan.map(({ artifact, action }) => {
|
||||||
|
const desc = {
|
||||||
|
buildLocally: " rebuild",
|
||||||
|
downloadFromRegistry: "download",
|
||||||
|
publishToRegistry: " publish",
|
||||||
|
}[action];
|
||||||
|
if (!desc) {
|
||||||
|
throw new Error(`unexpected action key ${action}`);
|
||||||
|
}
|
||||||
|
return { artifact, desc };
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
{ key: "artifact", title: "Artifact" },
|
||||||
|
{ key: "desc", title: "Action" },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
`Plan: ` +
|
||||||
|
["download", "rebuild", "publish"]
|
||||||
|
.filter((action) => totals[action])
|
||||||
|
.map((action) => `${totals[action]} to ${action}`)
|
||||||
|
.join(", ")
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
console.log("Do you want to perform these actions?");
|
||||||
|
console.log(" Depgraph will perform the actions described above.");
|
||||||
|
console.log(" Only 'yes' will be accepted to approve.");
|
||||||
|
console.log();
|
||||||
|
const resp = await getUserInput(" Enter a value: ");
|
||||||
|
if (resp !== "yes") {
|
||||||
|
console.log();
|
||||||
|
console.log("Apply cancelled.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
for (let idx = 0; idx < plan.length; idx++) {
|
||||||
|
const { artifact, action } = plan[idx];
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
`===== Step ${idx + 1} of ${plan.length}: ${
|
||||||
|
shortnames[action]
|
||||||
|
} ${artifact} =====`
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
await artifacts[artifact][action](info);
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
console.log("Apply successful!");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
@ -417,10 +594,11 @@ async function main() {
|
||||||
program.option("--list", "list available artifacts; ignore other arguments");
|
program.option("--list", "list available artifacts; ignore other arguments");
|
||||||
program.option("--manual", "operate explicitly on manual artifacts");
|
program.option("--manual", "operate explicitly on manual artifacts");
|
||||||
program.option("--hold-manual", "prefer local versions of manual artifacts");
|
program.option("--hold-manual", "prefer local versions of manual artifacts");
|
||||||
|
program.option("--all", "do not skip unneeded intermediate artifacts");
|
||||||
program.option("--publish", "publish artifacts to remote registries");
|
program.option("--publish", "publish artifacts to remote registries");
|
||||||
program.option("--yes", "execute plan without confirmation");
|
program.option("--yes", "execute plan without confirmation");
|
||||||
program.parse(process.argv);
|
program.parse(process.argv);
|
||||||
const { list, manual, holdManual, publish, yes } = program.opts();
|
const { list, manual, holdManual, all, publish, yes } = program.opts();
|
||||||
const depgraph = await getDepGraph();
|
const depgraph = await getDepGraph();
|
||||||
if (list) {
|
if (list) {
|
||||||
for (const { name } of depgraph.artifacts) {
|
for (const { name } of depgraph.artifacts) {
|
||||||
|
@ -437,6 +615,7 @@ async function main() {
|
||||||
depgraph,
|
depgraph,
|
||||||
manual,
|
manual,
|
||||||
holdManual,
|
holdManual,
|
||||||
|
all,
|
||||||
publish,
|
publish,
|
||||||
yes,
|
yes,
|
||||||
targets: program.args,
|
targets: program.args,
|
||||||
|
|
Loading…
Reference in New Issue