refactor: unify duplicate API types via with_replacement

Eliminate four parallel-type duplications between fabro-api generated
DTOs and fabro-types canonical types. The wire shape is owned by
OpenAPI; canonical types are reused via fabro-api/build.rs
with_replacement so the adapter functions and silent unwrap_or_default
defaults disappear.

- SecretType moves to fabro-types (was fabro-vault); deletes
  secret_type_from_api adapter.
- DiffLineStats renamed to DiffStats, moved to fabro-types, switched
  u64 -> i64 to match the OpenAPI integer; deletes line_stats_to_api.
- ManifestPreRunPushOutcome rewritten as a oneOf+discriminator
  PreRunPushOutcome over five variant schemas, deleting both
  pre_run_push_outcome_from_manifest and build_manifest_push_outcome.
- ManifestGit and PreRunGitContext unify as GitContext: dirty:
  DirtyStatus replaces clean: bool (preserving the Unknown state
  previously truncated on the wire), sha becomes Option<String>, and
  origin_url/branch fold into the unified context. RunSpec and
  RunCreatedProps flatten three fields (repo_origin_url, base_branch,
  pre_run_git) into a single git: Option<GitContext>.

Each replacement gets a fabro-api parity test (TypeId equality plus
JSON roundtrip) modeled on run_summary_round_trip.rs. TS client
regenerated.

Greenfield app, no production deployments — wire contract changed
directly without backwards-compat shims.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-28 12:29:37 -07:00
parent 7045a2d7a4
commit 642e312cca
No known key found for this signature in database
52 changed files with 937 additions and 472 deletions

1
Cargo.lock generated
View file

