mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-05 02:41:45 +00:00
## Summary
Adds a workflow-visible variables store and HTTP API for managing
non-sensitive run variables, then wires those variables into run config
interpolation before run creation, validation, and preflight.
## What Changed
- Adds `/api/v1/variables` CRUD endpoints backed by a JSON variable
store and generated Rust/TypeScript API types.
- Supports `{{ vars.NAME }}` interpolation alongside existing `{{
env.NAME }}` handling for run-owned config fields, including
environment, MCP, hook, artifact, checkpoint, SCM, and notification
settings.
- Reuses canonical `fabro-types` variable DTOs in `fabro-api` and adds
OpenAPI name patterns so clients see the same env-style variable
contract enforced by the server.
- Keeps variable updates store-owned with `update_existing`, avoiding
duplicated not-found/update semantics in the HTTP handler.
- Shares env-style name validation between variables, interpolation
parsing, and vault token names to avoid grammar drift.
Variables are intentionally non-sensitive: list/get responses include
values, unlike vault secrets.
## Validation
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo test -p fabro-types`
- `cargo test -p fabro-variable`
- `cargo test -p fabro-api --test variable_round_trip`
- `cargo test -p fabro-server --features test-support --test it
api::variables`
- `cargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-variable -p
fabro-vault --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 clippy -p fabro-server --features
test-support --all-targets -- -D warnings`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex/)
79 lines
2.5 KiB
Rust
79 lines
2.5 KiB
Rust
use std::any::{TypeId, type_name};
|
|
|
|
use fabro_api::types::{
|
|
CreateVariableRequest as ApiCreateVariableRequest,
|
|
UpdateVariableRequest as ApiUpdateVariableRequest, Variable as ApiVariable,
|
|
VariableListResponse as ApiVariableListResponse,
|
|
};
|
|
use fabro_types::{CreateVariableRequest, UpdateVariableRequest, Variable, VariableListResponse};
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn variable_api_types_reuse_canonical_types() {
|
|
assert_same_type::<ApiVariable, Variable>();
|
|
assert_same_type::<ApiVariableListResponse, VariableListResponse>();
|
|
assert_same_type::<ApiCreateVariableRequest, CreateVariableRequest>();
|
|
assert_same_type::<ApiUpdateVariableRequest, UpdateVariableRequest>();
|
|
}
|
|
|
|
#[test]
|
|
fn variable_round_trips_representative_json() {
|
|
let value = json!({
|
|
"name": "DEPLOY_ENV",
|
|
"value": "production",
|
|
"description": "Deployment target",
|
|
"created_at": "2026-05-27T12:34:56Z",
|
|
"updated_at": "2026-05-27T12:40:00Z"
|
|
});
|
|
|
|
let variable: Variable = serde_json::from_value(value.clone()).unwrap();
|
|
|
|
assert_eq!(variable.name, "DEPLOY_ENV");
|
|
assert_eq!(variable.value, "production");
|
|
assert_eq!(variable.description.as_deref(), Some("Deployment target"));
|
|
assert_eq!(serde_json::to_value(variable).unwrap(), value);
|
|
}
|
|
|
|
#[test]
|
|
fn variable_requests_round_trip_json() {
|
|
let create = json!({
|
|
"name": "EMPTY_ALLOWED",
|
|
"value": "",
|
|
"description": "Intentionally blank"
|
|
});
|
|
let parsed_create: CreateVariableRequest = serde_json::from_value(create.clone()).unwrap();
|
|
assert_eq!(serde_json::to_value(parsed_create).unwrap(), create);
|
|
|
|
let update = json!({
|
|
"value": "updated"
|
|
});
|
|
let parsed_update: UpdateVariableRequest = serde_json::from_value(update.clone()).unwrap();
|
|
assert_eq!(serde_json::to_value(parsed_update).unwrap(), update);
|
|
}
|
|
|
|
#[test]
|
|
fn variable_list_response_wraps_data() {
|
|
let value = json!({
|
|
"data": [{
|
|
"name": "DEPLOY_ENV",
|
|
"value": "production",
|
|
"created_at": "2026-05-27T12:34:56Z",
|
|
"updated_at": "2026-05-27T12:40:00Z"
|
|
}]
|
|
});
|
|
|
|
let response: VariableListResponse = serde_json::from_value(value.clone()).unwrap();
|
|
|
|
assert_eq!(response.data.len(), 1);
|
|
assert_eq!(serde_json::to_value(response).unwrap(), value);
|
|
}
|
|
|
|
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>()
|
|
);
|
|
}
|