views.js sat at the project's 750-line limit, so the diff cap in the previous commit pushed it over and every further change would have meant shaving comments elsewhere. That is the file asking for decomposition, which is what the architecture audit says to do. Diff rendering is self-contained: the line cap, the line classifier and the change-map illustration depend on nothing in views.js beyond ui and escapeHtml. They now live in src/renderer/diff-view.js and are registered in index.html and in the three renderer file lists that scan the bridge surface, so anything added there is covered by the existing contract tests. views.js drops from 755 to 714 lines and no source file exceeds 750 again. The nested ternary that classified a diff line became a named function with guard clauses on the way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
574 lines
21 KiB
JavaScript
574 lines
21 KiB
JavaScript
import { access, readFile, readdir } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawnSync } from "node:child_process";
|
|
import shellVerification from "../src/shared/shell-verification.cjs";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const required = [
|
|
"package.json",
|
|
"main.cjs",
|
|
"preload.cjs",
|
|
"src/renderer/index.html",
|
|
"src/renderer/styles.css",
|
|
"src/renderer/app.js",
|
|
"src/renderer/mock-bridge.js",
|
|
"src/renderer/assets/itworx-mark.png",
|
|
"src/renderer/assets/itworx-wordmark.png",
|
|
"src/renderer/assets/itworx-wordmark-light.png",
|
|
"src/renderer/assets/itworx-wordmark-dark.png",
|
|
"src/main/config-store.cjs",
|
|
"src/main/git-service.cjs",
|
|
"src/main/gitea-service.cjs",
|
|
"src/main/audit-service.cjs",
|
|
"src/main/configuration-backup.cjs",
|
|
"src/main/external-tools-service.cjs",
|
|
"src/main/repository-service.cjs",
|
|
"src/main/repository-monitor.cjs",
|
|
"src/main/deployment-service.cjs",
|
|
"src/main/unraid-deployment-service.cjs",
|
|
"src/main/server-inventory.cjs",
|
|
"src/main/ssh-service.cjs",
|
|
"src/main/update-service.cjs",
|
|
"src/main/diagnostics-service.cjs",
|
|
"src/main/preflight-service.cjs",
|
|
"src/main/log-redaction.cjs",
|
|
"src/main/ipc.cjs",
|
|
"src/shared/clone-target.cjs",
|
|
"src/shared/semver.cjs",
|
|
"src/shared/zip-writer.cjs",
|
|
"src/shared/tool-invocation.cjs",
|
|
"src/shared/shell-verification.cjs",
|
|
"START_HERE.md",
|
|
"README.md",
|
|
"SOURCE_MANIFEST.txt",
|
|
"src/shared/deployment-policy.cjs",
|
|
"scripts/acceptance.mjs",
|
|
"scripts/validate-installed-connections.cjs",
|
|
"scripts/publish-binary-release.cjs",
|
|
"scripts/write-release-checksums.mjs",
|
|
"scripts/prune-dist.mjs",
|
|
"scripts/generate-source-manifest.mjs",
|
|
"setup-windows.ps1",
|
|
"START-FORGEFLOW-OVERLAY.ps1",
|
|
"update-windows.ps1",
|
|
"build-windows.ps1",
|
|
"UPDATE_FROM_0.3.2.md",
|
|
"scripts/apply-source-update.ps1",
|
|
"scripts/apply-binary-update.ps1",
|
|
"docs/ARCHITECTURE.md",
|
|
"docs/CURRENT_STATE.md",
|
|
"docs/MUTATION_MODEL.md",
|
|
"docs/SECURITY.md",
|
|
"docs/ROADMAP.md",
|
|
"docs/SETUP_GUIDE.md",
|
|
"docs/ACCEPTANCE.md",
|
|
"docs/RELEASE_NOTES_0.8.0.md",
|
|
"docs/RELEASE_NOTES_0.8.1.md",
|
|
"docs/RELEASE_NOTES_0.8.2.md",
|
|
"docs/RELEASE_NOTES_0.8.3.md",
|
|
"docs/RELEASE_NOTES_0.8.4.md",
|
|
"docs/RELEASE_NOTES_0.8.5.md",
|
|
"docs/RELEASE_NOTES_0.8.6.md",
|
|
"docs/RELEASE_NOTES_0.8.7.md",
|
|
"docs/RELEASE_NOTES_0.8.8.md",
|
|
"docs/RELEASE_NOTES_0.8.9.md",
|
|
"docs/RELEASE_NOTES_0.9.0.md",
|
|
"docs/RELEASE_NOTES_0.9.1.md",
|
|
"docs/RELEASE_NOTES_0.9.2.md",
|
|
"docs/RELEASE_NOTES_0.9.3.md",
|
|
"docs/RELEASE_NOTES_0.9.4.md",
|
|
"docs/RELEASE_NOTES_0.9.5.md",
|
|
"docs/RELEASE_NOTES_0.10.0.md",
|
|
"docs/RELEASE_NOTES_0.10.1.md",
|
|
"docs/RELEASE_NOTES_0.10.2.md",
|
|
"docs/RELEASE_NOTES_0.10.3.md",
|
|
"docs/RELEASE_NOTES_0.10.4.md",
|
|
"docs/RELEASE_NOTES_0.10.5.md",
|
|
"docs/RELEASE_NOTES_0.10.6.md",
|
|
"docs/RELEASE_NOTES_0.10.7.md",
|
|
"docs/RELEASE_NOTES_0.10.8.md",
|
|
"docs/RELEASE_NOTES_0.10.9.md",
|
|
"docs/RELEASE_NOTES_0.10.10.md",
|
|
"docs/RELEASE_NOTES_0.10.11.md",
|
|
"docs/RELEASE_NOTES_0.10.12.md",
|
|
"docs/UPDATING.md",
|
|
"docs/DIAGNOSTICS.md",
|
|
"docs/DEPLOYMENT_SETUP.md",
|
|
"docs/SSH_UNRAID_DEPLOYMENT.md",
|
|
"docs/LUMAOPS_SERVER_AUDIT.md",
|
|
"docs/STATUS_ENDPOINT.md",
|
|
"docs/TEST_MATRIX.md",
|
|
"docs/RELEASE_NOTES_0.4.0.md",
|
|
"docs/RELEASE_NOTES_0.4.1.md",
|
|
"docs/RELEASE_NOTES_0.4.2.md",
|
|
"docs/RELEASE_NOTES_0.4.3.md",
|
|
"docs/RELEASE_AUDIT_0.6.0.md",
|
|
"docs/RELEASE_NOTES_0.6.1.md",
|
|
"docs/RELEASE_NOTES_0.7.0.md",
|
|
"docs/RELEASE_NOTES_0.5.0.md",
|
|
"docs/RELEASE_NOTES_0.5.1.md",
|
|
"docs/RELEASE_NOTES_0.5.2.md",
|
|
"docs/RELEASE_NOTES_0.5.3.md",
|
|
"docs/RELEASE_NOTES_0.5.4.md",
|
|
"docs/RELEASE_NOTES_0.6.0.md",
|
|
"Publish-ForgeFlow-Release.ps1",
|
|
"docs/RELEASE_NOTES_0.4.4.md",
|
|
"docs/RELEASE_NOTES_0.4.5.md",
|
|
"examples/gitea-actions/deploy.yml",
|
|
"examples/gitea-actions/rollback.yml",
|
|
"examples/server/forgeflow-deploy",
|
|
"examples/server/forgeflow-targets.conf",
|
|
"examples/server/forgeflow-runner.sudoers",
|
|
"examples/server/status-example.json",
|
|
"build/icon.png",
|
|
"build/icon.ico",
|
|
];
|
|
|
|
for (const file of required) await access(path.join(root, file));
|
|
|
|
const packageJson = JSON.parse(
|
|
await readFile(path.join(root, "package.json"), "utf8"),
|
|
);
|
|
if (packageJson.version !== "0.10.12")
|
|
throw new Error(
|
|
`Expected package version 0.10.12, got ${packageJson.version}.`,
|
|
);
|
|
const sourceManifest = await readFile(
|
|
path.join(root, "SOURCE_MANIFEST.txt"),
|
|
"utf8",
|
|
);
|
|
if (
|
|
!sourceManifest
|
|
.replace(/\r\n/g, "\n")
|
|
.startsWith(`ForgeFlow ${packageJson.version} source manifest\n`)
|
|
)
|
|
throw new Error("SOURCE_MANIFEST.txt does not match the package version.");
|
|
for (const group of ["dependencies", "devDependencies"]) {
|
|
for (const [name, version] of Object.entries(packageJson[group] || {})) {
|
|
if (/^[~^*]/.test(version))
|
|
throw new Error(
|
|
`${group} dependency ${name} must be pinned exactly, got ${version}.`,
|
|
);
|
|
}
|
|
}
|
|
if (packageJson.dependencies?.ssh2 !== "1.17.0")
|
|
throw new Error("ssh2 must remain pinned to 1.17.0.");
|
|
for (const script of ["start", "demo", "test", "verify", "check"]) {
|
|
if (!packageJson.scripts?.[script])
|
|
throw new Error(`Required npm script is missing: ${script}`);
|
|
}
|
|
if (
|
|
!packageJson.build?.win?.icon ||
|
|
!packageJson.build?.linux?.icon ||
|
|
!packageJson.build?.mac?.icon
|
|
) {
|
|
throw new Error("Package icon configuration is incomplete.");
|
|
}
|
|
|
|
async function collect(directory, extensions, output = []) {
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
if (["node_modules", "dist"].includes(entry.name)) continue;
|
|
const absolute = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) await collect(absolute, extensions, output);
|
|
else if (extensions.has(path.extname(entry.name))) output.push(absolute);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
const javascriptFiles = await collect(root, new Set([".js", ".cjs", ".mjs"]));
|
|
for (const file of javascriptFiles) {
|
|
const result = spawnSync(process.execPath, ["--check", file], {
|
|
encoding: "utf8",
|
|
});
|
|
if (result.status !== 0)
|
|
throw new Error(
|
|
`${path.relative(root, file)} failed syntax validation:\n${result.stderr}`,
|
|
);
|
|
}
|
|
|
|
const deploymentScript = await readFile(
|
|
path.join(root, "examples/server/forgeflow-deploy"),
|
|
"utf8",
|
|
);
|
|
shellVerification.validateShellScriptStructure(deploymentScript);
|
|
|
|
// The server deployment script targets Linux/Unraid. On Windows, different tools may
|
|
// register themselves as bash.exe (Git Bash, WSL launcher, MSYS), and several of
|
|
// those cannot reliably accept a script over stdin from Node. Publishing and applying
|
|
// a desktop update therefore never depend on a Windows Bash shim. Portable structural
|
|
// validation always runs; GNU Bash syntax validation additionally runs on non-Windows.
|
|
if (shellVerification.shouldRunExternalBash(process.platform)) {
|
|
const bashCheck =
|
|
shellVerification.bashSyntaxCheckFromTextInvocation(deploymentScript);
|
|
const shell = spawnSync(bashCheck.command, bashCheck.args, bashCheck.options);
|
|
if (shell.error)
|
|
throw new Error(
|
|
`Unable to start Bash for server deployment syntax validation: ${shell.error.message}`,
|
|
);
|
|
if (shell.status !== 0)
|
|
throw new Error(`Server deployment example failed bash syntax validation:
|
|
${shell.stderr || shell.stdout || "Bash returned a non-zero status."}`);
|
|
} else {
|
|
console.log(
|
|
"Windows: external Bash syntax validation skipped; portable server-script validation passed.",
|
|
);
|
|
}
|
|
|
|
JSON.parse(
|
|
await readFile(
|
|
path.join(root, "examples/server/status-example.json"),
|
|
"utf8",
|
|
),
|
|
);
|
|
const setupGuide = await readFile(
|
|
path.join(root, "docs/SETUP_GUIDE.md"),
|
|
"utf8",
|
|
);
|
|
const sshGuide = await readFile(
|
|
path.join(root, "docs/SSH_UNRAID_DEPLOYMENT.md"),
|
|
"utf8",
|
|
);
|
|
const audit = await readFile(
|
|
path.join(root, "docs/LUMAOPS_SERVER_AUDIT.md"),
|
|
"utf8",
|
|
);
|
|
const releaseNotes = await readFile(
|
|
path.join(root, "docs/RELEASE_NOTES_0.6.0.md"),
|
|
"utf8",
|
|
);
|
|
const updaterReleaseNotes = await readFile(
|
|
path.join(root, "docs/RELEASE_NOTES_0.6.1.md"),
|
|
"utf8",
|
|
);
|
|
if (
|
|
!setupGuide.includes("Gitea access token") ||
|
|
!setupGuide.includes("diagnostic bundle")
|
|
) {
|
|
throw new Error(
|
|
"Setup guide is missing required connection or diagnostics instructions.",
|
|
);
|
|
}
|
|
if (
|
|
!sshGuide.includes("/mnt/user/appdata") ||
|
|
!sshGuide.includes("host-key fingerprint")
|
|
) {
|
|
throw new Error(
|
|
"SSH / Unraid guide is missing its base path or host identity policy.",
|
|
);
|
|
}
|
|
if (
|
|
!audit.includes("d42d4a7f08240c478d07466e3fabec654dc71367") ||
|
|
!audit.includes("source/")
|
|
) {
|
|
throw new Error(
|
|
"LumaOps audit is missing the exact matching SHA or nested repository finding.",
|
|
);
|
|
}
|
|
for (const phrase of [
|
|
"DockerMan",
|
|
"HEAD.lock",
|
|
"deployment reconciliation",
|
|
"Portfolio",
|
|
"safety branch",
|
|
"high-contrast ITWorx",
|
|
]) {
|
|
if (!releaseNotes.includes(phrase))
|
|
throw new Error(`Release notes are missing: ${phrase}`);
|
|
}
|
|
for (const phrase of [
|
|
"Windows PowerShell 5.1",
|
|
"File.Replace",
|
|
"handshake-only",
|
|
"updateId",
|
|
]) {
|
|
if (!updaterReleaseNotes.includes(phrase))
|
|
throw new Error(`Updater release notes are missing: ${phrase}`);
|
|
}
|
|
const setupScript = await readFile(
|
|
path.join(root, "setup-windows.ps1"),
|
|
"utf8",
|
|
);
|
|
const sourceUpdateScript = await readFile(
|
|
path.join(root, "update-windows.ps1"),
|
|
"utf8",
|
|
);
|
|
for (const [name, script] of [
|
|
["setup-windows.ps1", setupScript],
|
|
["update-windows.ps1", sourceUpdateScript],
|
|
]) {
|
|
if (
|
|
!script.includes("$version = [string]$package.version") ||
|
|
!script.includes("npm ci --no-audit --no-fund")
|
|
)
|
|
throw new Error(
|
|
`${name} must use the package version dynamically and install from package-lock.json.`,
|
|
);
|
|
if (/v0\.4\.2|version -ne "0\.4\.2"/.test(script))
|
|
throw new Error(
|
|
`${name} still contains a stale hard-coded release version.`,
|
|
);
|
|
}
|
|
|
|
const updateHelperPath = path.join(root, "scripts/apply-source-update.ps1");
|
|
const updateHelperBytes = await readFile(updateHelperPath);
|
|
if (
|
|
updateHelperBytes[0] === 0xef &&
|
|
updateHelperBytes[1] === 0xbb &&
|
|
updateHelperBytes[2] === 0xbf
|
|
)
|
|
throw new Error("PowerShell update helper must not contain a UTF-8 BOM.");
|
|
const updateHelper = updateHelperBytes.toString("utf8");
|
|
if (
|
|
!updateHelper.trimStart().startsWith("param(") ||
|
|
updateHelper.trimStart().startsWith("\\")
|
|
)
|
|
throw new Error("PowerShell update helper must start directly with param(.");
|
|
|
|
const renderer = (await Promise.all(["app.js", "diff-view.js", "views.js", "dialogs.js", "operations.js", "actions/shell.js", "actions/inventory.js", "actions/deployment-profile.js", "actions/deployment-operation.js", "actions/setup-and-settings.js", "actions/recovery.js", "actions/command.js", "events.js"].map((file) =>
|
|
readFile(path.join(root, "src/renderer", file), "utf8"),
|
|
))).join("\n");
|
|
const styles = await readFile(
|
|
path.join(root, "src/renderer/styles.css"),
|
|
"utf8",
|
|
);
|
|
const preload = await readFile(path.join(root, "preload.cjs"), "utf8");
|
|
const ipc = (await Promise.all(["ipc.cjs", "ipc/repository-handlers.cjs", "ipc/deployment-handlers.cjs", "ipc/operations-handlers.cjs"].map((file) =>
|
|
readFile(path.join(root, "src/main", file), "utf8"),
|
|
))).join("\n");
|
|
for (const phrase of [
|
|
'data-action="commit-push"',
|
|
"checkForUpdates",
|
|
"saveServer",
|
|
"profile-provider",
|
|
"profile-icon-mode",
|
|
"itworx-mark.png",
|
|
"Repair DockerMan integration",
|
|
"Repository troubleshooting",
|
|
"repair-repository-sync",
|
|
]) {
|
|
if (!renderer.includes(phrase) && !preload.includes(phrase))
|
|
throw new Error(`Frontend integration is missing: ${phrase}`);
|
|
}
|
|
if (
|
|
!/\.file-list\s*\{[^}]*flex:\s*1 1 auto;/s.test(styles) ||
|
|
!styles.includes(".main-canvas.repository-canvas")
|
|
) {
|
|
throw new Error("Changed-file scrolling constraints are missing.");
|
|
}
|
|
for (const channel of [
|
|
"server:discover-existing",
|
|
"troubleshooter:scan",
|
|
"troubleshooter:repair",
|
|
"troubleshooter:auto-repair",
|
|
"updates:check",
|
|
"updates:download",
|
|
"updates:apply",
|
|
"server:save",
|
|
"server:test",
|
|
"server:inspect-project",
|
|
"repository:repair-git-locks",
|
|
"repository:repair-sync",
|
|
"deployment:apply-dockerman-metadata",
|
|
"deployment:reconcile",
|
|
"deployment:link-server-workload",
|
|
]) {
|
|
if (!ipc.includes(channel))
|
|
throw new Error(`IPC registration is missing: ${channel}`);
|
|
}
|
|
const release090 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.9.0.md"), "utf8");
|
|
for (const phrase of [
|
|
"Push bundle",
|
|
"manual wizard",
|
|
"Monitor only",
|
|
"DockerMan templates",
|
|
"SHA-256",
|
|
]) {
|
|
if (!release090.includes(phrase)) throw new Error(`0.9.0 release notes are missing: ${phrase}`);
|
|
}
|
|
|
|
const release091 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.9.1.md"), "utf8");
|
|
for (const phrase of [
|
|
"browser_download_url",
|
|
"cross-origin",
|
|
"manual installer",
|
|
"in-app updates",
|
|
]) {
|
|
if (!release091.includes(phrase)) throw new Error(`0.9.1 release notes are missing: ${phrase}`);
|
|
}
|
|
const release092 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.9.2.md"), "utf8");
|
|
for (const phrase of [
|
|
"Push bundle",
|
|
"server password",
|
|
"docker ps -a",
|
|
"DockerMan",
|
|
"zero counts",
|
|
]) {
|
|
if (!release092.includes(phrase)) throw new Error(`0.9.2 release notes are missing: ${phrase}`);
|
|
}
|
|
const release093 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.9.3.md"), "utf8");
|
|
for (const phrase of [
|
|
"Direct copy",
|
|
"Compose YAML",
|
|
"linked automatically",
|
|
"one-click",
|
|
"no remote `git ls-remote`",
|
|
]) {
|
|
if (!release093.includes(phrase)) throw new Error(`0.9.3 release notes are missing: ${phrase}`);
|
|
}
|
|
const release094 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.9.4.md"), "utf8");
|
|
for (const phrase of [
|
|
"real Compose files",
|
|
"stale service hints",
|
|
"force-recreate",
|
|
"container ID",
|
|
"previous container",
|
|
]) {
|
|
if (!release094.includes(phrase)) throw new Error(`0.9.4 release notes are missing: ${phrase}`);
|
|
}
|
|
const release095 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.9.5.md"), "utf8");
|
|
for (const phrase of [
|
|
"Check / fix write access",
|
|
"exact path, user, owner, group and mode",
|
|
"preserves existing executable bits",
|
|
"never implicitly executes `docker compose down`",
|
|
"retains the backup evidence",
|
|
]) {
|
|
if (!release095.includes(phrase)) throw new Error(`0.9.5 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0100 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.0.md"), "utf8");
|
|
for (const phrase of [
|
|
"Server pull",
|
|
"read-only deploy key",
|
|
"automatic discovery",
|
|
"Git Validator",
|
|
"SSH host fingerprint",
|
|
]) {
|
|
if (!release0100.includes(phrase)) throw new Error(`0.10.0 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0101 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.1.md"), "utf8");
|
|
for (const phrase of ["certificate-free updates", "case-insensitive", "read-only deploy keys", "SHA-256"]) {
|
|
if (!release0101.includes(phrase)) throw new Error(`0.10.1 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0102 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.2.md"), "utf8");
|
|
for (const phrase of ["internal HTTP", "public HTTPS", "same-origin", "SHA-256"]) {
|
|
if (!release0102.includes(phrase)) throw new Error(`0.10.2 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0103 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.3.md"), "utf8");
|
|
for (const phrase of ["concurrently", "debounce", "animation frame", "Git Validator", "stale or forged"]) {
|
|
if (!release0103.includes(phrase)) throw new Error(`0.10.3 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0104 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.4.md"), "utf8");
|
|
for (const phrase of ["Windows PowerShell 5.1", "atomic status", "handshake-only", "existing installations"]) {
|
|
if (!release0104.includes(phrase)) throw new Error(`0.10.4 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0105 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.5.md"), "utf8");
|
|
for (const phrase of ["repository workspace", "resolved profile", "Link unresolved", "reconciliation", "server workload"]) {
|
|
if (!release0105.includes(phrase)) throw new Error(`0.10.5 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0106 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.6.md"), "utf8");
|
|
for (const phrase of ["detached", "PowerShell", "production Node spawn", "source updater", "one-time direct installation"]) {
|
|
if (!release0106.includes(phrase)) throw new Error(`0.10.6 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0107 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.7.md"), "utf8");
|
|
for (const phrase of ["exact provenance", "automatic", "repository sidebar", "DevRunbook", "no container changes"]) {
|
|
if (!release0107.includes(phrase)) throw new Error(`0.10.7 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0108 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.8.md"), "utf8");
|
|
for (const phrase of ["Get-FileHash", ".NET SHA-256", "PSModulePath", "binary", "source update helpers"]) {
|
|
if (!release0108.includes(phrase)) throw new Error(`0.10.8 release notes are missing: ${phrase}`);
|
|
}
|
|
const release0109 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.9.md"), "utf8");
|
|
for (const phrase of ["containers without healthchecks", "single-instance", "exact Gitea commit", "deploy-ready", "no containers are changed"]) {
|
|
if (!release0109.includes(phrase)) throw new Error(`0.10.9 release notes are missing: ${phrase}`);
|
|
}
|
|
const release01010 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.10.md"), "utf8");
|
|
for (const phrase of ["read-only deploy keys", "repository deployment root", "Compose working directory", "Fix write access", "exact Gitea commit"]) {
|
|
if (!release01010.includes(phrase)) throw new Error(`0.10.10 release notes are missing: ${phrase}`);
|
|
}
|
|
const release01011 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.11.md"), "utf8");
|
|
for (const phrase of ["last-known-good", "closed output pipe", "linked checkout origin", "read-only deploy key", "browser test server"]) {
|
|
if (!release01011.includes(phrase)) throw new Error(`0.10.11 release notes are missing: ${phrase}`);
|
|
}
|
|
const release01012 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.12.md"), "utf8");
|
|
for (const phrase of ["coalesced", "exact Gitea commit parity", "batched Docker inspect", "bounded worker pools", "stopped container"]) {
|
|
if (!release01012.includes(phrase)) throw new Error(`0.10.12 release notes are missing: ${phrase}`);
|
|
}
|
|
const configSource = await readFile(path.join(root, "src/main/config-store.cjs"), "utf8");
|
|
for (const mode of ["server-git", "push-bundle", "monitor-only"]) {
|
|
if (!configSource.includes(mode)) throw new Error(`Deployment configuration is missing mode: ${mode}`);
|
|
}
|
|
const unraidDirectSource = (await Promise.all([
|
|
"unraid-deployment-service.cjs", "unraid-access-methods.cjs", "unraid-preflight-methods.cjs",
|
|
"unraid-runtime-methods.cjs", "unraid-deployment-methods.cjs", "unraid-inventory-methods.cjs", "unraid-state-methods.cjs",
|
|
].map((file) => readFile(path.join(root, "src/main", file), "utf8")))).join("\n");
|
|
for (const requiredPhrase of [
|
|
"executePushBundle",
|
|
"executeServerGitBundle",
|
|
"configureServerGitAccess",
|
|
"server-git-access",
|
|
"git ls-remote --exit-code",
|
|
"repository-scoped read-only deploy key",
|
|
]) {
|
|
if (!unraidDirectSource.includes(requiredPhrase)) throw new Error(`Deployment source is missing: ${requiredPhrase}`);
|
|
}
|
|
const serverInventorySource = await readFile(path.join(root, "src/main/server-inventory.cjs"), "utf8");
|
|
for (const requiredPhrase of ["server-compose-file", "composeDefinitions", "remoteFolderCandidate"]) {
|
|
if (!serverInventorySource.includes(requiredPhrase)) throw new Error(`Server inventory source is missing: ${requiredPhrase}`);
|
|
}
|
|
|
|
const giteaUpdateSource = await readFile(path.join(root, "src/main/gitea-service.cjs"), "utf8");
|
|
for (const phrase of [
|
|
"browser_download_url",
|
|
"insecure cross-origin",
|
|
"downloadReleaseAsset",
|
|
]) {
|
|
if (!giteaUpdateSource.includes(phrase)) throw new Error(`0.9.1 updater repair is missing: ${phrase}`);
|
|
}
|
|
const gitSource = await readFile(
|
|
path.join(root, "src/main/git-service.cjs"),
|
|
"utf8",
|
|
);
|
|
const unraidSource = unraidDirectSource;
|
|
const publisher = await readFile(
|
|
path.join(root, "Publish-ForgeFlow-Release.ps1"),
|
|
"utf8",
|
|
);
|
|
for (const phrase of [
|
|
"HEAD.lock",
|
|
"backup-reset",
|
|
"repairSync",
|
|
"segments.includes('objects')",
|
|
]) {
|
|
if (!gitSource.includes(phrase))
|
|
throw new Error(`Git recovery implementation is missing: ${phrase}`);
|
|
}
|
|
for (const phrase of [
|
|
"discoverExisting",
|
|
"deriveDetectedProfile",
|
|
"docker inspect",
|
|
"net.unraid.docker.managed",
|
|
"dockerman",
|
|
"iconCacheRefresh",
|
|
"[PORT:",
|
|
"Superseded by live commit",
|
|
"pushBundleScript",
|
|
"linkServerWorkload",
|
|
"deploymentMode",
|
|
]) {
|
|
if (!unraidSource.includes(phrase))
|
|
throw new Error(`Unraid recovery implementation is missing: ${phrase}`);
|
|
}
|
|
for (const phrase of [
|
|
"git ls-remote origin",
|
|
"apply-source-update.ps1",
|
|
"without changing its version",
|
|
]) {
|
|
if (!publisher.includes(phrase))
|
|
throw new Error(`Publishing workflow is missing: ${phrase}`);
|
|
}
|
|
|
|
console.log(
|
|
`Verified ${required.length} required project files and ${javascriptFiles.length} JavaScript files for ForgeFlow ${packageJson.version}.`,
|
|
);
|