mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-09 22:33:37 +00:00
Merge branch 'main' of github.com:fabro-sh/fabro
This commit is contained in:
commit
53b5ec6376
20 changed files with 434 additions and 19 deletions
|
|
@ -1 +1 @@
|
|||
6c53fc29b5f1309bfd026a883a23006db2bb441c
|
||||
f67bebae4b58380b84341d57471e75a197e76e45
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
6c53fc29b5f1309bfd026a883a23006db2bb441c
|
||||
f67bebae4b58380b84341d57471e75a197e76e45
|
||||
|
|
|
|||
4
.github/workflows/rust.yml
vendored
4
.github/workflows/rust.yml
vendored
|
|
@ -73,7 +73,7 @@ jobs:
|
|||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: taiki-e/install-action@773334c0e05d7e699e4d78234494308223f3a2cf # nextest
|
||||
- run: cargo nextest run --workspace
|
||||
|
||||
test-macos:
|
||||
|
|
@ -88,5 +88,5 @@ jobs:
|
|||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: taiki-e/install-action@773334c0e05d7e699e4d78234494308223f3a2cf # nextest
|
||||
- run: cargo nextest run --workspace
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
FROM rust:1-bookworm AS rust-builder
|
||||
FROM rust:1-bookworm@sha256:4ec71e955e6c08aeb238885083222ddff79d82eb87654a96c76e38e94da1a53b AS rust-builder
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY lib/crates lib/crates
|
||||
COPY openapi openapi
|
||||
RUN cargo build --release --bin fabro
|
||||
|
||||
FROM oven/bun:1
|
||||
FROM oven/bun:1@sha256:c9aa897b6028d54e510c4babd41bcdd481ad4a4b5030ad31135a9cabfe2c24df
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 make g++ ca-certificates nodejs npm && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN npm i -g mintlify && \
|
||||
RUN npm i -g mintlify@4.2.507 && \
|
||||
sed -i 's/const version = __VERSION__/const version = "0.0.0"/' \
|
||||
/usr/local/lib/node_modules/mintlify/node_modules/katex/dist/katex.mjs
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: "System management API"
|
||||
title: "System management API and log rotation"
|
||||
date: "2026-04-06"
|
||||
---
|
||||
|
||||
|
|
@ -14,6 +14,10 @@ fabro system events # global event stream
|
|||
fabro system prune # clean up completed runs
|
||||
```
|
||||
|
||||
## Automatic log rotation
|
||||
|
||||
Server logs now rotate daily and are automatically cleaned up after 7 days. Previously, log files could grow unbounded on long-running servers, requiring manual cleanup.
|
||||
|
||||
## More
|
||||
|
||||
<Accordion title="API">
|
||||
|
|
@ -21,6 +25,11 @@ fabro system prune # clean up completed runs
|
|||
- New `GET /api/v1/system/events` endpoint streams the global event log via SSE
|
||||
- New `GET /api/v1/system/df` endpoint returns disk usage by run
|
||||
- New `POST /api/v1/system/prune` endpoint removes completed run data
|
||||
- Removed deprecated `GET /runs/{id}/verification`, sessions, retros, and steer endpoints
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="CLI">
|
||||
- Removed `fabro skill install` command (skills are now managed through workflow definitions)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Improvements">
|
||||
|
|
|
|||
32
docs/changelog/2026-04-07.mdx
Normal file
32
docs/changelog/2026-04-07.mdx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: "Worker lifecycle hardening and object-backed artifacts"
|
||||
date: "2026-04-07"
|
||||
---
|
||||
|
||||
## Reliable worker lifecycle
|
||||
|
||||
Run workers now operate under tighter server supervision. The server tracks worker health, properly terminates active workers during force removal, and propagates cancellation into running command stages. Resumed runs no longer attempt to clean up stale workers from a previous session, preventing spurious errors after restarts.
|
||||
|
||||
## Object-backed artifact storage
|
||||
|
||||
Artifacts and offloaded agent context are now persisted as content-addressed objects in the global blob store instead of the local scratch directory. This makes artifacts portable across sessions and available through the server API regardless of which machine originally produced them.
|
||||
|
||||
## More
|
||||
|
||||
<Accordion title="API">
|
||||
- Removed deprecated workflows and steer endpoints
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="CLI">
|
||||
- `fabro run attach` now streams events over SSE for more reliable output
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Improvements">
|
||||
- Runs now record creation provenance (CLI, API, or scheduled trigger)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Fixes">
|
||||
- Fixed `fabro run attach` not following paginated event streams
|
||||
- Fixed terminal runs blocking deletion during the grace period
|
||||
- Fixed cancelled workers not stopping in-progress command stages
|
||||
</Accordion>
|
||||
31
docs/changelog/2026-04-08.mdx
Normal file
31
docs/changelog/2026-04-08.mdx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: "Embedded web UI and fabro uninstall"
|
||||
date: "2026-04-08"
|
||||
---
|
||||
|
||||
## Embedded web UI
|
||||
|
||||
The web dashboard is now bundled directly into the Fabro server binary. When you run `fabro server start`, the UI is available immediately at the server's address with no separate build step or dev server required. The web UI is optional and can be disabled in server settings. A demo mode toggle lets you explore the interface with sample data without connecting to a live workspace.
|
||||
|
||||
## `fabro uninstall`
|
||||
|
||||
A new `fabro uninstall` command cleanly removes Fabro's local state, server data, and configuration files. Previously, uninstalling required manually tracking down scattered directories.
|
||||
|
||||
```bash
|
||||
fabro uninstall # interactive confirmation
|
||||
fabro uninstall --force # skip confirmation
|
||||
```
|
||||
|
||||
## More
|
||||
|
||||
<Accordion title="CLI">
|
||||
- Added host-only TCP bind support for the server (e.g., `127.0.0.1:8080` without TLS)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Fixes">
|
||||
- Fixed GitHub App setup flow failing on nullable webhook secrets, duplicate POST requests, and incorrect port detection
|
||||
- Fixed session cookie decryption errors on server restart
|
||||
- Fixed devcontainer lifecycle commands not being cancelled during shutdown
|
||||
- Fixed setup commands continuing to run after the server received a shutdown signal
|
||||
- Fixed dark theme not being selected by default for new users
|
||||
</Accordion>
|
||||
31
docs/changelog/2026-04-09.mdx
Normal file
31
docs/changelog/2026-04-09.mdx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: "Settings v2 and events schema v2"
|
||||
date: "2026-04-09"
|
||||
---
|
||||
|
||||
## Settings v2
|
||||
|
||||
The TOML configuration format has been redesigned for clarity and consistency. Settings are now organized into logical sections (`[run]`, `[server]`, `[cli]`, `[hooks]`, `[sandbox]`, `[mcp]`) with typed fields throughout. Run goals can now reference external files using a tagged union syntax, so you can keep large goal prompts separate from your workflow config.
|
||||
|
||||
```toml
|
||||
[run]
|
||||
goal = { file = "goals/deploy-review.md" }
|
||||
model = "claude-sonnet-4-6"
|
||||
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8080
|
||||
```
|
||||
|
||||
Comments in your `settings.toml` are now preserved when Fabro edits the file during setup and registration.
|
||||
|
||||
## Events schema v2
|
||||
|
||||
The event wire format has been flattened and enriched. Each event envelope now carries stage scope (visit count, parallel group and branch IDs) and actor information directly, rather than requiring consumers to reconstruct context from surrounding events. The TypeScript API client has been regenerated to match.
|
||||
|
||||
## More
|
||||
|
||||
<Accordion title="Improvements">
|
||||
- Server auth resolver now fails closed instead of panicking on unexpected states
|
||||
- Config parse errors now include the file path for easier debugging
|
||||
</Accordion>
|
||||
15
docs/changelog/2026-04-10.mdx
Normal file
15
docs/changelog/2026-04-10.mdx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: "GitHub App owner selection"
|
||||
date: "2026-04-10"
|
||||
---
|
||||
|
||||
## GitHub App owner selection
|
||||
|
||||
When running `fabro install`, you can now choose whether to install the GitHub App under your personal account or an organization. Previously, the app was always installed under the authenticated user's personal account, requiring manual reconfiguration for org-level access.
|
||||
|
||||
## More
|
||||
|
||||
<Accordion title="Fixes">
|
||||
- Fixed an infinite loop when a goal-gate retry targeted a terminal node
|
||||
- Fixed detached workers not exiting cleanly after post-run shutdown
|
||||
</Accordion>
|
||||
15
docs/changelog/2026-04-11.mdx
Normal file
15
docs/changelog/2026-04-11.mdx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: "Unified template syntax"
|
||||
date: "2026-04-11"
|
||||
---
|
||||
|
||||
## Unified template syntax
|
||||
|
||||
Workflow definitions and configuration files now share the same template syntax for variable interpolation. Previously, workflow DOT files and TOML config files used slightly different templating rules, which made it easy to use the wrong syntax in the wrong context. The unified syntax works everywhere Fabro processes templates.
|
||||
|
||||
## More
|
||||
|
||||
<Accordion title="Improvements">
|
||||
- Project state files moved from the workspace root into `.fabro/` for a cleaner directory layout
|
||||
- Config parse errors now show the file path where the error occurred
|
||||
</Accordion>
|
||||
15
docs/changelog/2026-04-12.mdx
Normal file
15
docs/changelog/2026-04-12.mdx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: "Typed secrets and fabro pr create --force"
|
||||
date: "2026-04-12"
|
||||
---
|
||||
|
||||
## Typed secrets
|
||||
|
||||
Secrets now carry metadata describing their type and purpose. The new secrets API lets you inspect which secrets are configured, what they're used for, and whether they're present — without exposing the values themselves. This makes it easier to diagnose missing credentials and understand what a workflow needs before running it.
|
||||
|
||||
## More
|
||||
|
||||
<Accordion title="CLI">
|
||||
- Added `--force` option to `fabro pr create` to skip confirmation prompts
|
||||
- `fabro doctor` now checks that the storage directory exists and is writable
|
||||
</Accordion>
|
||||
|
|
@ -251,6 +251,12 @@
|
|||
"group": "April 2026",
|
||||
"icon": "clock-rotate-left",
|
||||
"pages": [
|
||||
"changelog/2026-04-12",
|
||||
"changelog/2026-04-11",
|
||||
"changelog/2026-04-10",
|
||||
"changelog/2026-04-09",
|
||||
"changelog/2026-04-08",
|
||||
"changelog/2026-04-07",
|
||||
"changelog/2026-04-06",
|
||||
"changelog/2026-04-04",
|
||||
"changelog/2026-04-02",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ The rest of this page describes the `app` strategy, which is required for browse
|
|||
|
||||
1. Navigate to the web app (default `http://localhost:3000`). If no GitHub App is configured, you'll be redirected to the setup page automatically.
|
||||
|
||||
2. Click **Register GitHub App**. This takes you to GitHub with a pre-filled [App Manifest](https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-from-a-manifest) containing:
|
||||
2. Choose where to register the app. If you have the `gh` CLI installed, Fabro detects your GitHub username and any organizations you administer and lets you pick. For organization-owned apps, the app is registered under that org's settings. If `gh` is not available, the app is registered under your personal account.
|
||||
|
||||
3. Click **Register GitHub App**. This takes you to GitHub with a pre-filled [App Manifest](https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-from-a-manifest) containing:
|
||||
|
||||
| Permission | Level | Purpose |
|
||||
|---|---|---|
|
||||
|
|
@ -56,15 +58,15 @@ The rest of this page describes the `app` strategy, which is required for browse
|
|||
| Issues | Write | Create issues from workflows |
|
||||
| Emails | Read | Read verified email for OAuth login |
|
||||
|
||||
3. Review the permissions on GitHub and click **Create GitHub App**.
|
||||
4. Review the permissions on GitHub and click **Create GitHub App**.
|
||||
|
||||
4. GitHub redirects back to Fabro, which automatically:
|
||||
5. GitHub redirects back to Fabro, which automatically:
|
||||
- Exchanges the temporary code for permanent app credentials
|
||||
- Writes `app_id`, `client_id`, and `slug` to `~/.fabro/settings.toml`
|
||||
- Stores `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_WEBHOOK_SECRET`, and `GITHUB_APP_PRIVATE_KEY` in `<data_dir>/server.env`
|
||||
- Marks the change as restart-bound so the server must be restarted before login
|
||||
|
||||
5. **Install the app** on your GitHub account or organization. Go to `https://github.com/settings/apps/<your-app-slug>/installations` and install it on the repositories Fabro should access.
|
||||
6. **Install the app** on your GitHub account or organization. Go to `https://github.com/settings/apps/<your-app-slug>/installations` and install it on the repositories Fabro should access.
|
||||
|
||||
### Verify the configuration
|
||||
|
||||
|
|
|
|||
|
|
@ -414,14 +414,16 @@ Create a GitHub pull request from a completed workflow run. Uses the run's persi
|
|||
```bash
|
||||
fabro pr create <run-id>
|
||||
fabro pr create <run-id> --model claude-opus-4-6
|
||||
fabro pr create <run-id> --force
|
||||
```
|
||||
|
||||
| Argument / Flag | Description |
|
||||
|---|---|
|
||||
| `<RUN_ID>` | Run ID or prefix (required) |
|
||||
| `--model <MODEL>` | LLM model for generating the PR description |
|
||||
| `-f, --force` | Create PR even if the run status is not success or partial success |
|
||||
|
||||
The run must have completed successfully (or with partial success) and have a stored diff with changes.
|
||||
The run must have completed successfully (or with partial success) and have a stored diff with changes. Use `--force` to override the status check.
|
||||
|
||||
### `fabro pr list`
|
||||
|
||||
|
|
@ -823,6 +825,21 @@ fabro install --web-url https://fabro.example.com
|
|||
|---|---|---|
|
||||
| `--web-url <URL>` | Web UI base URL for OAuth callback endpoints | `http://localhost:3000` |
|
||||
|
||||
When configuring a GitHub App, the wizard detects your GitHub organizations (via the `gh` CLI) and lets you choose whether to register the app under your personal account or an organization.
|
||||
|
||||
## `fabro uninstall`
|
||||
|
||||
Remove Fabro's local state, server data, and configuration files. Stops the server if it is running, removes the data directory, cleans up shell PATH entries, and optionally removes the binary.
|
||||
|
||||
```bash
|
||||
fabro uninstall
|
||||
fabro uninstall --yes
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|---|---|
|
||||
| `--yes` | Skip confirmation prompt |
|
||||
|
||||
---
|
||||
|
||||
## `fabro secret set`
|
||||
|
|
@ -840,10 +857,11 @@ fabro secret set ANTHROPIC_API_KEY sk-ant-...
|
|||
|
||||
## `fabro secret list`
|
||||
|
||||
List server-owned secret names. Values are never returned after storage.
|
||||
List server-owned secrets with their name, type, and last-updated timestamp. Values are never returned after storage.
|
||||
|
||||
```bash
|
||||
fabro secret list
|
||||
fabro secret list --json
|
||||
```
|
||||
|
||||
## `fabro secret rm`
|
||||
|
|
|
|||
|
|
@ -672,6 +672,9 @@ 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 success/partial_success
|
||||
#[arg(short, long)]
|
||||
pub(crate) force: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
|
|
@ -21,6 +21,7 @@ use tokio::process::Command as TokioCommand;
|
|||
use crate::args::{DoctorArgs, GlobalArgs};
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config;
|
||||
|
||||
pub(crate) struct DepSpec {
|
||||
pub name: &'static str,
|
||||
|
|
@ -231,6 +232,64 @@ fn check_legacy_env(path: Option<PathBuf>) -> CheckResult {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct StorageDirStatus {
|
||||
path: PathBuf,
|
||||
exists: bool,
|
||||
readable: bool,
|
||||
writable: bool,
|
||||
}
|
||||
|
||||
fn probe_storage_dir(path: &Path) -> StorageDirStatus {
|
||||
let exists = path.is_dir();
|
||||
let readable = exists && std::fs::read_dir(path).is_ok();
|
||||
let writable = exists && tempfile::tempfile_in(path).is_ok();
|
||||
|
||||
StorageDirStatus {
|
||||
path: path.to_path_buf(),
|
||||
exists,
|
||||
readable,
|
||||
writable,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_storage_dir(status: &StorageDirStatus) -> CheckResult {
|
||||
let display = status.path.display();
|
||||
let details = vec![
|
||||
CheckDetail::new(format!(
|
||||
"Exists: {}",
|
||||
if status.exists { "yes" } else { "no" }
|
||||
)),
|
||||
CheckDetail::new(format!(
|
||||
"Readable: {}",
|
||||
if status.readable { "yes" } else { "no" }
|
||||
)),
|
||||
CheckDetail::new(format!(
|
||||
"Writable: {}",
|
||||
if status.writable { "yes" } else { "no" }
|
||||
)),
|
||||
];
|
||||
let is_healthy = status.exists && status.readable && status.writable;
|
||||
|
||||
CheckResult {
|
||||
name: "Storage directory".to_string(),
|
||||
status: if is_healthy {
|
||||
CheckStatus::Pass
|
||||
} else {
|
||||
CheckStatus::Error
|
||||
},
|
||||
summary: display.to_string(),
|
||||
details,
|
||||
remediation: if is_healthy {
|
||||
None
|
||||
} else if !status.exists {
|
||||
Some(format!("Create the directory: mkdir -p {display}"))
|
||||
} else {
|
||||
Some(format!("Fix permissions on {display}"))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn check_version_parity(server_version: &str) -> CheckResult {
|
||||
let cli_version = FABRO_VERSION;
|
||||
if server_version == cli_version {
|
||||
|
|
@ -351,6 +410,11 @@ pub(crate) async fn run_doctor(
|
|||
p.exists().then_some(p)
|
||||
};
|
||||
|
||||
let settings = user_config::load_settings().unwrap_or_default();
|
||||
let storage_dir_path = user_config::storage_dir(&settings)
|
||||
.unwrap_or_else(|_| fabro_util::Home::from_env().storage_dir());
|
||||
let storage_dir = probe_storage_dir(&storage_dir_path);
|
||||
|
||||
let mut report = CheckReport {
|
||||
title: "Fabro Doctor".to_string(),
|
||||
sections: vec![CheckSection {
|
||||
|
|
@ -362,6 +426,7 @@ pub(crate) async fn run_doctor(
|
|||
.then_some(settings_config_path),
|
||||
&legacy_config_paths,
|
||||
),
|
||||
check_storage_dir(&storage_dir),
|
||||
check_legacy_env(legacy_env_path),
|
||||
],
|
||||
}],
|
||||
|
|
@ -529,6 +594,81 @@ mod tests {
|
|||
assert!(result.summary.contains("legacy secrets file"));
|
||||
}
|
||||
|
||||
// -- check_storage_dir --
|
||||
|
||||
#[test]
|
||||
fn probe_storage_dir_existing_dir_is_readable_and_writable() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let status = probe_storage_dir(dir.path());
|
||||
|
||||
assert_eq!(status, StorageDirStatus {
|
||||
path: dir.path().to_path_buf(),
|
||||
exists: true,
|
||||
readable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_storage_dir_missing_dir_is_not_usable() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("missing");
|
||||
let status = probe_storage_dir(&path);
|
||||
|
||||
assert_eq!(status, StorageDirStatus {
|
||||
path,
|
||||
exists: false,
|
||||
readable: false,
|
||||
writable: false,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_storage_dir_pass() {
|
||||
let result = check_storage_dir(&StorageDirStatus {
|
||||
path: PathBuf::from("/home/user/.fabro"),
|
||||
exists: true,
|
||||
readable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
assert_eq!(result.status, CheckStatus::Pass);
|
||||
assert_eq!(result.summary, "/home/user/.fabro");
|
||||
assert!(result.remediation.is_none());
|
||||
assert_eq!(result.details.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_storage_dir_not_exists() {
|
||||
let result = check_storage_dir(&StorageDirStatus {
|
||||
path: PathBuf::from("/tmp/nonexistent-fabro-doctor-test-xyz"),
|
||||
exists: false,
|
||||
readable: false,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
assert_eq!(result.status, CheckStatus::Error);
|
||||
assert!(result.summary.contains("nonexistent-fabro-doctor-test-xyz"));
|
||||
assert!(result.remediation.as_deref().unwrap().contains("mkdir -p"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_storage_dir_not_writable() {
|
||||
let result = check_storage_dir(&StorageDirStatus {
|
||||
path: PathBuf::from("/home/user/.fabro"),
|
||||
exists: true,
|
||||
readable: true,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
assert_eq!(result.status, CheckStatus::Error);
|
||||
assert_eq!(result.summary, "/home/user/.fabro");
|
||||
assert_eq!(
|
||||
result.remediation.as_deref(),
|
||||
Some("Fix permissions on /home/user/.fabro")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_version_parity_warns_on_mismatch() {
|
||||
let result = check_version_parity("0.0.0-test");
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ pub(super) async fn create_command(
|
|||
|
||||
match conclusion.status {
|
||||
StageStatus::Success | StageStatus::PartialSuccess => {}
|
||||
status if args.force => {
|
||||
tracing::warn!("Run status is '{status}', proceeding because --force was specified");
|
||||
}
|
||||
status => bail!("Run status is '{status}', expected success or partial_success"),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -223,8 +223,10 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
.output
|
||||
.verbosity
|
||||
== OutputVerbosity::Verbose;
|
||||
let exit_code =
|
||||
commands::doctor::run_doctor(&args, verbose, &globals, printer).await?;
|
||||
let exit_code = Box::pin(commands::doctor::run_doctor(
|
||||
&args, verbose, &globals, printer,
|
||||
))
|
||||
.await?;
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
Commands::Discord => {
|
||||
|
|
@ -247,7 +249,7 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
}
|
||||
Commands::Repo(ns) => commands::repo::dispatch(ns, &globals, printer).await?,
|
||||
Commands::Install(args) => {
|
||||
commands::install::run_install(&args, &globals, printer).await?;
|
||||
Box::pin(commands::install::run_install(&args, &globals, printer)).await?;
|
||||
}
|
||||
Commands::Uninstall(args) => {
|
||||
commands::uninstall::run_uninstall(&args, &globals, printer).await?;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use fabro_test::{fabro_snapshot, test_context};
|
||||
|
||||
use super::support::{setup_completed_fast_dry_run, setup_created_fast_dry_run};
|
||||
use super::support::{setup_completed_fast_dry_run, setup_created_fast_dry_run, setup_failed_run};
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
|
|
@ -23,6 +23,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 success/partial_success
|
||||
--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=]
|
||||
|
|
@ -79,3 +80,35 @@ fn pr_create_uses_store_run_record_without_run_json() {
|
|||
error: Run has no run_branch — was it run with git push enabled?
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pr_create_failed_run_rejects_without_force() {
|
||||
let context = test_context!();
|
||||
let run = setup_failed_run(&context);
|
||||
let mut cmd = context.command();
|
||||
cmd.args(["pr", "create", &run.run_id]);
|
||||
|
||||
fabro_snapshot!(context.filters(), cmd, @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Run status is 'fail', expected success or partial_success
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pr_create_failed_run_proceeds_with_force() {
|
||||
let context = test_context!();
|
||||
let run = setup_failed_run(&context);
|
||||
let mut cmd = context.command();
|
||||
cmd.args(["pr", "create", "--force", &run.run_id]);
|
||||
|
||||
fabro_snapshot!(context.filters(), cmd, @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Run has no run_branch — was it run with git push enabled?
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -378,6 +378,66 @@ worktree_mode = "never"
|
|||
WorkspaceRunSetup { run, workspace_dir }
|
||||
}
|
||||
|
||||
pub(crate) fn setup_failed_run(context: &TestContext) -> RunSetup {
|
||||
let workspace_dir = context.temp_dir.join("failed-run");
|
||||
std::fs::create_dir_all(&workspace_dir)
|
||||
.unwrap_or_else(|err| panic!("failed to create {}: {err}", workspace_dir.display()));
|
||||
|
||||
write_text_file(
|
||||
&workspace_dir.join("fail.fabro"),
|
||||
r#"digraph Fail {
|
||||
graph [goal="Always fail", default_max_retries=0]
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
boom [shape=parallelogram, script="exit 1", goal_gate=true]
|
||||
start -> boom -> exit
|
||||
}
|
||||
"#,
|
||||
);
|
||||
write_text_file(
|
||||
&workspace_dir.join("run.toml"),
|
||||
r#"_version = 1
|
||||
|
||||
[workflow]
|
||||
graph = "fail.fabro"
|
||||
|
||||
[run]
|
||||
goal = "Always fail"
|
||||
|
||||
[run.sandbox]
|
||||
provider = "local"
|
||||
|
||||
[run.sandbox.local]
|
||||
worktree_mode = "never"
|
||||
"#,
|
||||
);
|
||||
|
||||
// The workflow is expected to fail (script exits 1), but the CLI may still
|
||||
// exit 0. The `pr create` tests verify that the conclusion has a fail status.
|
||||
let run_id = unique_run_id();
|
||||
let mut cmd = context.run_cmd();
|
||||
cmd.current_dir(&workspace_dir);
|
||||
cmd.timeout(COMMAND_TIMEOUT);
|
||||
cmd.env("OPENAI_API_KEY", "test");
|
||||
cmd.args([
|
||||
"--run-id",
|
||||
run_id.as_str(),
|
||||
"--auto-approve",
|
||||
"--no-retro",
|
||||
"--sandbox",
|
||||
"local",
|
||||
"--provider",
|
||||
"openai",
|
||||
"run.toml",
|
||||
]);
|
||||
let _output = cmd.output().expect("command should execute");
|
||||
|
||||
RunSetup {
|
||||
run_dir: context.find_run_dir(&run_id),
|
||||
run_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_local_workflow(context: &TestContext, workspace_dir: &Path, workflow: &str) -> RunSetup {
|
||||
let run_id = unique_run_id();
|
||||
let mut cmd = context.run_cmd();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue