Centralize automation run identity fallback

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-13 02:34:22 -07:00
co-authored by Orca
parent 72883c8fbc
commit 5d5c258832
8 changed files with 91 additions and 10 deletions
@@ -295,5 +295,6 @@ Done means: reviewers and future agents can understand the vision, implementatio
- [x] 2026-06-13: Closed another request/cache ownership slice by routing right-sidebar search, create-review field generation, browser tab default profile selection, Smart Workspace GitHub/GitLab/Linear lookups, GitHub Project browse cache, Project slug comments, and standalone GitHub issue comments through explicit worktree/repo/source ownership instead of ambient focused runtime state. Verified with focused ownership/browser/smart-submit tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [x] 2026-06-13: Routed Quick Open file listing through the selected worktree runtime owner instead of ambient focused settings, matching File Explorer and sidebar search ownership. Verified with focused Quick Open and file-owner boundary tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [x] 2026-06-13: Routed typed GitHub issue/PR lookup entry points in Cmd-J, Smart Workspace cross-repo accept, and Composer submit fallback through source-aware local/SSH/runtime lookup helpers instead of direct preload calls. Verified with focused composer/palette/smart-submit tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [x] 2026-06-13: Centralized Automation run identity so renderer dispatch, Automations UI, main-process run-target resolution, persistence backfill, and CLI formatting resolve `runRepoId` through explicit `runContext` with a named `legacyRepoId` fallback instead of each caller reading the persisted compatibility field directly. The checklist row remains partial because `Automation.projectId` still exists for storage compatibility. Verified with shared identity, automation run-context, automation service, and CLI format tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [~] 2026-06-13: Remaining source-error gap: unavailable hosts, version-skewed runtimes, and GitHub/GitLab missing auth/tooling are surfaced for desktop-owned and runtime-owned source hosts, but unsupported-provider still has no production emitter per source host.
- [~] 2026-06-13: Remaining Automation gap: run-host/source-host availability and missing project-host setup are handled for manual/internal/external actions, but `Automation.projectId` remains the legacy repo-id compatibility field.
+3 -2
View File
@@ -1,4 +1,5 @@
import type { Automation, AutomationRun } from '../shared/automations-types'
import { getAutomationLegacyRepoId } from '../shared/automation-run-identity'
import { formatAutomationPrecheckTimeout } from '../shared/automation-precheck'
import { formatAutomationSchedule } from '../shared/automation-schedules'
import type { PublicKnownRuntimeEnvironment } from '../shared/runtime-environments'
@@ -186,9 +187,9 @@ export function formatAutomationShow(result: { automation: Automation }): string
`projectHostSetupId: ${runContext.projectHostSetupId}`,
`runRepoId: ${runContext.repoId}`,
`runPath: ${runContext.path}`,
`legacyRepoId: ${automation.projectId}`
`legacyRepoId: ${getAutomationLegacyRepoId(automation)}`
]
: [`legacyRepoId: ${automation.projectId}`]
: [`legacyRepoId: ${getAutomationLegacyRepoId(automation)}`]
return [
`id: ${automation.id}`,
`name: ${automation.name}`,
@@ -1,5 +1,6 @@
import type { Store } from '../persistence'
import type { Automation } from '../../shared/automations-types'
import { getAutomationLegacyRepoId } from '../../shared/automation-run-identity'
import { getRepoExecutionHostId, parseExecutionHostId } from '../../shared/execution-host'
import type { ProjectHostSetup, Repo } from '../../shared/types'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
@@ -15,7 +16,7 @@ function getLegacyPrecheckCwd(store: Store, automation: Automation): string | nu
: null
return parsed?.worktreePath ?? null
}
return store.getRepo(automation.projectId)?.path ?? null
return store.getRepo(getAutomationLegacyRepoId(automation))?.path ?? null
}
export function resolveAutomationRunTarget(
@@ -24,7 +25,7 @@ export function resolveAutomationRunTarget(
): AutomationRunTargetResult {
const context = automation.runContext ?? null
if (!context) {
const repo = store.getRepo(automation.projectId)
const repo = store.getRepo(getAutomationLegacyRepoId(automation))
const cwd = getLegacyPrecheckCwd(store, automation)
if (!repo || !cwd) {
return { ok: false, error: 'Automation run target is no longer available.' }
+2 -1
View File
@@ -32,6 +32,7 @@ import {
latestAutomationOccurrenceAtOrBefore,
nextAutomationOccurrenceAfter
} from '../shared/automation-schedules'
import { getAutomationLegacyRepoId } from '../shared/automation-run-identity'
import { normalizeAutomationPrecheck } from '../shared/automation-precheck'
import type {
PersistedState,
@@ -682,7 +683,7 @@ function backfillLegacyAutomationContexts(
const contextsByAutomationId = new Map<string, Pick<Automation, 'runContext' | 'sourceContext'>>()
const automations = (state.automations ?? []).map((automation) => {
const contexts = getAutomationContextsForRepo(
state.repos.find((repo) => repo.id === automation.projectId),
state.repos.find((repo) => repo.id === getAutomationLegacyRepoId(automation)),
state.projectHostSetups ?? []
)
const next: Automation = { ...automation }
@@ -53,6 +53,7 @@ import type {
AutomationRun,
AutomationUpdateInput
} from '../../../../shared/automations-types'
import { getAutomationRunRepoId } from '../../../../shared/automation-run-identity'
import { parseExecutionHostId } from '../../../../shared/execution-host'
import { TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import type { PreflightStatus } from '../../../../preload/api-types'
@@ -142,10 +143,6 @@ function getDefaultWorktree(worktrees: readonly Worktree[]): Worktree | null {
return worktrees.find((worktree) => worktree.isMainWorktree) ?? worktrees[0] ?? null
}
function getAutomationRunRepoId(automation: Automation): string {
return automation.runContext?.repoId ?? automation.projectId
}
function getRepoBackedAutomationSourceContext(
automation: Automation
): RepoBackedAutomationSourceContext | null {
@@ -11,6 +11,7 @@ import type {
AutomationDispatchResult,
AutomationPrecheckResult
} from '../../../shared/automations-types'
import { getAutomationRunRepoId } from '../../../shared/automation-run-identity'
import {
didAutomationPrecheckPass,
formatAutomationPrecheckFailure
@@ -57,7 +58,7 @@ export function useAutomationDispatchEvents(): void {
activeTabId: state.activeTabId,
activeTabType: state.activeTabType
}
const runRepoId = automation.runContext?.repoId ?? automation.projectId
const runRepoId = getAutomationRunRepoId(automation)
const repo = state.repos.find((entry) => entry.id === runRepoId)
const automationWorktree = automation.workspaceId
? state.allWorktrees().find((entry) => entry.id === automation.workspaceId)
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest'
import type { Automation } from './automations-types'
import {
getAutomationLegacyRepoId,
getAutomationRunProjectId,
getAutomationRunRepoId
} from './automation-run-identity'
function automation(overrides: Partial<Automation> = {}): Automation {
return {
id: 'auto-1',
name: 'Automation',
prompt: 'Run this',
precheck: null,
agentId: 'claude',
runContext: null,
sourceContext: null,
projectId: 'legacy-repo',
executionTargetType: 'local',
executionTargetId: 'local',
schedulerOwner: 'local_host_service',
workspaceMode: 'new_per_run',
workspaceId: null,
baseBranch: null,
reuseSession: false,
timezone: 'UTC',
rrule: 'FREQ=DAILY',
dtstart: 1,
enabled: true,
nextRunAt: 1,
missedRunPolicy: 'run_once_within_grace',
missedRunGraceMinutes: 720,
createdAt: 1,
updatedAt: 1,
...overrides
}
}
describe('automation run identity', () => {
it('uses explicit run context identity when present', () => {
const value = automation({
runContext: {
kind: 'workspace-run',
projectId: 'github:stablyai/orca',
hostId: 'ssh:builder',
projectHostSetupId: 'setup-builder',
repoId: 'remote-repo',
path: '/remote/orca'
}
})
expect(getAutomationLegacyRepoId(value)).toBe('legacy-repo')
expect(getAutomationRunRepoId(value)).toBe('remote-repo')
expect(getAutomationRunProjectId(value)).toBe('github:stablyai/orca')
})
it('falls back to the legacy repo id for pre-host-context automations', () => {
const value = automation()
expect(getAutomationLegacyRepoId(value)).toBe('legacy-repo')
expect(getAutomationRunRepoId(value)).toBe('legacy-repo')
expect(getAutomationRunProjectId(value)).toBe('legacy-repo')
})
})
+15
View File
@@ -0,0 +1,15 @@
import type { Automation } from './automations-types'
type AutomationRunIdentityFields = Pick<Automation, 'projectId' | 'runContext'>
export function getAutomationLegacyRepoId(automation: Pick<Automation, 'projectId'>): string {
return automation.projectId
}
export function getAutomationRunRepoId(automation: AutomationRunIdentityFields): string {
return automation.runContext?.repoId ?? getAutomationLegacyRepoId(automation)
}
export function getAutomationRunProjectId(automation: AutomationRunIdentityFields): string {
return automation.runContext?.projectId ?? getAutomationLegacyRepoId(automation)
}