mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 08:03:09 +00:00
Validate automation source availability
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -76,7 +76,7 @@ Done means: starting work from an external item carries the item’s source iden
|
||||
- [x] Update automation dispatch/precheck code to validate selected run host availability and project host setup.
|
||||
- [x] Verify automation live lookups do not use ambient settings: current Orca automations do not fetch issue/PR/task lists during dispatch, and branch/base-ref lookup routes through the selected repo host.
|
||||
- [x] Update session reuse logic so an automation never resumes on the wrong host.
|
||||
- [~] Add disabled states for disconnected run host, missing project host setup, missing source auth, unavailable source tool, and unsupported provider.
|
||||
- [x] Add disabled states for disconnected run host, missing project host setup, missing source auth, unavailable source tool, and unsupported provider.
|
||||
- [x] Add migration behavior for old automations that only know a repo id/path.
|
||||
- [x] Update CLI/RPC automation APIs with v2 selectors for logical project, source context, and run host.
|
||||
|
||||
@@ -291,5 +291,6 @@ Done means: reviewers and future agents can understand the vision, implementatio
|
||||
- [x] 2026-06-13: Routed agent draft-paste readiness probes and paste writes through the tab's worktree runtime owner when available, covering legacy remote PTY ids that do not embed their runtime owner. Verified with focused agent-paste tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
|
||||
- [x] 2026-06-13: Routed File Explorer directory reads, stat/import/create/move/duplicate operations, ignored-file checks, and background git/conflict polling through the selected worktree runtime owner instead of ambient focused settings. Verified with focused right-sidebar boundary/projection/polling/drag tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
|
||||
- [x] 2026-06-13: Fed runtime-owned GitHub/GitLab preflight into Tasks source availability by checking each available, task-source-capable runtime host with `preflight.check` and applying that host's `gh`/`glab` auth/tool status to only its own task sources. Verified with provider availability tests, TaskPage source-boundary tests, source-summary tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
|
||||
- [x] 2026-06-13: Fed saved automation source contexts into Run Now availability so internal Orca automations now disable before dispatch when the saved source host is unavailable, still checking capabilities, missing task-source support, missing GitHub/GitLab auth, or missing provider tooling. Verified with focused automation availability tests, provider availability 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 availability and missing project-host setup are handled for manual/internal/external actions, but `Automation.projectId` remains the legacy repo-id compatibility field and internal Orca automations do not yet validate saved `sourceContext` for missing provider auth/tooling or unsupported provider before dispatch.
|
||||
- [~] 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.
|
||||
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useAppStore } from '@/store'
|
||||
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
|
||||
import { getLocalPreflightContext, localPreflightContextKey } from '@/lib/local-preflight-context'
|
||||
import { cn } from '@/lib/utils'
|
||||
import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel'
|
||||
import { getAgentCatalog } from '@/lib/agent-catalog'
|
||||
@@ -51,6 +53,11 @@ import type {
|
||||
AutomationRun,
|
||||
AutomationUpdateInput
|
||||
} from '../../../../shared/automations-types'
|
||||
import { parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
|
||||
import type { PreflightStatus } from '../../../../preload/api-types'
|
||||
import type { RuntimeStatus } from '../../../../shared/runtime-types'
|
||||
import type { TaskSourceContext } from '../../../../shared/task-source-context'
|
||||
import type { Worktree } from '../../../../shared/types'
|
||||
import { getWorktreePathBasenameFromId } from '../../../../shared/worktree-id'
|
||||
import {
|
||||
@@ -90,6 +97,11 @@ import { AutomationRunHistory } from './AutomationRunHistory'
|
||||
import { getAutomationTemplates, type AutomationTemplate } from './automation-templates'
|
||||
import { getAutomationTargetAvailability } from './automation-target-availability'
|
||||
import { buildAutomationRunContextForRepo } from './automation-run-context'
|
||||
import {
|
||||
getRepoBackedProviderAvailability,
|
||||
type RuntimeProviderPreflightStatus
|
||||
} from '../task-source-provider-availability'
|
||||
import type { TaskSourceHostAvailability } from '../task-source-context-summary'
|
||||
import {
|
||||
getExternalAutomationActionDisabledMessage,
|
||||
getExternalAutomationSourceAvailability,
|
||||
@@ -105,6 +117,7 @@ const AGENTS = getAgentCatalog().map((agent) => agent.id)
|
||||
const DEFAULT_TIME = '09:00'
|
||||
const AUTOMATIONS_CHANGED_EVENT = 'orca:automations-changed'
|
||||
type AutomationPaneTab = 'overview' | 'runs'
|
||||
type RepoBackedAutomationSourceContext = TaskSourceContext & { provider: 'github' | 'gitlab' }
|
||||
|
||||
type ExternalAutomationListEntry =
|
||||
| {
|
||||
@@ -133,6 +146,46 @@ function getAutomationRunRepoId(automation: Automation): string {
|
||||
return automation.runContext?.repoId ?? automation.projectId
|
||||
}
|
||||
|
||||
function getRepoBackedAutomationSourceContext(
|
||||
automation: Automation
|
||||
): RepoBackedAutomationSourceContext | null {
|
||||
const context = automation.sourceContext
|
||||
return context?.provider === 'github' || context?.provider === 'gitlab'
|
||||
? (context as RepoBackedAutomationSourceContext)
|
||||
: null
|
||||
}
|
||||
|
||||
function getRuntimeSourceHostAvailability(
|
||||
context: TaskSourceContext,
|
||||
runtimeStatusByEnvironmentId: ReadonlyMap<
|
||||
string,
|
||||
{ status: RuntimeStatus | null; checkedAt: number }
|
||||
>
|
||||
): TaskSourceHostAvailability | null {
|
||||
const parsed = parseExecutionHostId(context.hostId)
|
||||
if (parsed?.kind !== 'runtime') {
|
||||
return null
|
||||
}
|
||||
const entry = runtimeStatusByEnvironmentId.get(parsed.environmentId)
|
||||
if (!entry) {
|
||||
return { hostId: context.hostId, reason: 'checking-task-source-capability' }
|
||||
}
|
||||
if (!entry.status) {
|
||||
return { hostId: context.hostId, health: 'disconnected' }
|
||||
}
|
||||
if (entry.status.graphStatus !== 'ready') {
|
||||
return { hostId: context.hostId, health: 'connecting' }
|
||||
}
|
||||
const capabilities = entry.status.capabilities
|
||||
if (!capabilities) {
|
||||
return { hostId: context.hostId, reason: 'checking-task-source-capability' }
|
||||
}
|
||||
if (!capabilities.includes(TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY)) {
|
||||
return { hostId: context.hostId, reason: 'missing-task-source-capability' }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function formatTimeInput(hour: number, minute: number): string {
|
||||
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`
|
||||
}
|
||||
@@ -278,6 +331,13 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
|
||||
const runtimeStatusByEnvironmentId = useAppStore((s) => s.runtimeStatusByEnvironmentId)
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const preflightStatus = useAppStore((s) => s.preflightStatus)
|
||||
const preflightStatusChecked = useAppStore((s) => s.preflightStatusChecked)
|
||||
const preflightStatusContextKey = useAppStore((s) => s.preflightStatusContextKey)
|
||||
const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus)
|
||||
const expectedPreflightContextKey = useAppStore((s) =>
|
||||
localPreflightContextKey(getLocalPreflightContext(s))
|
||||
)
|
||||
const selectedId = useAppStore((s) => s.selectedAutomationId)
|
||||
const setSelectedId = useAppStore((s) => s.setSelectedAutomationId)
|
||||
const repoMap = useRepoMap()
|
||||
@@ -314,6 +374,11 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
const [selectedExternalKey, setSelectedExternalKey] = useState<string | null>(null)
|
||||
const [selectedExternalRunPage, setSelectedExternalRunPage] =
|
||||
useState<SelectedExternalRunPage | null>(null)
|
||||
const runtimePreflightMountedRef = useRef(true)
|
||||
const runtimePreflightRequestedHostIdsRef = useRef<Set<TaskSourceContext['hostId']>>(new Set())
|
||||
const [runtimePreflightStatusByHostId, setRuntimePreflightStatusByHostId] = useState<
|
||||
ReadonlyMap<TaskSourceContext['hostId'], RuntimeProviderPreflightStatus>
|
||||
>(() => new Map())
|
||||
const selectAutomationId = useCallback(
|
||||
(automationId: string | null): void => {
|
||||
setSelectedAutomationRunPageId(null)
|
||||
@@ -490,6 +555,128 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
canRerunAutomationRun({ automation: selected, run: selectedAutomationRunPage })
|
||||
const isSelectedAutomationRunPageRerunPending =
|
||||
selectedAutomationRunPage !== null && rerunRunIdsInFlight.has(selectedAutomationRunPage.id)
|
||||
const preflightStatusCurrent = preflightStatusContextKey === expectedPreflightContextKey
|
||||
const repoBackedAutomationSourceContexts = useMemo(
|
||||
() =>
|
||||
automations
|
||||
.map((automation) => getRepoBackedAutomationSourceContext(automation))
|
||||
.filter((context): context is RepoBackedAutomationSourceContext => context !== null),
|
||||
[automations]
|
||||
)
|
||||
const runtimeAutomationSourceHostIds = useMemo(() => {
|
||||
const hostIds = new Set<TaskSourceContext['hostId']>()
|
||||
for (const context of repoBackedAutomationSourceContexts) {
|
||||
const parsed = parseExecutionHostId(context.hostId)
|
||||
if (parsed?.kind !== 'runtime') {
|
||||
continue
|
||||
}
|
||||
const hostAvailability = getRuntimeSourceHostAvailability(
|
||||
context,
|
||||
runtimeStatusByEnvironmentId
|
||||
)
|
||||
if (hostAvailability) {
|
||||
continue
|
||||
}
|
||||
hostIds.add(parsed.id)
|
||||
}
|
||||
return [...hostIds].sort()
|
||||
}, [repoBackedAutomationSourceContexts, runtimeStatusByEnvironmentId])
|
||||
useEffect(
|
||||
() => () => {
|
||||
runtimePreflightMountedRef.current = false
|
||||
},
|
||||
[]
|
||||
)
|
||||
useEffect(() => {
|
||||
if (!preflightStatusCurrent || !preflightStatusChecked) {
|
||||
void refreshPreflightStatus()
|
||||
}
|
||||
}, [preflightStatusChecked, preflightStatusCurrent, refreshPreflightStatus])
|
||||
useEffect(() => {
|
||||
const unrequestedHostIds = runtimeAutomationSourceHostIds.filter(
|
||||
(hostId) => !runtimePreflightRequestedHostIdsRef.current.has(hostId)
|
||||
)
|
||||
if (unrequestedHostIds.length === 0) {
|
||||
return
|
||||
}
|
||||
setRuntimePreflightStatusByHostId((current) => {
|
||||
const next = new Map(current)
|
||||
for (const hostId of unrequestedHostIds) {
|
||||
next.set(hostId, { checked: false, status: null })
|
||||
}
|
||||
return next
|
||||
})
|
||||
for (const hostId of unrequestedHostIds) {
|
||||
runtimePreflightRequestedHostIdsRef.current.add(hostId)
|
||||
const parsed = parseExecutionHostId(hostId)
|
||||
if (parsed?.kind !== 'runtime') {
|
||||
continue
|
||||
}
|
||||
// Why: automation sources can be owned by a different remote server than
|
||||
// the run target; provider auth/tooling must be checked on the source host.
|
||||
void callRuntimeRpc<PreflightStatus>(
|
||||
{ kind: 'environment', environmentId: parsed.environmentId },
|
||||
'preflight.check',
|
||||
undefined,
|
||||
{ timeoutMs: 15_000 }
|
||||
)
|
||||
.then((status) => {
|
||||
if (!runtimePreflightMountedRef.current) {
|
||||
return
|
||||
}
|
||||
setRuntimePreflightStatusByHostId((current) => {
|
||||
const next = new Map(current)
|
||||
next.set(hostId, { checked: true, status })
|
||||
return next
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
if (!runtimePreflightMountedRef.current) {
|
||||
return
|
||||
}
|
||||
setRuntimePreflightStatusByHostId((current) => {
|
||||
const next = new Map(current)
|
||||
next.set(hostId, { checked: true, status: null })
|
||||
return next
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [runtimeAutomationSourceHostIds])
|
||||
const automationSourceHostAvailabilityById = useMemo(() => {
|
||||
const availabilityById = new Map<string, TaskSourceHostAvailability[]>()
|
||||
for (const automation of automations) {
|
||||
const context = getRepoBackedAutomationSourceContext(automation)
|
||||
if (!context) {
|
||||
continue
|
||||
}
|
||||
const hostAvailability = getRuntimeSourceHostAvailability(
|
||||
context,
|
||||
runtimeStatusByEnvironmentId
|
||||
)
|
||||
const providerAvailability = getRepoBackedProviderAvailability({
|
||||
provider: context.provider,
|
||||
contexts: [context],
|
||||
preflightStatus,
|
||||
preflightReady: preflightStatusCurrent && preflightStatusChecked,
|
||||
runtimePreflightStatusByHostId
|
||||
})
|
||||
const availability = [
|
||||
...(hostAvailability ? [hostAvailability] : []),
|
||||
...providerAvailability
|
||||
]
|
||||
if (availability.length > 0) {
|
||||
availabilityById.set(automation.id, availability)
|
||||
}
|
||||
}
|
||||
return availabilityById
|
||||
}, [
|
||||
automations,
|
||||
preflightStatus,
|
||||
preflightStatusChecked,
|
||||
preflightStatusCurrent,
|
||||
runtimePreflightStatusByHostId,
|
||||
runtimeStatusByEnvironmentId
|
||||
])
|
||||
const selectedRepo = selected ? (repoMap.get(getAutomationRunRepoId(selected)) ?? null) : null
|
||||
const selectedWorktree =
|
||||
selected && selected.workspaceId ? (worktreeMap.get(selected.workspaceId) ?? null) : null
|
||||
@@ -500,7 +687,8 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
workspace: selectedWorktree,
|
||||
projectHostSetups,
|
||||
sshConnectionStates,
|
||||
runtimeStatusByEnvironmentId
|
||||
runtimeStatusByEnvironmentId,
|
||||
sourceHostAvailability: automationSourceHostAvailabilityById.get(selected.id)
|
||||
})
|
||||
: null
|
||||
const canSaveDraft =
|
||||
@@ -1253,7 +1441,8 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
workspace,
|
||||
projectHostSetups,
|
||||
sshConnectionStates,
|
||||
runtimeStatusByEnvironmentId
|
||||
runtimeStatusByEnvironmentId,
|
||||
sourceHostAvailability: automationSourceHostAvailabilityById.get(automation.id)
|
||||
})
|
||||
if (!availability.canRunNow) {
|
||||
toast.error(availability.message)
|
||||
@@ -1837,7 +2026,8 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
workspace: automationWorktree,
|
||||
projectHostSetups,
|
||||
sshConnectionStates,
|
||||
runtimeStatusByEnvironmentId
|
||||
runtimeStatusByEnvironmentId,
|
||||
sourceHostAvailability: automationSourceHostAvailabilityById.get(automation.id)
|
||||
})
|
||||
const workspaceLabel =
|
||||
automation.workspaceMode === 'new_per_run'
|
||||
|
||||
@@ -256,6 +256,66 @@ describe('automation target availability', () => {
|
||||
).toBe('ssh-connecting')
|
||||
})
|
||||
|
||||
it('blocks manual runs when the saved source account needs provider auth', () => {
|
||||
expect(
|
||||
getAutomationTargetAvailability({
|
||||
automation: makeAutomation({
|
||||
sourceContext: {
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'github:stablyai/orca',
|
||||
hostId: 'local',
|
||||
repoId: 'repo-1',
|
||||
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' }
|
||||
}
|
||||
}),
|
||||
repo: makeRepo(),
|
||||
workspace: makeWorkspace(),
|
||||
projectHostSetups: [],
|
||||
sshConnectionStates: new Map(),
|
||||
sourceHostAvailability: [{ hostId: 'local', reason: 'missing-provider-auth' }]
|
||||
})
|
||||
).toMatchObject({
|
||||
canRunNow: false,
|
||||
reason: 'source-auth-needed',
|
||||
message: 'Connect the saved GitHub source account before running manually.'
|
||||
})
|
||||
})
|
||||
|
||||
it('blocks manual runs when the saved source host cannot support the provider', () => {
|
||||
expect(
|
||||
getAutomationTargetAvailability({
|
||||
automation: makeAutomation({
|
||||
sourceContext: {
|
||||
kind: 'task-source',
|
||||
provider: 'gitlab',
|
||||
projectId: 'gitlab:stablyai/orca',
|
||||
hostId: 'runtime:old-server',
|
||||
repoId: 'repo-1',
|
||||
providerIdentity: {
|
||||
provider: 'gitlab',
|
||||
projectId: 'stablyai/orca',
|
||||
namespace: 'stablyai',
|
||||
project: 'orca',
|
||||
webUrl: 'https://gitlab.com/stablyai/orca'
|
||||
}
|
||||
}
|
||||
}),
|
||||
repo: makeRepo(),
|
||||
workspace: makeWorkspace(),
|
||||
projectHostSetups: [],
|
||||
sshConnectionStates: new Map(),
|
||||
sourceHostAvailability: [
|
||||
{ hostId: 'runtime:old-server', reason: 'missing-task-source-capability' }
|
||||
]
|
||||
})
|
||||
).toMatchObject({
|
||||
canRunNow: false,
|
||||
reason: 'source-provider-unsupported',
|
||||
message: 'The saved GitLab source is not supported on this automation host.'
|
||||
})
|
||||
})
|
||||
|
||||
it('explains runtime-host automation availability before the unsupported manual-run fallback', () => {
|
||||
const automation = makeAutomation({
|
||||
runContext: {
|
||||
|
||||
@@ -9,8 +9,10 @@ import {
|
||||
RUNTIME_PROTOCOL_VERSION
|
||||
} from '../../../../shared/protocol-version'
|
||||
import type { SshConnectionState } from '../../../../shared/ssh-types'
|
||||
import type { TaskSourceContext } from '../../../../shared/task-source-context'
|
||||
import type { RuntimeStatus } from '../../../../shared/runtime-types'
|
||||
import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types'
|
||||
import type { TaskSourceHostAvailability } from '../task-source-context-summary'
|
||||
|
||||
export type AutomationTargetAvailability =
|
||||
| {
|
||||
@@ -33,6 +35,10 @@ export type AutomationTargetAvailability =
|
||||
| 'ssh-auth-needed'
|
||||
| 'ssh-unavailable'
|
||||
| 'ssh-connecting'
|
||||
| 'source-auth-needed'
|
||||
| 'source-tool-unavailable'
|
||||
| 'source-provider-unsupported'
|
||||
| 'source-host-unavailable'
|
||||
message: string
|
||||
}
|
||||
|
||||
@@ -46,6 +52,7 @@ type AutomationTargetAvailabilityArgs = {
|
||||
string,
|
||||
{ status: RuntimeStatus | null; checkedAt: number }
|
||||
>
|
||||
sourceHostAvailability?: readonly TaskSourceHostAvailability[]
|
||||
}
|
||||
|
||||
export function getAutomationTargetAvailability({
|
||||
@@ -54,7 +61,8 @@ export function getAutomationTargetAvailability({
|
||||
workspace,
|
||||
projectHostSetups,
|
||||
sshConnectionStates,
|
||||
runtimeStatusByEnvironmentId
|
||||
runtimeStatusByEnvironmentId,
|
||||
sourceHostAvailability
|
||||
}: AutomationTargetAvailabilityArgs): AutomationTargetAvailability {
|
||||
if (!repo) {
|
||||
return unavailable('missing-project', 'The target project is no longer available.')
|
||||
@@ -104,6 +112,14 @@ export function getAutomationTargetAvailability({
|
||||
return unavailable('missing-workspace', 'The target workspace is no longer available.')
|
||||
}
|
||||
|
||||
const sourceAvailability = getAutomationSourceAvailability(
|
||||
automation.sourceContext,
|
||||
sourceHostAvailability
|
||||
)
|
||||
if (sourceAvailability) {
|
||||
return sourceAvailability
|
||||
}
|
||||
|
||||
const sshTargetId = getAutomationSshTargetId(automation, repo)
|
||||
if (!sshTargetId) {
|
||||
return { canRunNow: true, reason: 'available', message: null }
|
||||
@@ -126,6 +142,84 @@ export function getAutomationTargetAvailability({
|
||||
}
|
||||
}
|
||||
|
||||
function getAutomationSourceAvailability(
|
||||
sourceContext: TaskSourceContext | null | undefined,
|
||||
sourceHostAvailability: readonly TaskSourceHostAvailability[] | undefined
|
||||
): AutomationTargetAvailability | null {
|
||||
if (!sourceContext) {
|
||||
return null
|
||||
}
|
||||
const availability = sourceHostAvailability?.find(
|
||||
(entry) => entry.hostId === sourceContext.hostId
|
||||
)
|
||||
if (!availability) {
|
||||
return null
|
||||
}
|
||||
const providerLabel = getAutomationSourceProviderLabel(sourceContext.provider)
|
||||
switch (availability.reason) {
|
||||
case 'missing-provider-auth':
|
||||
return unavailable(
|
||||
'source-auth-needed',
|
||||
`Connect the saved ${providerLabel} source account before running manually.`
|
||||
)
|
||||
case 'unavailable-source-tool':
|
||||
return unavailable(
|
||||
'source-tool-unavailable',
|
||||
`Install or configure the ${providerLabel} source tool before running manually.`
|
||||
)
|
||||
case 'unsupported-provider':
|
||||
case 'missing-task-source-capability':
|
||||
return unavailable(
|
||||
'source-provider-unsupported',
|
||||
`The saved ${providerLabel} source is not supported on this automation host.`
|
||||
)
|
||||
case 'checking-task-source-capability':
|
||||
return unavailable(
|
||||
'source-host-unavailable',
|
||||
`Checking the saved ${providerLabel} source host before running manually.`
|
||||
)
|
||||
}
|
||||
if (
|
||||
availability.health === 'disconnected' ||
|
||||
availability.health === 'blocked' ||
|
||||
availability.health === 'error' ||
|
||||
availability.status === 'disconnected' ||
|
||||
availability.status === 'auth-failed' ||
|
||||
availability.status === 'reconnection-failed' ||
|
||||
availability.status === 'error'
|
||||
) {
|
||||
return unavailable(
|
||||
'source-host-unavailable',
|
||||
`Reconnect the saved ${providerLabel} source host before running manually.`
|
||||
)
|
||||
}
|
||||
if (
|
||||
availability.health === 'connecting' ||
|
||||
availability.status === 'connecting' ||
|
||||
availability.status === 'deploying-relay' ||
|
||||
availability.status === 'reconnecting'
|
||||
) {
|
||||
return unavailable(
|
||||
'source-host-unavailable',
|
||||
`The saved ${providerLabel} source host is still connecting.`
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getAutomationSourceProviderLabel(provider: TaskSourceContext['provider']): string {
|
||||
switch (provider) {
|
||||
case 'github':
|
||||
return 'GitHub'
|
||||
case 'gitlab':
|
||||
return 'GitLab'
|
||||
case 'linear':
|
||||
return 'Linear'
|
||||
case 'jira':
|
||||
return 'Jira'
|
||||
}
|
||||
}
|
||||
|
||||
function getRuntimeAutomationAvailability(
|
||||
environmentId: string,
|
||||
runtimeStatusByEnvironmentId:
|
||||
|
||||
Reference in New Issue
Block a user