Publish curated ForgeFlow source from 2ed1787c0b52
ForgeFlow quality gate / quality (push) Successful in 4m12s
ForgeFlow quality gate / secret-scan (push) Successful in 7s

This commit is contained in:
NuklearRabbit committed 2026-09-29 22:50:57 +02:00
commit f60b269686
254 files changed
+46204

No files matched your search

+524
View File
@@ -0,0 +1,524 @@
"use strict";
const fs = require("node:fs/promises");
const fileSystem = require("node:fs");
const path = require("node:path").posix;
const nativePath = require("node:path");
const crypto = require("node:crypto");
const os = require("node:os");
const { shellQuote } = require("./ssh-service.cjs");
const { assertFullCommitSha } = require("../shared/validation.cjs");
const { run } = require("./process-runner.cjs");
const {
parseServerInventory: parseWorkloadInventory,
buildWorkloadInventory,
inventoryContainerMatch: matchInventoryContainer,
remoteIdentity: inventoryRemoteIdentity,
} = require("./server-inventory.cjs");
const { classifyInventory } = require("./inventory-classifier.cjs");
const { createUnraidInventoryMethods } = require("./unraid-inventory-methods.cjs");
const { createUnraidAccessMethods } = require("./unraid-access-methods.cjs");
const { createUnraidPreflightMethods } = require("./unraid-preflight-methods.cjs");
const { createUnraidRuntimeMethods } = require("./unraid-runtime-methods.cjs");
const { createUnraidDeploymentMethods } = require("./unraid-deployment-methods.cjs");
const { createUnraidStateMethods } = require("./unraid-state-methods.cjs");
function safeRemoteFolder(value) {
const text = String(value || "").trim().replace(/\\/g, "/").replace(/^\.\//, "");
if (
!text ||
path.isAbsolute(text) ||
text.split("/").some((part) => !part || part === "." || part === ".." || !/^[a-zA-Z0-9._-]+$/.test(part))
) throw new Error("Remote folder must be a safe path below the configured server base path.");
return text;
}
function safeRelativeRemoteFile(value, fallback = "") {
const text = String(value || fallback)
.trim()
.replace(/\\/g, "/");
if (
!text ||
text.startsWith("/") ||
text.split("/").some((part) => !part || part === "." || part === "..")
) {
throw new Error("Remote file path must remain inside the project folder.");
}
return text;
}
function bash(command) {
const script = `set -euo pipefail
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND='ssh -o BatchMode=yes'
forgeflow_compose() {
if docker compose version >/dev/null 2>&1; then docker compose "$@";
elif command -v docker-compose >/dev/null 2>&1; then docker-compose "$@";
else echo "Docker Compose is not available on the server." >&2; return 127;
fi
}
${command}`;
const payload = Buffer.from(script, "utf8").toString("base64");
return `printf '%s' ${shellQuote(payload)} | base64 -d | bash`;
}
function parseInspection(text) {
const jsonMarker = "__FORGEFLOW_JSON__";
const jsonIndex = text.lastIndexOf(jsonMarker);
if (jsonIndex >= 0)
return JSON.parse(text.slice(jsonIndex + jsonMarker.length).trim());
const kvMarker = "__FORGEFLOW_KV__";
const kvIndex = text.lastIndexOf(kvMarker);
if (kvIndex < 0)
throw new Error("The server inspection did not return a ForgeFlow result.");
const fields = {};
for (const line of text
.slice(kvIndex + kvMarker.length)
.trim()
.split(/\r?\n/)) {
const separator = line.indexOf("=");
if (separator > 0)
fields[line.slice(0, separator)] = line.slice(separator + 1);
}
const decodeLines = (value) => {
try {
return value
? Buffer.from(value, "base64")
.toString("utf8")
.split(/\r?\n/)
.filter(Boolean)
: [];
} catch {
return [];
}
};
const decodeText = (value) => {
try {
return value ? Buffer.from(value, "base64").toString("utf8") : "";
} catch {
return "";
}
};
return {
exists: fields.exists === "true",
rootGit: fields.rootGit === "true",
head: fields.head || null,
branch: fields.branch || null,
remote: fields.remote
? Buffer.from(fields.remote, "base64").toString("utf8")
: null,
trackedChanges: decodeLines(fields.trackedChanges),
composeFiles: decodeLines(fields.composeFiles),
nestedGit: decodeLines(fields.nestedGit),
dockerfile: fields.dockerfile === "true",
dockerignoreContent: decodeText(fields.dockerignoreContent),
existingPreservePaths: decodeLines(fields.existingPreservePaths),
};
}
function dockerIgnoreHasPath(content, value) {
const target = String(value || "")
.replace(/\\/g, "/")
.replace(/^\.\//, "")
.replace(/^\//, "")
.replace(/\/$/, "");
if (!target) return false;
return String(content || "")
.split(/\r?\n/)
.some((line) => {
let rule = line.trim();
if (!rule || rule.startsWith("#") || rule.startsWith("!")) return false;
rule = rule.replace(/^\.\//, "").replace(/^\//, "").replace(/\/$/, "");
return (
rule === target || rule === `${target}/**` || rule === `${target}/**/*`
);
});
}
function checksSummary(checks) {
const counts = {
pass: checks.filter((item) => item.status === "pass").length,
warning: checks.filter((item) => item.status === "warning").length,
fail: checks.filter((item) => item.status === "fail").length,
};
return {
ready: counts.fail === 0,
counts,
blocking: checks
.filter((item) => item.status === "fail")
.map((item) => item.id),
};
}
function xmlEscape(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
function decodeBase64Json(value, fallback) {
try {
return value
? JSON.parse(Buffer.from(value, "base64").toString("utf8"))
: fallback;
} catch {
return fallback;
}
}
function parseDockerManXml(xml) {
const text = String(xml || "");
const tag = (name) => {
const match = text.match(
new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`, "i"),
);
return match
? match[1]
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.trim()
: "";
};
return {
name: tag("Name"),
webUiUrl: tag("WebUI"),
iconUrl: tag("Icon"),
shell: tag("Shell"),
};
}
function parsePermissionInspection(text) {
const marker = "__FORGEFLOW_PERMISSIONS__";
const index = String(text || "").lastIndexOf(marker);
if (index < 0)
throw new Error("The server permission check did not return a ForgeFlow marker.");
const decode = (value) => {
try {
return value ? Buffer.from(value, "base64").toString("utf8") : "";
} catch {
return "";
}
};
const result = {
identity: { user: "", uid: null, gid: null, groups: [], hasAcl: false, canElevate: false },
targets: [],
};
for (const line of String(text)
.slice(index + marker.length)
.trim()
.split(/\r?\n/)) {
const parts = line.split("\t");
if (parts[0] === "I") {
result.identity = {
user: decode(parts[1]),
uid: Number(parts[2]),
gid: Number(parts[3]),
groups: decode(parts[4]).split(/\s+/).filter(Boolean),
hasAcl: parts[5] === "true",
canElevate: parts[6] === "true",
};
} else if (parts[0] === "P") {
result.targets.push({
id: decode(parts[1]),
label: decode(parts[2]),
path: decode(parts[3]),
kind: parts[4] || "directory",
required: parts[5] === "true",
exists: parts[6] === "true",
readable: parts[7] === "true",
writable: parts[8] === "true",
parentWritable: parts[9] === "true",
effectiveWritable: parts[10] === "true",
owner: decode(parts[11]),
group: decode(parts[12]),
mode: parts[13] || "",
nearestWritableAncestor: decode(parts[14]),
detail: decode(parts[15]),
});
}
}
result.blocking = result.targets.filter(
(target) => target.required && !target.effectiveWritable,
);
result.ready = result.blocking.length === 0;
result.repairable = result.blocking.some((target) => target.id !== "server-base");
return result;
}
function deriveDetectedProfile({
repository,
server,
remoteFolder,
remotePath,
payload,
}) {
const compose = payload.compose || {};
const services =
compose.services && typeof compose.services === "object"
? compose.services
: {};
const inspections = Array.isArray(payload.containers)
? payload.containers
: [];
const primaryContainer =
inspections.find((item) => item?.State?.Running) || inspections[0] || null;
const labels = primaryContainer?.Config?.Labels || {};
const serviceName =
labels["com.docker.compose.service"] ||
Object.keys(services)[0] ||
remoteFolder;
const service = services[serviceName] || {};
const containerName = String(
primaryContainer?.Name || service.container_name || serviceName,
).replace(/^\//, "");
const ports = [];
for (const [containerKey, bindings] of Object.entries(
primaryContainer?.NetworkSettings?.Ports || {},
)) {
const [containerPortText, protocol = "tcp"] = containerKey.split("/");
const containerPort = Number(containerPortText) || null;
if (Array.isArray(bindings) && bindings.length) {
for (const binding of bindings)
ports.push({
hostIp: binding.HostIp || "",
hostPort: Number(binding.HostPort) || null,
containerPort,
protocol,
});
} else ports.push({ hostIp: "", hostPort: null, containerPort, protocol });
}
const primaryPort = ports.find((item) => item.hostPort) || ports[0] || {};
const mounts = (primaryContainer?.Mounts || []).map((item) => ({
type: item.Type,
source: item.Source,
target: item.Destination,
readOnly: item.RW === false,
}));
const networks = Object.keys(
primaryContainer?.NetworkSettings?.Networks || {},
);
const envNames = (primaryContainer?.Config?.Env || [])
.map((item) => String(item).split("=")[0])
.filter(Boolean);
const dockerMan = parseDockerManXml(payload.dockerManXml || "");
const webUiUrl =
dockerMan.webUiUrl || labels["net.unraid.docker.webui"] || "";
const iconUrl = dockerMan.iconUrl || labels["net.unraid.docker.icon"] || "";
const shell =
dockerMan.shell || labels["net.unraid.docker.shell"] || "/bin/sh";
const preservePaths = [
...new Set([
".env",
"appdata",
"data",
"logs",
"config",
"compose.override.yml",
...mounts
.filter((item) =>
String(item.source || "").startsWith(`${remotePath}/`),
)
.map(
(item) =>
String(item.source)
.slice(remotePath.length + 1)
.split("/")[0],
)
.filter(Boolean),
]),
];
const source = (value, origin, confidence = "confirmed") => ({
value,
origin,
confidence,
detectedAt: new Date().toISOString(),
overridden: false,
});
const composeFiles = payload.composeFiles || [];
const composeFile =
composeFiles[0] ||
labels["com.docker.compose.project.config_files"]
?.split(",")[0]
?.replace(`${remotePath}/`, "") ||
"docker-compose.yml";
return {
profile: {
name: "Production",
environment: "production",
provider: "ssh-unraid",
branch: payload.branch || repository.defaultBranch || "main",
serverId: server.id,
remoteFolder,
cloneUrl: payload.remote || repository.sshUrl || "",
alignRemote: false,
generatedCompose: false,
composeFile,
composeService: serviceName,
containerName,
hostPort: primaryPort.hostPort || null,
containerPort: primaryPort.containerPort || null,
webUiUrl,
iconMode: /^https?:\/\//i.test(iconUrl) ? "url" : "none",
iconUrl: /^https?:\/\//i.test(iconUrl) ? iconUrl : "",
serverIconReference: iconUrl,
iconFilePath: "",
dockerShell: ["/bin/bash", "/bin/sh"].includes(shell) ? shell : "/bin/sh",
healthcheckUrl: "",
preservePaths,
confirmationRequired: true,
adoptedFromServer: true,
serverSourceOfTruth: true,
detectedAt: new Date().toISOString(),
detectedMetadata: {
head: payload.head || null,
composeProject: labels["com.docker.compose.project"] || "",
composeFiles,
services: Object.keys(services),
ports,
mounts,
networks,
envNames,
restartPolicy: primaryContainer?.HostConfig?.RestartPolicy?.Name || "",
healthcheck: primaryContainer?.Config?.Healthcheck || null,
image: primaryContainer?.Config?.Image || service.image || "",
dockerMan,
},
},
provenance: {
remoteFolder: source(remoteFolder, "server-path"),
cloneUrl: source(payload.remote || "", "git-origin"),
branch: source(payload.branch || "", "git"),
composeFile: source(composeFile, "docker-compose"),
composeService: source(serviceName, "docker-labels"),
containerName: source(containerName, "docker-inspect"),
hostPort: source(primaryPort.hostPort || null, "docker-inspect"),
containerPort: source(
primaryPort.containerPort || null,
"docker-inspect",
),
webUiUrl: source(
webUiUrl,
dockerMan.webUiUrl ? "unraid-dockerman" : "docker-labels",
),
iconUrl: source(
iconUrl,
dockerMan.iconUrl ? "unraid-dockerman" : "docker-labels",
),
dockerShell: source(
shell,
dockerMan.shell ? "unraid-dockerman" : "docker-labels",
),
},
runtime: {
remotePath,
containerRunning: Boolean(primaryContainer?.State?.Running),
containers: inspections.length,
services: Object.keys(services).length,
ports,
mounts,
networks,
envNames,
},
};
}
function iconReferenceLocalPath(iconReference) {
const value = String(iconReference || "").trim();
if (value.startsWith("file:///")) return `/${value.slice("file:///".length)}`;
if (value.startsWith("/")) return value;
return "";
}
class UnraidDeploymentService {
constructor({
store,
ssh,
git,
gitea,
diagnostics,
sourcePath = process.cwd(),
onOperationChange = null,
}) {
this.store = store;
this.ssh = ssh;
this.git = git;
this.gitea = gitea;
this.diagnostics = diagnostics;
this.sourcePath = sourcePath;
this.onOperationChange = onOperationChange;
}
}
const stateMethods = createUnraidStateMethods({ path, bash, shellQuote, inventoryRemoteIdentity });
for (const name of Object.getOwnPropertyNames(stateMethods)) {
if (name !== "constructor") Object.defineProperty(UnraidDeploymentService.prototype, name, Object.getOwnPropertyDescriptor(stateMethods, name));
}
const deploymentMethods = createUnraidDeploymentMethods({
path, crypto, bash, shellQuote, assertFullCommitSha, nativePath, fs,
});
for (const name of Object.getOwnPropertyNames(deploymentMethods)) {
if (name !== "constructor") Object.defineProperty(UnraidDeploymentService.prototype, name, Object.getOwnPropertyDescriptor(deploymentMethods, name));
}
const runtimeMethods = createUnraidRuntimeMethods({
safeRelativeRemoteFile, xmlEscape, nativePath, fileSystem, fs, crypto, os, run,
bash, shellQuote, iconReferenceLocalPath,
});
for (const name of Object.getOwnPropertyNames(runtimeMethods)) {
if (name !== "constructor") Object.defineProperty(UnraidDeploymentService.prototype, name, Object.getOwnPropertyDescriptor(runtimeMethods, name));
}
const preflightMethods = createUnraidPreflightMethods({
safeRemoteFolder, path, bash, parseInspection, dockerIgnoreHasPath, checksSummary,
inventoryRemoteIdentity, deriveDetectedProfile, decodeBase64Json, shellQuote,
assertFullCommitSha, nativePath, safeRelativeRemoteFile, fs,
});
for (const name of Object.getOwnPropertyNames(preflightMethods)) {
if (name !== "constructor") Object.defineProperty(UnraidDeploymentService.prototype, name, Object.getOwnPropertyDescriptor(preflightMethods, name));
}
const accessMethods = createUnraidAccessMethods({ shellQuote, path, bash, inventoryRemoteIdentity, checksSummary, crypto, parsePermissionInspection, safeRelativeRemoteFile });
for (const name of Object.getOwnPropertyNames(accessMethods)) {
if (name !== "constructor") Object.defineProperty(UnraidDeploymentService.prototype, name, Object.getOwnPropertyDescriptor(accessMethods, name));
}
const inventoryMethods = createUnraidInventoryMethods({
shellQuote, path, parseWorkloadInventory, buildWorkloadInventory, classifyInventory,
inventoryRemoteIdentity, deriveDetectedProfile, crypto, matchInventoryContainer,
safeRemoteFolder, bash,
});
for (const name of Object.getOwnPropertyNames(inventoryMethods)) {
if (name !== "constructor") Object.defineProperty(UnraidDeploymentService.prototype, name, Object.getOwnPropertyDescriptor(inventoryMethods, name));
}
module.exports = {
UnraidDeploymentService,
safeRemoteFolder,
safeRelativeRemoteFile,
parseInspection,
dockerIgnoreHasPath,
checksSummary,
xmlEscape,
iconReferenceLocalPath,
decodeBase64Json,
parseDockerManXml,
parsePermissionInspection,
parseServerInventory: parseWorkloadInventory,
inventoryContainerMatch: matchInventoryContainer,
remoteIdentity: inventoryRemoteIdentity,
deriveDetectedProfile,
bash,
};