fix(seed): an untouched draft no longer holds a workflow back from the new template
ci / check (push) Successful in 1m44s
ci / publish (push) Successful in 1m28s

jellyfin stayed on the first template because it had an unedited copy of v1 as a draft.
A draft the owner changed still keeps the workflow where it is. No new dependencies.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
homelab-updaterandClaude Sonnet 5.5 committed 2026-10-23 13:51:33 +02:00
1 parent 77a7027065
commit 3dfac076ee
3 files changed
+35 -6

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ Aplikacja wygodna na co dzień: stan „tu i teraz”, historia wersji, cisza no
- Logowanie Claude'a z UI (`8ea9f88`, wdrożone 23.10): `#/settings` → „Anthropic account” (status konta, „Sign in” / „Sign in with another account” → link + wklejony kod, „Sign out”); sidecar `model-claude` ma `/auth/*`, serwer WWW proxuje `/api/model-auth*`.
- Orcaslicer 22.10 (run `feeb48d6`): `verify_initial` nie doczekał się przebudowy kontenera (140 s, `image_mismatch`), a `repair` padł na wygasłym OAuth Claude'a (`auth_failed`, 0 USD), więc run czekał na właściciela mimo udanej aktualizacji. Poprawka: gdy `repair`/`repair_full` się poddaje, workflow robi jeszcze jeden `verify` (`verify_after_gave_up`; zdrowa appka → `terminal_updated`, inaczej bramka jak dotąd); czas `verify` bez zmian. Szablon ma nowy digest.
- Jellyfin został na v1 (35 węzłów), bo miał nietknięty szkic (kopia v1), a seed pomijał workflow ze szkicem; teraz nietknięty szkic (digest = ostatnia wersja) nie blokuje przeniesienia na szablon, edytowany nadal tak.
- Odczyt działającej appki: `tools/vm-api.sh /api/...` (GET przez SSH na VM103, bez Traefika i Keycloaka), zob. AGENTS.md.
- H1–H6 na `master` i na VM103: migawka hosta, historia wersji, raport i cisza nocna, decyzje z telefonu, PWA, `Ctrl+K`, logowanie Keycloak.
+21
View File
@@ -136,6 +136,27 @@ describe('ensureAppWorkflows', () => {
expect(again.workflows).toEqual([]);
});
it('moves a workflow whose draft is an untouched copy of its version, and leaves one with an edited draft', async () => {
const { deps, workflowCatalog } = setup();
await ensureAppWorkflows(deps, ['jackett', 'radarr']);
for (const app of ['jackett', 'radarr']) {
const cloned = workflowCatalog.clonePublishedVersion({ sourceWorkflowId: `update-${app}`, sourceVersion: 1, workflowId: `update-${app}`, name: `Update ${app}`, now });
if (!cloned.ok) throw new Error(cloned.reason);
}
// radarr's draft is being edited by the owner.
const edited = { ...COMPONENT_UPDATE_WORKFLOW_DOCUMENT_V2, nodes: [...COMPONENT_UPDATE_WORKFLOW_DOCUMENT_V2.nodes].reverse() };
expect(workflowCatalog.saveDraft({ workflowId: 'update-radarr', expectedRevision: 1, document: edited }).ok).toBe(true);
const next = { ...COMPONENT_UPDATE_WORKFLOW_DOCUMENT_V2, nodes: [...COMPONENT_UPDATE_WORKFLOW_DOCUMENT_V2.nodes.slice(1), COMPONENT_UPDATE_WORKFLOW_DOCUMENT_V2.nodes[0]!] };
const moved = await ensureAppWorkflows({ ...deps, template: { document: next, digests: [...COMPONENT_UPDATE_TEMPLATE_DIGESTS, workflowDocumentDigest(next)] } }, ['jackett', 'radarr']);
expect(moved).toEqual({ functions: [], workflows: ['update-jackett'], edited: [], failures: [] });
expect(workflowCatalog.listPublishedVersions?.('update-jackett')).toHaveLength(2);
expect(workflowCatalog.getDraft('update-jackett')).toBeNull();
expect(workflowCatalog.listPublishedVersions?.('update-radarr')).toHaveLength(1);
expect(workflowCatalog.getDraft('update-radarr')).not.toBeNull();
});
it('reports a function whose cases fail and skips the workflows that need it', async () => {
const failingRuntime: FunctionTestExecutor = {
execute: () => Promise.resolve({ event: 'error', summary: 'bwrap: sandbox unavailable', data: {} }),
+13 -6
View File
@@ -114,8 +114,11 @@ export async function ensureAppWorkflows(deps: WorkflowSeedDeps, apps: readonly
edited.push(workflowId);
continue;
}
if (!functionsReady || deps.workflowCatalog.getDraft(workflowId) !== null) continue;
const reason = upgradeWorkflow(deps, workflowId, app, latest!.version, template.document);
if (!functionsReady) continue;
// A draft the owner is editing stays; an untouched copy of the version (opened and left) must not hold the workflow back.
const draft = deps.workflowCatalog.getDraft(workflowId);
if (draft !== null && workflowDocumentDigest(draft.document) !== digest) continue;
const reason = upgradeWorkflow(deps, workflowId, app, latest!.version, template.document, draft?.revision ?? null);
if (reason === null) workflows.push(workflowId);
else failures.push({ id: workflowId, reason });
continue;
@@ -132,14 +135,18 @@ export async function ensureAppWorkflows(deps: WorkflowSeedDeps, apps: readonly
}
/** Publishes the current template as the next version of an app's workflow that is still on an older template. */
function upgradeWorkflow(deps: WorkflowSeedDeps, workflowId: string, app: string, latestVersion: number, document: WorkflowDocumentV2): string | null {
function upgradeWorkflow(deps: WorkflowSeedDeps, workflowId: string, app: string, latestVersion: number, document: WorkflowDocumentV2, draftRevision: number | null): string | null {
const workflowApp = createWorkflowCatalogApp(
deps.workflowCatalog,
createWorkflowGraphValidator({ workflowId, version: latestVersion + 1 }, [], deps.extensions),
);
const cloned = workflowApp.clonePublishedVersion({ sourceWorkflowId: workflowId, sourceVersion: latestVersion, workflowId, name: `Update ${app}`, now: deps.now() });
if (!cloned.ok) return `draft: ${cloned.reason}`;
const saved = workflowApp.saveDraft({ workflowId, expectedRevision: cloned.draft.revision, document });
let revision = draftRevision;
if (revision === null) {
const cloned = workflowApp.clonePublishedVersion({ sourceWorkflowId: workflowId, sourceVersion: latestVersion, workflowId, name: `Update ${app}`, now: deps.now() });
if (!cloned.ok) return `draft: ${cloned.reason}`;
revision = cloned.draft.revision;
}
const saved = workflowApp.saveDraft({ workflowId, expectedRevision: revision, document });
if (!saved.ok) return `draft: ${saved.reason}`;
return validateAndPublish(deps, workflowApp, workflowId, saved.draft.revision);
}