From f988231e3bc6120ccbc8d74a093b8a40bb67e420 Mon Sep 17 00:00:00 2001 From: Saul Moro Date: Mon, 19 Oct 2026 08:02:07 +0200 Subject: [PATCH 1/4] test(workflow): keep the team revision across an unrelated unpushed commit --- src/__tests__/workflow-cache.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/__tests__/workflow-cache.test.ts b/src/__tests__/workflow-cache.test.ts index 028337617..f6b2a2b46 100644 --- a/src/__tests__/workflow-cache.test.ts +++ b/src/__tests__/workflow-cache.test.ts @@ -128,6 +128,21 @@ describe('workflow cache', () => { expect((await loadCachedWorkflow(config, 'build'))?.teamRevision).toBe(localRevision); }); + it('keeps a verified revision when an unrelated unpushed commit moves HEAD', async () => { + const config = testConfig(); + const repository = initializeTeamRepository(config); + writeWorkflow(config, 'build', VALID); + const pushed = repository.commit('add workflow'); + repository.push(); + await syncWorkflowDefinitions(config); + + fs.writeFileSync(path.join(config.repo.localPath, 'README.md'), 'unrelated\n'); + repository.commit('unrelated local commit'); + await syncWorkflowDefinitions(config); + + expect((await loadCachedWorkflow(config, 'build'))?.teamRevision).toBe(pushed); + }); + it('omits the team revision for an untracked workflow at a remote-reachable commit', async () => { const config = testConfig(); const repository = initializeTeamRepository(config); From 80cd1ce68f509b83f38a4e9a2a3bace8928de7fe Mon Sep 17 00:00:00 2001 From: Saul Moro Date: Mon, 19 Oct 2026 08:04:27 +0200 Subject: [PATCH 2/4] fix(workflow): keep the team revision of unchanged workflow bytes --- src/workflow/cache.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/workflow/cache.ts b/src/workflow/cache.ts index 78c828840..a0e89b500 100644 --- a/src/workflow/cache.ts +++ b/src/workflow/cache.ts @@ -15,7 +15,7 @@ import { formatDiagnostic, parseWorkflow, promptFileDiagnostics, stepField } fro import type { WorkflowFiles } from './parse-context.js'; import type { CachedWorkflow, WorkflowDefinition, WorkflowDiagnostic, WorkflowStep } from './types.js'; import { assertSafePath, assertWithinRoot } from '../utils/path-safety.js'; -import { createGit } from '../utils/git.js'; +import { createGit, getHeadCommit } from '../utils/git.js'; export interface SyncWorkflowOptions { dryRun?: boolean; @@ -136,11 +136,10 @@ async function isTeamRemoteRevision(root: string, revision: string): Promise { try { - const git = createGit(root); - const revision = (await git.revparse(['HEAD'])).trim(); - if (!await isTeamRemoteRevision(root, revision)) return undefined; + const revision = await getHeadCommit(root); + if (!revision || !await isTeamRemoteRevision(root, revision)) return undefined; return { - repositoryRoot: await fs.realpath((await git.revparse(['--show-toplevel'])).trim()), + repositoryRoot: await fs.realpath((await createGit(root).revparse(['--show-toplevel'])).trim()), revision, }; } catch { @@ -269,13 +268,15 @@ export async function syncWorkflowDefinitions( reject(file, problems); continue; } - const previous = await readCache(config, file.definition.name); + const previous = kept.get(file.stem); result.cached.push(file.definition.name); - if (options.dryRun || (previous?.sourceHash === file.sourceHash && previous.teamRevision === file.teamRevision)) continue; + // Equal bytes are still stored at the kept revision, which the team remote still reaches. + const teamRevision = file.teamRevision ?? (previous?.sourceHash === file.sourceHash ? previous.teamRevision : undefined); + if (options.dryRun || (previous?.sourceHash === file.sourceHash && previous.teamRevision === teamRevision)) continue; await writeJsonAtomic(cachePath(config, file.definition.name), { relativePath: file.relativePath, sourceHash: file.sourceHash, - ...(file.teamRevision ? { teamRevision: file.teamRevision } : {}), + ...(teamRevision ? { teamRevision } : {}), content: file.content, definition: file.definition, validatedAt: new Date().toISOString(), From 6a8eeb6054ca6e43be8ea90d255d5254ff4567a6 Mon Sep 17 00:00:00 2001 From: Saul Moro Date: Mon, 19 Oct 2026 08:04:28 +0200 Subject: [PATCH 3/4] test(workflow): check the team revision in a fresh clone and on a kept run --- ...rkflow-records-reports-branch-1083.test.ts | 6 ++++-- src/__tests__/workflow-cache.test.ts | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/__tests__/e2e/workflow-records-reports-branch-1083.test.ts b/src/__tests__/e2e/workflow-records-reports-branch-1083.test.ts index 5aa3648f6..b1ec8995e 100644 --- a/src/__tests__/e2e/workflow-records-reports-branch-1083.test.ts +++ b/src/__tests__/e2e/workflow-records-reports-branch-1083.test.ts @@ -113,8 +113,10 @@ describe('#1083 workflow run records reach the reports branch', () => { expect(startRecord.username).toBe('alice'); expect(startRecord.sourceHash).toBe(sourceHash); expect(startRecord.teamRevision).toBe(teamRevision); - expect(() => git(repo, 'cat-file', '-e', teamRevision)).not.toThrow(); - expect(git(repo, 'show', `${teamRevision}:workflows/build.yaml`)).toBe(TWO_STEPS); + git(sandbox, 'clone', 'origin.git', 'fresh'); + const fresh = path.join(sandbox, 'fresh'); + expect(() => git(fresh, 'cat-file', '-e', teamRevision)).not.toThrow(); + expect(git(fresh, 'show', `${teamRevision}:workflows/build.yaml`)).toBe(TWO_STEPS); const done = await runLocalDriver(config, runId, work, { tools: { default: 'claude' }, diff --git a/src/__tests__/workflow-cache.test.ts b/src/__tests__/workflow-cache.test.ts index f6b2a2b46..1aae7b678 100644 --- a/src/__tests__/workflow-cache.test.ts +++ b/src/__tests__/workflow-cache.test.ts @@ -263,6 +263,26 @@ steps: expect(mixedRun.teamRevision).toBeUndefined(); }); + it('records the earlier team revision on a run of a kept workflow', async () => { + const config = testConfig(); + const repository = initializeTeamRepository(config); + writeWorkflow(config, 'build', VALID); + const pushed = repository.commit('add workflow'); + repository.push(); + await syncWorkflowDefinitions(config); + + writeWorkflow(config, 'build', 'schemaVersion: [\n'); + repository.commit('break workflow'); + repository.push(); + expect((await syncWorkflowDefinitions(config)).kept).toEqual(['build']); + + const started = await startRun(config, 'build'); + if ('error' in started) throw new Error(started.error.message); + const run = await readRun(config, String(started.data.run)); + if (typeof run === 'string') throw new Error(`Could not read run: ${run}`); + expect(run.teamRevision).toBe(pushed); + }); + it('clears a kept workflow revision that the team remote no longer reaches', async () => { const config = testConfig(); const repository = initializeTeamRepository(config); From f6fddb7838280d25b1d5f7b489548037f105a252 Mon Sep 17 00:00:00 2001 From: Saul Moro Date: Mon, 19 Oct 2026 08:16:51 +0200 Subject: [PATCH 4/4] test(workflow): drop an unchanged revision that origin no longer reaches --- src/__tests__/workflow-cache.test.ts | 25 +++++++++++++++++++++++++ src/workflow/cache.ts | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/__tests__/workflow-cache.test.ts b/src/__tests__/workflow-cache.test.ts index 1aae7b678..4396dd61d 100644 --- a/src/__tests__/workflow-cache.test.ts +++ b/src/__tests__/workflow-cache.test.ts @@ -141,6 +141,31 @@ describe('workflow cache', () => { await syncWorkflowDefinitions(config); expect((await loadCachedWorkflow(config, 'build'))?.teamRevision).toBe(pushed); + const started = await startRun(config, 'build'); + if ('error' in started) throw new Error(started.error.message); + const run = await readRun(config, String(started.data.run)); + if (typeof run === 'string') throw new Error(`Could not read run: ${run}`); + expect(run.teamRevision).toBe(pushed); + }); + + it('drops an unchanged workflow revision that the team remote no longer reaches', async () => { + const config = testConfig(); + const repository = initializeTeamRepository(config); + writeWorkflow(config, 'build', VALID); + const pushed = repository.commit('add workflow'); + repository.push(); + await syncWorkflowDefinitions(config); + expect((await loadCachedWorkflow(config, 'build'))?.teamRevision).toBe(pushed); + + const tree = git(config.repo.localPath, 'write-tree'); + const rewritten = git(config.repo.localPath, 'commit-tree', tree, '-m', 'rewrite team history'); + git(config.repo.localPath, 'checkout', '-B', 'main', rewritten); + git(config.repo.localPath, 'push', '--force', '--set-upstream', 'origin', 'main'); + fs.writeFileSync(path.join(config.repo.localPath, 'README.md'), 'unrelated\n'); + repository.commit('unrelated local commit'); + await syncWorkflowDefinitions(config); + + expect((await loadCachedWorkflow(config, 'build'))?.teamRevision).toBeUndefined(); }); it('omits the team revision for an untracked workflow at a remote-reachable commit', async () => { diff --git a/src/workflow/cache.ts b/src/workflow/cache.ts index a0e89b500..07dd02208 100644 --- a/src/workflow/cache.ts +++ b/src/workflow/cache.ts @@ -270,7 +270,7 @@ export async function syncWorkflowDefinitions( } const previous = kept.get(file.stem); result.cached.push(file.definition.name); - // Equal bytes are still stored at the kept revision, which the team remote still reaches. + // Equal bytes are still stored at the earlier entry's revision; the loop that filled `kept` dropped it if origin no longer reaches it. const teamRevision = file.teamRevision ?? (previous?.sourceHash === file.sourceHash ? previous.teamRevision : undefined); if (options.dryRun || (previous?.sourceHash === file.sourceHash && previous.teamRevision === teamRevision)) continue; await writeJsonAtomic(cachePath(config, file.definition.name), {