mirror of
https://github.com/github/awesome-copilot.git
synced 2026-09-16 20:01:09 +00:00
Add modernization workflow canvas (#2758)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
7568a482ce
commit
5e59c61ead
@@ -0,0 +1,227 @@
|
||||
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
version: 1,
|
||||
language: "auto",
|
||||
source: ".",
|
||||
goal: "modernize this application for Azure",
|
||||
upgradeTarget: "",
|
||||
planName: "modernization-plan",
|
||||
delegate: "local",
|
||||
steps: {
|
||||
assess: { status: "pending", updatedAt: null },
|
||||
plan: { status: "pending", updatedAt: null },
|
||||
execute: { status: "pending", updatedAt: null },
|
||||
},
|
||||
};
|
||||
|
||||
function statePath(workspace, stateId) {
|
||||
return join(workspace, ".github", "modernize", `canvas-${stateId}.json`);
|
||||
}
|
||||
|
||||
function cleanStateId(value) {
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value)) {
|
||||
throw new Error("Invalid workflow state identifier.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function resolveWorkspace(input, fallback) {
|
||||
const candidate = input || fallback || process.cwd();
|
||||
return resolve(candidate);
|
||||
}
|
||||
|
||||
async function exists(path) {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadState(workspace, stateId) {
|
||||
const path = statePath(workspace, cleanStateId(stateId));
|
||||
try {
|
||||
const saved = JSON.parse(await readFile(path, "utf8"));
|
||||
return {
|
||||
...DEFAULT_STATE,
|
||||
...saved,
|
||||
steps: {
|
||||
...DEFAULT_STATE.steps,
|
||||
...(saved.steps ?? {}),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") return structuredClone(DEFAULT_STATE);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveState(workspace, stateId, state) {
|
||||
const path = statePath(workspace, cleanStateId(stateId));
|
||||
await mkdir(join(workspace, ".github", "modernize"), { recursive: true });
|
||||
const next = { ...state, updatedAt: new Date().toISOString() };
|
||||
await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
||||
return next;
|
||||
}
|
||||
|
||||
async function walkNames(root, maxDepth = 3, depth = 0) {
|
||||
if (depth > maxDepth || !(await exists(root))) return [];
|
||||
const output = [];
|
||||
for (const entry of await readdir(root, { withFileTypes: true })) {
|
||||
if ([".git", "node_modules", "bin", "obj", "target", "build"].includes(entry.name)) continue;
|
||||
const path = join(root, entry.name);
|
||||
output.push(path);
|
||||
if (entry.isDirectory()) output.push(...await walkNames(path, maxDepth, depth + 1));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function detectLanguage(paths) {
|
||||
const names = paths.map((path) => path.toLowerCase());
|
||||
if (names.some((path) => /\.(sln|csproj|fsproj|vbproj)$/.test(path))) return "dotnet";
|
||||
if (names.some((path) => /\/(pom\.xml|build\.gradle|build\.gradle\.kts)$/.test(path.replaceAll("\\", "/")))) return "java";
|
||||
if (names.some((path) => /\/(cmakelists\.txt|makefile)$/.test(path.replaceAll("\\", "/")) || /\.(cpp|cc|cxx|vcxproj)$/.test(path))) return "cpp";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
async function scanArtifacts(workspace, planName) {
|
||||
const modernizeRoot = join(workspace, ".github", "modernize");
|
||||
const assessmentRoot = join(modernizeRoot, "assessment");
|
||||
const planRoot = join(modernizeRoot, planName);
|
||||
const assessmentFiles = (await walkNames(assessmentRoot, 3))
|
||||
.filter((path) => /\.(json|md|html|ya?ml)$/i.test(path));
|
||||
const planFiles = (await walkNames(planRoot, 2))
|
||||
.filter((path) => /\.(json|md|html)$/i.test(path));
|
||||
const assessmentOutputs = assessmentFiles.filter((path) =>
|
||||
!/config|canvas-/i.test(path) && /\.(json|md|html)$/i.test(path));
|
||||
const summaryFiles = planFiles.filter((path) => /summary|progress|result/i.test(path));
|
||||
|
||||
return {
|
||||
assessment: assessmentFiles.map((path) => path.slice(workspace.length + 1)),
|
||||
plan: planFiles.map((path) => path.slice(workspace.length + 1)),
|
||||
execution: summaryFiles.map((path) => path.slice(workspace.length + 1)),
|
||||
hasAssessment: assessmentOutputs.length > 0,
|
||||
hasPlan: planFiles.some((path) => /plan\.md$/i.test(path))
|
||||
&& planFiles.some((path) => /tasks\.json$/i.test(path)),
|
||||
hasExecution: summaryFiles.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
function quote(value) {
|
||||
const text = String(value).trim();
|
||||
if (/^[A-Za-z0-9_./:\\-]+$/.test(text)) return text;
|
||||
return `"${text.replaceAll('"', '\\"')}"`;
|
||||
}
|
||||
|
||||
function languageFlag(language) {
|
||||
if (language === "java" || language === "dotnet") return ` --language ${language}`;
|
||||
return "";
|
||||
}
|
||||
|
||||
function delegateFlag(delegate) {
|
||||
return delegate === "cloud" ? " --delegate cloud" : "";
|
||||
}
|
||||
|
||||
function commands(state, artifacts) {
|
||||
const source = quote(state.source);
|
||||
const name = quote(state.planName);
|
||||
const goal = quote(state.goal);
|
||||
const upgradeTarget = state.upgradeTarget ? ` ${quote(state.upgradeTarget)}` : "";
|
||||
const language = state.language === "auto" ? state.detectedLanguage : state.language;
|
||||
const assessment = artifacts.assessment.find((path) => /assessment.*\.json$/i.test(path))
|
||||
?? artifacts.assessment.find((path) => /\.json$/i.test(path));
|
||||
const assessmentFlag = assessment ? ` --assess-file-path ${quote(assessment)}` : "";
|
||||
|
||||
return {
|
||||
assess: `modernize assess --source ${source}${delegateFlag(state.delegate)}${state.delegate === "cloud" ? " --wait" : ""}`,
|
||||
plan: `modernize plan create ${goal} --source ${source} --plan-name ${name}${languageFlag(language)}${assessmentFlag}`,
|
||||
execute: `modernize plan execute --source ${source} --plan-name ${name}${languageFlag(language)}${delegateFlag(state.delegate)}`,
|
||||
upgrade: `modernize upgrade${upgradeTarget} --source ${source}${delegateFlag(state.delegate)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function deriveSteps(state, artifacts) {
|
||||
const steps = structuredClone(state.steps);
|
||||
if (artifacts.hasAssessment) steps.assess.status = "complete";
|
||||
if (artifacts.hasPlan) {
|
||||
steps.assess.status = "complete";
|
||||
steps.plan.status = "complete";
|
||||
}
|
||||
if (artifacts.hasExecution) {
|
||||
steps.assess.status = "complete";
|
||||
steps.plan.status = "complete";
|
||||
steps.execute.status = "complete";
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
export async function getWorkflowState(workspace, stateId = "default") {
|
||||
const saved = await loadState(workspace, stateId);
|
||||
const paths = await walkNames(workspace, 3);
|
||||
const detectedLanguage = detectLanguage(paths);
|
||||
const artifacts = await scanArtifacts(workspace, saved.planName);
|
||||
const state = {
|
||||
...saved,
|
||||
stateId,
|
||||
workspace,
|
||||
detectedLanguage,
|
||||
effectiveLanguage: saved.language === "auto" ? detectedLanguage : saved.language,
|
||||
artifacts,
|
||||
steps: deriveSteps(saved, artifacts),
|
||||
};
|
||||
const notices = [];
|
||||
if (state.effectiveLanguage === "cpp") {
|
||||
notices.push("The current CLI reference documents explicit --language values for Java and .NET only. C++ commands use CLI auto-detection; verify support with your installed Modernize CLI version.");
|
||||
}
|
||||
if (state.delegate === "cloud" && !/^https:\/\/github\.com\//i.test(state.source)) {
|
||||
notices.push("Cloud delegation requires a github.com repository URL as the source. Local paths and other Git providers are not supported.");
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
commands: commands(state, artifacts),
|
||||
compatibilityNotice: notices.length ? notices.join(" ") : null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function configureWorkflow(workspace, stateId, input) {
|
||||
const current = await loadState(workspace, stateId);
|
||||
const allowed = ["language", "source", "goal", "upgradeTarget", "planName", "delegate"];
|
||||
const next = { ...current };
|
||||
for (const key of allowed) {
|
||||
if (input[key] !== undefined) next[key] = String(input[key]).trim();
|
||||
}
|
||||
if (!["auto", "dotnet", "java", "cpp"].includes(next.language)) {
|
||||
throw new Error("Language must be auto, dotnet, java, or cpp.");
|
||||
}
|
||||
if (!["local", "cloud"].includes(next.delegate)) {
|
||||
throw new Error("Execution must be local or cloud.");
|
||||
}
|
||||
if (!next.source || !next.goal) {
|
||||
throw new Error("Source and modernization goal are required.");
|
||||
}
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(next.planName)) {
|
||||
throw new Error("Plan name must contain only letters, numbers, dots, underscores, and hyphens.");
|
||||
}
|
||||
await saveState(workspace, stateId, next);
|
||||
return getWorkflowState(workspace, stateId);
|
||||
}
|
||||
|
||||
export async function setStepStatus(workspace, stateId, step, status) {
|
||||
if (!["assess", "plan", "execute"].includes(step)) throw new Error("Unknown workflow step.");
|
||||
if (!["pending", "active", "complete", "blocked"].includes(status)) throw new Error("Unknown step status.");
|
||||
const current = await loadState(workspace, stateId);
|
||||
current.steps[step] = { status, updatedAt: new Date().toISOString() };
|
||||
await saveState(workspace, stateId, current);
|
||||
return getWorkflowState(workspace, stateId);
|
||||
}
|
||||
|
||||
export async function refreshWorkflow(workspace, stateId = "default") {
|
||||
const current = await loadState(workspace, stateId);
|
||||
const refreshed = await getWorkflowState(workspace, stateId);
|
||||
current.steps = refreshed.steps;
|
||||
await saveState(workspace, stateId, current);
|
||||
return getWorkflowState(workspace, stateId);
|
||||
}
|
||||
Reference in New Issue
Block a user