From 730d252949797c55a518fe526c7ea6456934d791 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp <19+brynary@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:56:01 -0400 Subject: [PATCH 1/5] Add `--force` option for `fabro pr create` (#155) Co-authored-by: Fabro Co-authored-by: Claude Opus 4.6 (1M context) --- lib/crates/fabro-cli/src/args.rs | 3 + .../fabro-cli/src/commands/pr/create.rs | 3 + .../fabro-cli/tests/it/cmd/pr_create.rs | 35 ++++++++++- lib/crates/fabro-cli/tests/it/cmd/support.rs | 60 +++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index e2b348c23..228e50921 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -672,6 +672,9 @@ pub(crate) struct PrCreateArgs { /// LLM model for generating PR description #[arg(long)] pub(crate) model: Option, + /// Create PR even if the run status is not success/partial_success + #[arg(short, long)] + pub(crate) force: bool, } #[derive(Args)] diff --git a/lib/crates/fabro-cli/src/commands/pr/create.rs b/lib/crates/fabro-cli/src/commands/pr/create.rs index cb8ddcf2f..52cae606c 100644 --- a/lib/crates/fabro-cli/src/commands/pr/create.rs +++ b/lib/crates/fabro-cli/src/commands/pr/create.rs @@ -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"), } diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_create.rs b/lib/crates/fabro-cli/tests/it/cmd/pr_create.rs index eea4e179a..f80603aa7 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/pr_create.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/pr_create.rs @@ -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 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 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? + "); +} diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index a33c489b6..e9b023e83 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -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(); From a0c4b3b7dba422785556a397d1d23b9d17b8ee47 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp <19+brynary@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:43:30 -0400 Subject: [PATCH 2/5] Check for storage dir in `fabro doctor` (#153) Co-authored-by: Fabro Co-authored-by: Claude Opus 4.6 (1M context) --- lib/crates/fabro-cli/src/commands/doctor.rs | 142 +++++++++++++++++++- lib/crates/fabro-cli/src/main.rs | 8 +- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs index 57fe554f8..1c0870dcf 100644 --- a/lib/crates/fabro-cli/src/commands/doctor.rs +++ b/lib/crates/fabro-cli/src/commands/doctor.rs @@ -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) -> 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"); diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 768ba0ce4..3021038e1 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -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?; From 85e068ffa99e9577500350048b8a61b2d047e6a2 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 12 Apr 2026 17:07:19 -0400 Subject: [PATCH 3/5] =?UTF-8?q?docs(changelog):=20add=20entries=20for=20Ap?= =?UTF-8?q?r=207=E2=80=9312?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover embedded web UI, settings v2, events schema v2, typed secrets, fabro uninstall, unified templates, GitHub App owner selection, worker lifecycle hardening, object-backed artifacts, and other changes since Apr 6. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/changelog/watermark | 2 +- docs/changelog/2026-04-06.mdx | 11 +++++++++- docs/changelog/2026-04-07.mdx | 32 ++++++++++++++++++++++++++++++ docs/changelog/2026-04-08.mdx | 31 +++++++++++++++++++++++++++++ docs/changelog/2026-04-09.mdx | 31 +++++++++++++++++++++++++++++ docs/changelog/2026-04-10.mdx | 15 ++++++++++++++ docs/changelog/2026-04-11.mdx | 15 ++++++++++++++ docs/changelog/2026-04-12.mdx | 15 ++++++++++++++ docs/docs.json | 6 ++++++ 9 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/2026-04-07.mdx create mode 100644 docs/changelog/2026-04-08.mdx create mode 100644 docs/changelog/2026-04-09.mdx create mode 100644 docs/changelog/2026-04-10.mdx create mode 100644 docs/changelog/2026-04-11.mdx create mode 100644 docs/changelog/2026-04-12.mdx diff --git a/.claude/skills/changelog/watermark b/.claude/skills/changelog/watermark index 01c80ce3d..bd495cdc6 100644 --- a/.claude/skills/changelog/watermark +++ b/.claude/skills/changelog/watermark @@ -1 +1 @@ -6c53fc29b5f1309bfd026a883a23006db2bb441c +f67bebae4b58380b84341d57471e75a197e76e45 diff --git a/docs/changelog/2026-04-06.mdx b/docs/changelog/2026-04-06.mdx index 1a068d1d0..dcb1b3fb7 100644 --- a/docs/changelog/2026-04-06.mdx +++ b/docs/changelog/2026-04-06.mdx @@ -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 @@ -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 + + + +- Removed `fabro skill install` command (skills are now managed through workflow definitions) diff --git a/docs/changelog/2026-04-07.mdx b/docs/changelog/2026-04-07.mdx new file mode 100644 index 000000000..17c86878d --- /dev/null +++ b/docs/changelog/2026-04-07.mdx @@ -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 + + +- Removed deprecated workflows and steer endpoints + + + +- `fabro run attach` now streams events over SSE for more reliable output + + + +- Runs now record creation provenance (CLI, API, or scheduled trigger) + + + +- 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 + diff --git a/docs/changelog/2026-04-08.mdx b/docs/changelog/2026-04-08.mdx new file mode 100644 index 000000000..d4bfd153b --- /dev/null +++ b/docs/changelog/2026-04-08.mdx @@ -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 + + +- Added host-only TCP bind support for the server (e.g., `127.0.0.1:8080` without TLS) + + + +- 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 + diff --git a/docs/changelog/2026-04-09.mdx b/docs/changelog/2026-04-09.mdx new file mode 100644 index 000000000..aacb495cb --- /dev/null +++ b/docs/changelog/2026-04-09.mdx @@ -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 + + +- Server auth resolver now fails closed instead of panicking on unexpected states +- Config parse errors now include the file path for easier debugging + diff --git a/docs/changelog/2026-04-10.mdx b/docs/changelog/2026-04-10.mdx new file mode 100644 index 000000000..6726b0909 --- /dev/null +++ b/docs/changelog/2026-04-10.mdx @@ -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 + + +- Fixed an infinite loop when a goal-gate retry targeted a terminal node +- Fixed detached workers not exiting cleanly after post-run shutdown + diff --git a/docs/changelog/2026-04-11.mdx b/docs/changelog/2026-04-11.mdx new file mode 100644 index 000000000..894f6b27e --- /dev/null +++ b/docs/changelog/2026-04-11.mdx @@ -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 + + +- 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 + diff --git a/docs/changelog/2026-04-12.mdx b/docs/changelog/2026-04-12.mdx new file mode 100644 index 000000000..9dcbb32c4 --- /dev/null +++ b/docs/changelog/2026-04-12.mdx @@ -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 + + +- Added `--force` option to `fabro pr create` to skip confirmation prompts +- `fabro doctor` now checks that the storage directory exists and is writable + diff --git a/docs/docs.json b/docs/docs.json index fb1d74a40..519dbe8be 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -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", From 7f9f4e964ee258153679a7d673f8a24f2db10ec1 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 12 Apr 2026 17:07:28 -0400 Subject: [PATCH 4/5] docs: update CLI reference and GitHub integration for recent changes Add fabro uninstall, pr create --force, secret list metadata, and install owner selection to CLI reference. Add GitHub App owner selection step to integration setup flow. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/docs/watermark | 2 +- docs/integrations/github.mdx | 10 ++++++---- docs/reference/cli.mdx | 22 ++++++++++++++++++++-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.claude/skills/docs/watermark b/.claude/skills/docs/watermark index 01c80ce3d..bd495cdc6 100644 --- a/.claude/skills/docs/watermark +++ b/.claude/skills/docs/watermark @@ -1 +1 @@ -6c53fc29b5f1309bfd026a883a23006db2bb441c +f67bebae4b58380b84341d57471e75a197e76e45 diff --git a/docs/integrations/github.mdx b/docs/integrations/github.mdx index db44151c8..ff9507c16 100644 --- a/docs/integrations/github.mdx +++ b/docs/integrations/github.mdx @@ -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 `/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//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//installations` and install it on the repositories Fabro should access. ### Verify the configuration diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 60b5e3025..5fa3a6288 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -414,14 +414,16 @@ Create a GitHub pull request from a completed workflow run. Uses the run's persi ```bash fabro pr create fabro pr create --model claude-opus-4-6 +fabro pr create --force ``` | Argument / Flag | Description | |---|---| | `` | Run ID or prefix (required) | | `--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 ` | 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` From 51d764f6e3ac582b408241f411c275e56956e4a4 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 12 Apr 2026 17:52:53 -0400 Subject: [PATCH 5/5] Pin GHA action, Docker base images, and mintlify version Addresses supply chain hardening items from #160: - Pin taiki-e/install-action to commit SHA - Pin rust:1-bookworm and oven/bun:1 to image digests - Pin mintlify to 4.2.507 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/rust.yml | 4 ++-- docker/Dockerfile | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 14a738134..574389ae4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 diff --git a/docker/Dockerfile b/docker/Dockerfile index 70c2c2465..32a57261b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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