Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ All notable changes to dotbot are documented in this file. The format follows [K
## [Unreleased]

### Added
- **`Start-DotbotRuntimeDetached`** in `Dotbot.Runtime` — brings the per-project runtime up in a process of its own by spawning `dotbot serve` as a child and waiting for `.control/runtime.json`, which `Start-DotbotRuntime` writes only once the listener is accepting. `Start-DotbotRuntime` hosts the listener in-process and records its own `$PID`, so it cannot serve a CLI that exits immediately after spawning a detached runner. The result carries no `listener` handle, so a caller can never tear down a runtime it does not own.

### Changed
- **`dotbot run <workflow>` and `dotbot tasks run` now start the runtime themselves** when none is alive, instead of only doing so under `--watch`. `--no-auto-runtime` is the opt-out on both paths and now refuses synchronously with exit 1. `--watch` still hosts its runtime in-process and tears it down on exit; the runtime auto-started for a detached run deliberately outlives the command (`dotbot runtime-status` reports its PID). For `dotbot run` the runtime is settled *before* `Initialize-WorkflowRun`, so a runtime that cannot start leaves no run, no tasks and no integration branch behind.
- **`dotbot tasks run` propagates its exit code** through `bin/dotbot.ps1`, matching `dotbot run` / `dotbot workflow run` / `dotbot doctor`. `exit` inside a `&`-invoked script only ends that script, so every `tasks run` failure — including the pre-existing "no .bot" and "Invoke-DotbotProcess.ps1 not found" guards — previously reported success to the shell.

### Fixed
- **`dotbot run <workflow>` without `--watch` reported success for a run that could not progress (#682).** The runtime precondition and auto-start were both nested inside `if ($Watch)`, so the default path spawned the detached task-runner with no runtime and exited 0; the runner then parked every task in `needs-input` with `Dotbot runtime endpoint not available` — asynchronously, in a log file the caller never sees — after creating and pushing an integration branch. `--no-auto-runtime` was bound but unreachable without `--watch`.
- **`--poll-interval-ms` consumed the following token even when it was another flag**, so `dotbot run <wf> --poll-interval-ms --watch` cast `[int]"--watch"`, silently dropped `--watch`, and took the very detached path above. It now uses the same `-notmatch '^--?'` guard as the parser's `default` arm.

### Removed

Expand Down
8 changes: 6 additions & 2 deletions bin/dotbot.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ function Get-WorkflowRunInvocation {
$i++
}
'pollintervalms' {
if (($i + 1) -lt $RunArgs.Count) {
if (($i + 1) -lt $RunArgs.Count -and [string]$RunArgs[$i + 1] -notmatch '^--?') {
$runSplat['PollIntervalMs'] = [int]$RunArgs[$i + 1]
$i += 2
} else {
Expand Down Expand Up @@ -545,7 +545,11 @@ function Invoke-Tasks {
switch ($sub) {
'run' {
$script = Join-Path $ScriptsDir 'tasks-run.ps1'
if (Test-Path $script) { & $script } else { Write-DotbotError "tasks-run.ps1 not found" }
if (Test-Path $script) {
$global:LASTEXITCODE = 0
& $script
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} else { Write-DotbotError "tasks-run.ps1 not found" }
}
'stop' {
$script = Join-Path $ScriptsDir 'tasks-stop.ps1'
Expand Down
16 changes: 16 additions & 0 deletions src/cli/tasks-run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,25 @@ if (-not (Test-Path $lpPath)) {
exit 1
}

Import-Module (Join-Path $DotbotBase "src/runtime/Modules/Dotbot.Runtime/Dotbot.Runtime.psd1") -Force -DisableNameChecking

Write-DotbotBanner -Title "D O T B O T" -Subtitle "Pending tasks runner"
Write-Status "Launching workflow-agnostic task runner..."

if (-not (Test-RuntimeAlive -BotRoot $BotDir)) {
Write-Status "Starting headless runtime..."
try {
$runtimeStart = Start-DotbotRuntimeDetached -BotRoot $BotDir
} catch {
Write-DotbotError $_.Exception.Message
Write-DotbotCommand "Start it manually with 'dotbot serve' in another shell."
exit 1
}
Write-Success ("Runtime ready at {0}" -f $runtimeStart.url)
} else {
Write-DotbotCommand "Using existing headless runtime."
}

$wfArgs = @(
"-Type", "task-runner",
"-Continue",
Expand Down
29 changes: 21 additions & 8 deletions src/cli/workflow-run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -360,14 +360,27 @@ if ($tasks.Count -eq 0) {

$runtimeStart = $null
$runtimeStartedHere = $false
$runtimeAlive = $false
if ($Watch) {
Import-Module (Join-Path $DotbotBase "src/runtime/Modules/Dotbot.Runtime/Dotbot.Runtime.psd1") -Force -DisableNameChecking
$runtimeAlive = Test-RuntimeAlive -BotRoot $BotDir
if (-not $runtimeAlive -and $NoAutoRuntime) {
Write-DotbotError "The dotbot runtime is not running."
Write-DotbotCommand "Run 'dotbot serve' in another shell, or omit --no-auto-runtime."
exit 1
Import-Module (Join-Path $DotbotBase "src/runtime/Modules/Dotbot.Runtime/Dotbot.Runtime.psd1") -Force -DisableNameChecking
$runtimeAlive = Test-RuntimeAlive -BotRoot $BotDir
if (-not $runtimeAlive -and $NoAutoRuntime) {
Write-DotbotError "The dotbot runtime is not running."
Write-DotbotCommand "Run 'dotbot serve' in another shell, or omit --no-auto-runtime."
exit 1
}

if (-not $Watch) {
if (-not $runtimeAlive) {
Write-Status "Starting headless runtime..."
try {
$runtimeStart = Start-DotbotRuntimeDetached -BotRoot $BotDir
} catch {
Write-DotbotError $_.Exception.Message
Write-DotbotCommand "Start it manually with 'dotbot serve' in another shell."
exit 1
}
Write-Success ("Runtime ready at {0}" -f $runtimeStart.url)
} else {
Write-DotbotCommand "Using existing headless runtime."
}
}

Expand Down
1 change: 1 addition & 0 deletions src/runtime/Modules/Dotbot.Runtime/Dotbot.Runtime.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@

# Lifecycle
'Start-DotbotRuntime'
'Start-DotbotRuntimeDetached'
'Stop-DotbotRuntime'
'Test-RuntimeAlive'
'New-RuntimeBearerToken'
Expand Down
60 changes: 60 additions & 0 deletions src/runtime/Modules/Dotbot.Runtime/Private/Lifecycle.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,65 @@ function Start-DotbotRuntime {
return $result
}

function Start-DotbotRuntimeDetached {
<#
.SYNOPSIS
Bring the runtime up in a process of its own so it outlives the caller.

.DESCRIPTION
Start-DotbotRuntime hosts the listener in-process and records $PID, so a
CLI that spawns a detached runner and then exits would take the runtime
down with it. This spawns 'dotbot serve' as a child process instead — the
background mode serve.ps1 documents — and waits for the connection file,
which Start-DotbotRuntime writes only once the listener is accepting.

Attaches instead of spawning when a runtime is already alive.

Returns @{ url; pid; attached }. There is deliberately no 'listener' key:
the caller does not own this runtime and must never stop it.

Throws when the runtime does not come up within TimeoutSeconds.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$BotRoot,

[int]$TimeoutSeconds = 30
)

$child = $null
if (-not (Test-RuntimeAlive -BotRoot $BotRoot)) {
$serveScript = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../../../cli/serve.ps1'))
if (-not (Test-Path -LiteralPath $serveScript -PathType Leaf)) {
throw "Runtime host script not found at $serveScript."
}

$child = Start-DotbotChildProcess `
-File $serveScript `
-WorkingDirectory (Split-Path -Parent $BotRoot) `
-WindowStyle Hidden

$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
while (-not (Test-RuntimeAlive -BotRoot $BotRoot)) {
if ($child.HasExited) {
throw "The dotbot runtime host exited before the runtime was ready."
}
if ([DateTime]::UtcNow -ge $deadline) {
throw "The dotbot runtime did not become ready within $TimeoutSeconds seconds."
}
Start-Sleep -Milliseconds 250
}
}

$conn = Read-RuntimeConnectionFile -BotRoot $BotRoot
return [ordered]@{
url = [string]$conn.url
pid = $conn.pid
attached = ($null -eq $child)
}
}

function Stop-DotbotRuntime {
<#
.SYNOPSIS
Expand Down Expand Up @@ -311,6 +370,7 @@ function Stop-DotbotRuntime {

Export-ModuleMember -Function @(
'Start-DotbotRuntime'
'Start-DotbotRuntimeDetached'
'Stop-DotbotRuntime'
'Test-RuntimeAlive'
'New-RuntimeBearerToken'
Expand Down
60 changes: 60 additions & 0 deletions tests/Test-ProcessDispatch.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -274,12 +274,72 @@ if (Test-Path $dispatcherScript) {
Assert-True -Name "dotbot.ps1 propagates workflow-run exit codes" `
-Condition ($dispatcherCliContent -match 'LASTEXITCODE\s*=\s*0' -and $dispatcherCliContent -match 'exit \$LASTEXITCODE') `
-Message "dotbot run should return workflow-run.ps1 failures to shell callers"

$dispatcherAst = [System.Management.Automation.Language.Parser]::ParseFile($dispatcherScript, [ref]$null, [ref]$null)
$parserFn = $dispatcherAst.Find({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -eq 'Get-WorkflowRunInvocation'
}, $true)

if ($parserFn) {
. ([scriptblock]::Create($parserFn.Extent.Text))

$swallowed = Get-WorkflowRunInvocation -RunArgs @('smoke-test', '--poll-interval-ms', '--watch')
Assert-True -Name "--poll-interval-ms does not swallow the flag that follows it" `
-Condition ([bool]$swallowed.Parameters['Watch']) `
-Message "--watch must still bind when it follows a valueless --poll-interval-ms"
Assert-Equal -Name "--poll-interval-ms without a value falls back to the default" `
-Expected 1000 -Actual $swallowed.Parameters['PollIntervalMs']

$withValue = Get-WorkflowRunInvocation -RunArgs @('smoke-test', '--poll-interval-ms', '500', '--watch')
Assert-Equal -Name "--poll-interval-ms still reads its value when given one" `
-Expected 500 -Actual $withValue.Parameters['PollIntervalMs']
Assert-True -Name "--poll-interval-ms <ms> leaves a following --watch bound" `
-Condition ([bool]$withValue.Parameters['Watch']) `
-Message "Regression: the valued form must keep parsing the rest of the line"
} else {
Write-TestResult -Name "Get-WorkflowRunInvocation parser tests" -Status Skip -Message "Function not found in $dispatcherScript"
}
} else {
Write-TestResult -Name "dotbot.ps1 workflow run dispatch" -Status Skip -Message "Script not found at $dispatcherScript"
}

Write-Host ""

Write-Host " CLI RUNTIME PRECONDITION" -ForegroundColor Cyan
Write-Host " --------------------------------------------" -ForegroundColor DarkGray

$precondProject = $null
try {
$precondProject = New-TestProjectFromGolden -Flavor 'start-from-prompt' -Prefix 'dotbot-test-runtime-precond'

Push-Location $precondProject.ProjectRoot
try {
$precondOut = & pwsh -NoProfile -ExecutionPolicy Bypass -File $dispatcherScript run smoke-test --no-auto-runtime 2>&1
$precondCode = $LASTEXITCODE
} finally {
Pop-Location
}
$precondOut = $precondOut | Out-String

Assert-Equal -Name "dotbot run --no-auto-runtime exits non-zero without --watch when no runtime is up" `
-Expected 1 -Actual $precondCode -Message $precondOut

Assert-True -Name "dotbot run --no-auto-runtime says the runtime is not running" `
-Condition ($precondOut -match 'runtime is not running') `
-Message $precondOut

$mintedRuns = @(Get-ChildItem -LiteralPath (Join-Path $precondProject.BotDir 'workspace/tasks/workflow-runs') -Directory -ErrorAction SilentlyContinue)
Assert-Equal -Name "dotbot run --no-auto-runtime mints no WorkflowRun" `
-Expected 0 -Actual $mintedRuns.Count `
-Message "The runtime check must run before Initialize-WorkflowRun so a refused run leaves no tasks or branches behind"
} finally {
if ($precondProject) { Remove-TestProject -Path $precondProject.ProjectRoot }
}

Write-Host ""

# ===================================================================
# SUMMARY
# ===================================================================
Expand Down
Loading