Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 14 additions & 6 deletions docs/public/api-reference/fabro-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2575,7 +2575,11 @@ paths:
operationId: createRunPullRequest
tags: [Runs]
summary: Create Run Pull Request
description: Creates a pull request for a completed run on GitHub and persists the record on the server.
description: >-
Creates a pull request on GitHub and persists the record on the
server. By default the run must have a successful conclusion. With
force=true, failed conclusions are accepted and running, blocked, or
paused runs use their latest committed snapshot.
parameters:
- $ref: "#/components/parameters/RunId"
requestBody:
Expand All @@ -2586,7 +2590,7 @@ paths:
$ref: "#/components/schemas/CreateRunPullRequestRequest"
responses:
"200":
description: Pull request created
description: Pull request created or an existing matching pull request linked
content:
application/json:
schema:
Expand All @@ -2611,8 +2615,9 @@ paths:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: >-
Pull request already exists for this run. Clients can GET
/runs/{id}/pull_request to retrieve the stored record.
Pull request already exists for this run, or the requested active
snapshot is not ready, eligible, or available on the remote run
branch.
headers:
x-request-id:
$ref: "#/components/headers/XRequestId"
Expand All @@ -2630,7 +2635,7 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
"503":
description: GitHub integration is unavailable on the server
description: GitHub integration or the active run snapshot is unavailable
headers:
x-request-id:
$ref: "#/components/headers/XRequestId"
Expand Down Expand Up @@ -12168,7 +12173,9 @@ components:
properties:
force:
type: boolean
description: Create the pull request even if the run did not finish with succeeded or partially_succeeded.
description: >-
Create from a non-successful conclusion, or from the latest
committed snapshot when the run is running, blocked, or paused.
example: false
model:
type: ["string", "null"]
Expand Down Expand Up @@ -12592,6 +12599,7 @@ components:
- agent
- prompt
- command
- pull_request
- human
- conditional
- parallel
Expand Down
12 changes: 12 additions & 0 deletions docs/public/execution/context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ Agents can also emit arbitrary context updates by including a JSON object with a
|---|---|
| `command.output` | The command's ordered output stream. Durable context stores this as a `blob://sha256/...` ref after the command completes; downstream prompts resolve it back to text. |

### Pull request nodes

| Key | Value |
|---|---|
| `pull_request.url` | Public GitHub pull request URL |
| `pull_request.number` | Pull request number |
| `pull_request.owner` | Repository owner |
| `pull_request.repo` | Repository name |
| `pull_request.base_branch` | PR base branch |
| `pull_request.head_branch` | Managed run branch |
| `pull_request.draft` | `true` for PRs created by this node |

### Human gates

| Key | Value |
Expand Down
8 changes: 8 additions & 0 deletions docs/public/execution/run-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ push = true
| `enabled` | When `false`, Fabro does not create the managed run branch or checkpoint commits. This also disables metadata branch writes. |
| `push` | When `false`, Fabro creates local checkpoint commits but does not push `fabro/run/<id>` to the remote. |

The built-in `type="pull_request"` workflow node requires both fields to be
`true`, because it creates the PR from the latest committed and pushed run
snapshot.

### `[run.meta_branch]`

Configure Fabro's managed `fabro/meta/<id>` metadata branch.
Expand Down Expand Up @@ -497,6 +501,10 @@ The `sandbox` transport runs the MCP server inside the workflow's sandbox. This

Automatically open a GitHub pull request when the workflow run completes successfully. Requires a [GitHub App](/integrations/github) to be configured.

To open a draft PR earlier and use it in later workflow stages, add an explicit
`type="pull_request"` node instead. That node is independently opt-in and does
not require this section.

```toml title="run.toml"
[run.pull_request]
enabled = true
Expand Down
8 changes: 5 additions & 3 deletions docs/public/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ fabro pr [OPTIONS] <COMMAND>
| Command | Description |
| --- | --- |
| `fabro pr close` | Close a pull request |
| `fabro pr create` | Create a pull request from a completed run |
| `fabro pr create` | Create a pull request from a run |
| `fabro pr link` | Link or replace the GitHub pull request associated with a run |
| `fabro pr merge` | Merge a pull request |
| `fabro pr unlink` | Unlink the pull request associated with a run |
Expand Down Expand Up @@ -794,7 +794,9 @@ fabro pr close [OPTIONS] <RUN_ID>

#### `fabro pr create`

Create a pull request from a completed run
Create a pull request from a run. By default, the run must have completed
successfully. Pass `--force` to use a failed conclusion or the latest committed
snapshot of a running, blocked, or paused run.

```bash
fabro pr create [OPTIONS] <RUN_ID>
Expand All @@ -810,7 +812,7 @@ fabro pr create [OPTIONS] <RUN_ID>

