Release ForgeFlow 0.6.0

This commit is contained in:
NuklearRabbit
2026-07-25 05:59:07 +02:00
parent cf1f67a823
commit 9d3933c878
48 changed files with 2208 additions and 466 deletions
+59 -5
View File
@@ -64,7 +64,32 @@ function registerIpc({ store, git, gitea, repositories, deployments, unraid, ssh
const withRepositoryMutation = async (localPath, action) => {
const key = path.resolve(localPath);
const previous = repositoryMutations.get(key) || Promise.resolve();
const current = previous.catch(() => {}).then(() => withRepositoryPause(key, action));
const execute = async () => {
try { return await withRepositoryPause(key, action); }
catch (error) {
if (!git.isGitLockError(error)) throw error;
let repair = null;
let lockDiagnosis = null;
try {
repair = await git.repairStaleGitLocks(key, { minimumAgeMs: 2_000 });
} catch (repairError) {
lockDiagnosis = repairError;
if (repairError?.code === 'GIT_LOCKS_RECENT') {
await new Promise((resolve) => setTimeout(resolve, 2_500));
try {
repair = await git.repairStaleGitLocks(key, { minimumAgeMs: 2_000 });
lockDiagnosis = null;
} catch (retryError) {
lockDiagnosis = retryError;
}
}
}
if (!repair?.repaired) throw lockDiagnosis || error;
await diagnostics.info('git.lock.auto-repaired', { localPath: key, locks: repair.removed.map((item) => item.name) });
return withRepositoryPause(key, action);
}
};
const current = previous.catch(() => {}).then(execute);
repositoryMutations.set(key, current);
try { return await current; }
finally { if (repositoryMutations.get(key) === current) repositoryMutations.delete(key); }
@@ -139,7 +164,8 @@ function registerIpc({ store, git, gitea, repositories, deployments, unraid, ssh
platform: process.platform,
state: store.getPublicState(),
git: await git.isAvailable(),
diagnostics: await diagnostics.getStatus()
diagnostics: await diagnostics.getStatus(),
updateResult: await updates.consumeLatestResult()
}));
register('dialog:select-directory', async ({ title = 'Select folder', defaultPath }) => {
@@ -156,6 +182,16 @@ function registerIpc({ store, git, gitea, repositories, deployments, unraid, ssh
return result.canceled ? null : result.filePaths[0];
});
register('dialog:select-image-file', async ({ title = 'Select PNG image', defaultPath }) => {
const result = await dialog.showOpenDialog({
title,
defaultPath,
properties: ['openFile'],
filters: [{ name: 'PNG image', extensions: ['png'] }]
});
return result.canceled ? null : result.filePaths[0];
});
register('setup:preflight', ({ baseUrl, token, roots }) => preflight.runSystem({ baseUrl, token, roots }));
register('setup:validate-gitea', ({ baseUrl, token }) => gitea.validateConnection(baseUrl, token));
register('setup:complete', async ({ baseUrl, token, workspaceRoots }) => {
@@ -201,7 +237,8 @@ function registerIpc({ store, git, gitea, repositories, deployments, unraid, ssh
register('updates:download', () => updates.download());
register('updates:apply', async () => {
const result = await updates.apply();
setTimeout(() => app.quit(), 650).unref?.();
if (!result?.confirmed) throw new Error('The update helper did not confirm ownership of the update. ForgeFlow will remain open.');
setTimeout(() => app.quit(), 350).unref?.();
return result;
});
@@ -286,7 +323,11 @@ function registerIpc({ store, git, gitea, repositories, deployments, unraid, ssh
register('repository:stash-list', async ({ localPath }) => git.stashList(await assertKnownRepositoryPath(localPath)));
register('repository:stash-pop', async ({ localPath, ref }) => { const safePath = await assertKnownRepositoryPath(localPath); return withRepositoryMutation(safePath, () => git.popStash(safePath, ref)); });
register('repository:index-lock', async ({ localPath }) => git.getIndexLockInfo(await assertKnownRepositoryPath(localPath)));
register('repository:git-recovery-status', async ({ localPath }) => git.reconcile(await assertKnownRepositoryPath(localPath)));
register('repository:repair-index-lock', async ({ localPath }) => { const safePath = await assertKnownRepositoryPath(localPath); return withRepositoryMutation(safePath, () => git.removeStaleIndexLock(safePath)); });
register('repository:repair-git-locks', async ({ localPath, force = false }) => { const safePath = await assertKnownRepositoryPath(localPath); return withRepositoryMutation(safePath, () => git.repairStaleGitLocks(safePath, { minimumAgeMs: force ? 0 : 10_000, allowWithoutProcessProbe: force === true })); });
register('repository:reconcile', async ({ localPath }) => git.reconcile(await assertKnownRepositoryPath(localPath)));
register('repository:repair-sync', async ({ localPath, strategy }) => { const safePath = await assertKnownRepositoryPath(localPath); return withRepositoryMutation(safePath, () => git.repairSync(safePath, strategy)); });
register('repository:set-origin', async ({ localPath, remoteUrl }) => { const safePath = await assertKnownRepositoryPath(localPath); return withRepositoryMutation(safePath, () => git.setRemoteUrl(safePath, remoteUrl)); });
register('repositories:normalize-origins', async () => {
@@ -371,13 +412,26 @@ function registerIpc({ store, git, gitea, repositories, deployments, unraid, ssh
if (profile?.provider === 'ssh-unraid') return unraid.refreshProfileState(fullName, profileId);
return deployments.refreshProfileState(fullName, profileId);
});
register('deployment:apply-dockerman-metadata', async ({ repository, profileId }) => {
const current = await resolveRepository(repository);
return unraid.applyDockerManMetadata({ repository: current, profileId });
});
register('deployment:reconcile', async ({ fullName, profileId }) => {
const profile = store.getDeploymentProfile(fullName, profileId);
if (profile?.provider !== 'ssh-unraid') return deployments.refreshProfileState(fullName, profileId);
const state = await unraid.refreshProfileState(fullName, profileId);
const operations = store.data.operations.filter((item) => item.profileId === profileId && item.provider === 'ssh-unraid' && !['success', 'failed', 'cancelled', 'rolled-back'].includes(item.status));
for (const operation of operations) await unraid.refreshOperation(operation.id);
return { state, operations: store.data.operations.filter((item) => item.profileId === profileId).slice(0, 10) };
});
register('operations:refresh', async ({ operationId }) => {
if (operationId) {
const operation = store.getOperation(operationId);
if (operation?.provider === 'ssh-unraid') return operation;
if (operation?.provider === 'ssh-unraid') return unraid.refreshOperation(operationId);
return deployments.refreshOperation(operationId);
}
return deployments.refreshActiveOperations();
const [actions, sshOperations] = await Promise.all([deployments.refreshActiveOperations(), unraid.refreshActiveOperations()]);
return [...actions, ...sshOperations];
});
register('operations:get', ({ operationId }) => store.getOperation(operationId));