fix: harden repository refresh and server pull
ForgeFlow quality gate / quality (push) Canceled after 0s
ForgeFlow quality gate / quality (push) Canceled after 0s
This commit is contained in:
@@ -72,7 +72,6 @@ function register(channel, handler) {
|
||||
stack: error?.stack,
|
||||
},
|
||||
});
|
||||
console.error(`[${channel}]`, error);
|
||||
return { ok: false, error: toErrorPayload(error) };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
function isBrokenPipeError(error) {
|
||||
return error?.code === 'EPIPE';
|
||||
}
|
||||
|
||||
function installOutputPipeGuards({
|
||||
stdout = process.stdout,
|
||||
stderr = process.stderr,
|
||||
onBrokenPipe = () => {}
|
||||
} = {}) {
|
||||
const guardedStreams = [stdout, stderr].filter(Boolean);
|
||||
const handlers = guardedStreams.map((stream) => {
|
||||
const handler = (error) => {
|
||||
if (!isBrokenPipeError(error)) throw error;
|
||||
onBrokenPipe(error);
|
||||
};
|
||||
stream.on('error', handler);
|
||||
return { stream, handler };
|
||||
});
|
||||
return () => {
|
||||
for (const { stream, handler } of handlers) stream.off('error', handler);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { installOutputPipeGuards, isBrokenPipeError };
|
||||
@@ -29,6 +29,8 @@ class RepositoryService {
|
||||
this.gitea = giteaService;
|
||||
this.diagnostics = diagnostics;
|
||||
this.lastKnownLocalPaths = [];
|
||||
this.lastKnownRemoteRepositories = [];
|
||||
this.lastSuccessfulRemoteRefreshAt = null;
|
||||
}
|
||||
|
||||
async discoverInRoot(root, maxDepth = 4) {
|
||||
@@ -80,11 +82,37 @@ class RepositoryService {
|
||||
return [...this.lastKnownLocalPaths];
|
||||
}
|
||||
|
||||
async getRemoteRepositories() {
|
||||
if (!this.store.data.gitea.baseUrl || !this.store.getToken()) {
|
||||
this.lastKnownRemoteRepositories = [];
|
||||
this.lastSuccessfulRemoteRefreshAt = null;
|
||||
return { repositories: [], stale: false, error: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const repositories = await this.gitea.listRepositories();
|
||||
this.lastKnownRemoteRepositories = repositories.map((repository) => ({ ...repository }));
|
||||
this.lastSuccessfulRemoteRefreshAt = new Date().toISOString();
|
||||
return { repositories, stale: false, error: null };
|
||||
} catch (error) {
|
||||
if (!this.lastSuccessfulRemoteRefreshAt) throw error;
|
||||
await this.diagnostics?.warning('repositories.remote-refresh.degraded', {
|
||||
message: error.message,
|
||||
cachedCount: this.lastKnownRemoteRepositories.length,
|
||||
lastSuccessfulAt: this.lastSuccessfulRemoteRefreshAt
|
||||
});
|
||||
return {
|
||||
repositories: this.lastKnownRemoteRepositories.map((repository) => ({ ...repository })),
|
||||
stale: true,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
const started = Date.now();
|
||||
const remoteRepositories = this.store.data.gitea.baseUrl && this.store.getToken()
|
||||
? await this.gitea.listRepositories()
|
||||
: [];
|
||||
const remoteResult = await this.getRemoteRepositories();
|
||||
const remoteRepositories = remoteResult.repositories;
|
||||
|
||||
const discoveredPaths = await this.discoverAll(this.store.data.workspaceRoots);
|
||||
const mappedPaths = Object.values(this.store.data.repositoryMappings || {});
|
||||
@@ -106,7 +134,12 @@ class RepositoryService {
|
||||
...profile,
|
||||
state: this.store.getDeploymentState(profile.id)
|
||||
}));
|
||||
repositories.push(this.decorate(remote, local, profiles));
|
||||
repositories.push({
|
||||
...this.decorate(remote, local, profiles),
|
||||
remoteStale: remoteResult.stale,
|
||||
remoteRefreshError: remoteResult.error,
|
||||
remoteLastRefreshedAt: this.lastSuccessfulRemoteRefreshAt
|
||||
});
|
||||
}
|
||||
|
||||
for (const local of localDescriptors.filter((item) => !usedLocalPaths.has(item.localPath))) {
|
||||
@@ -145,6 +178,7 @@ class RepositoryService {
|
||||
await this.diagnostics?.debug('repositories.refresh.completed', {
|
||||
durationMs: Date.now() - started,
|
||||
remoteCount: remoteRepositories.length,
|
||||
remoteStale: remoteResult.stale,
|
||||
discoveredCount: discoveredPaths.length,
|
||||
linkedCount: sorted.filter((item) => item.localPath).length,
|
||||
attentionCount: sorted.filter((item) => item.attention).length,
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
function createUnraidAccessMethods({ shellQuote, path, bash, inventoryRemoteIdentity, checksSummary, crypto, parsePermissionInspection }) {
|
||||
class UnraidAccessMethods {
|
||||
serverGitRemote(repository, profile) {
|
||||
const candidates = [repository.sshUrl, profile.cloneUrl, repository.preferredCloneUrl]
|
||||
const candidates = [
|
||||
repository.localStatus?.remoteUrl,
|
||||
repository.sshUrl,
|
||||
repository.preferredCloneUrl,
|
||||
profile.cloneUrl,
|
||||
]
|
||||
.map((value) => String(value || "").trim())
|
||||
.filter(Boolean);
|
||||
const value = candidates.find((candidate) => /^ssh:\/\//i.test(candidate) || /^[^@\s]+@[^:\s]+:.+/.test(candidate));
|
||||
|
||||
@@ -20,7 +20,7 @@ class UnraidDeployKeyHost {
|
||||
return { directory, privateKey: path.join(directory, "deploy-key"), publicKey: path.join(directory, "deploy-key.pub"), knownHosts: path.join(directory, "known_hosts"), recovery: path.join(directory, "recovery") };
|
||||
}
|
||||
remote(repository, profile) {
|
||||
const value = [repository.sshUrl, profile.cloneUrl, repository.preferredCloneUrl].map((item) => String(item || "").trim()).find((item) => /^ssh:\/\//i.test(item) || /^[^@\s]+@[^:\s]+:.+/.test(item));
|
||||
const value = [repository.localStatus?.remoteUrl, repository.sshUrl, repository.preferredCloneUrl, profile.cloneUrl].map((item) => String(item || "").trim()).find((item) => /^ssh:\/\//i.test(item) || /^[^@\s]+@[^:\s]+:.+/.test(item));
|
||||
if (!value) throw Object.assign(new Error("Server pull requires a Gitea SSH URL."), { code: "SERVER_GIT_SSH_URL_REQUIRED" });
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -418,9 +418,12 @@ function createUnraidDeploymentMethods({
|
||||
|
||||
const generated = profile.generatedCompose ? this.generatedCompose(profile, repository) : "";
|
||||
const iconReference = await this.prepareIcon(profile, repository, server);
|
||||
const deploymentRepositoryUrl = mode === "server-git"
|
||||
? this.serverGitRemote(repository, profile)
|
||||
: repository.localStatus?.remoteUrl || repository.sshUrl || repository.cloneUrl || repository.htmlUrl || repository.fullName;
|
||||
const metadata = this.metadataCompose(profile, repository, iconReference, {
|
||||
sha: targetSha,
|
||||
repositoryUrl: profile.cloneUrl || repository.sshUrl || repository.cloneUrl || repository.htmlUrl || repository.fullName,
|
||||
repositoryUrl: deploymentRepositoryUrl,
|
||||
});
|
||||
const previousState = this.store.getDeploymentState?.(profileId) || null;
|
||||
|
||||
@@ -518,9 +521,12 @@ function createUnraidDeploymentMethods({
|
||||
});
|
||||
const generated = profile.generatedCompose ? this.generatedCompose(profile, repository) : "";
|
||||
const iconReference = await this.prepareIcon(profile, repository, server);
|
||||
const rollbackRepositoryUrl = rollbackMode === "server-git"
|
||||
? this.serverGitRemote(repository, profile)
|
||||
: repository.localStatus?.remoteUrl || repository.sshUrl || repository.cloneUrl || repository.htmlUrl || repository.fullName;
|
||||
const metadata = this.metadataCompose(profile, repository, iconReference, {
|
||||
sha: target,
|
||||
repositoryUrl: profile.cloneUrl || repository.sshUrl || repository.cloneUrl || repository.htmlUrl || repository.fullName,
|
||||
repositoryUrl: rollbackRepositoryUrl,
|
||||
});
|
||||
try {
|
||||
const mode = rollbackMode;
|
||||
|
||||
Reference in New Issue
Block a user