mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
parent
a71b6882cf
commit
ab98df4581
4 changed files with 688 additions and 139 deletions
564
run.json
564
run.json
File diff suppressed because one or more lines are too long
252
stages/007-simplify_gpt@1/diff.patch
Normal file
252
stages/007-simplify_gpt@1/diff.patch
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
diff --git a/lib/crates/fabro-server/src/server/handler/automations.rs b/lib/crates/fabro-server/src/server/handler/automations.rs
|
||||
index c29920d1d..51e09a980 100644
|
||||
--- a/lib/crates/fabro-server/src/server/handler/automations.rs
|
||||
+++ b/lib/crates/fabro-server/src/server/handler/automations.rs
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
-use axum::http::{HeaderMap, HeaderValue, header};
|
||||
+use axum::http::HeaderMap;
|
||||
use axum_extra::extract::Query as ExtraQuery;
|
||||
use chrono::Utc;
|
||||
use fabro_automation::{
|
||||
- Automation, AutomationDraft, AutomationId, AutomationReplace, AutomationRevision,
|
||||
- AutomationStoreError,
|
||||
+ Automation, AutomationDraft, AutomationId, AutomationReplace, AutomationStoreError,
|
||||
};
|
||||
use fabro_config::Storage;
|
||||
use fabro_types::{AutomationRef, RunId};
|
||||
@@ -15,7 +14,7 @@ use super::super::{
|
||||
ApiError, AppState, IntoResponse, Json, PaginationParams, Path, RequiredUser, Response, Router,
|
||||
State, StatusCode, get, paginate_items,
|
||||
};
|
||||
-use super::{lifecycle, runs};
|
||||
+use super::{json_with_etag_response, lifecycle, parse_required_if_match, runs};
|
||||
use crate::automation_materializer::AutomationRunMaterializeInput;
|
||||
use crate::principal_middleware::RequiredRunToolActor;
|
||||
|
||||
@@ -227,7 +226,7 @@ async fn replace_automation(
|
||||
Json(replacement): Json<AutomationReplace>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let id = parse_path_id(id)?;
|
||||
- let expected = parse_required_if_match(&headers, &id)?;
|
||||
+ let expected = parse_required_if_match(&headers, "automation", &id)?;
|
||||
let automation = state
|
||||
.automation_store()
|
||||
.replace(&id, &expected, replacement)
|
||||
@@ -242,7 +241,7 @@ async fn delete_automation(
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let id = parse_path_id(id)?;
|
||||
- let expected = parse_required_if_match(&headers, &id)?;
|
||||
+ let expected = parse_required_if_match(&headers, "automation", &id)?;
|
||||
state.automation_store().delete(&id, &expected).await?;
|
||||
Ok(StatusCode::NO_CONTENT.into_response())
|
||||
}
|
||||
@@ -252,38 +251,9 @@ fn parse_path_id(id: String) -> Result<AutomationId, ApiError> {
|
||||
.map_err(|err| ApiError::bad_request(format!("invalid automation id: {err}")))
|
||||
}
|
||||
|
||||
-fn parse_required_if_match(
|
||||
- headers: &HeaderMap,
|
||||
- id: &AutomationId,
|
||||
-) -> Result<AutomationRevision, ApiError> {
|
||||
- let Some(value) = headers.get(header::IF_MATCH) else {
|
||||
- return Err(ApiError::new(
|
||||
- StatusCode::PRECONDITION_REQUIRED,
|
||||
- format!("If-Match header is required for automation: {id}"),
|
||||
- ));
|
||||
- };
|
||||
- let value = value
|
||||
- .to_str()
|
||||
- .map_err(|_| ApiError::bad_request("If-Match header must be visible ASCII"))?;
|
||||
- let value = unquote_etag(value.trim());
|
||||
- value.parse::<AutomationRevision>().map_err(|err| {
|
||||
- ApiError::bad_request(format!("invalid If-Match automation revision: {err}"))
|
||||
- })
|
||||
-}
|
||||
-
|
||||
-fn unquote_etag(value: &str) -> &str {
|
||||
- value
|
||||
- .strip_prefix('"')
|
||||
- .and_then(|unquoted| unquoted.strip_suffix('"'))
|
||||
- .unwrap_or(value)
|
||||
-}
|
||||
-
|
||||
fn automation_with_etag_response(status: StatusCode, automation: Automation) -> Response {
|
||||
- let etag = HeaderValue::from_str(&format!("\"{}\"", automation.revision))
|
||||
- .expect("automation revisions are valid ETag header values");
|
||||
- let mut response = (status, Json(automation)).into_response();
|
||||
- response.headers_mut().insert(header::ETAG, etag);
|
||||
- response
|
||||
+ let revision = automation.revision.clone();
|
||||
+ json_with_etag_response(status, "automation", &revision, automation)
|
||||
}
|
||||
|
||||
impl From<AutomationStoreError> for ApiError {
|
||||
diff --git a/lib/crates/fabro-server/src/server/handler/environments.rs b/lib/crates/fabro-server/src/server/handler/environments.rs
|
||||
index d0bf0bf55..686bd089f 100644
|
||||
--- a/lib/crates/fabro-server/src/server/handler/environments.rs
|
||||
+++ b/lib/crates/fabro-server/src/server/handler/environments.rs
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
-use axum::http::{HeaderMap, HeaderValue, header};
|
||||
-use fabro_environment::{
|
||||
- Environment, EnvironmentDraft, EnvironmentId, EnvironmentRevision, EnvironmentStoreError,
|
||||
-};
|
||||
+use axum::http::HeaderMap;
|
||||
+use fabro_environment::{Environment, EnvironmentDraft, EnvironmentId, EnvironmentStoreError};
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::{
|
||||
DockerfileSource, EnvironmentImageSettings, EnvironmentLifecycleSettings,
|
||||
@@ -18,6 +16,7 @@ use super::super::{
|
||||
ApiError, AppState, IntoResponse, Json, Path, RequiredUser, Response, Router, State,
|
||||
StatusCode, get,
|
||||
};
|
||||
+use super::{json_with_etag_response, parse_required_if_match};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EnvironmentListResponse {
|
||||
@@ -67,7 +66,9 @@ struct ApiEnvironmentImageSettings {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
enum ApiDockerfileSource {
|
||||
- Inline { value: String },
|
||||
+ Inline {
|
||||
+ value: String,
|
||||
+ },
|
||||
// Recognized so the handler can return a 422 with bespoke guidance.
|
||||
// The `path` payload is parsed and discarded — never read from disk.
|
||||
Path {
|
||||
@@ -193,7 +194,7 @@ async fn replace_environment(
|
||||
Json(request): Json<ReplaceEnvironmentRequest>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let id = parse_path_id(id)?;
|
||||
- let expected = parse_required_if_match(&headers, &id)?;
|
||||
+ let expected = parse_required_if_match(&headers, "environment", &id)?;
|
||||
let environment = state
|
||||
.environment_store()
|
||||
.replace(&id, &expected, request.into_settings()?)
|
||||
@@ -209,7 +210,7 @@ async fn delete_environment(
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let id = parse_path_id(id)?;
|
||||
- let expected = parse_required_if_match(&headers, &id)?;
|
||||
+ let expected = parse_required_if_match(&headers, "environment", &id)?;
|
||||
state.environment_store().delete(&id, &expected).await?;
|
||||
state.refresh_manifest_run_settings_from_environment_catalog();
|
||||
Ok(StatusCode::NO_CONTENT.into_response())
|
||||
@@ -220,38 +221,9 @@ fn parse_path_id(id: String) -> Result<EnvironmentId, ApiError> {
|
||||
.map_err(|err| ApiError::bad_request(format!("invalid environment id: {err}")))
|
||||
}
|
||||
|
||||
-fn parse_required_if_match(
|
||||
- headers: &HeaderMap,
|
||||
- id: &EnvironmentId,
|
||||
-) -> Result<EnvironmentRevision, ApiError> {
|
||||
- let Some(value) = headers.get(header::IF_MATCH) else {
|
||||
- return Err(ApiError::new(
|
||||
- StatusCode::PRECONDITION_REQUIRED,
|
||||
- format!("If-Match header is required for environment: {id}"),
|
||||
- ));
|
||||
- };
|
||||
- let value = value
|
||||
- .to_str()
|
||||
- .map_err(|_| ApiError::bad_request("If-Match header must be visible ASCII"))?;
|
||||
- let value = unquote_etag(value.trim());
|
||||
- value.parse::<EnvironmentRevision>().map_err(|err| {
|
||||
- ApiError::bad_request(format!("invalid If-Match environment revision: {err}"))
|
||||
- })
|
||||
-}
|
||||
-
|
||||
-fn unquote_etag(value: &str) -> &str {
|
||||
- value
|
||||
- .strip_prefix('"')
|
||||
- .and_then(|unquoted| unquoted.strip_suffix('"'))
|
||||
- .unwrap_or(value)
|
||||
-}
|
||||
-
|
||||
fn environment_with_etag_response(status: StatusCode, environment: Environment) -> Response {
|
||||
- let etag = HeaderValue::from_str(&format!("\"{}\"", environment.revision))
|
||||
- .expect("environment revisions are valid ETag header values");
|
||||
- let mut response = (status, Json(environment)).into_response();
|
||||
- response.headers_mut().insert(header::ETAG, etag);
|
||||
- response
|
||||
+ let revision = environment.revision.clone();
|
||||
+ json_with_etag_response(status, "environment", &revision, environment)
|
||||
}
|
||||
|
||||
impl From<EnvironmentStoreError> for ApiError {
|
||||
diff --git a/lib/crates/fabro-server/src/server/handler/mod.rs b/lib/crates/fabro-server/src/server/handler/mod.rs
|
||||
index ff3c3648a..5b931f76f 100644
|
||||
--- a/lib/crates/fabro-server/src/server/handler/mod.rs
|
||||
+++ b/lib/crates/fabro-server/src/server/handler/mod.rs
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
+use axum::http::{HeaderMap, HeaderValue, header};
|
||||
use axum::routing::{get, post};
|
||||
+use serde::Serialize;
|
||||
|
||||
-use super::{ApiError, AppState, IntoResponse, Response, StatusCode, demo};
|
||||
+use super::{ApiError, AppState, IntoResponse, Json, Response, StatusCode, demo};
|
||||
|
||||
mod artifacts;
|
||||
mod automations;
|
||||
@@ -32,6 +34,53 @@ async fn not_implemented() -> Response {
|
||||
ApiError::new(StatusCode::NOT_IMPLEMENTED, "Not implemented.").into_response()
|
||||
}
|
||||
|
||||
+fn parse_required_if_match<R>(
|
||||
+ headers: &HeaderMap,
|
||||
+ resource: &str,
|
||||
+ id: &impl std::fmt::Display,
|
||||
+) -> Result<R, ApiError>
|
||||
+where
|
||||
+ R: std::str::FromStr,
|
||||
+ R::Err: std::fmt::Display,
|
||||
+{
|
||||
+ let Some(value) = headers.get(header::IF_MATCH) else {
|
||||
+ return Err(ApiError::new(
|
||||
+ StatusCode::PRECONDITION_REQUIRED,
|
||||
+ format!("If-Match header is required for {resource}: {id}"),
|
||||
+ ));
|
||||
+ };
|
||||
+ let value = value
|
||||
+ .to_str()
|
||||
+ .map_err(|_| ApiError::bad_request("If-Match header must be visible ASCII"))?;
|
||||
+ let value = unquote_etag(value.trim());
|
||||
+ value.parse::<R>().map_err(|err| {
|
||||
+ ApiError::bad_request(format!("invalid If-Match {resource} revision: {err}"))
|
||||
+ })
|
||||
+}
|
||||
+
|
||||
+fn unquote_etag(value: &str) -> &str {
|
||||
+ value
|
||||
+ .strip_prefix('"')
|
||||
+ .and_then(|unquoted| unquoted.strip_suffix('"'))
|
||||
+ .unwrap_or(value)
|
||||
+}
|
||||
+
|
||||
+fn json_with_etag_response<T>(
|
||||
+ status: StatusCode,
|
||||
+ resource: &str,
|
||||
+ revision: &impl std::fmt::Display,
|
||||
+ body: T,
|
||||
+) -> Response
|
||||
+where
|
||||
+ T: Serialize,
|
||||
+{
|
||||
+ let etag = HeaderValue::from_str(&format!("\"{revision}\""))
|
||||
+ .unwrap_or_else(|_| panic!("{resource} revisions are valid ETag header values"));
|
||||
+ let mut response = (status, Json(body)).into_response();
|
||||
+ response.headers_mut().insert(header::ETAG, etag);
|
||||
+ response
|
||||
+}
|
||||
+
|
||||
pub(super) fn demo_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/runs", get(demo::list_runs).post(demo::create_run_stub))
|
||||
6
stages/007-simplify_gpt@1/status.json
Normal file
6
stages/007-simplify_gpt@1/status.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"outcome": "succeeded",
|
||||
"notes": "Stage completed: simplify_gpt",
|
||||
"failure_reason": null,
|
||||
"timestamp": "2026-05-29T18:49:12.697915Z"
|
||||
}
|
||||
5
stages/008-verify@1/script_invocation.json
Normal file
5
stages/008-verify@1/script_invocation.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"script": "git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
|
||||
"command": "exec 2>&1\ngit fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\\bActorRef\\b|\\bActorKind\\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\\s*==\\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1",
|
||||
"language": "shell"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue