131 lines
8.3 KiB
JavaScript
131 lines
8.3 KiB
JavaScript
"use strict";
|
|
|
|
const path = require("node:path");
|
|
const { app } = require("electron");
|
|
const { ConfigStore } = require("../src/main/config-store.cjs");
|
|
const { GitService } = require("../src/main/git-service.cjs");
|
|
const { GiteaService } = require("../src/main/gitea-service.cjs");
|
|
const { RepositoryService } = require("../src/main/repository-service.cjs");
|
|
const { SshService } = require("../src/main/ssh-service.cjs");
|
|
const { UnraidDeploymentService } = require("../src/main/unraid-deployment-service.cjs");
|
|
|
|
const userDataPath = process.env.FORGEFLOW_USER_DATA
|
|
? path.resolve(process.env.FORGEFLOW_USER_DATA)
|
|
: path.join(app.getPath("appData"), "forgeflow");
|
|
app.setPath("userData", userDataPath);
|
|
|
|
app.whenReady().then(async () => {
|
|
try {
|
|
const configureAccess = process.argv.includes("--configure-access");
|
|
const reconcile = process.argv.includes("--reconcile");
|
|
const summaryOnly = process.argv.includes("--summary");
|
|
const inventoryOnly = process.argv.includes("--inventory-only");
|
|
const repositoryFilter = new Set(String(process.argv.find((value) => value.startsWith("--repository=")) || "")
|
|
.slice("--repository=".length).toLowerCase().split(",").map((value) => value.trim()).filter(Boolean));
|
|
const store = new ConfigStore(userDataPath);
|
|
await store.load();
|
|
const git = new GitService();
|
|
const gitea = new GiteaService(store);
|
|
const repositories = await new RepositoryService(store, git, gitea).refresh();
|
|
const ssh = new SshService({ store });
|
|
const deployments = new UnraidDeploymentService({ store, ssh, git, gitea, sourcePath: path.resolve(__dirname, "..") });
|
|
const reports = [];
|
|
for (const server of store.data.servers || []) {
|
|
const report = await deployments.scanServerInventory(server.id, repositories);
|
|
let reconciliation = null;
|
|
if (reconcile) {
|
|
for (let attempt = 1; attempt <= 3 && !reconciliation; attempt += 1) {
|
|
const preview = await deployments.planServerInventoryReconciliation(server.id, repositories, { autoLink: true });
|
|
try {
|
|
reconciliation = await deployments.reconcileServerInventory(server.id, repositories, { autoLink: true, expectedPlanId: preview.plan.id });
|
|
} catch (error) {
|
|
if (error.code !== "RECONCILIATION_PLAN_STALE" || attempt === 3) throw error;
|
|
}
|
|
}
|
|
}
|
|
const access = [];
|
|
const seenProfiles = new Set();
|
|
for (const repository of inventoryOnly || configureAccess ? [] : repositories) {
|
|
for (const profile of repository.deploymentProfiles || store.getDeploymentProfiles(repository.fullName) || []) {
|
|
if (profile.serverId !== server.id || profile.deploymentMode !== "server-git" || seenProfiles.has(profile.id)) continue;
|
|
seenProfiles.add(profile.id);
|
|
try {
|
|
let verification = await deployments.verifyServerGitProfile({ repository, profileId: profile.id });
|
|
await deployments.refreshProfileState(repository.fullName, profile.id, verification.branchSha);
|
|
verification = await deployments.verifyServerGitProfile({ repository, profileId: profile.id });
|
|
access.push(verification);
|
|
} catch (error) {
|
|
access.push({ repository: repository.fullName, profileId: profile.id, readiness: "Verification incomplete", ready: false, error: error.message });
|
|
}
|
|
}
|
|
}
|
|
if (configureAccess) {
|
|
const refreshedRepositories = await new RepositoryService(store, git, gitea).refresh();
|
|
for (const workload of report.workloads.filter((item) => item.runtime?.running && item.link?.profileId && item.link?.repositoryFullName)) {
|
|
const repository = refreshedRepositories.find((item) => String(item.fullName).toLowerCase() === String(workload.link.repositoryFullName).toLowerCase());
|
|
if (!repository) continue;
|
|
if (repositoryFilter.size && !repositoryFilter.has(String(repository.fullName).toLowerCase())) continue;
|
|
try {
|
|
const configured = await deployments.configureServerGitAccess({ repository, profileId: workload.link.profileId });
|
|
access.push({ repository: repository.fullName, profileId: workload.link.profileId, action: "configured", ready: true, created: configured.created, remoteSha: configured.remoteSha });
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
} catch (error) {
|
|
access.push({ repository: repository.fullName, ready: false, error: error.message });
|
|
}
|
|
}
|
|
}
|
|
reports.push({
|
|
server: server.name,
|
|
reconciliation: reconciliation ? {
|
|
adopted: reconciliation.adopted,
|
|
refreshed: reconciliation.refreshed,
|
|
retired: reconciliation.retired,
|
|
staleProfiles: reconciliation.staleProfiles,
|
|
recoverySnapshot: reconciliation.recoverySnapshot,
|
|
} : null,
|
|
capabilities: report.capabilities,
|
|
warnings: (report.warnings || []).map((warning) => String(warning).slice(0, 300)),
|
|
summary: {
|
|
detected: report.detected,
|
|
running: report.running,
|
|
linked: report.linked,
|
|
needsReview: report.needsReview,
|
|
},
|
|
reviewBreakdown: report.workloads.filter((workload) => !workload.reviewDecision && (workload.classification?.type === "stale-link" || (workload.runtime?.running && workload.classification?.type === "duplicate") || (workload.runtime?.running && !["system-container", "external-container", "temporary-runtime", "backup", "release-folder", "historical-compose", "manually-excluded"].includes(workload.classification?.type) && ["suggested", "ambiguous", "unmatched"].includes(workload.status)))).reduce((counts, workload) => {
|
|
const key = `${workload.classification?.type || "unknown"}:${workload.status || "unknown"}`;
|
|
counts[key] = (counts[key] || 0) + 1;
|
|
return counts;
|
|
}, {}),
|
|
reviewSamples: report.workloads.filter((workload) => !workload.reviewDecision && (workload.classification?.type === "stale-link" || (workload.runtime?.running && workload.classification?.type === "duplicate") || (workload.runtime?.running && !["system-container", "external-container", "temporary-runtime", "backup", "release-folder", "historical-compose", "manually-excluded"].includes(workload.classification?.type) && ["suggested", "ambiguous", "unmatched"].includes(workload.status)))).slice(0, 30).map((workload) => ({ name: workload.displayName, type: workload.classification?.type, status: workload.status, running: workload.runtime?.running, folder: workload.remoteFolderCandidate, containers: (workload.containers || []).map((container) => container.name) })),
|
|
access,
|
|
workloads: report.workloads.filter((workload) => workload.link || (workload.runtime?.running && workload.status !== "unmatched")).map((workload) => ({
|
|
name: workload.displayName,
|
|
running: workload.runtime?.running === true,
|
|
health: workload.runtime?.health || "unknown",
|
|
repository: workload.link?.repositoryFullName || workload.suggestedRepository?.fullName || null,
|
|
confidence: workload.matchConfidence || workload.status,
|
|
folder: workload.remoteFolderCandidate || null,
|
|
containers: (workload.containers || []).map((container) => container.name),
|
|
})),
|
|
});
|
|
}
|
|
const output = summaryOnly ? reports.map((report) => ({
|
|
server: report.server,
|
|
capabilities: report.capabilities,
|
|
warnings: report.warnings,
|
|
summary: report.summary,
|
|
reviewBreakdown: Object.fromEntries(Object.entries(report.reviewBreakdown || {}).sort(([left], [right]) => left.localeCompare(right))),
|
|
reviewSamples: report.reviewSamples,
|
|
reconciliation: report.reconciliation,
|
|
access: report.access.map((item) => ({ repository: item.repository, profileId: item.profileId || null, ready: item.ready, readiness: item.readiness || item.action || null, remoteSha: item.remoteSha || item.branchSha || null, liveSha: item.liveSha || null, error: item.error || null })),
|
|
review: report.workloads.filter((item) => !item.repository && item.running).map((item) => ({ name: item.name, confidence: item.confidence, folder: item.folder })),
|
|
})) : reports;
|
|
console.log(JSON.stringify(output, null, 2));
|
|
} catch (error) {
|
|
console.error(error?.stack || error?.message || String(error));
|
|
process.exitCode = 1;
|
|
} finally {
|
|
app.quit();
|
|
}
|
|
});
|