| Option | Description |
| --- | --- |
| `-f, --force` | Create PR even if the run status is not succeeded/partially_succeeded |
| `-f, --force` | Use a non-successful conclusion or an active run's latest committed snapshot |
| `--model <model>` | LLM model for generating PR description |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |

Expand Down
9 changes: 9 additions & 0 deletions docs/public/reference/dot-language.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Each node's `shape` attribute determines its execution behavior. See [Nodes & St
| `box` (default) | agent | Multi-turn LLM with tool access |
| `tab` | prompt | Single LLM call, no tools |
| `parallelogram` | command | Execute a shell script |
| *(explicit `type` only)* | pull_request | Create or adopt a draft pull request during the run |
| `hexagon` | human | Human-in-the-loop decision gate |
| `diamond` | conditional | Route based on conditions |
| `component` | parallel | Fan-out to concurrent branches |
Expand Down Expand Up @@ -248,6 +249,14 @@ audit [
| `language` | String | `"shell"` (default) or `"python"` |
| `output_schema` | String | Optional structured output validation. Accepts `routing`, `@path/to/schema.json`, or an inline JSON Schema object string. See [Structured output validation](#structured-output-validation). |

### Pull request nodes

Set `type="pull_request"` explicitly; there is no shape alias. The node creates
or adopts a draft GitHub PR from the committed `base_sha..HEAD` snapshot and
stores its coordinates in `pull_request.*` context keys. It requires an enabled,
pushed run branch and cannot execute inside a parallel branch. Dirty and
untracked files are not included.

### Parallel (fan-out) nodes

| Attribute | Type | Description |
Expand Down
37 changes: 36 additions & 1 deletion docs/public/workflows/stages-and-nodes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ This distinction matters for observability and debugging: the workflow graph sho

## Node types

Every node's Graphviz `shape` attribute determines its execution behavior. If no shape is specified, the node defaults to an agent.
Most nodes use a Graphviz `shape` to select their execution behavior. A node
can instead set `type` explicitly. If neither selects a handler, the node
defaults to an agent.

### Start

Expand Down Expand Up @@ -107,6 +109,39 @@ test [label="Run Tests", shape=parallelogram, script="cargo test 2>&1 || true"]
| `script` | The shell command to execute |
| `language` | `"shell"` (default) or `"python"` |

### Pull request

**Type:** `pull_request` (explicit only)

Creates or adopts a draft GitHub pull request during the run, so downstream
nodes can work with it:

```dot
implement [label="Implement"]
create_pr [label="Open draft PR", type="pull_request"]
comment [label="Comment on PR"]

implement -> create_pr -> comment
```

The node snapshots committed work from the run's base SHA through one captured
`HEAD`, pushes that exact commit to the managed run branch without force, and
opens a draft PR. Dirty and untracked files are excluded. Later checkpoint
pushes update the same PR branch. Retries are idempotent: Fabro reuses the
stored PR or adopts a matching open GitHub PR instead of creating a duplicate.

This node requires a GitHub origin, configured GitHub credentials, and
`[run.run_branch] enabled = true, push = true`. It cannot run inside a parallel
branch. Dry runs are side-effect free and return placeholder context values.
Set the optional `model` attribute to override the run model used to generate
the PR title and body.

Use this node rather than invoking `fabro pr create` from a command node. Fabro
does not expose the worker API token inside the sandbox. The draft remains open
if a later node fails. It does not use `[run.pull_request]`; that setting remains
the separate end-of-run PR mode. In this initial behavior, end-of-run processing
does not refresh the in-run PR body, mark it ready, or enable auto-merge.

### Human

**Shape:** `hexagon`
Expand Down
4 changes: 2 additions & 2 deletions lib/apps/fabro-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ pub(crate) struct PrCreateArgs {
/// LLM model for generating PR description
#[arg(long)]
pub(crate) model: Option<String>,
/// Create PR even if the run status is not succeeded/partially_succeeded
/// Use a non-successful conclusion or an active run's committed snapshot
#[arg(short, long)]
pub(crate) force: bool,
}
Expand Down Expand Up @@ -1453,7 +1453,7 @@ pub(crate) struct PrNamespace {

#[derive(Subcommand)]
pub(crate) enum PrCommand {
/// Create a pull request from a completed run
/// Create a pull request from a run
Create(PrCreateArgs),
/// Link or replace the GitHub pull request associated with a run
Link(PrLinkArgs),
Expand Down
2 changes: 1 addition & 1 deletion lib/apps/fabro-cli/src/commands/pr/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub(super) async fn create_command(args: PrCreateArgs, base_ctx: &CommandContext
number = record.number,
owner = %record.owner,
repo = %record.repo,
"Created pull request"
"Created or linked pull request"
);

if ctx.json_output() {
Expand Down
32 changes: 27 additions & 5 deletions lib/apps/fabro-cli/src/commands/run/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use fabro_store::{EventEnvelope, RunProjection, RunProjectionReducer};
use fabro_tool::fabro_client::ClientBackend;
use fabro_types::settings::run::{RunMode, RunNamespace};
use fabro_types::{
ArtifactUpload, EventBody, FailureReason, Principal, RunBlobId, RunEvent, RunId,
ArtifactUpload, EventBody, FailureReason, Graph, Principal, RunBlobId, RunEvent, RunId,
WorkflowSettings,
};
use fabro_vault::{SecretStore, Vault};
Expand Down Expand Up @@ -143,7 +143,7 @@ pub(crate) async fn execute(
Some(arc) => Some(arc.read().await),
None => None,
};
maybe_build_github_credentials(&run_spec.settings, vault_guard.as_deref())?
maybe_build_github_credentials(&run_spec.settings, &run_spec.graph, vault_guard.as_deref())?
};
let services = StartServices {
run_id,
Expand Down Expand Up @@ -1112,6 +1112,7 @@ fn stamp_system_worker(mut event: RunEvent) -> RunEvent {

fn maybe_build_github_credentials(
settings: &WorkflowSettings,
graph: &Graph,
vault: Option<&fabro_vault::Vault>,
) -> Result<Option<fabro_github::GitHubCredentials>> {
let resolved_run = &settings.run;
Expand All @@ -1127,8 +1128,8 @@ fn maybe_build_github_credentials(
return build_github_credentials(strategy, app_id.as_deref(), app_slug.as_deref(), vault);
}

let pull_request_enabled =
resolved_run.execution.mode != RunMode::DryRun && resolved_run.pull_request.is_some();
let pull_request_enabled = resolved_run.execution.mode != RunMode::DryRun
&& (resolved_run.pull_request.is_some() || graph_has_pull_request_node(graph));
if pull_request_enabled {
return Ok(build_github_credentials(
strategy,
Expand All @@ -1143,6 +1144,13 @@ fn maybe_build_github_credentials(
Ok(None)
}

fn graph_has_pull_request_node(graph: &Graph) -> bool {
graph
.nodes
.values()
.any(|node| node.node_type() == Some("pull_request"))
}

#[expect(
clippy::disallowed_methods,
reason = "CLI worker InterpString resolution facade for {{ env.* }} values."
Expand Down Expand Up @@ -1766,8 +1774,9 @@ mod tests {
EnvironmentProvider, RunIntegrationsGithubSettings, RunIntegrationsSettings, RunMode,
RunNamespace,
};
use fabro_types::{AttrValue, Graph, Node};

use super::super::requires_github_credentials;
use super::super::{graph_has_pull_request_node, requires_github_credentials};

fn run_with(
permissions: HashMap<String, InterpString>,
Expand Down Expand Up @@ -1814,5 +1823,18 @@ mod tests {
let run = run_with(HashMap::new(), "docker", RunMode::DryRun);
assert!(!requires_github_credentials(&run));
}

#[test]
fn detects_explicit_pull_request_node_for_soft_credential_loading() {
let mut graph = Graph::new("test");
let mut node = Node::new("create_pr");
node.attrs.insert(
"type".to_string(),
AttrValue::String("pull_request".to_string()),
);
graph.nodes.insert(node.id.clone(), node);

assert!(graph_has_pull_request_node(&graph));
}
}
}
2 changes: 1 addition & 1 deletion lib/apps/fabro-cli/tests/it/cmd/pr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn help() {
Usage: fabro pr [OPTIONS] <COMMAND>

Commands:
create Create a pull request from a completed run
create Create a pull request from a run
link Link or replace the GitHub pull request associated with a run
unlink Unlink the pull request associated with a run
view View pull request details
Expand Down
4 changes: 2 additions & 2 deletions lib/apps/fabro-cli/tests/it/cmd/pr_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fn help() {
success: true
exit_code: 0
----- stdout -----
Create a pull request from a completed run
Create a pull request from a run

Usage: fabro pr create [OPTIONS] <RUN_ID>

Expand All @@ -30,7 +30,7 @@ fn help() {
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--model <MODEL> LLM model for generating PR description
-f, --force Create PR even if the run status is not succeeded/partially_succeeded
-f, --force Use a non-successful conclusion or an active run's committed snapshot
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
Expand Down
2 changes: 1 addition & 1 deletion lib/apps/fabro-server/src/run_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ async fn load_projection(
Ok(state.cached_run(run_id).await?.projection)
}

async fn reconnect_run_sandbox(
pub(crate) async fn reconnect_run_sandbox(
state: &Arc<AppState>,
run_id: &RunId,
projection: &fabro_store::RunProjection,
Expand Down
Loading
Loading