Files
ForgeFlow/src/renderer/mock-deployment-bridge.js
T
NuklearRabbit 58d361bbab
ForgeFlow quality gate / quality (push) Canceled after 0s
fix: align deployment links across repository views
2026-08-01 17:05:03 +02:00

699 lines
28 KiB
JavaScript

function createMockDeploymentBridge(context) {
const { wait, clone, iso, storage, repositoryListeners, operationListeners, updateListeners, emitRepositories, emitOperations, randomSha, now, profile, state, repositories, recompute, snapshot, commitHistory, advanceOperation, syncState, diffs, findRepo, findProfileRepo, branchesByRepo, stashesByRepo, updateOperation } = context;
return {
async saveDeploymentProfile(fullName, input) {
const repo = repositories.find((item) => item.fullName === fullName);
const existing = repo.deploymentProfiles.find(
(item) => item.id === input.id,
);
const saved = {
...(existing ||
profile(
input.id || `profile-${Date.now()}`,
input.name || input.environment,
input.environment || "production",
)),
...input,
id: input.id || `profile-${Date.now()}`,
provider: input.provider || existing?.provider || "gitea-actions",
inputs: existing?.inputs || {},
state: existing?.state || {
liveSha: null,
previousSha: null,
healthy: null,
healthConfigured: Boolean(input.healthcheckUrl),
statusConfigured: Boolean(input.statusUrl),
checkedAt: null,
},
};
repo.deploymentProfiles = [
...repo.deploymentProfiles.filter((item) => item.id !== saved.id),
saved,
];
snapshot();
return { profile: clone(saved), state: clone(state) };
},
async deleteDeploymentProfile(fullName, profileId) {
const repo = repositories.find((item) => item.fullName === fullName);
repo.deploymentProfiles = repo.deploymentProfiles.filter(
(item) => item.id !== profileId,
);
snapshot();
return { profiles: clone(repo.deploymentProfiles), state: clone(state) };
},
async deploymentPreflight(repository, profileId) {
await wait(280);
const profile = repository.deploymentProfiles.find(
(item) => item.id === profileId,
);
const status = repository.localStatus;
const checks = [
{
id: "repository.linked",
label: "Local repository link",
status: repository.localPath ? "pass" : "fail",
detail: repository.localPath || "No local folder linked.",
required: true,
},
{
id: "git.branch",
label: "Allowed branch",
status: status?.branch.head === profile?.branch ? "pass" : "fail",
detail: `Current: ${status?.branch.head || "unknown"}; required: ${profile?.branch || "unknown"}.`,
required: true,
},
{
id: "git.clean",
label: "Clean working tree",
status: status?.clean ? "pass" : "fail",
detail: status?.clean
? "No uncommitted changes."
: `${status?.counts.changed || 0} changed file(s).`,
required: true,
},
{
id: "git.sync",
label: "Local and Gitea synchronized",
status:
!status?.branch.ahead && !status?.branch.behind ? "pass" : "fail",
detail: `${status?.branch.ahead || 0} ahead, ${status?.branch.behind || 0} behind.`,
required: true,
},
{
id: "workflow.deploy.remote",
label: "Deploy workflow on Gitea branch",
status: "pass",
detail: `${profile?.workflowFile || "deploy.yml"} exists on ${profile?.branch || "main"}.`,
required: true,
},
{
id: "gitea.actions",
label: "Gitea Actions API",
status: "pass",
detail: "The Actions runs endpoint is accessible.",
required: true,
},
{
id: "server.status",
label: "Server version endpoint",
status: profile?.statusUrl ? "pass" : "warning",
detail: profile?.statusUrl
? `Endpoint reachable; live ${profile.state?.liveSha?.slice(0, 7) || "unknown"}.`
: "No status URL configured.",
required: false,
},
{
id: "server.health",
label: "Application healthcheck",
status: profile?.healthcheckUrl ? "pass" : "warning",
detail: profile?.healthcheckUrl
? "HTTP 200 in 42 ms."
: "No healthcheck URL configured.",
required: false,
},
];
const blocking = checks
.filter((i) => i.required && i.status === "fail")
.map((i) => i.id);
return {
kind: "deployment",
repository: repository.fullName,
profileId,
startedAt: iso(-100),
completedAt: iso(),
checks,
summary: {
counts: {
pass: checks.filter((i) => i.status === "pass").length,
warning: checks.filter((i) => i.status === "warning").length,
fail: checks.filter((i) => i.status === "fail").length,
skipped: 0,
},
blocking,
ready: blocking.length === 0,
},
head: status?.head || null,
};
},
async deploy(repository, profileId, sha) {
await wait(320);
const selected = repository.deploymentProfiles.find(
(item) => item.id === profileId,
);
const operation = {
id: `deploy-${Date.now()}`,
type: "deployment",
action: "deploy",
status: "queued",
repository: repository.fullName,
profileId,
profileName: selected.name,
environment: selected.environment,
workflowFile: selected.workflowFile,
branch: selected.branch,
sha,
shortSha: sha.slice(0, 7),
dispatchedAt: iso(),
createdAt: iso(),
updatedAt: iso(),
demoPolls: 0,
stages: [
{ id: "requested", label: "Requested", status: "complete" },
{ id: "verified", label: "Verified", status: "complete" },
{ id: "queued", label: "Workflow queued", status: "active" },
{ id: "runner", label: "Runner execution", status: "pending" },
{ id: "healthcheck", label: "Healthcheck", status: "pending" },
{ id: "complete", label: "Complete", status: "pending" },
],
logs: [
`[info] Verified clean ${selected.branch} at ${sha}`,
`[ok] Gitea accepted ${selected.workflowFile}.`,
],
};
return updateOperation(operation);
},
async rollback(repository, profileId, targetSha) {
await wait(320);
const selected = repository.deploymentProfiles.find(
(item) => item.id === profileId,
);
const operation = {
id: `rollback-${Date.now()}`,
type: "deployment",
action: "rollback",
status: "queued",
repository: repository.fullName,
profileId,
profileName: selected.name,
environment: selected.environment,
workflowFile: selected.rollbackWorkflowFile,
branch: selected.branch,
sha: targetSha,
shortSha: targetSha.slice(0, 7),
dispatchedAt: iso(),
createdAt: iso(),
updatedAt: iso(),
demoPolls: 0,
stages: [
{ id: "requested", label: "Requested", status: "complete" },
{ id: "verified", label: "Verified", status: "complete" },
{ id: "queued", label: "Workflow queued", status: "active" },
{ id: "runner", label: "Runner execution", status: "pending" },
{ id: "healthcheck", label: "Healthcheck", status: "pending" },
{ id: "complete", label: "Complete", status: "pending" },
],
logs: [
`[warning] Rollback target verified: ${targetSha}`,
`[ok] Gitea accepted ${selected.rollbackWorkflowFile}.`,
],
};
return updateOperation(operation);
},
async healthcheck() {
await wait(160);
return { configured: true, healthy: true, status: 200, latencyMs: 42 };
},
async refreshProfileState(fullName, profileId) {
await wait(240);
const repo =
repositories.find((item) => item.fullName === fullName) ||
findProfileRepo(profileId);
const target = repo?.deploymentProfiles.find(
(item) => item.id === profileId,
);
if (!target) throw new Error("Deployment profile not found.");
target.state = {
...target.state,
checkedAt: iso(),
healthy: target.state.healthy !== false,
healthConfigured: Boolean(target.healthcheckUrl),
statusConfigured: Boolean(target.statusUrl),
};
syncState();
return clone(target.state);
},
async discoverServerDeployments() {
await wait(80);
return [
{
serverId: "server-unraid",
serverName: "Unraid",
detected: 3,
adopted: 0,
verified: 1,
linked: 2,
unmatched: 0,
needsReview: 2,
running: 3,
stopped: 0,
capabilities: {
docker: true,
dockerReady: true,
compose: true,
git: false,
tar: true,
checksum: true,
},
warnings: [],
workloads: [
{
workloadId: "workload-demo-linked",
displayName: "Portfolio",
status: "linked",
runtime: { running: true, health: "healthy" },
compose: {
project: "portfolio",
workingDir: "/mnt/user/appdata/portfolio",
configFiles: ["/mnt/user/appdata/portfolio/docker-compose.yml"],
services: ["web"],
},
containers: [{ name: "Portfolio", running: true }],
candidates: [],
link: {
profileId: "profile-portfolio",
repositoryFullName: "jens/portfolio",
source: "manual",
},
},
{
workloadId: "workload-demo-review",
displayName: "OmniRoute",
status: "suggested",
runtime: { running: true, health: "unverified" },
compose: {
project: "omniroute",
workingDir: "/mnt/user/appdata/OmniRoute",
configFiles: ["/mnt/user/appdata/OmniRoute/docker-compose.yml"],
services: ["omniroute"],
},
containers: [{ name: "omniroute", running: true }],
remoteFolderCandidate: "OmniRoute",
candidates: repositories.slice(0, 1).map((repository) => ({
repositoryFullName: repository.fullName,
repositoryName: repository.name,
score: 55,
exact: false,
reasons: ["container and repository names are similar"],
})),
},
{
workloadId: "workload-demo-unresolved",
displayName: "Legacy Worker",
status: "linked",
runtime: { running: true, health: "healthy" },
containers: [{ name: "legacy-worker", running: true }],
candidates: [],
link: {
profileId: "profile-that-no-longer-exists",
repositoryFullName: "jens/removed-repository",
source: "manual",
},
},
],
},
];
},
async planServerReconciliation(serverId) {
await wait(90);
const id = "a".repeat(64);
return {
inventory: (await this.discoverServerDeployments()).find((item) => item.serverId === serverId),
plan: {
id,
serverId,
summary: { additions: 0, updates: 1, stale: 0, conflicts: 1 },
additions: [],
updates: [{ workloadId: "workload-demo-linked", profileId: "profile-portfolio", repositoryFullName: "jens/portfolio", impact: "Refresh detected Compose identity and observed deployment state" }],
stale: [],
conflicts: [{ workloadId: "workload-demo-review", displayName: "OmniRoute", status: "suggested", candidates: [{ repositoryFullName: repositories[0].fullName, score: 55, exact: false }] }],
},
};
},
async applyServerReconciliation(serverId, planId) {
await wait(120);
if (serverId !== "server-unraid" || planId !== "a".repeat(64)) throw new Error("The reconciliation plan is stale.");
return { adopted: 0, refreshed: 1, retired: 0, state: clone(state) };
},
async planInventoryReview(serverId, workloadId, action, reason = "", repositoryFullName = null) {
if (["ignore", "manual-exclude", "exclude-scan-root"].includes(action) && reason.length < 5) throw new Error("A meaningful review reason is required.");
return { id: "b".repeat(64), serverId, workloadId, action, reason, repositoryFullName, evidenceHash: "c".repeat(64), classification: "ambiguous", containersUnaffected: true, configurationChanges: [`Persist review decision ${action}`], recovery: "Remove the decision or rescan after evidence changes." };
},
async applyInventoryReview(serverId, workloadId, action, reason, repositoryFullName, planId) {
if (planId !== "b".repeat(64)) throw new Error("The inventory review plan is stale.");
const inventory = (await this.discoverServerDeployments()).find((item) => item.serverId === serverId);
const workload = inventory.workloads.find((item) => item.workloadId === workloadId);
if (workload) workload.reviewDecision = { action, reason, repositoryFullName, evidenceHash: "c".repeat(64) };
return { decision: workload?.reviewDecision, inventory, state: clone(state) };
},
async linkServerWorkload(repository, serverId, workloadId, deploymentMode = "server-git", remoteFolder = "") {
await wait(120);
const repo = repositories.find((item) => item.fullName === repository.fullName);
if (!repo) throw new Error("Repository not found.");
const id = `profile-${workloadId}`;
const saved = {
id,
name: `Unraid · ${remoteFolder || repo.name}`,
environment: "production",
provider: "ssh-unraid",
branch: repo.defaultBranch || "main",
serverId,
remoteFolder: remoteFolder || repo.name,
deploymentMode,
composeFile: "docker-compose.yml",
composeFiles: ["docker-compose.yml"],
composeProject: String(remoteFolder || repo.name).toLowerCase(),
composeService: String(remoteFolder || repo.name).toLowerCase(),
composeServices: [String(remoteFolder || repo.name).toLowerCase()],
containerName: remoteFolder || repo.name,
preservePaths: [".env", "appdata", "data", "logs", "config"],
generatedCompose: false,
adoptedFromServer: true,
serverSourceOfTruth: true,
manageDockerMan: false,
forceRecreate: false,
removeOrphans: false,
workloadIdentity: { workloadId, linkSource: "manual", linkedAt: iso() },
confirmationRequired: true,
state: {
liveSha: null,
healthy: null,
containerRunning: true,
runtimeVerification: "running-unverified",
checkedAt: iso(),
},
};
repo.deploymentProfiles = [
...repo.deploymentProfiles.filter((item) => item.id !== id),
saved,
];
syncState();
return { profile: clone(saved), state: clone(state) };
},
async configureServerGitAccess(repository, profileId) {
const repo = repositories.find((item) => item.fullName === repository.fullName);
const target = repo?.deploymentProfiles.find((item) => item.id === profileId);
if (!target) throw new Error("Deployment profile not found.");
target.deploymentMode = "server-git";
target.serverGitAccess = { configured: true, keyFingerprint: "SHA256:demo", hostFingerprint: "SHA256:gitea", configuredAt: iso() };
syncState();
return { profile: clone(target), created: true, remoteSha: target.state?.giteaSha || repo.localStatus?.head };
},
async verifyServerGitProfile(repository, profileId) {
const repo = repositories.find((item) => item.fullName === repository.fullName);
const target = repo?.deploymentProfiles.find((item) => item.id === profileId);
if (!target) throw new Error("Deployment profile not found.");
const branchSha = target.state?.giteaSha || repo.localStatus?.head || null;
const liveSha = target.state?.liveSha || null;
return {
readiness: branchSha && liveSha === branchSha ? "Ready" : "Commit mismatch",
ready: true,
checkedAt: iso(),
repository: repo.fullName,
profileId,
branchSha,
liveSha,
checks: [
{ id: "remote-branch", label: "Gitea branch", status: "pass", detail: "Exact branch resolved." },
{ id: "deploy-key-scope", label: "Repository deploy key", status: "pass", detail: "Repository-scoped and read-only." },
{ id: "server-git-access", label: "Unraid to Gitea", status: "pass", detail: "Pinned SSH access verified." },
],
};
},
async deployKeyInventory(repository, profileId) {
const repo = repositories.find((item) => item.fullName === repository.fullName);
const profile = repo?.deploymentProfiles.find((item) => item.id === profileId);
return { repository: repo.fullName, profileId, server: { id: profile.serverId, name: "Unraid" }, configuredKey: { id: profile.serverGitAccess?.deployKeyId || 17, readOnly: true }, serverKey: { privateKeyPresent: true, fingerprint: profile.serverGitAccess?.keyFingerprint || "SHA256:demo" }, stale: false, orphaned: [], shared: [], conflicts: [], ready: true, checkedAt: iso() };
},
async planDeployKeyRotation(repository, profileId) {
const evidence = await this.deployKeyInventory(repository, profileId);
return { id: `rotation-${profileId}`, operation: "rotate-deploy-key", impact: ["Generate a new server-side key", "Verify read-only access", "Switch atomically", "Revoke the previous key"], recovery: "Previous access remains recoverable until verification succeeds.", evidence };
},
async applyDeployKeyRotation(repository, profileId) {
const repo = repositories.find((item) => item.fullName === repository.fullName); const profile = repo.deploymentProfiles.find((item) => item.id === profileId);
profile.serverGitAccess = { ...profile.serverGitAccess, configured: true, deployKeyId: 18, keyFingerprint: "SHA256:rotated", rotatedAt: iso() }; syncState(); return { profile: clone(profile), state: clone(state) };
},
async planDeployKeyRevocation(repository, profileId) {
const evidence = await this.deployKeyInventory(repository, profileId);
return { id: `revocation-${profileId}`, operation: "revoke-deploy-key", impact: ["Remove the repository key", "Disable server pull", "Preserve recovery material"], containersUnaffected: true, evidence };
},
async applyDeployKeyRevocation(repository, profileId) {
const repo = repositories.find((item) => item.fullName === repository.fullName); const profile = repo.deploymentProfiles.find((item) => item.id === profileId);
profile.deploymentMode = "monitor-only"; profile.serverGitAccess = { ...profile.serverGitAccess, configured: false, revokedAt: iso(), recoveryAvailable: true }; syncState(); return { profile: clone(profile), state: clone(state) };
},
async restoreDeployKey(repository, profileId) {
const repo = repositories.find((item) => item.fullName === repository.fullName); const profile = repo.deploymentProfiles.find((item) => item.id === profileId);
profile.deploymentMode = "server-git"; profile.serverGitAccess = { ...profile.serverGitAccess, configured: true, deployKeyId: 19, keyFingerprint: "SHA256:restored", restoredAt: iso() }; syncState(); return { profile: clone(profile), state: clone(state), proof: { ready: true } };
},
async refreshOperations(operationId = null) {
await wait(300);
if (operationId) {
const operation = state.operations.find(
(item) => item.id === operationId,
);
if (!operation) throw new Error("Operation not found.");
return updateOperation(advanceOperation(operation));
}
const active = state.operations
.filter(
(item) =>
!["success", "failed", "cancelled", "rolled-back"].includes(
item.status,
),
)
.map(advanceOperation);
if (active.length) emitOperations(active);
state.operations = state.operations.map(
(item) => active.find((entry) => entry.id === item.id) || item,
);
return clone(active);
},
async getOperation(operationId) {
return clone(
state.operations.find((item) => item.id === operationId) || null,
);
},
async gitValidatorScan(fullName) {
await wait(260);
const policy = state.gitValidatorPolicy || { id: "standard", label: "Standard", requiredScore: 70 };
const activeWarnings = 3;
return {
repository: fullName,
checkedAt: iso(),
score: 78,
grade: "Good",
policy,
ready: 78 >= policy.requiredScore && (policy.id === "minimal" || activeWarnings === 0),
commitSha: "8cbaf303aa3bb9b4023a7c89aa13fb70ce612847",
trend: { newlyFound: ["working-tree"], resolved: ["editorconfig"], regressions: [], suppressions: [] },
expiredSuppressions: [],
summary: { passed: 7, warnings: 3, errors: 0, repairable: 2 },
checks: [
{
id: "origin",
category: "Repository identity",
title: "Origin matches Gitea",
status: "pass",
detail: "The local origin resolves to this Gitea repository.",
weight: 15,
},
{
id: "default-branch-protection",
category: "Gitea governance",
title: "Default branch protection",
status: "warning",
detail: "main accepts unprotected direct changes.",
weight: 18,
fixAction: "protect-default-branch",
safe: false,
confirmation:
"Protect main on Gitea and block direct and force pushes?",
},
{
id: "force-push",
category: "Gitea governance",
title: "Force-push protection",
status: "pass",
detail: "Force pushes are blocked.",
weight: 8,
},
{
id: "upstream",
category: "Branch hygiene",
title: "Current branch has an upstream",
status: "pass",
detail: "main tracks origin/main.",
weight: 8,
},
{
id: "working-tree",
category: "Branch hygiene",
title: "Working tree is intentional",
status: "warning",
detail: "3 changed files require review, commit or stash.",
weight: 5,
},
{
id: "identity",
category: "Commit integrity",
title: "Repository author identity",
status: "pass",
detail: "Jens <jens@example.test>",
weight: 7,
},
{
id: "local-safety",
category: "Local configuration",
title: "Safe synchronization defaults",
status: "warning",
detail: "Recommended repository-local safeguards are incomplete.",
weight: 10,
fixAction: "configure-local-safety",
safe: true,
},
{
id: "readme",
category: "Repository documentation",
title: "README is versioned",
status: "pass",
detail: "Repository documentation is tracked.",
weight: 7,
},
{
id: "gitignore",
category: "Repository hygiene",
title: ".gitignore is versioned",
status: "pass",
detail: "Generated files are excluded centrally.",
weight: 8,
},
{
id: "tracked-secrets",
category: "Security",
title: "No secret-shaped files are tracked",
status: "pass",
detail:
"No tracked environment, key or credential filenames detected.",
weight: 22,
},
{
id: "large-files",
category: "Repository performance",
title: "No oversized tracked files",
status: "pass",
detail: "No tracked files above 10 MB were found.",
weight: 7,
},
],
};
},
async gitValidatorSetPolicy(_fullName, policy) {
const requiredScores = { minimal: 55, standard: 70, strict: 82, production: 90 };
state.gitValidatorPolicy = {
id: policy.id,
label: policy.id[0].toUpperCase() + policy.id.slice(1),
requiredScore: requiredScores[policy.id] || 70,
};
return clone(state.gitValidatorPolicy);
},
async gitValidatorSuppress(_fullName, suppression) {
return { ...suppression, id: `suppression-${Date.now()}`, createdAt: iso() };
},
async gitValidatorPreviewRepair(_fullName, check) {
return { checkId: check.id, action: check.fixAction, files: [".git/config"], diff: "+ reviewed configuration change\n", remoteMutation: check.fixAction === "protect-default-branch" };
},
async gitValidatorExport(fullName, format) {
return { extension: format === "markdown" ? "md" : format, mimeType: "text/plain", content: `# Git assurance — ${fullName}\n` };
},
async gitValidatorRepair() {
await wait(180);
return { repaired: true };
},
async diagnosticsStatus() {
return {
enabled: state.preferences.diagnosticsEnabled !== false,
level: state.preferences.diagnosticLevel,
retentionDays: state.preferences.logRetentionDays,
maxFileMb: state.preferences.maxLogFileMb,
directory: "<HOME>/AppData/Roaming/ForgeFlow/diagnostics",
fileCount: 2,
totalBytes: 18432,
totalSize: "18.0 KB",
latestAt: iso(-2000),
lastWriteError: null,
};
},
async exportConfigurationBackup() {
return {
filePath: "C:\\Downloads\\ForgeFlow-Configuration-demo.ffbackup",
};
},
async importConfigurationBackup() {
return { state: clone(state), exportedAt: iso(-86400000) };
},
async listAuditEvents() {
return [
{
id: "audit-1",
timestamp: iso(-60000),
event: "deployment.completed",
details: { repository: "Jens/ForgeFlow", result: "success" },
},
];
},
async exportAuditLog() {
return { filePath: "C:\\Downloads\\ForgeFlow-Audit-demo.json", count: 1 };
},
async clearDiagnostics() {
return {
enabled: true,
level: state.preferences.diagnosticLevel,
retentionDays: state.preferences.logRetentionDays,
maxFileMb: state.preferences.maxLogFileMb,
directory: "<HOME>/AppData/Roaming/ForgeFlow/diagnostics",
fileCount: 1,
totalBytes: 256,
totalSize: "256 B",
latestAt: iso(),
lastWriteError: null,
};
},
async openDiagnosticsFolder() {
return true;
},
async exportDiagnostics(privacyMode = "standard") {
await wait(500);
return {
path: `C:\Users\Jens\Downloads\ForgeFlow-Diagnostics-demo.zip`,
bytes: 38221,
size: "37.3 KB",
sha256: "b".repeat(64),
privacyMode,
generatedAt: iso(),
};
},
async showDiagnosticBundle() {
return true;
},
async reportRendererEvent() {
return true;
},
onRepositoriesChanged(listener) {
repositoryListeners.add(listener);
return () => repositoryListeners.delete(listener);
},
onOperationsChanged(listener) {
operationListeners.add(listener);
return () => operationListeners.delete(listener);
},
onUpdatesChanged(listener) {
updateListeners.add(listener);
return () => updateListeners.delete(listener);
},
async reset() {
state.setupComplete = false;
storage.set("forgeflow-demo-setup", "false");
return clone(state);
},
};
}