fix: reconcile server deployments across repository views
ForgeFlow quality gate / quality (push) Canceled after 0s
ForgeFlow quality gate / quality (push) Canceled after 0s
This commit is contained in:
@@ -384,15 +384,41 @@ function createUnraidInventoryMethods({
|
||||
};
|
||||
}
|
||||
|
||||
async scanServerInventory(serverId, repositories) {
|
||||
async scanServerInventory(serverId, repositories, { autoLink = false } = {}) {
|
||||
const { server, inventory, workloads } = await this.collectServerInventory(serverId, repositories);
|
||||
const response = this.inventoryResponse(server, inventory, workloads);
|
||||
let adopted = 0;
|
||||
const adoptedLinks = [];
|
||||
if (autoLink) {
|
||||
const plan = this.reconciliationPlan(server, workloads, repositories, { autoLink: true });
|
||||
if (plan.additions.length) await this.store.createRecoverySnapshot?.(`automatic-server-links-${serverId}`);
|
||||
const linkedRepositories = new Set(workloads
|
||||
.filter((workload) => workload.link?.repositoryFullName)
|
||||
.map((workload) => String(workload.link.repositoryFullName).toLowerCase()));
|
||||
for (const addition of plan.additions) {
|
||||
const workload = workloads.find((item) => item.workloadId === addition.workloadId);
|
||||
const repository = (repositories || []).find((item) => String(item.fullName).toLowerCase() === String(addition.repositoryFullName).toLowerCase());
|
||||
const key = String(repository?.fullName || "").toLowerCase();
|
||||
if (!workload || !repository || linkedRepositories.has(key)) continue;
|
||||
const linkSource = addition.evidence === "exact-provenance" ? "automatic" : "automatic-runtime-identity";
|
||||
const profile = this.profileFromWorkload(repository, server, workload, { linkSource, deploymentMode: "server-git" });
|
||||
const saved = await this.store.saveDeploymentProfile(repository.fullName, profile);
|
||||
await this.saveWorkloadState(saved, workload, server);
|
||||
workload.status = "linked";
|
||||
workload.link = { status: "linked", profileId: saved.id, repositoryFullName: repository.fullName, source: linkSource };
|
||||
linkedRepositories.add(key);
|
||||
adopted += 1;
|
||||
adoptedLinks.push({ repositoryFullName: repository.fullName, profileId: saved.id, workloadId: workload.workloadId });
|
||||
}
|
||||
}
|
||||
const response = this.inventoryResponse(server, inventory, workloads, { adopted });
|
||||
await this.diagnostics?.info("unraid.workloads.scanned", {
|
||||
serverId,
|
||||
detected: response.detected,
|
||||
linked: response.linked,
|
||||
needsReview: response.needsReview,
|
||||
readOnly: true,
|
||||
adopted,
|
||||
adoptedLinks,
|
||||
readOnly: !autoLink,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
@@ -428,6 +454,7 @@ function createUnraidInventoryMethods({
|
||||
evidence: candidate.exact ? "exact-provenance" : "exact-runtime-identity",
|
||||
impact: "Create a server-pull deployment profile; no container changes",
|
||||
});
|
||||
linkedRepositories.add(String(candidate.repositoryFullName).toLowerCase());
|
||||
} else if (["suggested", "ambiguous"].includes(workload.status) || (workload.runtime?.running && workload.candidates?.length)) {
|
||||
conflicts.push({
|
||||
workloadId: workload.workloadId,
|
||||
@@ -559,7 +586,7 @@ function createUnraidInventoryMethods({
|
||||
}
|
||||
|
||||
async discoverServerWorkloads(serverId, repositories) {
|
||||
return this.scanServerInventory(serverId, repositories);
|
||||
return this.scanServerInventory(serverId, repositories, { autoLink: true });
|
||||
}
|
||||
|
||||
async linkServerWorkload({ repository, serverId, workloadId, deploymentMode = "server-git", remoteFolder = "" }) {
|
||||
|
||||
@@ -5,7 +5,7 @@ function createMockRepositoryBridge(context) {
|
||||
await wait(80);
|
||||
snapshot();
|
||||
return {
|
||||
appVersion: "0.10.6-demo",
|
||||
appVersion: "0.10.7-demo",
|
||||
platform: "win32",
|
||||
state: clone(state),
|
||||
git: { available: true, version: "git version 2.47.3" },
|
||||
|
||||
+12
-2
@@ -31,6 +31,9 @@ function renderTitlebar() {
|
||||
|
||||
function renderRepositoryRow(repository) {
|
||||
const status = repository.localStatus;
|
||||
const profiles = repository.deploymentProfiles || [];
|
||||
const workloads = linkedWorkloadsForRepository(repository);
|
||||
const runningWorkloads = workloads.filter((workload) => workload.runtime?.running);
|
||||
const badges = [];
|
||||
if (status?.counts.conflicts)
|
||||
badges.push('<span class="mini-badge danger" title="Conflicts">!</span>');
|
||||
@@ -50,10 +53,14 @@ function renderRepositoryRow(repository) {
|
||||
badges.push(
|
||||
'<span class="mini-badge success" title="Ready to deploy">↗</span>',
|
||||
);
|
||||
if (profiles.length)
|
||||
badges.push(
|
||||
`<span class="mini-badge ${runningWorkloads.length ? "success" : "warning"} deployment-badge" title="${attr(`${profiles.length} server deployment${profiles.length === 1 ? "" : "s"} linked${runningWorkloads.length ? ` · ${runningWorkloads.length} running` : ""}`)}">S${profiles.length}</span>`,
|
||||
);
|
||||
if (!repository.localPath)
|
||||
badges.push('<span class="mini-badge" title="No local folder">—</span>');
|
||||
const branch = status?.branch.head || repository.defaultBranch || "remote";
|
||||
return `<button class="repo-row ${String(repository.id) === String(ui.selectedRepoId) ? "active" : ""} ${repository.attention ? "attention" : ""}" data-action="select-repo" data-id="${attr(repository.id)}">
|
||||
return `<button class="repo-row ${String(repository.id) === String(ui.selectedRepoId) ? "active" : ""} ${repository.attention ? "attention" : ""}" data-action="select-repo" data-id="${attr(repository.id)}" data-deployment-count="${profiles.length}">
|
||||
<span class="repo-icon">${repository.favorite ? icon("star") : icon(repository.localPath ? "git" : "cloud")}</span>
|
||||
<span class="repo-main"><span class="repo-name">${escapeHtml(repository.name)}</span><span class="repo-sub"><span>${escapeHtml(branch)}</span>${status?.shortHead ? `<span>• ${escapeHtml(status.shortHead)}</span>` : ""}</span></span>
|
||||
<span class="repo-badges">${badges.join("")}</span>
|
||||
@@ -432,6 +439,7 @@ function renderRepositoryWorkspace(repository) {
|
||||
const profiles = repository.deploymentProfiles || [];
|
||||
const linkedWorkloads = linkedWorkloadsForRepository(repository);
|
||||
const profile = selectedProfile(repository);
|
||||
const profileWorkload = linkedWorkloads.find((workload) => workload.link?.profileId === profile?.id);
|
||||
const serverState = profile?.state || {};
|
||||
const localTone = status?.counts.conflicts
|
||||
? "danger"
|
||||
@@ -452,6 +460,8 @@ function renderRepositoryWorkspace(repository) {
|
||||
? "danger"
|
||||
: serverState.healthy === true
|
||||
? "success"
|
||||
: profileWorkload?.runtime?.running
|
||||
? "success"
|
||||
: "";
|
||||
const content = (
|
||||
{
|
||||
@@ -474,7 +484,7 @@ function renderRepositoryWorkspace(repository) {
|
||||
: "";
|
||||
return `<div class="repo-workspace"><header class="repo-header illustrated-repo-header"><div class="repo-heading"><h1><button class="favorite-button ${repository.favorite ? "active" : ""}" data-action="toggle-favorite" title="Toggle favorite">${icon("star")}</button>${escapeHtml(repository.fullName)}</h1><p>${escapeHtml(repository.localPath || "No local working tree linked")}</p></div>${projectIllustration("repo")}<div class="repo-header-actions"><button class="button" data-action="fetch" ${!repository.localPath ? "disabled" : ""}>${icon("refresh")}Fetch</button><button class="button" data-action="open-path" ${!repository.localPath ? "disabled" : ""}>${icon("folder")}Folder</button><button class="button" data-action="open-gitea" ${!repository.htmlUrl ? "disabled" : ""}>${icon("external")}Gitea</button></div></header>
|
||||
${repository.localPath ? `<div class="repo-quick-actions"><button class="button" data-action="open-editor">${icon("external")}Open in editor</button><button class="button" data-action="open-terminal">${icon("terminal")}Open terminal</button><button class="button" data-action="check-branch-protection">${icon("shield")}Check branch protection</button><button class="button primary" data-action="open-pull-request">${icon("git")}Create pull request</button>${ui.branchProtection ? `<span class="status-pill ${ui.branchProtection.protected ? "warning" : "success"}">${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}</span>` : ""}</div>` : ""}
|
||||
<div class="release-rail">${releaseNode("Local", status?.shortHead || "Not linked", status ? `${status.counts.changed} changes · ${status.branch.head}` : "No working tree", localTone)}${releaseNode("Gitea", status?.shortHead || "Unknown", status?.branch.upstream ? `${status.branch.ahead} ahead · ${status.branch.behind} behind` : "Branch not published", remoteTone)}${releaseNode(`Server${profile ? ` · ${profile.environment}` : ""}`, serverState.liveSha ? shortSha(serverState.liveSha) : "Unknown", profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "not checked") : "No deployment profile", serverTone)}</div>
|
||||
<div class="release-rail">${releaseNode("Local", status?.shortHead || "Not linked", status ? `${status.counts.changed} changes · ${status.branch.head}` : "No working tree", localTone)}${releaseNode("Gitea", status?.shortHead || "Unknown", status?.branch.upstream ? `${status.branch.ahead} ahead · ${status.branch.behind} behind` : "Branch not published", remoteTone)}${releaseNode(`Server${profile ? ` · ${profile.environment}` : ""}`, serverState.liveSha ? shortSha(serverState.liveSha) : profile ? "Linked" : "Unknown", profileWorkload ? `${profileWorkload.displayName || profile.containerName || "Container"} · ${profileWorkload.runtime?.running ? "running" : "stopped"} on ${profileWorkload.serverName}` : profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "profile linked · awaiting live scan") : "No deployment profile", serverTone)}</div>
|
||||
${deploymentLinks}
|
||||
<nav class="tabs">${[
|
||||
["changes", "Changes"],
|
||||
|
||||
Reference in New Issue
Block a user