Skip to content

Commit 2263927

Browse files
committed
feat(functions push): add --manifest path for non-SDK function types
The runner-based push path can only ship function types the Braintrust SDK exposes builders for: tools, scorers, prompts, parameters. Topics- pipeline types (facet, classifier) and inline-quickjs preprocessors have no SDK builder, so AI-agent workflows that author lenses cannot push them through `bt`. `--manifest <path>` accepts a JSON file with three shapes — the `/insert-functions` wire body `{"functions": [...]}`, a bare entry array, or a single entry object. The body bypasses the SDK runner and is posted directly to `/insert-functions` (the same endpoint `bt topics config enable` uses to create classifier functions). Reuses the runner path's preflight machinery: `parse_project_selector` + `add_selector_requirement` for project classification, `validate_direct_project_ids` for direct-id verification, `resolve_named_projects` (honors `--create-missing-projects`) + `resolve_default_project_id` for project resolution, and `validate_duplicate_slugs` for cross-specifier collision detection. A pre-resolution dedup pass canonicalizes Fallback to its default project name so duplicate slugs never trigger fresh-project creation on the server. Failure paths emit a manifest-aware `PushSummary` with `total_files: 1` and `source_file: <manifest-path>` so JSON consumers see the right context. Two-step parse distinguishes `ManifestInvalidJson` from `ManifestSchemaInvalid`. Spinner is suppressed in `--json` mode to keep stderr clean. Mutually exclusive only with `--file` and the positional file paths (the genuinely ambiguous overlap). Runner-specific options (`--runner`, `--language`, etc.) are silently ignored on this path so env-backed defaults don't block the command. Closes #149.
1 parent fd5aed7 commit 2263927

6 files changed

Lines changed: 974 additions & 13 deletions

File tree

src/functions/mod.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,15 @@ pub(crate) struct PushArgs {
270270
)]
271271
pub file_flag: Vec<PathBuf>,
272272

273+
/// Push raw JSON function definitions, skipping the SDK runner.
274+
#[arg(
275+
long = "manifest",
276+
env = "BT_FUNCTIONS_PUSH_MANIFEST",
277+
value_name = "PATH",
278+
conflicts_with_all = ["files", "file_flag"]
279+
)]
280+
pub manifest: Option<PathBuf>,
281+
273282
/// Behavior when a function with the same slug already exists.
274283
#[arg(
275284
long = "if-exists",
@@ -687,6 +696,35 @@ mod tests {
687696
assert!(msg.contains("--type"));
688697
}
689698

699+
#[test]
700+
fn push_manifest_flag_parses() {
701+
let _guard = test_lock();
702+
let parsed =
703+
parse(&["functions", "push", "--manifest", "fn.json"]).expect("parse push manifest");
704+
let FunctionsCommands::Push(push) = parsed.command.expect("subcommand") else {
705+
panic!("expected push command");
706+
};
707+
assert_eq!(
708+
push.manifest,
709+
Some(std::path::PathBuf::from("fn.json")),
710+
"manifest path should be parsed"
711+
);
712+
assert!(push.files.is_empty());
713+
assert!(push.file_flag.is_empty());
714+
}
715+
716+
#[test]
717+
fn push_manifest_conflicts_with_file_path() {
718+
let _guard = test_lock();
719+
let err = parse(&["functions", "push", "--manifest", "fn.json", "extra.ts"])
720+
.expect_err("manifest+file path should conflict");
721+
let msg = err.to_string();
722+
assert!(
723+
msg.contains("manifest") || msg.contains("--manifest") || msg.contains("conflict"),
724+
"expected conflict-related error, got: {msg}"
725+
);
726+
}
727+
690728
#[test]
691729
fn top_level_type_flag_still_parses_for_functions_namespace() {
692730
let _guard = test_lock();

0 commit comments

Comments
 (0)