@ -2355,6 +2355,7 @@ name = "fabro-vault"
version = "0.217.0-nightly.0"
dependencies = [
"chrono",
"fabro-types",
"serde",
"serde_json",
"tempfile",

View file

@ -3583,7 +3583,7 @@ components:
description: CLI working directory at invocation time.
example: "/tmp/project"
git:
$ref: "#/components/schemas/ManifestGit"
$ref: "#/components/schemas/GitContext"
goal:
$ref: "#/components/schemas/ManifestGoal"
args:
@ -3599,14 +3599,13 @@ components:
additionalProperties:
$ref: "#/components/schemas/ManifestWorkflow"
ManifestGit:
description: Observable git state from the CLI working directory.
GitContext:
description: Observable git state captured before the run starts.
type: object
required:
- origin_url
- branch
- sha
- clean
- dirty
- push_outcome
properties:
origin_url:
@ -3618,17 +3617,32 @@ components:
description: Current branch name.
example: feature/foo
sha:
type: string
description: Current commit SHA.
type: ["string", "null"]
description: Current commit SHA, when known.
example: abc123def
clean:
type: boolean
description: Whether the working tree has uncommitted changes.
dirty:
$ref: "#/components/schemas/DirtyStatus"
push_outcome:
$ref: "#/components/schemas/ManifestPreRunPushOutcome"
$ref: "#/components/schemas/PreRunPushOutcome"
ManifestPreRunPushOutcome:
PreRunPushOutcome:
description: Outcome of the CLI's best-effort pre-run push.
oneOf:
- $ref: "#/components/schemas/PreRunPushOutcomeNotAttempted"
- $ref: "#/components/schemas/PreRunPushOutcomeSucceeded"
- $ref: "#/components/schemas/PreRunPushOutcomeFailed"
- $ref: "#/components/schemas/PreRunPushOutcomeSkippedNoRemote"
- $ref: "#/components/schemas/PreRunPushOutcomeSkippedRemoteMismatch"
discriminator:
propertyName: type
mapping:
not_attempted: "#/components/schemas/PreRunPushOutcomeNotAttempted"
succeeded: "#/components/schemas/PreRunPushOutcomeSucceeded"
failed: "#/components/schemas/PreRunPushOutcomeFailed"
skipped_no_remote: "#/components/schemas/PreRunPushOutcomeSkippedNoRemote"
skipped_remote_mismatch: "#/components/schemas/PreRunPushOutcomeSkippedRemoteMismatch"
PreRunPushOutcomeNotAttempted:
type: object
required:
- type
@ -3637,18 +3651,67 @@ components:
type: string
enum:
- not_attempted
PreRunPushOutcomeSucceeded:
type: object
required:
- type
- remote
- branch
properties:
type:
type: string
enum:
- succeeded
remote:
type: string
branch:
type: string
PreRunPushOutcomeFailed:
type: object
required:
- type
- remote
- branch
- message
properties:
type:
type: string
enum:
- failed
remote:
type: string
branch:
type: string
message:
type: string
PreRunPushOutcomeSkippedNoRemote:
type: object
required:
- type
properties:
type:
type: string
enum:
- skipped_no_remote
PreRunPushOutcomeSkippedRemoteMismatch:
type: object
required:
- type
- remote
- repo_origin_url
properties:
type:
type: string
enum:
- skipped_remote_mismatch
remote:
type: ["string", "null"]
branch:
type: ["string", "null"]
message:
type: ["string", "null"]
type: string
repo_origin_url:
type: ["string", "null"]
type: string
ManifestGoal:
description: Resolved goal with provenance.
@ -4466,43 +4529,6 @@ components:
- dirty
- unknown
PreRunPushOutcome:
description: Outcome of the CLI submitter's best-effort pre-run push.
type: object
required:
- type
properties:
type:
type: string
enum:
- not_attempted
- succeeded
- failed
- skipped_no_remote
- skipped_remote_mismatch
remote:
type: ["string", "null"]
branch:
type: ["string", "null"]
message:
type: ["string", "null"]
repo_origin_url:
type: ["string", "null"]
PreRunGitContext:
description: Submitter-side git context captured before run creation.
type: object
required:
- local_dirty
- push_outcome
properties:
display_base_sha:
type: ["string", "null"]
local_dirty:
$ref: "#/components/schemas/DirtyStatus"
push_outcome:
$ref: "#/components/schemas/PreRunPushOutcome"
ForkSourceRef:
description: Source checkpoint used to initialize a forked or rewound run.
type: object
@ -4535,10 +4561,6 @@ components:
type: ["string", "null"]
source_directory:
type: ["string", "null"]
repo_origin_url:
type: ["string", "null"]
base_branch:
type: ["string", "null"]
labels:
type: object
additionalProperties:
@ -4550,9 +4572,9 @@ components:
type: ["string", "null"]
definition_blob:
type: ["string", "null"]
pre_run_git:
git:
oneOf:
- $ref: "#/components/schemas/PreRunGitContext"
- $ref: "#/components/schemas/GitContext"
- type: "null"
fork_source_ref:
oneOf:

View file

@ -318,6 +318,11 @@ fn main() {
"fabro_types::settings::run::MergeStrategy",
&[],
),
("SecretType", "fabro_types::SecretType", &[]),
("DiffStats", "fabro_types::DiffStats", &[]),
("PreRunPushOutcome", "fabro_types::PreRunPushOutcome", &[]),
("DirtyStatus", "fabro_types::DirtyStatus", &[]),
("GitContext", "fabro_types::GitContext", &[]),
];
for (name, path, impls) in replacements {
settings.with_replacement(*name, *path, impls.iter().copied());

View file

@ -27,7 +27,10 @@ pub mod types {
pub use fabro_types::status::{
BlockedReason, FailureReason, RunControlAction, RunStatus, SuccessReason, TerminalStatus,
};
pub use fabro_types::{RepositoryReference, RunSummary, ServerSettings, WorkflowSettings};
pub use fabro_types::{
DiffStats, DirtyStatus, GitContext, PreRunPushOutcome, RepositoryReference, RunSummary,
SecretType, ServerSettings, WorkflowSettings,
};
pub use crate::generated::types::*;
}

View file

@ -0,0 +1,46 @@
use std::any::{TypeId, type_name};
use fabro_api::types::DiffStats as ApiDiffStats;
use fabro_types::DiffStats;
use serde_json::json;
#[test]
fn diff_stats_reuses_canonical_type() {
assert_same_type::<ApiDiffStats, DiffStats>();
}
#[test]
fn diff_stats_serializes_with_required_integer_fields() {
let stats = DiffStats {
additions: 567,
deletions: 234,
};
assert_eq!(
serde_json::to_value(stats).unwrap(),
json!({
"additions": 567,
"deletions": 234,
})
);
}
#[test]
fn diff_stats_deserializes_from_required_payload() {
let stats: DiffStats = serde_json::from_value(json!({
"additions": 1,
"deletions": 0,
}))
.unwrap();
assert_eq!(stats.additions, 1);
assert_eq!(stats.deletions, 0);
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -0,0 +1,101 @@
use std::any::{TypeId, type_name};
use fabro_api::types::{
DirtyStatus as ApiDirtyStatus, GitContext as ApiGitContext,
PreRunPushOutcome as ApiPreRunPushOutcome,
};
use fabro_types::{DirtyStatus, GitContext, PreRunPushOutcome};
use serde_json::json;
#[test]
fn git_context_reuses_canonical_types() {
assert_same_type::<ApiGitContext, GitContext>();
assert_same_type::<ApiDirtyStatus, DirtyStatus>();
assert_same_type::<ApiPreRunPushOutcome, PreRunPushOutcome>();
}
#[test]
fn dirty_status_serializes_with_snake_case_strings() {
assert_eq!(
serde_json::to_value(DirtyStatus::Clean).unwrap(),
json!("clean")
);
assert_eq!(
serde_json::to_value(DirtyStatus::Dirty).unwrap(),
json!("dirty")
);
assert_eq!(
serde_json::to_value(DirtyStatus::Unknown).unwrap(),
json!("unknown")
);
}
#[test]
fn git_context_with_known_sha_round_trips() {
let ctx = GitContext {
origin_url: "https://github.com/acme/widgets".to_string(),
branch: "main".to_string(),
sha: Some("abc123".to_string()),
dirty: DirtyStatus::Clean,
push_outcome: PreRunPushOutcome::Succeeded {
remote: "origin".to_string(),
branch: "main".to_string(),
},
};
let json = serde_json::to_value(&ctx).unwrap();
assert_eq!(
json,
json!({
"origin_url": "https://github.com/acme/widgets",
"branch": "main",
"sha": "abc123",
"dirty": "clean",
"push_outcome": {
"type": "succeeded",
"remote": "origin",
"branch": "main",
},
})
);
let round_trip: GitContext = serde_json::from_value(json).unwrap();
assert_eq!(round_trip, ctx);
}
#[test]
fn git_context_omits_absent_sha_on_serialize() {
let ctx = GitContext {
origin_url: "https://github.com/acme/widgets".to_string(),
branch: "feature/foo".to_string(),
sha: None,
dirty: DirtyStatus::Unknown,
push_outcome: PreRunPushOutcome::SkippedNoRemote,
};
let json = serde_json::to_value(&ctx).unwrap();
assert!(json.get("sha").is_none());
assert_eq!(json["dirty"], "unknown");
assert_eq!(json["push_outcome"]["type"], "skipped_no_remote");
}
#[test]
fn git_context_deserializes_when_sha_is_absent() {
let ctx: GitContext = serde_json::from_value(json!({
"origin_url": "https://github.com/acme/widgets",
"branch": "main",
"dirty": "dirty",
"push_outcome": { "type": "not_attempted" },
}))
.unwrap();
assert_eq!(ctx.sha, None);
assert_eq!(ctx.dirty, DirtyStatus::Dirty);
assert_eq!(ctx.push_outcome, PreRunPushOutcome::NotAttempted);
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -0,0 +1,128 @@
use std::any::{TypeId, type_name};
use fabro_api::types::PreRunPushOutcome as ApiPreRunPushOutcome;
use fabro_types::PreRunPushOutcome;
use serde_json::json;
#[test]
fn pre_run_push_outcome_reuses_canonical_type() {
assert_same_type::<ApiPreRunPushOutcome, PreRunPushOutcome>();
}
#[test]
fn singletons_serialize_with_only_a_type_field() {
assert_eq!(
serde_json::to_value(PreRunPushOutcome::NotAttempted).unwrap(),
json!({ "type": "not_attempted" })
);
assert_eq!(
serde_json::to_value(PreRunPushOutcome::SkippedNoRemote).unwrap(),
json!({ "type": "skipped_no_remote" })
);
}
#[test]
fn succeeded_carries_remote_and_branch() {
let outcome = PreRunPushOutcome::Succeeded {
remote: "origin".to_string(),
branch: "feature/foo".to_string(),
};
assert_eq!(
serde_json::to_value(&outcome).unwrap(),
json!({
"type": "succeeded",
"remote": "origin",
"branch": "feature/foo",
})
);
}
#[test]
fn failed_carries_message_alongside_remote_and_branch() {
let outcome = PreRunPushOutcome::Failed {
remote: "origin".to_string(),
branch: "feature/foo".to_string(),
message: "permission denied".to_string(),
};
assert_eq!(
serde_json::to_value(&outcome).unwrap(),
json!({
"type": "failed",
"remote": "origin",
"branch": "feature/foo",
"message": "permission denied",
})
);
}
#[test]
fn skipped_remote_mismatch_carries_remote_and_repo_origin_url() {
let outcome = PreRunPushOutcome::SkippedRemoteMismatch {
remote: "git@github.com:user/fork.git".to_string(),
repo_origin_url: "https://github.com/acme/canonical.git".to_string(),
};
assert_eq!(
serde_json::to_value(&outcome).unwrap(),
json!({
"type": "skipped_remote_mismatch",
"remote": "git@github.com:user/fork.git",
"repo_origin_url": "https://github.com/acme/canonical.git",
})
);
}
#[test]
fn deserializes_each_variant_from_discriminator_payloads() {
let not_attempted: PreRunPushOutcome =
serde_json::from_value(json!({ "type": "not_attempted" })).unwrap();
assert_eq!(not_attempted, PreRunPushOutcome::NotAttempted);
let succeeded: PreRunPushOutcome = serde_json::from_value(json!({
"type": "succeeded",
"remote": "origin",
"branch": "main",
}))
.unwrap();
assert_eq!(succeeded, PreRunPushOutcome::Succeeded {
remote: "origin".to_string(),
branch: "main".to_string(),
});
let failed: PreRunPushOutcome = serde_json::from_value(json!({
"type": "failed",
"remote": "origin",
"branch": "main",
"message": "denied",
}))
.unwrap();
assert_eq!(failed, PreRunPushOutcome::Failed {
remote: "origin".to_string(),
branch: "main".to_string(),
message: "denied".to_string(),
});
let skipped_no_remote: PreRunPushOutcome =
serde_json::from_value(json!({ "type": "skipped_no_remote" })).unwrap();
assert_eq!(skipped_no_remote, PreRunPushOutcome::SkippedNoRemote);
let skipped_mismatch: PreRunPushOutcome = serde_json::from_value(json!({
"type": "skipped_remote_mismatch",
"remote": "git@github.com:user/fork.git",
"repo_origin_url": "https://github.com/acme/canonical.git",
}))
.unwrap();
assert_eq!(skipped_mismatch, PreRunPushOutcome::SkippedRemoteMismatch {
remote: "git@github.com:user/fork.git".to_string(),
repo_origin_url: "https://github.com/acme/canonical.git".to_string(),
});
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -0,0 +1,46 @@
use std::any::{TypeId, type_name};
use fabro_api::types::SecretType as ApiSecretType;
use fabro_types::SecretType;
use serde_json::json;
#[test]
fn secret_type_reuses_canonical_type() {
assert_same_type::<ApiSecretType, SecretType>();
}
#[test]
fn secret_type_serializes_as_snake_case_strings() {
assert_eq!(
serde_json::to_value(SecretType::Environment).unwrap(),
json!("environment")
);
assert_eq!(
serde_json::to_value(SecretType::File).unwrap(),
json!("file")
);
assert_eq!(
serde_json::to_value(SecretType::Credential).unwrap(),
json!("credential")
);
}
#[test]
fn secret_type_deserializes_each_variant() {
let env: SecretType = serde_json::from_value(json!("environment")).unwrap();
assert_eq!(env, SecretType::Environment);
let file: SecretType = serde_json::from_value(json!("file")).unwrap();
assert_eq!(file, SecretType::File);
let cred: SecretType = serde_json::from_value(json!("credential")).unwrap();
assert_eq!(cred, SecretType::Credential);
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -31,7 +31,7 @@ pub(crate) async fn ensure_origin_if_local(
let state = client.get_run_state(run_id).await?;
if let Some(run_spec) = state.spec {
ensure_matching_repo_origin(run_spec.repo_origin_url.as_deref(), verb)?;
ensure_matching_repo_origin(run_spec.repo_origin_url(), verb)?;
}
Ok(())
}

View file

@ -7,14 +7,14 @@ use std::collections::{HashMap, HashSet};
use std::path::{Component, Path, PathBuf};
use anyhow::{Context, Result, anyhow};
use fabro_api::types::{self, ManifestPreRunPushOutcome, ManifestPreRunPushOutcomeType};
use fabro_api::types;
use fabro_config::project::{self, discover_project_config, resolve_workflow_path};
use fabro_config::run::{resolve_run_goal_from_layer, resolve_run_goal_from_namespace};
use fabro_config::{CliLayer, DaytonaDockerfileLayer, RunLayer, WorkflowSettingsBuilder};
use fabro_graphviz::graph::AttrValue;
use fabro_graphviz::parser;
use fabro_types::settings::run::{ResolvedGoalSource, ResolvedRunGoal};
use fabro_types::{RunId, WorkflowSettings};
use fabro_types::{DirtyStatus, GitContext, PreRunPushOutcome, RunId, WorkflowSettings};
use fabro_workflow::git::{GitSyncStatus, branch_needs_push, head_sha, push_branch, sync_status};
use crate::args::{PreflightArgs, RunArgs};
@ -138,7 +138,7 @@ pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result<BuiltManif
)?;
let configured_repo_origin_url = configured_repo_origin_url(&workflow_settings);
let git = build_manifest_git(&working_directory, configured_repo_origin_url.as_deref());
let git = build_git_context(&working_directory, configured_repo_origin_url.as_deref());
let args = input.args.filter(|args| !manifest_args_is_empty(args));
Ok(BuiltManifest {
@ -498,14 +498,16 @@ fn resolved_goal_to_manifest(resolved: ResolvedRunGoal) -> types::ManifestGoal {
}
}
fn build_manifest_git(
fn build_git_context(
repo_path: &Path,
configured_repo_origin_url: Option<&str>,
) -> Option<types::ManifestGit> {
) -> Option<GitContext> {
let (origin_url, branch) = detect_manifest_repo_info(repo_path)?;
let sha = head_sha(repo_path).ok()?;
let status = sync_status(repo_path, "origin", Some(&branch));
let clean = status != GitSyncStatus::Dirty;
let sha = head_sha(repo_path).ok();
let dirty = match sync_status(repo_path, "origin", Some(&branch)) {
GitSyncStatus::Dirty => DirtyStatus::Dirty,
GitSyncStatus::Synced | GitSyncStatus::Unsynced => DirtyStatus::Clean,
};
let repo_origin_url = configured_repo_origin_url
.map(fabro_github::normalize_repo_origin_url)
.filter(|url| !url.is_empty())
@ -522,12 +524,12 @@ fn build_manifest_git(
origin_url.as_deref(),
configured_repo_origin_url,
);
Some(types::ManifestGit {
branch,
clean,
Some(GitContext {
origin_url: repo_origin_url,
push_outcome,
branch,
sha,
dirty,
push_outcome,
})
}
@ -565,58 +567,37 @@ fn build_manifest_push_outcome(
branch: &str,
origin_url: Option<&str>,
configured_repo_origin_url: Option<&str>,
) -> ManifestPreRunPushOutcome {
if origin_url.is_none() {
return ManifestPreRunPushOutcome {
type_: ManifestPreRunPushOutcomeType::SkippedNoRemote,
remote: None,
branch: Some(branch.to_string()),
message: None,
repo_origin_url: None,
};
}
) -> PreRunPushOutcome {
let Some(origin_url) = origin_url else {
return PreRunPushOutcome::SkippedNoRemote;
};
if let Some(repo_origin_url) = configured_repo_origin_url
.map(fabro_github::normalize_repo_origin_url)
.filter(|url| !url.is_empty())
{
let remote = origin_url
.map(fabro_github::normalize_repo_origin_url)
.unwrap_or_default();
let remote = fabro_github::normalize_repo_origin_url(origin_url);
if remote != repo_origin_url {
return ManifestPreRunPushOutcome {
type_: ManifestPreRunPushOutcomeType::SkippedRemoteMismatch,
remote: Some(remote),
branch: Some(branch.to_string()),
message: None,
repo_origin_url: Some(repo_origin_url),
return PreRunPushOutcome::SkippedRemoteMismatch {
remote,
repo_origin_url,
};
}
}
if !branch_needs_push(repo_path, "origin", branch) {
return ManifestPreRunPushOutcome {
type_: ManifestPreRunPushOutcomeType::NotAttempted,
remote: None,
branch: None,
message: None,
repo_origin_url: None,
};
return PreRunPushOutcome::NotAttempted;
}
match push_branch(repo_path, "origin", branch) {
Ok(()) => ManifestPreRunPushOutcome {
type_: ManifestPreRunPushOutcomeType::Succeeded,
remote: Some("origin".to_string()),
branch: Some(branch.to_string()),
message: None,
repo_origin_url: None,
Ok(()) => PreRunPushOutcome::Succeeded {
remote: "origin".to_string(),
branch: branch.to_string(),
},
Err(err) => ManifestPreRunPushOutcome {
type_: ManifestPreRunPushOutcomeType::Failed,
remote: Some("origin".to_string()),
branch: Some(branch.to_string()),
message: Some(err.to_string()),
repo_origin_url: None,
Err(err) => PreRunPushOutcome::Failed {
remote: "origin".to_string(),
branch: branch.to_string(),
message: err.to_string(),
},
}
}
@ -1018,18 +999,10 @@ repository = "target"
.git
.expect("manifest git info should be detected");
assert_eq!(git.origin_url, "https://github.com/example/target");
assert_eq!(
git.push_outcome.type_,
ManifestPreRunPushOutcomeType::SkippedRemoteMismatch
);
assert_eq!(
git.push_outcome.remote.as_deref(),
Some("https://github.com/user/forked-target")
);
assert_eq!(
git.push_outcome.repo_origin_url.as_deref(),
Some("https://github.com/example/target")
);
assert_eq!(git.push_outcome, PreRunPushOutcome::SkippedRemoteMismatch {
remote: "https://github.com/user/forked-target".to_string(),
repo_origin_url: "https://github.com/example/target".to_string(),
});
}
fn init_git_repo(path: &Path, branch: &str, origin_url: &str) {

View file

@ -34,8 +34,8 @@ use fabro_sandbox::reconnect::reconnect;
use fabro_static::EnvVars;
use fabro_types::RunId;
use fabro_workflow::sandbox_git::{
DiffError, DiffLineStats, DiffNumstat, RawDiffEntry, SubmoduleChange, SymlinkChange,
list_changed_files_raw, list_diff_numstat, stream_blob_metadata, stream_blobs,
DiffError, DiffNumstat, RawDiffEntry, SubmoduleChange, SymlinkChange, list_changed_files_raw,
list_diff_numstat, stream_blob_metadata, stream_blobs,
};
use futures_util::FutureExt;
use serde::Deserialize;
@ -373,13 +373,6 @@ async fn materialize_sandbox_path(state: &Arc<AppState>, run_id: &RunId) -> List
})
}
fn line_stats_to_api(stats: DiffLineStats) -> DiffStats {
DiffStats {
additions: i64::try_from(stats.additions).unwrap_or(i64::MAX),
deletions: i64::try_from(stats.deletions).unwrap_or(i64::MAX),
}
}
/// Choose a degraded reason given the current projection. Docker-provider
/// runs aren't supported by the deployed server; completed runs are "gone";
/// everything else is a transient "unreachable" (sandbox may come back).
@ -451,7 +444,7 @@ fn build_fallback_response(
/// summation we use on the live-sandbox path so the toolbar shows the same
/// numbers regardless of which response branch we took.
fn patch_to_stats(patch: &str) -> DiffStats {
let mut stats = DiffLineStats::default();
let mut stats = DiffStats::default();
for section in patch.split("diff --git ").skip(1) {
if patch_section_omits_line_stats(section) {
continue;
@ -467,7 +460,7 @@ fn patch_to_stats(patch: &str) -> DiffStats {
}
}
}
line_stats_to_api(stats)
stats
}
fn patch_section_omits_line_stats(section: &str) -> bool {
@ -729,7 +722,7 @@ fn classify_entries(
is_sensitive_fn: fn(&str) -> bool,
) -> ClassifiedEntries {
let mut out = Vec::with_capacity(raw.len());
let mut stats = DiffLineStats::default();
let mut stats = DiffStats::default();
for entry in raw {
let (new_path, old_path) = match entry {
@ -786,7 +779,7 @@ fn classify_entries(
ClassifiedEntries {
entries: out,
stats: line_stats_to_api(stats),
stats,
}
}
@ -1638,7 +1631,7 @@ diff --git a/src/main.rs b/src/main.rs
] {
numstat
.line_stats_by_path
.insert(path.to_string(), DiffLineStats {
.insert(path.to_string(), DiffStats {
additions,
deletions,
});

View file

@ -27,7 +27,7 @@ use fabro_types::settings::run::{
ApprovalMode, DaytonaNetworkLayer, DaytonaSettings, DockerSettings, DockerfileSource, RunGoal,
RunMode, RunNamespace, WorktreeMode,
};
use fabro_types::{DirtyStatus, PreRunGitContext, PreRunPushOutcome, RunId, WorkflowSettings};
use fabro_types::{RunId, WorkflowSettings};
use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};
use fabro_validate::Severity;
use fabro_workflow::Error as WorkflowError;
@ -41,7 +41,7 @@ use crate::server::AppState;
#[derive(Clone)]
pub(crate) struct PreparedManifest {
pub cwd: PathBuf,
pub git: Option<types::ManifestGit>,
pub git: Option<types::GitContext>,
pub root_source: String,
pub run_id: Option<RunId>,
pub settings: WorkflowSettings,
@ -165,12 +165,7 @@ pub(crate) fn create_run_input(
workflow_bundle: Some(prepared.workflow_bundle),
submitted_manifest_bytes: None,
run_id: prepared.run_id,
repo_origin_url: prepared.git.as_ref().and_then(|git| {
let origin_url = fabro_github::normalize_repo_origin_url(&git.origin_url);
(!origin_url.is_empty()).then_some(origin_url)
}),
base_branch: prepared.git.as_ref().map(|git| git.branch.clone()),
pre_run_git: prepared.git.as_ref().map(pre_run_git_from_manifest),
git: prepared.git,
fork_source_ref: None,
in_place: prepared.in_place,
provenance: None,
@ -178,48 +173,6 @@ pub(crate) fn create_run_input(
}
}
fn pre_run_git_from_manifest(git: &types::ManifestGit) -> PreRunGitContext {
PreRunGitContext {
display_base_sha: Some(git.sha.clone()),
local_dirty: if git.clean {
DirtyStatus::Clean
} else {
DirtyStatus::Dirty
},
push_outcome: pre_run_push_outcome_from_manifest(&git.push_outcome),
}
}
fn pre_run_push_outcome_from_manifest(
outcome: &types::ManifestPreRunPushOutcome,
) -> PreRunPushOutcome {
match outcome.type_ {
types::ManifestPreRunPushOutcomeType::NotAttempted => PreRunPushOutcome::NotAttempted,
types::ManifestPreRunPushOutcomeType::Succeeded => PreRunPushOutcome::Succeeded {
remote: outcome
.remote
.clone()
.unwrap_or_else(|| "origin".to_string()),
branch: outcome.branch.clone().unwrap_or_default(),
},
types::ManifestPreRunPushOutcomeType::Failed => PreRunPushOutcome::Failed {
remote: outcome
.remote
.clone()
.unwrap_or_else(|| "origin".to_string()),
branch: outcome.branch.clone().unwrap_or_default(),
message: outcome.message.clone().unwrap_or_default(),
},
types::ManifestPreRunPushOutcomeType::SkippedNoRemote => PreRunPushOutcome::SkippedNoRemote,
types::ManifestPreRunPushOutcomeType::SkippedRemoteMismatch => {
PreRunPushOutcome::SkippedRemoteMismatch {
remote: outcome.remote.clone().unwrap_or_default(),
repo_origin_url: outcome.repo_origin_url.clone().unwrap_or_default(),
}
}
}
}
pub(crate) async fn run_preflight(
state: &AppState,
prepared: &PreparedManifest,
@ -565,13 +518,19 @@ fn base_preflight_checks(prepared: &PreparedManifest, graph: &Graph) -> Vec<Chec
CheckDetail {
text: format!(
"Git: {}",
prepared.git.as_ref().map_or("unknown", |git| if git.clean {
"clean"
} else {
"dirty"
})
prepared
.git
.as_ref()
.map_or("unknown", |git| match git.dirty {
fabro_types::DirtyStatus::Clean => "clean",
fabro_types::DirtyStatus::Dirty => "dirty",
fabro_types::DirtyStatus::Unknown => "unknown",
})
),
warn: prepared.git.as_ref().is_some_and(|git| !git.clean),
warn: prepared
.git
.as_ref()
.is_some_and(|git| git.dirty != fabro_types::DirtyStatus::Clean),
},
],
remediation: None,

View file

@ -35,9 +35,9 @@ pub use fabro_api::types::{
QuestionType as ApiQuestionType, RenderWorkflowGraphDirection, RenderWorkflowGraphRequest,
RewindRequest, RewindResponse, RunArtifactEntry, RunArtifactListResponse, RunBilling,
RunBillingStage, RunBillingTotals, RunError, RunManifest, RunStage, RunStatusResponse,
SandboxFileEntry, SandboxFileListResponse, SecretType as ApiSecretType, SshAccessRequest,
SshAccessResponse, StageStatus as ApiStageStatus, StartRunRequest, SubmitAnswerRequest,
SystemFeatures, SystemInfoResponse, SystemRunCounts, TimelineEntryResponse, WriteBlobResponse,
SandboxFileEntry, SandboxFileListResponse, SshAccessRequest, SshAccessResponse,
StageStatus as ApiStageStatus, StartRunRequest, SubmitAnswerRequest, SystemFeatures,
SystemInfoResponse, SystemRunCounts, TimelineEntryResponse, WriteBlobResponse,
};
use fabro_auth::{
CredentialSource, VaultCredentialSource, auth_issue_message, parse_credential_secret,
@ -1826,20 +1826,12 @@ async fn list_secrets(_auth: AuthenticatedService, State(state): State<Arc<AppSt
(StatusCode::OK, Json(serde_json::json!({ "data": data }))).into_response()
}
fn secret_type_from_api(secret_type: ApiSecretType) -> SecretType {
match secret_type {
ApiSecretType::Environment => SecretType::Environment,
ApiSecretType::File => SecretType::File,
ApiSecretType::Credential => SecretType::Credential,
}
}
async fn create_secret(
_auth: AuthenticatedService,
State(state): State<Arc<AppState>>,
Json(body): Json<CreateSecretRequest>,
) -> Response {
let secret_type = secret_type_from_api(body.type_);
let secret_type = body.type_;
let name = body.name;
let value = body.value;
let description = body.description;
@ -5407,14 +5399,14 @@ impl<'a> RunPrInputs<'a> {
"Run spec missing from store.",
)
})?;
let origin_url = run_spec.repo_origin_url.as_deref().ok_or_else(|| {
let origin_url = run_spec.repo_origin_url().ok_or_else(|| {
ApiError::with_code(
StatusCode::BAD_REQUEST,
"Run has no repo origin URL — pull request creation requires git metadata.",
"missing_repo_origin",
)
})?;
let base_branch = run_spec.base_branch.as_deref().ok_or_else(|| {
let base_branch = run_spec.base_branch().ok_or_else(|| {
ApiError::with_code(
StatusCode::BAD_REQUEST,
"Run has no base branch — pull request creation requires git metadata.",
@ -9724,19 +9716,27 @@ strategy = "token"
"goal".to_string(),
AttrValue::String("Ship the server-side PR".to_string()),
);
let git = match (repo_origin_url, base_branch) {
(Some(origin), Some(branch)) => Some(fabro_types::GitContext {
origin_url: origin.to_string(),
branch: branch.to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
_ => None,
};
let run_spec = RunSpec {
run_id,
settings: fabro_types::WorkflowSettings::default(),
graph,
workflow_slug: Some("test".to_string()),
source_directory: Some("/tmp/project".to_string()),
repo_origin_url: repo_origin_url.map(str::to_string),
base_branch: base_branch.map(str::to_string),
git: git.clone(),
labels: HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
};
@ -9751,13 +9751,11 @@ strategy = "token"
labels: run_spec.labels.clone().into_iter().collect(),
run_dir: run_spec.source_directory.clone().unwrap_or_default(),
source_directory: run_spec.source_directory.clone(),
repo_origin_url: run_spec.repo_origin_url.clone(),
base_branch: run_spec.base_branch.clone(),
workflow_slug: run_spec.workflow_slug.clone(),
db_prefix: None,
provenance: run_spec.provenance.clone(),
manifest_blob: None,
pre_run_git: None,
git,
fork_source_ref: None,
in_place: false,
},

View file

@ -54,13 +54,11 @@ impl RunProjectionReducer for RunProjection {
graph: props.graph.clone(),
workflow_slug: props.workflow_slug.clone(),
source_directory: props.source_directory.clone(),
repo_origin_url: props.repo_origin_url.clone(),
base_branch: props.base_branch.clone(),
labels,
provenance: props.provenance.clone(),
manifest_blob: props.manifest_blob,
definition_blob: None,
pre_run_git: props.pre_run_git.clone(),
git: props.git.clone(),
fork_source_ref: props.fork_source_ref.clone(),
in_place: props.in_place,
});
@ -397,7 +395,8 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> RunSummary
state
.spec
.as_ref()
.and_then(|spec| spec.repo_origin_url.clone()),
.and_then(|spec| spec.git.as_ref())
.map(|git| git.origin_url.clone()),
state.start.as_ref().map(|start| start.start_time),
state.status.unwrap_or(RunStatus::Submitted),
state.pending_control,
@ -990,13 +989,11 @@ mod tests {
graph: fabro_types::Graph::new("test"),
workflow_slug: Some("test".to_string()),
source_directory: Some("/tmp/repo".to_string()),
repo_origin_url: None,
base_branch: None,
git: None,
labels: HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
});

View file

@ -357,13 +357,17 @@ mod tests {
graph,
workflow_slug: Some("night-sky".to_string()),
source_directory: Some(format!("/tmp/{label}")),
repo_origin_url: Some("https://github.com/fabro-sh/fabro".to_string()),
base_branch: Some("main".to_string()),
labels: std::collections::HashMap::from([("team".to_string(), "infra".to_string())]),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
git: Some(fabro_types::GitContext {
origin_url: "https://github.com/fabro-sh/fabro".to_string(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
}
@ -400,8 +404,7 @@ mod tests {
"workflow_slug": run_spec.workflow_slug,
"source_directory": run_spec.source_directory,
"run_dir": format!("/tmp/{label}"),
"repo_origin_url": run_spec.repo_origin_url,
"base_branch": run_spec.base_branch,
"git": run_spec.git,
"labels": run_spec.labels,
}),
))

View file

@ -17,13 +17,17 @@ fn sample_run_spec() -> RunSpec {
graph: Graph::new("ship"),
workflow_slug: Some("demo".to_string()),
source_directory: Some("/tmp/project".to_string()),
repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()),
base_branch: Some("main".to_string()),
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
git: Some(fabro_types::GitContext {
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
}

View file

@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiffStats {
pub additions: i64,
pub deletions: i64,
}

View file

@ -7,6 +7,7 @@ pub mod blob_ref;
pub mod checkpoint;
pub mod conclusion;
pub mod dense;
pub mod diff;
pub mod event_envelope;
pub mod failure_signature;
pub mod graph;
@ -23,6 +24,7 @@ pub mod run_id;
pub mod run_projection;
pub mod run_summary;
pub mod sandbox_record;
pub mod secret;
pub mod settings;
pub mod stage_id;
pub mod start;
@ -42,6 +44,7 @@ pub use blob_ref::{
pub use checkpoint::Checkpoint;
pub use conclusion::{Conclusion, StageSummary};
pub use dense::{ServerSettings, UserSettings, WorkflowSettings};
pub use diff::DiffStats;
pub use event_envelope::EventEnvelope;
pub use failure_signature::FailureSignature;
pub use graph::{AttrValue, Edge, Graph, Node, is_llm_handler_type, shape_to_handler_type};
@ -57,8 +60,8 @@ pub use retro::{
OpenItemKind, Retro, RetroNarrative, SmoothnessRating, StageRetro,
};
pub use run::{
DirtyStatus, ForkSourceRef, PreRunGitContext, PreRunPushOutcome, RunAuthMethod,
RunClientProvenance, RunProvenance, RunServerProvenance, RunSpec, RunSubjectProvenance,
DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunAuthMethod, RunClientProvenance,
RunProvenance, RunServerProvenance, RunSpec, RunSubjectProvenance,
};
pub use run_blob_id::RunBlobId;
pub use run_event::{ActorKind, ActorRef, EventBody, RunEvent, RunNoticeLevel};
@ -66,6 +69,7 @@ pub use run_id::{RunId, fixtures};
pub use run_projection::{NodeState, PendingInterviewRecord, RunProjection};
pub use run_summary::RunSummary;
pub use sandbox_record::SandboxRecord;
pub use secret::SecretType;
pub use stage_id::{ParallelBranchId, StageId};
pub use start::StartRecord;
pub use status::{

View file

@ -76,11 +76,13 @@ pub enum PreRunPushOutcome {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PreRunGitContext {
pub struct GitContext {
pub origin_url: String,
pub branch: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_base_sha: Option<String>,
pub local_dirty: DirtyStatus,
pub push_outcome: PreRunPushOutcome,
pub sha: Option<String>,
pub dirty: DirtyStatus,
pub push_outcome: PreRunPushOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -98,10 +100,6 @@ pub struct RunSpec {
pub workflow_slug: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_directory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repo_origin_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_branch: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub labels: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -111,7 +109,7 @@ pub struct RunSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub definition_blob: Option<RunBlobId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pre_run_git: Option<PreRunGitContext>,
pub git: Option<GitContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fork_source_ref: Option<ForkSourceRef>,
#[serde(default)]
@ -151,17 +149,17 @@ impl RunSpec {
#[must_use]
pub fn repo_origin_url(&self) -> Option<&str> {
self.repo_origin_url.as_deref()
self.git.as_ref().map(|git| git.origin_url.as_str())
}
#[must_use]
pub fn base_branch(&self) -> Option<&str> {
self.base_branch.as_deref()
self.git.as_ref().map(|git| git.branch.as_str())
}
#[must_use]
pub fn pre_run_git(&self) -> Option<&PreRunGitContext> {
self.pre_run_git.as_ref()
pub fn git(&self) -> Option<&GitContext> {
self.git.as_ref()
}
#[must_use]

View file

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use super::{ActorRef, BilledTokenCounts, RunNoticeLevel};
use crate::status::{BlockedReason, FailureReason, SuccessReason};
use crate::{
ForkSourceRef, Graph, PreRunGitContext, RunBlobId, RunControlAction, RunId, RunProvenance,
ForkSourceRef, GitContext, Graph, RunBlobId, RunControlAction, RunId, RunProvenance,
WorkflowSettings,
};
@ -23,10 +23,6 @@ pub struct RunCreatedProps {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_directory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repo_origin_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workflow_slug: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub db_prefix: Option<String>,
@ -35,7 +31,7 @@ pub struct RunCreatedProps {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub manifest_blob: Option<RunBlobId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pre_run_git: Option<PreRunGitContext>,
pub git: Option<GitContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fork_source_ref: Option<ForkSourceRef>,
#[serde(default)]

View file

@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
use strum::Display;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Display, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum SecretType {
#[default]
Environment,
File,
Credential,
}

View file

@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use fabro_types::graph::Graph;
use fabro_types::run::{DirtyStatus, ForkSourceRef, PreRunGitContext, PreRunPushOutcome};
use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome};
use fabro_types::run_event::run::RunCreatedProps;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::RunGoal;
@ -23,16 +23,16 @@ fn run_created_props_round_trip_templated_settings() {
labels: BTreeMap::from([("team".to_string(), "platform".to_string())]),
run_dir: "/tmp/run".to_string(),
source_directory: Some("/Users/client/project".to_string()),
repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()),
base_branch: Some("main".to_string()),
workflow_slug: Some("demo".to_string()),
db_prefix: Some("run_".to_string()),
provenance: None,
manifest_blob: None,
pre_run_git: Some(PreRunGitContext {
display_base_sha: Some("abc123".to_string()),
local_dirty: DirtyStatus::Unknown,
push_outcome: PreRunPushOutcome::SkippedNoRemote,
git: Some(GitContext {
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
branch: "main".to_string(),
sha: None,
dirty: DirtyStatus::Unknown,
push_outcome: PreRunPushOutcome::SkippedNoRemote,
}),
fork_source_ref: Some(ForkSourceRef {
source_run_id: fixtures::RUN_2,
@ -46,9 +46,12 @@ fn run_created_props_round_trip_templated_settings() {
assert!(json.get("host_repo_path").is_none());
assert_eq!(json["source_directory"], "/Users/client/project");
assert_eq!(
json["pre_run_git"]["push_outcome"]["type"],
"skipped_no_remote"
json["git"]["origin_url"],
"https://github.com/fabro-sh/fabro.git"
);
assert_eq!(json["git"]["branch"], "main");
assert_eq!(json["git"]["dirty"], "unknown");
assert_eq!(json["git"]["push_outcome"]["type"], "skipped_no_remote");
assert_eq!(json["in_place"], true);
let round_trip: RunCreatedProps =

View file

@ -1,7 +1,7 @@
use std::collections::HashMap;
use fabro_types::graph::Graph;
use fabro_types::run::{DirtyStatus, PreRunGitContext, PreRunPushOutcome, RunSpec};
use fabro_types::run::{DirtyStatus, GitContext, PreRunPushOutcome, RunSpec};
use fabro_types::{WorkflowSettings, fixtures};
fn sample_run_spec() -> RunSpec {
@ -11,16 +11,16 @@ fn sample_run_spec() -> RunSpec {
graph: Graph::new("ship"),
workflow_slug: Some("demo".to_string()),
source_directory: Some("/Users/client/project".to_string()),
repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()),
base_branch: Some("main".to_string()),
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: Some(PreRunGitContext {
display_base_sha: Some("abc123".to_string()),
local_dirty: DirtyStatus::Dirty,
push_outcome: PreRunPushOutcome::SkippedRemoteMismatch {
git: Some(GitContext {
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
branch: "main".to_string(),
sha: Some("abc123".to_string()),
dirty: DirtyStatus::Dirty,
push_outcome: PreRunPushOutcome::SkippedRemoteMismatch {
remote: "https://github.com/user/fork.git".to_string(),
repo_origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
},
@ -44,9 +44,7 @@ fn run_spec_getters_return_declared_fields() {
Some("platform")
);
assert_eq!(
run_spec
.pre_run_git()
.and_then(|ctx| ctx.display_base_sha.as_deref()),
run_spec.git().and_then(|ctx| ctx.sha.as_deref()),
Some("abc123")
);
assert_eq!(

View file

@ -1,7 +1,7 @@
use std::collections::HashMap;
use fabro_types::graph::Graph;
use fabro_types::run::{DirtyStatus, ForkSourceRef, PreRunGitContext, PreRunPushOutcome, RunSpec};
use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome, RunSpec};
use fabro_types::settings::InterpString;
use fabro_types::settings::run::RunGoal;
use fabro_types::{WorkflowSettings, fixtures};
@ -20,16 +20,16 @@ fn run_spec_round_trips_templated_settings() {
graph: Graph::new("ship"),
workflow_slug: Some("demo".to_string()),
source_directory: Some("/Users/client/project".to_string()),
repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()),
base_branch: Some("main".to_string()),
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: Some(PreRunGitContext {
display_base_sha: Some("abc123".to_string()),
local_dirty: DirtyStatus::Clean,
push_outcome: PreRunPushOutcome::Succeeded {
git: Some(GitContext {
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
branch: "main".to_string(),
sha: Some("abc123".to_string()),
dirty: DirtyStatus::Clean,
push_outcome: PreRunPushOutcome::Succeeded {
remote: "origin".to_string(),
branch: "main".to_string(),
},
@ -45,8 +45,14 @@ fn run_spec_round_trips_templated_settings() {
assert!(json.get("working_directory").is_none());
assert!(json.get("host_repo_path").is_none());
assert_eq!(json["source_directory"], "/Users/client/project");
assert_eq!(json["pre_run_git"]["local_dirty"], "clean");
assert_eq!(json["pre_run_git"]["push_outcome"]["type"], "succeeded");
assert_eq!(
json["git"]["origin_url"],
"https://github.com/fabro-sh/fabro.git"
);
assert_eq!(json["git"]["branch"], "main");
assert_eq!(json["git"]["sha"], "abc123");
assert_eq!(json["git"]["dirty"], "clean");
assert_eq!(json["git"]["push_outcome"]["type"], "succeeded");
assert_eq!(json["fork_source_ref"]["checkpoint_sha"], "def456");
assert_eq!(json["in_place"], false);

View file

@ -14,6 +14,7 @@ workspace = true
[dependencies]
chrono.workspace = true
fabro-types = { path = "../fabro-types" }
serde.workspace = true
serde_json.workspace = true
ulid.workspace = true

View file

@ -7,14 +7,7 @@ use std::collections::HashMap;
use std::path::{Component, Path, PathBuf};
use std::{fmt, io};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SecretType {
#[default]
Environment,
File,
Credential,
}
pub use fabro_types::SecretType;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SecretEntry {

View file

@ -5,8 +5,8 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use ::fabro_types::{
ActorRef, BilledTokenCounts, BlockedReason, FailureReason, ForkSourceRef, ParallelBranchId,
PreRunGitContext, PullRequestRecord, RunBlobId, RunControlAction, RunEvent, RunId,
ActorRef, BilledTokenCounts, BlockedReason, FailureReason, ForkSourceRef, GitContext,
ParallelBranchId, PullRequestRecord, RunBlobId, RunControlAction, RunEvent, RunId,
RunProvenance, StageId, StageStatus, SuccessReason, run_event as fabro_types,
};
use anyhow::{Context, Result};
@ -49,10 +49,6 @@ pub enum Event {
#[serde(default, skip_serializing_if = "Option::is_none")]
source_directory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
repo_origin_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
base_branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
workflow_slug: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
db_prefix: Option<String>,
@ -61,7 +57,7 @@ pub enum Event {
#[serde(default, skip_serializing_if = "Option::is_none")]
manifest_blob: Option<RunBlobId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pre_run_git: Option<PreRunGitContext>,
git: Option<GitContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
fork_source_ref: Option<ForkSourceRef>,
#[serde(default)]
@ -1522,13 +1518,11 @@ fn event_body_from_event(event: &Event) -> EventBody {
labels,
run_dir,
source_directory,
repo_origin_url,
base_branch,
workflow_slug,
db_prefix,
provenance,
manifest_blob,
pre_run_git,
git,
fork_source_ref,
in_place,
..
@ -1541,13 +1535,11 @@ fn event_body_from_event(event: &Event) -> EventBody {
labels: labels.clone(),
run_dir: run_dir.clone(),
source_directory: source_directory.clone(),
repo_origin_url: repo_origin_url.clone(),
base_branch: base_branch.clone(),
workflow_slug: workflow_slug.clone(),
db_prefix: db_prefix.clone(),
provenance: provenance.clone(),
manifest_blob: *manifest_blob,
pre_run_git: pre_run_git.clone(),
git: git.clone(),
fork_source_ref: fork_source_ref.clone(),
in_place: *in_place,
}),
@ -3679,13 +3671,11 @@ mod tests {
labels: BTreeMap::default(),
run_dir: "/tmp/run".to_string(),
source_directory: Some("/tmp/run".to_string()),
repo_origin_url: None,
base_branch: None,
workflow_slug: None,
db_prefix: None,
provenance: Some(provenance),
manifest_blob: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
});

View file

@ -15,7 +15,7 @@ use fabro_sandbox::SandboxProvider;
use fabro_store::Database;
use fabro_template::{TemplateContext, render as render_template};
use fabro_types::settings::run::{RunMode, RunNamespace};
use fabro_types::{ForkSourceRef, PreRunGitContext, RunId, RunProvenance, WorkflowSettings};
use fabro_types::{ForkSourceRef, GitContext, RunId, RunProvenance, WorkflowSettings};
use fabro_util::json::normalize_json_value;
use tokio::task::spawn_blocking;
@ -41,9 +41,7 @@ pub struct CreateRunInput {
pub workflow_bundle: Option<WorkflowBundle>,
pub submitted_manifest_bytes: Option<Vec<u8>>,
pub run_id: Option<RunId>,
pub repo_origin_url: Option<String>,
pub base_branch: Option<String>,
pub pre_run_git: Option<PreRunGitContext>,
pub git: Option<GitContext>,
pub fork_source_ref: Option<ForkSourceRef>,
pub in_place: bool,
pub provenance: Option<RunProvenance>,
@ -64,10 +62,8 @@ struct PersistCreateOptions {
run_dir: Option<PathBuf>,
workflow_slug: Option<String>,
labels: HashMap<String, String>,
base_branch: Option<String>,
source_directory: Option<String>,
repo_origin_url: Option<String>,
pre_run_git: Option<PreRunGitContext>,
git: Option<GitContext>,
fork_source_ref: Option<ForkSourceRef>,
in_place: bool,
provenance: Option<RunProvenance>,
@ -101,9 +97,7 @@ pub async fn create(
workflow_bundle,
submitted_manifest_bytes,
run_id,
repo_origin_url,
base_branch,
pre_run_git,
git,
fork_source_ref,
in_place,
provenance,
@ -138,10 +132,8 @@ pub async fn create(
run_dir: Some(persisted_run_dir),
workflow_slug: workflow_slug.or(resolved_workflow_slug),
labels,
base_branch,
source_directory,
repo_origin_url,
pre_run_git,
git,
fork_source_ref,
in_place,
provenance,
@ -228,13 +220,11 @@ async fn persist_created_run(
.collect::<BTreeMap<_, _>>(),
run_dir: persisted.run_dir().display().to_string(),
source_directory: record.source_directory.clone(),
repo_origin_url: record.repo_origin_url.clone(),
base_branch: record.base_branch.clone(),
workflow_slug: record.workflow_slug.clone(),
db_prefix: None,
provenance: record.provenance.clone(),
manifest_blob,
pre_run_git: record.pre_run_git.clone(),
git: record.git.clone(),
fork_source_ref: record.fork_source_ref.clone(),
in_place: record.in_place,
},
@ -350,10 +340,8 @@ fn persist_validated(
run_dir,
workflow_slug,
labels,
base_branch,
source_directory,
repo_origin_url,
pre_run_git,
git,
fork_source_ref,
in_place,
provenance,
@ -376,13 +364,11 @@ fn persist_validated(
graph: validated.graph().clone(),
workflow_slug,
source_directory,
repo_origin_url,
base_branch,
labels,
provenance,
manifest_blob: None,
definition_blob: None,
pre_run_git,
git,
fork_source_ref,
in_place,
};
@ -722,9 +708,7 @@ mod tests {
workflow_bundle: None,
submitted_manifest_bytes: None,
run_id: None,
repo_origin_url: None,
base_branch: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
@ -767,9 +751,7 @@ mod tests {
workflow_bundle: None,
submitted_manifest_bytes: None,
run_id: None,
repo_origin_url: None,
base_branch: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
@ -828,9 +810,13 @@ mod tests {
workflow_bundle: None,
submitted_manifest_bytes: None,
run_id: Some(fixtures::RUN_1),
repo_origin_url: None,
base_branch: Some("main".to_string()),
pre_run_git: None,
git: Some(fabro_types::GitContext {
origin_url: String::new(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
provenance: None,
@ -938,9 +924,7 @@ mod tests {
workflow_bundle: None,
submitted_manifest_bytes: None,
run_id: Some(fixtures::RUN_2),
repo_origin_url: None,
base_branch: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
@ -976,9 +960,13 @@ mod tests {
workflow_bundle: None,
submitted_manifest_bytes: None,
run_id: Some(fixtures::RUN_2),
repo_origin_url: Some("https://github.com/acme/widgets".to_string()),
base_branch: None,
pre_run_git: None,
git: Some(fabro_types::GitContext {
origin_url: "https://github.com/acme/widgets".to_string(),
branch: String::new(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
provenance: None,
@ -990,7 +978,7 @@ mod tests {
.unwrap();
assert_eq!(
created.persisted.run_spec().repo_origin_url.as_deref(),
created.persisted.run_spec().repo_origin_url(),
Some("https://github.com/acme/widgets")
);
}
@ -1042,9 +1030,7 @@ mod tests {
workflow_bundle: None,
submitted_manifest_bytes: None,
run_id: Some(fixtures::RUN_3),
repo_origin_url: None,
base_branch: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
@ -1087,9 +1073,7 @@ mod tests {
workflow_bundle: None,
submitted_manifest_bytes: None,
run_id: Some(fixtures::RUN_64),
repo_origin_url: None,
base_branch: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: Some(fabro_types::RunProvenance {

View file

@ -123,7 +123,7 @@ fn validate_source_spec(
"target checkpoint has an empty git_commit_sha; cannot fork".to_string(),
));
}
let Some(origin) = spec.repo_origin_url.as_ref() else {
let Some(origin) = spec.repo_origin_url() else {
return Err(Error::Validation(
"source run has no repo_origin_url; cannot validate fork origin".to_string(),
));
@ -180,13 +180,11 @@ async fn persist_forked_run(
labels: spec.labels.clone().into_iter().collect(),
run_dir: String::new(),
source_directory: spec.source_directory.clone(),
repo_origin_url: spec.repo_origin_url.clone(),
base_branch: spec.base_branch.clone(),
workflow_slug: spec.workflow_slug.clone(),
db_prefix: None,
provenance: spec.provenance.clone(),
manifest_blob: spec.manifest_blob,
pre_run_git: spec.pre_run_git.clone(),
git: spec.git.clone(),
fork_source_ref: spec.fork_source_ref.clone(),
in_place: spec.in_place,
})
@ -330,13 +328,17 @@ mod tests {
labels: BTreeMap::new(),
run_dir: "/tmp/source".to_string(),
source_directory: Some("/client/source".to_string()),
repo_origin_url: Some("https://github.com/example/repo.git".to_string()),
base_branch: Some("main".to_string()),
workflow_slug: Some("fork-source".to_string()),
db_prefix: None,
provenance: None,
manifest_blob: None,
pre_run_git: None,
git: Some(fabro_types::GitContext {
origin_url: "https://github.com/example/repo.git".to_string(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
fork_source_ref: None,
in_place: false,
})

View file

@ -359,8 +359,8 @@ impl RunSession {
config: resolve_docker_config(resolved).unwrap_or_default(),
github_app: services.github_app.clone(),
run_id: Some(record.run_id),
clone_origin_url: record.repo_origin_url.clone(),
clone_branch: record.base_branch.clone(),
clone_origin_url: record.repo_origin_url().map(str::to_string),
clone_branch: record.base_branch().map(str::to_string),
},
SandboxProvider::Daytona => {
let api_key = match &services.vault {
@ -375,8 +375,8 @@ impl RunSession {
config: resolve_daytona_config(resolved).unwrap_or_default(),
github_app: services.github_app.clone(),
run_id: Some(record.run_id),
clone_origin_url: record.repo_origin_url.clone(),
clone_branch: record.base_branch.clone(),
clone_origin_url: record.repo_origin_url().map(str::to_string),
clone_branch: record.base_branch().map(str::to_string),
api_key,
}
}
@ -394,7 +394,7 @@ impl RunSession {
devcontainer_env: HashMap::new(),
toml_env,
github_permissions,
origin_url: record.repo_origin_url.clone(),
origin_url: record.repo_origin_url().map(str::to_string),
};
let devcontainer = resolved.sandbox.devcontainer.then(|| DevcontainerSpec {
@ -447,7 +447,7 @@ impl RunSession {
preserve_sandbox: resolved.sandbox.preserve,
pr_config,
pr_github_app: services.github_app,
pr_origin_url: record.repo_origin_url.clone(),
pr_origin_url: record.repo_origin_url().map(str::to_string),
pr_model: model,
workflow_path,
workflow_bundle,
@ -698,9 +698,9 @@ impl RunSession {
labels: record.labels.clone(),
workflow_slug: record.workflow_slug.clone(),
github_app: self.github_app.clone(),
pre_run_git: record.pre_run_git.clone(),
pre_run_git: record.git.clone(),
fork_source_ref: record.fork_source_ref.clone(),
base_branch: record.base_branch.clone(),
base_branch: record.base_branch().map(str::to_string),
display_base_sha: None,
git: self.git.clone(),
};
@ -1074,9 +1074,7 @@ mod tests {
workflow_bundle: None,
submitted_manifest_bytes: None,
run_id: Some(fixtures::RUN_1),
repo_origin_url: None,
base_branch: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,
@ -1254,9 +1252,7 @@ mod tests {
workflow_bundle: Some(workflow_bundle),
submitted_manifest_bytes: None,
run_id: Some(fixtures::RUN_1),
repo_origin_url: None,
base_branch: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
provenance: None,

View file

@ -145,13 +145,17 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI
.display()
.to_string(),
),
repo_origin_url: None,
base_branch: Some("main".to_string()),
git: Some(fabro_types::GitContext {
origin_url: String::new(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
labels: HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
},

View file

@ -138,7 +138,7 @@ fn resolve_worktree_plan(options: &mut InitOptions) -> Option<WorktreePlan> {
.run_options
.pre_run_git
.as_ref()
.map(|git| git.local_dirty);
.map(|git| git.dirty);
if matches!(local_dirty, Some(fabro_types::DirtyStatus::Dirty)) {
let env_name = if !is_local {
@ -162,7 +162,7 @@ fn resolve_worktree_plan(options: &mut InitOptions) -> Option<WorktreePlan> {
.run_options
.pre_run_git
.as_ref()
.and_then(|git| git.display_base_sha.clone());
.and_then(|git| git.sha.clone());
return None;
}
@ -184,7 +184,7 @@ fn resolve_worktree_plan(options: &mut InitOptions) -> Option<WorktreePlan> {
.run_options
.pre_run_git
.as_ref()
.and_then(|git| git.display_base_sha.clone()),
.and_then(|git| git.sha.clone()),
)
};
options.run_options.display_base_sha.clone_from(&base_sha);
@ -865,13 +865,17 @@ mod tests {
graph,
workflow_slug: Some("test".to_string()),
source_directory: Some(std::env::current_dir().unwrap().display().to_string()),
repo_origin_url: None,
base_branch: Some("main".to_string()),
git: Some(fabro_types::GitContext {
origin_url: String::new(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
labels: HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
},

View file

@ -137,8 +137,13 @@ mod tests {
graph,
workflow_slug: Some("ship".to_string()),
source_directory: Some("/tmp/project".to_string()),
repo_origin_url: None,
base_branch: Some("main".to_string()),
git: Some(fabro_types::GitContext {
origin_url: String::new(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
labels: HashMap::from([
("env".to_string(), "test".to_string()),
("team".to_string(), "workflow".to_string()),
@ -146,7 +151,6 @@ mod tests {
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
}
@ -164,13 +168,11 @@ mod tests {
labels: record.labels.clone().into_iter().collect(),
run_dir: run_dir.to_string_lossy().to_string(),
source_directory: record.source_directory.clone(),
repo_origin_url: record.repo_origin_url.clone(),
base_branch: record.base_branch.clone(),
workflow_slug: record.workflow_slug.clone(),
db_prefix: None,
provenance: record.provenance.clone(),
manifest_blob: None,
pre_run_git: record.pre_run_git.clone(),
git: record.git.clone(),
fork_source_ref: record.fork_source_ref.clone(),
in_place: record.in_place,
})
@ -265,7 +267,7 @@ mod tests {
);
assert_eq!(loaded_record.workflow_slug, expected.workflow_slug);
assert_eq!(loaded_record.source_directory, expected.source_directory);
assert_eq!(loaded_record.base_branch, expected.base_branch);
assert_eq!(loaded_record.base_branch(), expected.base_branch());
assert_eq!(loaded_record.labels, expected.labels);
assert_eq!(loaded.source(), source);
assert!(loaded.diagnostics().is_empty());

View file

@ -1172,13 +1172,17 @@ mod tests {
graph: Graph::new("test"),
workflow_slug: Some("test".to_string()),
source_directory: Some("/tmp/project".to_string()),
repo_origin_url: None,
base_branch: Some("main".to_string()),
git: Some(fabro_types::GitContext {
origin_url: String::new(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
labels: HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
};
@ -1191,13 +1195,11 @@ mod tests {
labels: run_spec.labels.clone().into_iter().collect(),
run_dir: "/tmp/project".to_string(),
source_directory: run_spec.source_directory.clone(),
repo_origin_url: run_spec.repo_origin_url.clone(),
base_branch: run_spec.base_branch.clone(),
workflow_slug: run_spec.workflow_slug.clone(),
db_prefix: None,
provenance: run_spec.provenance.clone(),
manifest_blob: None,
pre_run_git: None,
git: run_spec.git.clone(),
fork_source_ref: None,
in_place: false,
})
@ -1239,13 +1241,17 @@ mod tests {
graph: Graph::new("test"),
workflow_slug: Some("test".to_string()),
source_directory: Some("/tmp/project".to_string()),
repo_origin_url: None,
base_branch: Some("main".to_string()),
git: Some(fabro_types::GitContext {
origin_url: String::new(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
labels: HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
};
@ -1258,13 +1264,11 @@ mod tests {
labels: run_spec.labels.clone().into_iter().collect(),
run_dir: "/tmp/project".to_string(),
source_directory: run_spec.source_directory.clone(),
repo_origin_url: run_spec.repo_origin_url.clone(),
base_branch: run_spec.base_branch.clone(),
workflow_slug: run_spec.workflow_slug.clone(),
db_prefix: None,
provenance: run_spec.provenance.clone(),
manifest_blob: None,
pre_run_git: None,
git: run_spec.git.clone(),
fork_source_ref: None,
in_place: false,
})
@ -1539,13 +1543,11 @@ mod tests {
graph: Graph::new("test"),
workflow_slug: None,
source_directory: Some(tmp.path().display().to_string()),
repo_origin_url: None,
base_branch: None,
git: None,
labels: std::collections::HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
};
@ -1558,13 +1560,11 @@ mod tests {
labels: run_spec.labels.clone().into_iter().collect(),
run_dir: tmp.path().display().to_string(),
source_directory: run_spec.source_directory.clone(),
repo_origin_url: run_spec.repo_origin_url.clone(),
base_branch: None,
workflow_slug: None,
db_prefix: None,
provenance: run_spec.provenance.clone(),
manifest_blob: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
})

View file

@ -216,13 +216,11 @@ mod tests {
graph: Graph::new("test"),
workflow_slug: None,
source_directory: Some(run_dir.to_string_lossy().to_string()),
repo_origin_url: None,
base_branch: None,
git: None,
labels: std::collections::HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
};
@ -235,13 +233,11 @@ mod tests {
labels: run_spec.labels.clone().into_iter().collect(),
run_dir: run_dir.to_string_lossy().to_string(),
source_directory: run_spec.source_directory.clone(),
repo_origin_url: run_spec.repo_origin_url.clone(),
base_branch: None,
workflow_slug: None,
db_prefix: None,
provenance: run_spec.provenance.clone(),
manifest_blob: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
})

View file

@ -428,13 +428,17 @@ mod tests {
graph: Graph::new("ship"),
workflow_slug: Some("demo".to_string()),
source_directory: Some("/tmp/project".to_string()),
repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()),
base_branch: Some("main".to_string()),
git: Some(fabro_types::GitContext {
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
labels: HashMap::from([("team".to_string(), "platform".to_string())]),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
}

View file

@ -439,13 +439,17 @@ mod tests {
graph: Graph::new("test"),
workflow_slug: Some("test".to_string()),
source_directory: Some("/tmp/project".to_string()),
repo_origin_url: None,
base_branch: Some("main".to_string()),
git: Some(fabro_types::GitContext {
origin_url: String::new(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
labels: HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
}
@ -469,13 +473,11 @@ mod tests {
labels: run_spec.labels.clone().into_iter().collect(),
run_dir: run_dir.display().to_string(),
source_directory: run_spec.source_directory.clone(),
repo_origin_url: run_spec.repo_origin_url.clone(),
base_branch: run_spec.base_branch.clone(),
workflow_slug: run_spec.workflow_slug.clone(),
db_prefix: None,
provenance: run_spec.provenance.clone(),
manifest_blob: None,
pre_run_git: run_spec.pre_run_git.clone(),
git: run_spec.git.clone(),
fork_source_ref: run_spec.fork_source_ref.clone(),
in_place: run_spec.in_place,
})

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use fabro_types::settings::run::RunMode;
use fabro_types::{ForkSourceRef, PreRunGitContext, RunId, WorkflowSettings};
use fabro_types::{ForkSourceRef, GitContext, RunId, WorkflowSettings};
use crate::git::{GitAuthor, git_author_from_settings};
@ -31,7 +31,7 @@ pub struct RunOptions {
/// GitHub credentials for pushing metadata branches to origin.
pub github_app: Option<fabro_github::GitHubCredentials>,
/// Submitter-side git context captured before the run was created.
pub pre_run_git: Option<PreRunGitContext>,
pub pre_run_git: Option<GitContext>,
/// Source checkpoint ref used by fork/rewind-created runs.
pub fork_source_ref: Option<ForkSourceRef>,
/// Name of the branch the run was started from (for PR base).

View file

@ -135,13 +135,11 @@ mod tests {
graph: Graph::new("test"),
workflow_slug: Some("test".to_string()),
source_directory: Some("/tmp/test".to_string()),
repo_origin_url: None,
base_branch: None,
git: None,
labels: HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
}
@ -160,13 +158,11 @@ mod tests {
labels: std::collections::BTreeMap::new(),
run_dir: "/tmp/test".to_string(),
source_directory: Some("/tmp/test".to_string()),
repo_origin_url: None,
base_branch: None,
workflow_slug: Some("test".to_string()),
db_prefix: None,
provenance: None,
manifest_blob: None,
pre_run_git: None,
git: None,
fork_source_ref: None,
in_place: false,
})

View file

@ -527,11 +527,7 @@ fn classify_entry(
})
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct DiffLineStats {
pub additions: u64,
pub deletions: u64,
}
pub use fabro_types::DiffStats;
/// Output of `git diff --numstat`: which paths are binary, plus per-path
/// `+/-` line totals for text files in the range. Both pieces come from a
@ -541,7 +537,7 @@ pub struct DiffNumstat {
/// Repo-relative paths (post-rename) that git classifies as binary.
pub binary_paths: HashSet<String>,
/// Repo-relative paths (post-rename) to line stats for text files.
pub line_stats_by_path: HashMap<String, DiffLineStats>,
pub line_stats_by_path: HashMap<String, DiffStats>,
}
/// Run `git diff --numstat` once and return both the set of binary paths and
@ -593,14 +589,14 @@ pub async fn list_diff_numstat(
let Some(path_s) = parts.next() else {
continue;
};
let Ok(adds) = adds_s.parse::<u64>() else {
let Ok(adds) = adds_s.parse::<i64>() else {
continue;
};
let Ok(dels) = dels_s.parse::<u64>() else {
let Ok(dels) = dels_s.parse::<i64>() else {
continue;
};
let path = extract_new_path_from_numstat(path_s);
out.line_stats_by_path.insert(path, DiffLineStats {
out.line_stats_by_path.insert(path, DiffStats {
additions: adds,
deletions: dels,
});
@ -1186,13 +1182,17 @@ mod tests {
graph: fabro_types::Graph::new("metadata"),
workflow_slug: Some("metadata".to_string()),
source_directory: Some("/Users/client/project".to_string()),
repo_origin_url: Some("https://github.com/fabro-sh/fabro.git".to_string()),
base_branch: Some("main".to_string()),
git: Some(fabro_types::GitContext {
origin_url: "https://github.com/fabro-sh/fabro.git".to_string(),
branch: "main".to_string(),
sha: None,
dirty: fabro_types::DirtyStatus::Clean,
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
}),
labels: HashMap::new(),
provenance: None,
manifest_blob: None,
definition_blob: None,
pre_run_git: None,
fork_source_ref: None,
in_place: false,
});

View file

@ -120,13 +120,11 @@ async fn initialized(
.collect::<BTreeMap<_, _>>(),
run_dir: run_options.run_dir.display().to_string(),
source_directory: Some(sandbox.working_directory().to_string()),
repo_origin_url: None,
base_branch: run_options.base_branch.clone(),
workflow_slug: run_options.workflow_slug.clone(),
db_prefix: None,
provenance: None,
manifest_blob: None,
pre_run_git: run_options.pre_run_git.clone(),
git: run_options.pre_run_git.clone(),
fork_source_ref: run_options.fork_source_ref.clone(),
in_place: false,
})

View file

@ -75,6 +75,7 @@ models/file-diff.ts
models/fork-request.ts
models/fork-response.ts
models/fork-source-ref.ts
models/git-context.ts
models/git-hub-meta-hooks-entry.ts
models/github-integration-settings.ts
models/github-integration-strategy.ts
@ -113,9 +114,7 @@ models/manifest-args.ts
models/manifest-config.ts
models/manifest-file-entry.ts
models/manifest-file-ref.ts
models/manifest-git.ts
models/manifest-goal.ts
models/manifest-pre-run-push-outcome.ts
models/manifest-target.ts
models/manifest-workflow-config.ts
models/manifest-workflow.ts
@ -146,7 +145,11 @@ models/paginated-saved-query-list.ts
models/paginated-stage-turn-list.ts
models/pagination-meta.ts
models/pending-interview-record.ts
models/pre-run-git-context.ts
models/pre-run-push-outcome-failed.ts
models/pre-run-push-outcome-not-attempted.ts
models/pre-run-push-outcome-skipped-no-remote.ts
models/pre-run-push-outcome-skipped-remote-mismatch.ts
models/pre-run-push-outcome-succeeded.ts
models/pre-run-push-outcome.ts
models/preflight-check-detail.ts
models/preflight-check-report.ts

View file

@ -0,0 +1,44 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { DirtyStatus } from './dirty-status';
// May contain unused imports in some cases
// @ts-ignore
import type { PreRunPushOutcome } from './pre-run-push-outcome';
/**
* Observable git state captured before the run starts.
*/
export interface GitContext {
/**
* Remote origin URL with any embedded credentials removed.
*/
'origin_url': string;
/**
* Current branch name.
*/
'branch': string;
/**
* Current commit SHA, when known.
*/
'sha'?: string | null;
'dirty': DirtyStatus;
'push_outcome': PreRunPushOutcome;
}

View file

@ -55,6 +55,7 @@ export * from './file-diff';
export * from './fork-request';
export * from './fork-response';
export * from './fork-source-ref';
export * from './git-context';
export * from './git-hub-meta-hooks-entry';
export * from './github-integration-settings';
export * from './github-integration-strategy';
@ -92,9 +93,7 @@ export * from './manifest-args';
export * from './manifest-config';
export * from './manifest-file-entry';
export * from './manifest-file-ref';
export * from './manifest-git';
export * from './manifest-goal';
export * from './manifest-pre-run-push-outcome';
export * from './manifest-target';
export * from './manifest-workflow';
export * from './manifest-workflow-config';
@ -125,8 +124,12 @@ export * from './paginated-saved-query-list';
export * from './paginated-stage-turn-list';
export * from './pagination-meta';
export * from './pending-interview-record';
export * from './pre-run-git-context';
export * from './pre-run-push-outcome';
export * from './pre-run-push-outcome-failed';
export * from './pre-run-push-outcome-not-attempted';
export * from './pre-run-push-outcome-skipped-no-remote';
export * from './pre-run-push-outcome-skipped-remote-mismatch';
export * from './pre-run-push-outcome-succeeded';
export * from './preflight-check-detail';
export * from './preflight-check-report';
export * from './preflight-check-result';

View file

@ -0,0 +1,30 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface PreRunPushOutcomeFailed {
'type': PreRunPushOutcomeFailedTypeEnum;
'remote': string;
'branch': string;
'message': string;
}
export const PreRunPushOutcomeFailedTypeEnum = {
FAILED: 'failed'
} as const;
export type PreRunPushOutcomeFailedTypeEnum = typeof PreRunPushOutcomeFailedTypeEnum[keyof typeof PreRunPushOutcomeFailedTypeEnum];

View file

@ -0,0 +1,27 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface PreRunPushOutcomeNotAttempted {
'type': PreRunPushOutcomeNotAttemptedTypeEnum;
}
export const PreRunPushOutcomeNotAttemptedTypeEnum = {
NOT_ATTEMPTED: 'not_attempted'
} as const;
export type PreRunPushOutcomeNotAttemptedTypeEnum = typeof PreRunPushOutcomeNotAttemptedTypeEnum[keyof typeof PreRunPushOutcomeNotAttemptedTypeEnum];

View file

@ -0,0 +1,27 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface PreRunPushOutcomeSkippedNoRemote {
'type': PreRunPushOutcomeSkippedNoRemoteTypeEnum;
}
export const PreRunPushOutcomeSkippedNoRemoteTypeEnum = {
SKIPPED_NO_REMOTE: 'skipped_no_remote'
} as const;
export type PreRunPushOutcomeSkippedNoRemoteTypeEnum = typeof PreRunPushOutcomeSkippedNoRemoteTypeEnum[keyof typeof PreRunPushOutcomeSkippedNoRemoteTypeEnum];

View file

@ -0,0 +1,29 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface PreRunPushOutcomeSkippedRemoteMismatch {
'type': PreRunPushOutcomeSkippedRemoteMismatchTypeEnum;
'remote': string;
'repo_origin_url': string;
}
export const PreRunPushOutcomeSkippedRemoteMismatchTypeEnum = {
SKIPPED_REMOTE_MISMATCH: 'skipped_remote_mismatch'
} as const;
export type PreRunPushOutcomeSkippedRemoteMismatchTypeEnum = typeof PreRunPushOutcomeSkippedRemoteMismatchTypeEnum[keyof typeof PreRunPushOutcomeSkippedRemoteMismatchTypeEnum];

View file

@ -0,0 +1,29 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface PreRunPushOutcomeSucceeded {
'type': PreRunPushOutcomeSucceededTypeEnum;
'remote': string;
'branch': string;
}
export const PreRunPushOutcomeSucceededTypeEnum = {
SUCCEEDED: 'succeeded'
} as const;
export type PreRunPushOutcomeSucceededTypeEnum = typeof PreRunPushOutcomeSucceededTypeEnum[keyof typeof PreRunPushOutcomeSucceededTypeEnum];

View file

@ -13,26 +13,26 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PreRunPushOutcomeFailed } from './pre-run-push-outcome-failed';
// May contain unused imports in some cases
// @ts-ignore
import type { PreRunPushOutcomeNotAttempted } from './pre-run-push-outcome-not-attempted';
// May contain unused imports in some cases
// @ts-ignore
import type { PreRunPushOutcomeSkippedNoRemote } from './pre-run-push-outcome-skipped-no-remote';
// May contain unused imports in some cases
// @ts-ignore
import type { PreRunPushOutcomeSkippedRemoteMismatch } from './pre-run-push-outcome-skipped-remote-mismatch';
// May contain unused imports in some cases
// @ts-ignore
import type { PreRunPushOutcomeSucceeded } from './pre-run-push-outcome-succeeded';
/**
* Outcome of the CLI submitter\'s best-effort pre-run push.
* @type PreRunPushOutcome
* Outcome of the CLI\'s best-effort pre-run push.
*/
export interface PreRunPushOutcome {
'type': PreRunPushOutcomeTypeEnum;
'remote'?: string | null;
'branch'?: string | null;
'message'?: string | null;
'repo_origin_url'?: string | null;
}
export const PreRunPushOutcomeTypeEnum = {
NOT_ATTEMPTED: 'not_attempted',
SUCCEEDED: 'succeeded',
FAILED: 'failed',
SKIPPED_NO_REMOTE: 'skipped_no_remote',
SKIPPED_REMOTE_MISMATCH: 'skipped_remote_mismatch'
} as const;
export type PreRunPushOutcomeTypeEnum = typeof PreRunPushOutcomeTypeEnum[keyof typeof PreRunPushOutcomeTypeEnum];
export type PreRunPushOutcome = { type: 'failed' } & PreRunPushOutcomeFailed | { type: 'not_attempted' } & PreRunPushOutcomeNotAttempted | { type: 'skipped_no_remote' } & PreRunPushOutcomeSkippedNoRemote | { type: 'skipped_remote_mismatch' } & PreRunPushOutcomeSkippedRemoteMismatch | { type: 'succeeded' } & PreRunPushOutcomeSucceeded;

View file

@ -13,6 +13,9 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { GitContext } from './git-context';
// May contain unused imports in some cases
// @ts-ignore
import type { ManifestArgs } from './manifest-args';
@ -21,9 +24,6 @@ import type { ManifestArgs } from './manifest-args';
import type { ManifestConfig } from './manifest-config';
// May contain unused imports in some cases
// @ts-ignore
import type { ManifestGit } from './manifest-git';
// May contain unused imports in some cases
// @ts-ignore
import type { ManifestGoal } from './manifest-goal';
// May contain unused imports in some cases
// @ts-ignore
@ -48,7 +48,7 @@ export interface RunManifest {
* CLI working directory at invocation time.
*/
'cwd': string;
'git'?: ManifestGit;
'git'?: GitContext;
'goal'?: ManifestGoal;
'args'?: ManifestArgs;
'target': ManifestTarget;

View file

@ -18,7 +18,7 @@
import type { ForkSourceRef } from './fork-source-ref';
// May contain unused imports in some cases
// @ts-ignore
import type { PreRunGitContext } from './pre-run-git-context';
import type { GitContext } from './git-context';
/**
* Durable workflow run specification reconstructed from run.created events.
@ -32,13 +32,11 @@ export interface RunSpec {
'graph': { [key: string]: any; };
'workflow_slug'?: string | null;
'source_directory'?: string | null;
'repo_origin_url'?: string | null;
'base_branch'?: string | null;
'labels'?: { [key: string]: string; };
'provenance'?: { [key: string]: any; } | null;
'manifest_blob'?: string | null;
'definition_blob'?: string | null;
'pre_run_git'?: PreRunGitContext | null;
'git'?: GitContext | null;
'fork_source_ref'?: ForkSourceRef | null;
'in_place': boolean;
}