mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
fabro(01KSE2PAVXD56N4TWNK4T5H5VA): implement (succeeded)
Fabro-Run: 01KSE2PAVXD56N4TWNK4T5H5VA
Fabro-Completed: 7
Fabro-Checkpoint: 86b9c2e810
⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
parent
2a9ad6f0c1
commit
afdd4900fa
85 changed files with 5138 additions and 48 deletions
140
Cargo.lock
generated
140
Cargo.lock
generated
|
|
@ -105,7 +105,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
@ -1022,6 +1022,17 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "croner"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4aa42bcd3d846ebf66e15bd528d1087f75d1c6c1c66ebff626178a106353c576"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"derive_builder",
|
||||
"strum 0.27.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
|
|
@ -1166,6 +1177,16 @@ dependencies = [
|
|||
"darling_macro 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core 0.20.11",
|
||||
"darling_macro 0.20.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
|
|
@ -1190,6 +1211,20 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.23.0"
|
||||
|
|
@ -1214,6 +1249,17 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core 0.20.11",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
|
|
@ -1332,6 +1378,37 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
|
|
@ -1632,7 +1709,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"sha2",
|
||||
"shell-escape",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
|
|
@ -1647,6 +1724,7 @@ name = "fabro-api"
|
|||
version = "0.243.0-nightly.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"fabro-automation",
|
||||
"fabro-config",
|
||||
"fabro-model",
|
||||
"fabro-types",
|
||||
|
|
@ -1687,6 +1765,22 @@ dependencies = [
|
|||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-automation"
|
||||
version = "0.243.0-nightly.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"croner",
|
||||
"hex",
|
||||
"serde",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"toml 0.8.23",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fabro-build-support"
|
||||
version = "0.243.0-nightly.1"
|
||||
|
|
@ -1963,7 +2057,7 @@ dependencies = [
|
|||
"nom",
|
||||
"regex",
|
||||
"serde",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
|
|
@ -2056,7 +2150,7 @@ dependencies = [
|
|||
"rand 0.9.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
|
@ -2137,7 +2231,7 @@ dependencies = [
|
|||
"schemars 1.2.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml 0.8.23",
|
||||
|
|
@ -2153,7 +2247,7 @@ dependencies = [
|
|||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"thiserror 2.0.18",
|
||||
"toml 0.8.23",
|
||||
"tracing",
|
||||
|
|
@ -2245,7 +2339,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
|
|
@ -2274,6 +2368,7 @@ dependencies = [
|
|||
"fabro-agent",
|
||||
"fabro-api",
|
||||
"fabro-auth",
|
||||
"fabro-automation",
|
||||
"fabro-build-support",
|
||||
"fabro-client",
|
||||
"fabro-config",
|
||||
|
|
@ -2322,7 +2417,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"sysinfo",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
|
|
@ -2355,7 +2450,7 @@ dependencies = [
|
|||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.26.2",
|
||||
|
|
@ -2480,7 +2575,7 @@ dependencies = [
|
|||
"schemars 1.2.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml 0.8.23",
|
||||
|
|
@ -2514,7 +2609,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"strum",
|
||||
"strum 0.28.0",
|
||||
"tempfile",
|
||||
"toml 0.8.23",
|
||||
"ulid",
|
||||
|
|
@ -6689,13 +6784,34 @@ version = "0.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros 0.27.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
"strum_macros 0.28.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ tags:
|
|||
description: Browser authentication and demo-mode controls
|
||||
- name: Runs
|
||||
description: Run management operations
|
||||
- name: Automations
|
||||
description: Server-owned runnable automation bindings
|
||||
- name: Sessions
|
||||
description: Ask Fabro sessions bound to runs
|
||||
- name: Human-in-the-Loop
|
||||
|
|
@ -1086,6 +1088,408 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
# ── Automations ───────────────────────────────────────────────────────
|
||||
|
||||
/api/v1/automations:
|
||||
get:
|
||||
operationId: listAutomations
|
||||
tags: [Automations]
|
||||
summary: List Automations
|
||||
description: Returns automation definitions sorted by automation ID.
|
||||
responses:
|
||||
"200":
|
||||
description: Automation definitions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AutomationListResponse"
|
||||
"400":
|
||||
description: Invalid request
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
post:
|
||||
operationId: createAutomation
|
||||
tags: [Automations]
|
||||
summary: Create Automation
|
||||
description: Creates an automation and persists it as canonical TOML.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateAutomationRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Automation created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Automation"
|
||||
"400":
|
||||
description: Malformed JSON
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"409":
|
||||
description: Automation already exists
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"422":
|
||||
description: Invalid automation domain data, including unknown trigger types
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/automations/{id}:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Automation ID.
|
||||
schema:
|
||||
type: string
|
||||
pattern: '^[a-z0-9][a-z0-9-]{0,62}$'
|
||||
get:
|
||||
operationId: getAutomation
|
||||
tags: [Automations]
|
||||
summary: Get Automation
|
||||
responses:
|
||||
"200":
|
||||
description: Automation definition
|
||||
headers:
|
||||
ETag:
|
||||
description: Current automation revision.
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Automation"
|
||||
"400":
|
||||
description: Invalid path syntax
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Automation not found
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
put:
|
||||
operationId: replaceAutomation
|
||||
tags: [Automations]
|
||||
summary: Replace Automation
|
||||
parameters:
|
||||
- name: If-Match
|
||||
in: header
|
||||
required: true
|
||||
description: Current automation revision, quoted or unquoted.
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ReplaceAutomationRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Automation replaced
|
||||
headers:
|
||||
ETag:
|
||||
description: New automation revision.
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Automation"
|
||||
"400":
|
||||
description: Malformed JSON or invalid path syntax
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Automation not found
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"409":
|
||||
description: Stale automation revision
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"422":
|
||||
description: Invalid automation domain data, including unknown trigger types
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"428":
|
||||
description: Missing If-Match header
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
patch:
|
||||
operationId: patchAutomation
|
||||
tags: [Automations]
|
||||
summary: Patch Automation
|
||||
parameters:
|
||||
- name: If-Match
|
||||
in: header
|
||||
required: true
|
||||
description: Current automation revision, quoted or unquoted.
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PatchAutomationRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Automation patched
|
||||
headers:
|
||||
ETag:
|
||||
description: New automation revision.
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Automation"
|
||||
"400":
|
||||
description: Malformed JSON or invalid path syntax
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Automation not found
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"409":
|
||||
description: Stale automation revision
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"422":
|
||||
description: Invalid automation domain data, including unknown trigger types
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"428":
|
||||
description: Missing If-Match header
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
delete:
|
||||
operationId: deleteAutomation
|
||||
tags: [Automations]
|
||||
summary: Delete Automation
|
||||
parameters:
|
||||
- name: If-Match
|
||||
in: header
|
||||
required: true
|
||||
description: Current automation revision, quoted or unquoted.
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: Automation deleted
|
||||
"400":
|
||||
description: Invalid path syntax
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Automation not found
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"409":
|
||||
description: Stale automation revision
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"428":
|
||||
description: Missing If-Match header
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/automations/{id}/runs:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Automation ID.
|
||||
schema:
|
||||
type: string
|
||||
pattern: '^[a-z0-9][a-z0-9-]{0,62}$'
|
||||
get:
|
||||
operationId: listAutomationRuns
|
||||
tags: [Automations]
|
||||
summary: List Automation Runs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PageLimit"
|
||||
- $ref: "#/components/parameters/PageOffset"
|
||||
responses:
|
||||
"200":
|
||||
description: Runs associated with the automation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PaginatedRunList"
|
||||
"400":
|
||||
description: Invalid path syntax
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Automation not found
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
post:
|
||||
operationId: createAutomationRun
|
||||
tags: [Automations]
|
||||
summary: Start Automation Run
|
||||
description: Materializes the automation target and creates a run when an enabled `api` trigger is present.
|
||||
responses:
|
||||
"201":
|
||||
description: Run created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Run"
|
||||
"400":
|
||||
description: Invalid path syntax
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Automation not found
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"409":
|
||||
description: Automation disabled or missing an enabled api trigger
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"422":
|
||||
description: Automation target could not be materialized
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
# ── Runs ──────────────────────────────────────────────────────────────
|
||||
|
||||
/api/v1/runs:
|
||||
|
|
@ -5341,6 +5745,200 @@ components:
|
|||
meta:
|
||||
$ref: "#/components/schemas/PaginationMeta"
|
||||
|
||||
Automation:
|
||||
description: Server-owned runnable automation binding.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
- revision
|
||||
- name
|
||||
- description
|
||||
- enabled
|
||||
- target
|
||||
- triggers
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
pattern: '^[a-z0-9][a-z0-9-]{0,62}$'
|
||||
revision:
|
||||
type: string
|
||||
description: Lowercase hex SHA-256 revision of the canonical TOML bytes.
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: ["string", "null"]
|
||||
enabled:
|
||||
type: boolean
|
||||
target:
|
||||
$ref: "#/components/schemas/AutomationTarget"
|
||||
triggers:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AutomationTrigger"
|
||||
|
||||
AutomationTarget:
|
||||
description: Repository, ref, and Fabro workflow selector materialized when the automation starts.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- repository
|
||||
- ref
|
||||
- workflow
|
||||
properties:
|
||||
repository:
|
||||
type: string
|
||||
description: GitHub owner/repo slug.
|
||||
example: fabro-sh/fabro
|
||||
ref:
|
||||
type: string
|
||||
description: Branch, tag, or SHA selector to check out.
|
||||
example: main
|
||||
workflow:
|
||||
type: string
|
||||
description: Fabro workflow slug or relative workflow path.
|
||||
example: dependency-update
|
||||
|
||||
AutomationTrigger:
|
||||
description: Automation trigger definition.
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/AutomationApiTrigger"
|
||||
- $ref: "#/components/schemas/AutomationScheduleTrigger"
|
||||
discriminator:
|
||||
propertyName: type
|
||||
mapping:
|
||||
api: "#/components/schemas/AutomationApiTrigger"
|
||||
schedule: "#/components/schemas/AutomationScheduleTrigger"
|
||||
|
||||
AutomationApiTrigger:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
pattern: '^[a-z0-9][a-z0-9_-]{0,62}$'
|
||||
example: api
|
||||
type:
|
||||
type: string
|
||||
enum: [api]
|
||||
enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
AutomationScheduleTrigger:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
- expression
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
pattern: '^[a-z0-9][a-z0-9_-]{0,62}$'
|
||||
example: nightly
|
||||
type:
|
||||
type: string
|
||||
enum: [schedule]
|
||||
enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
expression:
|
||||
type: string
|
||||
description: Five-field cron expression accepted by croner.
|
||||
example: "0 3 * * *"
|
||||
|
||||
CreateAutomationRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- target
|
||||
- triggers
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
pattern: '^[a-z0-9][a-z0-9-]{0,62}$'
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: ["string", "null"]
|
||||
enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
target:
|
||||
$ref: "#/components/schemas/AutomationTarget"
|
||||
triggers:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AutomationTrigger"
|
||||
|
||||
ReplaceAutomationRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- name
|
||||
- enabled
|
||||
- target
|
||||
- triggers
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: ["string", "null"]
|
||||
enabled:
|
||||
type: boolean
|
||||
target:
|
||||
$ref: "#/components/schemas/AutomationTarget"
|
||||
triggers:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AutomationTrigger"
|
||||
|
||||
PatchAutomationRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: ["string", "null"]
|
||||
enabled:
|
||||
type: boolean
|
||||
target:
|
||||
$ref: "#/components/schemas/AutomationTarget"
|
||||
triggers:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AutomationTrigger"
|
||||
|
||||
AutomationListResponse:
|
||||
description: List of automation definitions.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Automation"
|
||||
meta:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- total
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
|
||||
BatchRunLifecycleRequest:
|
||||
description: Run IDs to archive or unarchive as one bounded fail-soft batch.
|
||||
type: object
|
||||
|
|
@ -9445,6 +10043,8 @@ components:
|
|||
type: string
|
||||
name:
|
||||
type: ["string", "null"]
|
||||
trigger_id:
|
||||
type: string
|
||||
|
||||
RunOrigin:
|
||||
type: object
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ wildcard_imports = "warn"
|
|||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
fabro-automation = { path = "../fabro-automation" }
|
||||
fabro-config = { path = "../fabro-config" }
|
||||
fabro-model = { path = "../fabro-model" }
|
||||
fabro-types = { path = "../fabro-types" }
|
||||
|
|
|
|||
|
|
@ -201,6 +201,29 @@ fn main() {
|
|||
&[],
|
||||
),
|
||||
("Run", "fabro_types::Run", &[]),
|
||||
("Automation", "fabro_automation::Automation", &[]),
|
||||
("AutomationTarget", "fabro_automation::AutomationTarget", &[
|
||||
]),
|
||||
(
|
||||
"AutomationTrigger",
|
||||
"fabro_automation::AutomationTrigger",
|
||||
&[],
|
||||
),
|
||||
(
|
||||
"CreateAutomationRequest",
|
||||
"fabro_automation::AutomationDraft",
|
||||
&[],
|
||||
),
|
||||
(
|
||||
"ReplaceAutomationRequest",
|
||||
"fabro_automation::AutomationReplace",
|
||||
&[],
|
||||
),
|
||||
(
|
||||
"PatchAutomationRequest",
|
||||
"fabro_automation::AutomationPatch",
|
||||
&[],
|
||||
),
|
||||
("RunApproval", "fabro_types::RunApproval", &[]),
|
||||
("RunApprovalState", "fabro_types::RunApprovalState", &[]),
|
||||
("RunRunnableSource", "fabro_types::RunRunnableSource", &[]),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ mod generated {
|
|||
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
|
||||
}
|
||||
pub mod types {
|
||||
pub use fabro_automation::{
|
||||
ApiTrigger as AutomationApiTrigger, Automation, AutomationDraft as CreateAutomationRequest,
|
||||
AutomationPatch as PatchAutomationRequest, AutomationReplace as ReplaceAutomationRequest,
|
||||
AutomationTarget, AutomationTrigger, ScheduleTrigger as AutomationScheduleTrigger,
|
||||
};
|
||||
pub use fabro_model::{
|
||||
Model, ModelCosts, ModelFeatures, ModelLimits, ModelRef as BillingModelRef, ModelTestMode,
|
||||
Provider, ReasoningEffort, ReasoningEffortFeature, Speed as BillingSpeed,
|
||||
|
|
|
|||
133
lib/crates/fabro-api/tests/automation_round_trip.rs
Normal file
133
lib/crates/fabro-api/tests/automation_round_trip.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use std::any::{TypeId, type_name};
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use fabro_api::types::{
|
||||
Automation as ApiAutomation, AutomationTarget as ApiAutomationTarget,
|
||||
AutomationTrigger as ApiAutomationTrigger, CreateAutomationRequest, PatchAutomationRequest,
|
||||
ReplaceAutomationRequest,
|
||||
};
|
||||
use fabro_automation::{
|
||||
ApiTrigger, Automation, AutomationDraft, AutomationPatch, AutomationReplace,
|
||||
AutomationRevision, AutomationTarget, AutomationTrigger, AutomationTriggerId, GitRefSelector,
|
||||
RepositorySlug, ScheduleTrigger, WorkflowSlug,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn automation_api_reuses_domain_types() {
|
||||
assert_same_type::<ApiAutomation, Automation>();
|
||||
assert_same_type::<ApiAutomationTarget, AutomationTarget>();
|
||||
assert_same_type::<ApiAutomationTrigger, AutomationTrigger>();
|
||||
assert_same_type::<CreateAutomationRequest, AutomationDraft>();
|
||||
assert_same_type::<ReplaceAutomationRequest, AutomationReplace>();
|
||||
assert_same_type::<PatchAutomationRequest, AutomationPatch>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automation_json_matches_openapi_shape() {
|
||||
let automation = Automation {
|
||||
id: "nightly-deps".parse().unwrap(),
|
||||
revision: AutomationRevision::from_str("abc123").unwrap(),
|
||||
name: "Nightly dependency update".to_string(),
|
||||
description: Some("Open a PR for dependency updates.".to_string()),
|
||||
enabled: true,
|
||||
target: target(),
|
||||
triggers: vec![
|
||||
AutomationTrigger::Api(ApiTrigger {
|
||||
id: "api".parse().unwrap(),
|
||||
enabled: false,
|
||||
}),
|
||||
AutomationTrigger::Schedule(ScheduleTrigger {
|
||||
id: "nightly".parse().unwrap(),
|
||||
enabled: true,
|
||||
expression: "0 3 * * *".to_string(),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(automation).unwrap(),
|
||||
json!({
|
||||
"id": "nightly-deps",
|
||||
"revision": "abc123",
|
||||
"name": "Nightly dependency update",
|
||||
"description": "Open a PR for dependency updates.",
|
||||
"enabled": true,
|
||||
"target": {
|
||||
"repository": "fabro-sh/fabro",
|
||||
"ref": "main",
|
||||
"workflow": "dependency-update"
|
||||
},
|
||||
"triggers": [
|
||||
{ "id": "api", "type": "api", "enabled": false },
|
||||
{ "id": "nightly", "type": "schedule", "enabled": true, "expression": "0 3 * * *" }
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automation_request_json_matches_openapi_shape() {
|
||||
let create = AutomationDraft {
|
||||
id: "nightly-deps".parse().unwrap(),
|
||||
name: "Nightly dependency update".to_string(),
|
||||
description: None,
|
||||
enabled: None,
|
||||
target: target(),
|
||||
triggers: vec![AutomationTrigger::Api(ApiTrigger {
|
||||
id: "api".parse().unwrap(),
|
||||
enabled: true,
|
||||
})],
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(create).unwrap(),
|
||||
json!({
|
||||
"id": "nightly-deps",
|
||||
"name": "Nightly dependency update",
|
||||
"target": {
|
||||
"repository": "fabro-sh/fabro",
|
||||
"ref": "main",
|
||||
"workflow": "dependency-update"
|
||||
},
|
||||
"triggers": [
|
||||
{ "id": "api", "type": "api", "enabled": true }
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
let patch = AutomationPatch {
|
||||
name: None,
|
||||
description: Some(None),
|
||||
enabled: None,
|
||||
target: None,
|
||||
triggers: None,
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(patch).unwrap(),
|
||||
json!({ "description": null })
|
||||
);
|
||||
}
|
||||
|
||||
fn target() -> AutomationTarget {
|
||||
AutomationTarget {
|
||||
repository: RepositorySlug::from_str("fabro-sh/fabro").unwrap(),
|
||||
ref_: GitRefSelector::from_str("main").unwrap(),
|
||||
workflow: WorkflowSlug::from_str("dependency-update").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_id_json_shape_is_string() {
|
||||
let id = AutomationTriggerId::from_str("api_1").unwrap();
|
||||
assert_eq!(serde_json::to_value(id).unwrap(), json!("api_1"));
|
||||
}
|
||||
|
||||
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>()
|
||||
);
|
||||
}
|
||||
|
|
@ -134,6 +134,7 @@ fn run_spec_json() -> serde_json::Value {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
automation: None,
|
||||
fork_source_ref: None,
|
||||
})
|
||||
.unwrap()
|
||||
|
|
|
|||
27
lib/crates/fabro-automation/Cargo.toml
Normal file
27
lib/crates/fabro-automation/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "fabro-automation"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
publish = false
|
||||
license.workspace = true
|
||||
description = "Automation domain model and file-backed store"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
croner = "3.0.1"
|
||||
hex.workspace = true
|
||||
serde.workspace = true
|
||||
sha2.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
toml.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
75
lib/crates/fabro-automation/src/error.rs
Normal file
75
lib/crates/fabro-automation/src/error.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use toml::de::Error as TomlDeError;
|
||||
|
||||
use crate::id::AutomationId;
|
||||
use crate::model::AutomationRevision;
|
||||
|
||||
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum AutomationValidationError {
|
||||
#[error("invalid automation id: {0}")]
|
||||
InvalidAutomationId(String),
|
||||
#[error("invalid automation trigger id: {0}")]
|
||||
InvalidTriggerId(String),
|
||||
#[error("automation name must not be empty")]
|
||||
EmptyName,
|
||||
#[error("invalid repository slug: {0}")]
|
||||
InvalidRepositorySlug(String),
|
||||
#[error("invalid git ref selector: {0}")]
|
||||
InvalidGitRefSelector(String),
|
||||
#[error("invalid workflow selector: {0}")]
|
||||
InvalidWorkflowSelector(String),
|
||||
#[error("duplicate trigger id: {0}")]
|
||||
DuplicateTriggerId(String),
|
||||
#[error("at most one api trigger is allowed")]
|
||||
MultipleApiTriggers,
|
||||
#[error("invalid schedule expression: {0}")]
|
||||
InvalidScheduleExpression(String),
|
||||
#[error("invalid trigger shape: {0}")]
|
||||
InvalidTriggerShape(String),
|
||||
#[error("unknown trigger type: {0}")]
|
||||
UnknownTriggerType(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AutomationStoreError {
|
||||
#[error("automation not found: {0}")]
|
||||
NotFound(AutomationId),
|
||||
#[error("automation already exists: {0}")]
|
||||
AlreadyExists(AutomationId),
|
||||
#[error("missing automation revision")]
|
||||
MissingRevision,
|
||||
#[error("automation revision mismatch")]
|
||||
RevisionMismatch {
|
||||
expected: AutomationRevision,
|
||||
actual: AutomationRevision,
|
||||
},
|
||||
#[error(transparent)]
|
||||
Validation(#[from] AutomationValidationError),
|
||||
#[error("failed to parse automation TOML at {}: {source}", path.display())]
|
||||
Parse {
|
||||
path: PathBuf,
|
||||
source: TomlDeError,
|
||||
},
|
||||
#[error("I/O error at {}: {source}", path.display())]
|
||||
Io {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl AutomationStoreError {
|
||||
pub(crate) fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
|
||||
Self::Io {
|
||||
path: path.into(),
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse(path: impl Into<PathBuf>, source: TomlDeError) -> Self {
|
||||
Self::Parse {
|
||||
path: path.into(),
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
155
lib/crates/fabro-automation/src/id.rs
Normal file
155
lib/crates/fabro-automation/src/id.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::error::AutomationValidationError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct AutomationId(String);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct AutomationTriggerId(String);
|
||||
|
||||
impl AutomationId {
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AutomationTriggerId {
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AutomationId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AutomationTriggerId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AutomationId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AutomationTriggerId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for AutomationId {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
validate_id(&value, false)
|
||||
.then_some(Self(value.clone()))
|
||||
.ok_or(AutomationValidationError::InvalidAutomationId(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for AutomationTriggerId {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
validate_id(&value, true)
|
||||
.then_some(Self(value.clone()))
|
||||
.ok_or(AutomationValidationError::InvalidTriggerId(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AutomationId {
|
||||
type Err = AutomationValidationError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AutomationTriggerId {
|
||||
type Err = AutomationValidationError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AutomationId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AutomationTriggerId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AutomationId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Self::try_from(value).map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AutomationTriggerId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Self::try_from(value).map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_id(value: &str, allow_underscore: bool) -> bool {
|
||||
let bytes = value.as_bytes();
|
||||
matches!(bytes.first(), Some(first) if first.is_ascii_lowercase() || first.is_ascii_digit())
|
||||
&& bytes.len() <= 63
|
||||
&& bytes.iter().skip(1).all(|b| {
|
||||
b.is_ascii_lowercase()
|
||||
|| b.is_ascii_digit()
|
||||
|| *b == b'-'
|
||||
|| (allow_underscore && *b == b'_')
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AutomationId, AutomationTriggerId};
|
||||
|
||||
#[test]
|
||||
fn automation_id_accepts_locked_format() {
|
||||
assert!("a".parse::<AutomationId>().is_ok());
|
||||
assert!("a0-b".parse::<AutomationId>().is_ok());
|
||||
assert!("0".parse::<AutomationId>().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_id_accepts_underscore_after_first_character() {
|
||||
assert!("api_1".parse::<AutomationTriggerId>().is_ok());
|
||||
assert!("a-b_c".parse::<AutomationTriggerId>().is_ok());
|
||||
}
|
||||
}
|
||||
14
lib/crates/fabro-automation/src/lib.rs
Normal file
14
lib/crates/fabro-automation/src/lib.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
pub mod error;
|
||||
pub mod id;
|
||||
pub mod model;
|
||||
|
||||
mod store;
|
||||
|
||||
pub use error::{AutomationStoreError, AutomationValidationError};
|
||||
pub use id::{AutomationId, AutomationTriggerId};
|
||||
pub use model::{
|
||||
ApiTrigger, Automation, AutomationDraft, AutomationPatch, AutomationReplace,
|
||||
AutomationRevision, AutomationTarget, AutomationTrigger, GitRefSelector, RepositorySlug,
|
||||
ScheduleTrigger, WorkflowSlug,
|
||||
};
|
||||
pub use store::AutomationStore;
|
||||
776
lib/crates/fabro-automation/src/model.rs
Normal file
776
lib/crates/fabro-automation/src/model.rs
Normal file
|
|
@ -0,0 +1,776 @@
|
|||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::path::{Component, Path};
|
||||
use std::str::FromStr;
|
||||
|
||||
use croner::parser::{CronParser, Seconds, Year};
|
||||
use serde::de::Error as DeError;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use sha2::{Digest, Sha256};
|
||||
use toml::de::Error as TomlDeError;
|
||||
use toml_edit::ser::{Error as TomlEditSerError, to_document};
|
||||
|
||||
use crate::error::AutomationValidationError;
|
||||
use crate::id::{AutomationId, AutomationTriggerId};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct AutomationRevision(String);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct RepositorySlug(String);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GitRefSelector(String);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct WorkflowSlug(String);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Automation {
|
||||
pub id: AutomationId,
|
||||
pub revision: AutomationRevision,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub enabled: bool,
|
||||
pub target: AutomationTarget,
|
||||
pub triggers: Vec<AutomationTrigger>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AutomationTarget {
|
||||
pub repository: RepositorySlug,
|
||||
#[serde(rename = "ref")]
|
||||
pub ref_: GitRefSelector,
|
||||
pub workflow: WorkflowSlug,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AutomationTrigger {
|
||||
Api(ApiTrigger),
|
||||
Schedule(ScheduleTrigger),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ApiTrigger {
|
||||
pub id: AutomationTriggerId,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ScheduleTrigger {
|
||||
pub id: AutomationTriggerId,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub expression: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AutomationDraft {
|
||||
pub id: AutomationId,
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
pub target: AutomationTarget,
|
||||
pub triggers: Vec<AutomationTrigger>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AutomationReplace {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
pub enabled: bool,
|
||||
pub target: AutomationTarget,
|
||||
pub triggers: Vec<AutomationTrigger>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AutomationPatch {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<Option<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target: Option<AutomationTarget>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub triggers: Option<Vec<AutomationTrigger>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct PersistedAutomation {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub target: AutomationTarget,
|
||||
#[serde(default)]
|
||||
pub triggers: Vec<AutomationTrigger>,
|
||||
}
|
||||
|
||||
impl AutomationRevision {
|
||||
#[must_use]
|
||||
pub fn from_bytes(bytes: &[u8]) -> Self {
|
||||
Self(hex::encode(Sha256::digest(bytes)))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl RepositorySlug {
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn owner_repo(&self) -> (&str, &str) {
|
||||
self.0
|
||||
.split_once('/')
|
||||
.expect("repository slugs are validated to contain one slash")
|
||||
}
|
||||
}
|
||||
|
||||
impl GitRefSelector {
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkflowSlug {
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Automation {
|
||||
pub fn from_toml_bytes(id: AutomationId, bytes: &[u8]) -> Result<Self, TomlDeError> {
|
||||
let source = std::str::from_utf8(bytes).map_err(TomlDeError::custom)?;
|
||||
let persisted = toml::from_str::<PersistedAutomation>(source)?;
|
||||
// serde has already validated newtypes and trigger shapes. This call
|
||||
// checks cross-field invariants.
|
||||
persisted
|
||||
.into_automation(id, AutomationRevision::from_bytes(bytes))
|
||||
.map_err(TomlDeError::custom)
|
||||
}
|
||||
|
||||
pub fn from_draft(
|
||||
draft: AutomationDraft,
|
||||
revision: AutomationRevision,
|
||||
) -> Result<Self, AutomationValidationError> {
|
||||
let automation = Self {
|
||||
id: draft.id,
|
||||
revision,
|
||||
name: draft.name,
|
||||
description: draft.description,
|
||||
enabled: draft.enabled.unwrap_or(true),
|
||||
target: draft.target,
|
||||
triggers: draft.triggers,
|
||||
};
|
||||
automation.validate()?;
|
||||
Ok(automation)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_replace(self) -> AutomationReplace {
|
||||
AutomationReplace {
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
enabled: self.enabled,
|
||||
target: self.target,
|
||||
triggers: self.triggers,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_toml_bytes(&self) -> Result<Vec<u8>, TomlEditSerError> {
|
||||
let persisted = PersistedAutomation::from(self);
|
||||
to_document(&persisted).map(|document| document.to_string().into_bytes())
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), AutomationValidationError> {
|
||||
validate_common(&self.name, &self.triggers)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn api_trigger(&self) -> Option<&ApiTrigger> {
|
||||
self.triggers.iter().find_map(|trigger| match trigger {
|
||||
AutomationTrigger::Api(trigger) => Some(trigger),
|
||||
AutomationTrigger::Schedule(_) => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AutomationReplace {
|
||||
pub(crate) fn into_automation(
|
||||
self,
|
||||
id: AutomationId,
|
||||
revision: AutomationRevision,
|
||||
) -> Result<Automation, AutomationValidationError> {
|
||||
let automation = Automation {
|
||||
id,
|
||||
revision,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
enabled: self.enabled,
|
||||
target: self.target,
|
||||
triggers: self.triggers,
|
||||
};
|
||||
automation.validate()?;
|
||||
Ok(automation)
|
||||
}
|
||||
}
|
||||
|
||||
impl AutomationPatch {
|
||||
pub(crate) fn apply_to(self, current: &Automation) -> AutomationReplace {
|
||||
AutomationReplace {
|
||||
name: self.name.unwrap_or_else(|| current.name.clone()),
|
||||
description: self
|
||||
.description
|
||||
.unwrap_or_else(|| current.description.clone()),
|
||||
enabled: self.enabled.unwrap_or(current.enabled),
|
||||
target: self.target.unwrap_or_else(|| current.target.clone()),
|
||||
triggers: self.triggers.unwrap_or_else(|| current.triggers.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PersistedAutomation {
|
||||
pub(crate) fn into_automation(
|
||||
self,
|
||||
id: AutomationId,
|
||||
revision: AutomationRevision,
|
||||
) -> Result<Automation, AutomationValidationError> {
|
||||
let automation = Automation {
|
||||
id,
|
||||
revision,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
enabled: self.enabled,
|
||||
target: self.target,
|
||||
triggers: self.triggers,
|
||||
};
|
||||
automation.validate()?;
|
||||
Ok(automation)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Automation> for PersistedAutomation {
|
||||
fn from(value: &Automation) -> Self {
|
||||
Self {
|
||||
name: value.name.clone(),
|
||||
description: value.description.clone(),
|
||||
enabled: value.enabled,
|
||||
target: value.target.clone(),
|
||||
triggers: value.triggers.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AutomationTrigger {
|
||||
#[must_use]
|
||||
pub fn id(&self) -> &AutomationTriggerId {
|
||||
match self {
|
||||
Self::Api(trigger) => &trigger.id,
|
||||
Self::Schedule(trigger) => &trigger.id,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn enabled(&self) -> bool {
|
||||
match self {
|
||||
Self::Api(trigger) => trigger.enabled,
|
||||
Self::Schedule(trigger) => trigger.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_api(&self) -> bool {
|
||||
matches!(self, Self::Api(_))
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), AutomationValidationError> {
|
||||
match self {
|
||||
Self::Api(_) => Ok(()),
|
||||
Self::Schedule(trigger) => validate_schedule_expression(&trigger.expression),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_common(
|
||||
name: &str,
|
||||
triggers: &[AutomationTrigger],
|
||||
) -> Result<(), AutomationValidationError> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(AutomationValidationError::EmptyName);
|
||||
}
|
||||
|
||||
let mut ids = HashSet::new();
|
||||
let mut api_count = 0_usize;
|
||||
for trigger in triggers {
|
||||
if !ids.insert(trigger.id().clone()) {
|
||||
return Err(AutomationValidationError::DuplicateTriggerId(
|
||||
trigger.id().to_string(),
|
||||
));
|
||||
}
|
||||
if trigger.is_api() {
|
||||
api_count += 1;
|
||||
}
|
||||
trigger.validate()?;
|
||||
}
|
||||
if api_count > 1 {
|
||||
return Err(AutomationValidationError::MultipleApiTriggers);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_schedule_expression(expression: &str) -> Result<(), AutomationValidationError> {
|
||||
if expression.trim().is_empty() || expression.split_whitespace().count() != 5 {
|
||||
return Err(AutomationValidationError::InvalidScheduleExpression(
|
||||
expression.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
CronParser::builder()
|
||||
.seconds(Seconds::Disallowed)
|
||||
.year(Year::Disallowed)
|
||||
.build()
|
||||
.parse(expression)
|
||||
.map(|_| ())
|
||||
.map_err(|_| AutomationValidationError::InvalidScheduleExpression(expression.to_string()))
|
||||
}
|
||||
|
||||
impl TryFrom<String> for RepositorySlug {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
let Some((owner, repo)) = value.split_once('/') else {
|
||||
return Err(AutomationValidationError::InvalidRepositorySlug(value));
|
||||
};
|
||||
if repo.contains('/')
|
||||
|| !valid_github_slug_segment(owner, 39)
|
||||
|| !valid_github_slug_segment(repo, 100)
|
||||
{
|
||||
return Err(AutomationValidationError::InvalidRepositorySlug(value));
|
||||
}
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for GitRefSelector {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
if valid_git_ref_selector(&value) {
|
||||
Ok(Self(value))
|
||||
} else {
|
||||
Err(AutomationValidationError::InvalidGitRefSelector(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for WorkflowSlug {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
if valid_workflow_selector(&value) {
|
||||
Ok(Self(value))
|
||||
} else {
|
||||
Err(AutomationValidationError::InvalidWorkflowSelector(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_string_newtype {
|
||||
($type:ty) => {
|
||||
impl AsRef<str> for $type {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for $type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for $type {
|
||||
type Err = AutomationValidationError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for $type {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for $type {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Self::try_from(value).map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_string_newtype!(RepositorySlug);
|
||||
impl_string_newtype!(GitRefSelector);
|
||||
impl_string_newtype!(WorkflowSlug);
|
||||
|
||||
impl AsRef<str> for AutomationRevision {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AutomationRevision {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AutomationRevision {
|
||||
type Err = AutomationValidationError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AutomationRevision {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AutomationRevision {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(Self(String::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_github_slug_segment(value: &str, max_len: usize) -> bool {
|
||||
!value.is_empty()
|
||||
&& value.len() <= max_len
|
||||
&& !matches!(value, "." | "..")
|
||||
&& value
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
|
||||
}
|
||||
|
||||
fn valid_git_ref_selector(value: &str) -> bool {
|
||||
let value = value.trim();
|
||||
!value.is_empty()
|
||||
&& !value.starts_with('-')
|
||||
&& !value.contains("..")
|
||||
&& !value.contains("@{")
|
||||
&& !has_lock_suffix(value)
|
||||
&& !value.ends_with('/')
|
||||
&& !value.starts_with('/')
|
||||
&& !value.bytes().any(|b| {
|
||||
b.is_ascii_control()
|
||||
|| b.is_ascii_whitespace()
|
||||
|| matches!(
|
||||
b,
|
||||
b'\\'
|
||||
| b'^'
|
||||
| b'~'
|
||||
| b':'
|
||||
| b'?'
|
||||
| b'*'
|
||||
| b'['
|
||||
| b';'
|
||||
| b'&'
|
||||
| b'|'
|
||||
| b'$'
|
||||
| b'`'
|
||||
| b'\''
|
||||
| b'"'
|
||||
| b'<'
|
||||
| b'>'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn has_lock_suffix(value: &str) -> bool {
|
||||
value.rsplit('/').any(|component| {
|
||||
component
|
||||
.get(component.len().saturating_sub(".lock".len())..)
|
||||
.is_some_and(|suffix| suffix.eq_ignore_ascii_case(".lock"))
|
||||
})
|
||||
}
|
||||
|
||||
fn valid_workflow_selector(value: &str) -> bool {
|
||||
let value = value.trim();
|
||||
if value.is_empty()
|
||||
|| value == "."
|
||||
|| value.contains('\\')
|
||||
|| value.bytes().any(|b| b.is_ascii_control())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let path = Path::new(value);
|
||||
!path.is_absolute()
|
||||
&& path.components().all(|component| {
|
||||
matches!(component, Component::Normal(_) | Component::CurDir)
|
||||
&& !matches!(component, Component::ParentDir)
|
||||
})
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
Automation, AutomationDraft, AutomationReplace, AutomationRevision, AutomationTrigger,
|
||||
GitRefSelector, RepositorySlug, WorkflowSlug,
|
||||
};
|
||||
use crate::AutomationId;
|
||||
|
||||
fn valid_toml() -> &'static str {
|
||||
r#"
|
||||
name = "Nightly dependency update"
|
||||
description = "Open a PR for dependency updates."
|
||||
enabled = true
|
||||
|
||||
[target]
|
||||
repository = "fabro-sh/fabro"
|
||||
ref = "main"
|
||||
workflow = "dependency-update"
|
||||
|
||||
[[triggers]]
|
||||
id = "api"
|
||||
type = "api"
|
||||
enabled = false
|
||||
|
||||
[[triggers]]
|
||||
id = "nightly"
|
||||
type = "schedule"
|
||||
enabled = true
|
||||
expression = "0 3 * * *"
|
||||
"#
|
||||
}
|
||||
|
||||
fn valid_draft_toml(id: &str, triggers: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
id = "{id}"
|
||||
name = "Nightly"
|
||||
|
||||
[target]
|
||||
repository = "fabro-sh/fabro"
|
||||
ref = "main"
|
||||
workflow = "deps"
|
||||
|
||||
{triggers}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_toml_deserializes_and_computes_revision() {
|
||||
let id = AutomationId::try_from("nightly-deps".to_string())
|
||||
.expect("automation id should be valid");
|
||||
let automation = Automation::from_toml_bytes(id, valid_toml().as_bytes())
|
||||
.expect("automation TOML should parse");
|
||||
|
||||
assert_eq!(automation.name, "Nightly dependency update");
|
||||
assert!(automation.enabled);
|
||||
assert_eq!(automation.triggers.len(), 2);
|
||||
assert_eq!(
|
||||
automation.revision,
|
||||
AutomationRevision::from_bytes(valid_toml().as_bytes())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_defaults_enabled_and_description() {
|
||||
let source = r#"
|
||||
name = "Defaulted"
|
||||
|
||||
[target]
|
||||
repository = "fabro-sh/fabro"
|
||||
ref = "main"
|
||||
workflow = "dependency-update"
|
||||
|
||||
[[triggers]]
|
||||
id = "api"
|
||||
type = "api"
|
||||
"#;
|
||||
let id =
|
||||
AutomationId::try_from("defaulted".to_string()).expect("automation id should be valid");
|
||||
let automation = Automation::from_toml_bytes(id, source.as_bytes())
|
||||
.expect("automation TOML should parse");
|
||||
|
||||
assert!(automation.enabled);
|
||||
assert_eq!(automation.description, None);
|
||||
assert!(automation.triggers[0].enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_automation_ids_are_rejected() {
|
||||
for value in ["", "-bad", "Bad", "bad_", &"a".repeat(64)] {
|
||||
assert!(AutomationId::try_from(value.to_string()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_trigger_ids_are_rejected() {
|
||||
let result: Result<AutomationDraft, _> = toml::from_str(&valid_draft_toml(
|
||||
"nightly",
|
||||
r#"
|
||||
[[triggers]]
|
||||
id = "_api"
|
||||
type = "api"
|
||||
"#,
|
||||
));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_trigger_ids_are_rejected() {
|
||||
let draft: AutomationDraft = toml::from_str(&valid_draft_toml(
|
||||
"nightly",
|
||||
r#"
|
||||
[[triggers]]
|
||||
id = "api"
|
||||
type = "api"
|
||||
|
||||
[[triggers]]
|
||||
id = "api"
|
||||
type = "schedule"
|
||||
expression = "0 3 * * *"
|
||||
"#,
|
||||
))
|
||||
.expect("draft should deserialize");
|
||||
assert!(Automation::from_draft(draft, AutomationRevision::from_bytes(b"")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_api_triggers_are_rejected() {
|
||||
let draft: AutomationDraft = toml::from_str(&valid_draft_toml(
|
||||
"nightly",
|
||||
r#"
|
||||
[[triggers]]
|
||||
id = "api"
|
||||
type = "api"
|
||||
|
||||
[[triggers]]
|
||||
id = "api2"
|
||||
type = "api"
|
||||
"#,
|
||||
))
|
||||
.expect("draft should deserialize");
|
||||
assert!(Automation::from_draft(draft, AutomationRevision::from_bytes(b"")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_repository_slug_is_rejected() {
|
||||
for value in [
|
||||
"fabro-sh",
|
||||
"fabro-sh/fabro/extra",
|
||||
"../fabro",
|
||||
"owner/repo/name",
|
||||
] {
|
||||
assert!(RepositorySlug::try_from(value.to_string()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_schedule_expression_is_rejected() {
|
||||
let draft: AutomationDraft = toml::from_str(&valid_draft_toml(
|
||||
"nightly",
|
||||
r#"
|
||||
[[triggers]]
|
||||
id = "nightly"
|
||||
type = "schedule"
|
||||
expression = "* * * * * *"
|
||||
"#,
|
||||
))
|
||||
.expect("draft should deserialize");
|
||||
assert!(Automation::from_draft(draft, AutomationRevision::from_bytes(b"")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newtypes_have_toml_string_shape() {
|
||||
let replace: AutomationReplace = toml::from_str(
|
||||
r#"
|
||||
name = "Nightly"
|
||||
enabled = true
|
||||
|
||||
[target]
|
||||
repository = "fabro-sh/fabro"
|
||||
ref = "main"
|
||||
workflow = "deps"
|
||||
|
||||
[[triggers]]
|
||||
id = "api"
|
||||
type = "api"
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.expect("replace should deserialize");
|
||||
let target_toml = toml::to_string(&replace.target).expect("target should serialize");
|
||||
assert!(target_toml.contains("repository = \"fabro-sh/fabro\""));
|
||||
assert!(target_toml.contains("ref = \"main\""));
|
||||
assert!(target_toml.contains("workflow = \"deps\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_ref_and_workflow_selectors_are_rejected() {
|
||||
assert!(GitRefSelector::try_from("-main".to_string()).is_err());
|
||||
assert!(GitRefSelector::try_from("feature..main".to_string()).is_err());
|
||||
assert!(WorkflowSlug::try_from("/tmp/workflow".to_string()).is_err());
|
||||
assert!(WorkflowSlug::try_from("../workflow".to_string()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_variant_type_is_api() {
|
||||
let trigger: AutomationTrigger = toml::from_str(
|
||||
r#"
|
||||
id = "api"
|
||||
type = "api"
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.expect("api trigger should deserialize");
|
||||
assert!(matches!(trigger, AutomationTrigger::Api(_)));
|
||||
}
|
||||
}
|
||||
490
lib/crates/fabro-automation/src/store.rs
Normal file
490
lib/crates/fabro-automation/src/store.rs
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use tokio::fs::{self, OpenOptions};
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::error::{AutomationStoreError, AutomationValidationError};
|
||||
use crate::id::AutomationId;
|
||||
use crate::model::{
|
||||
Automation, AutomationDraft, AutomationPatch, AutomationReplace, AutomationRevision,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AutomationStore {
|
||||
dir: PathBuf,
|
||||
items: RwLock<BTreeMap<AutomationId, Automation>>,
|
||||
}
|
||||
|
||||
impl AutomationStore {
|
||||
pub async fn load(dir: impl Into<PathBuf>) -> Result<Self, AutomationStoreError> {
|
||||
let dir = dir.into();
|
||||
let mut items = BTreeMap::new();
|
||||
|
||||
match fs::read_dir(&dir).await {
|
||||
Ok(mut entries) => {
|
||||
while let Some(entry) = entries
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|err| AutomationStoreError::io(&dir, err))?
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|ext| ext.to_str()) != Some("toml") {
|
||||
continue;
|
||||
}
|
||||
let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
|
||||
return Err(AutomationValidationError::InvalidAutomationId(
|
||||
path.display().to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
let id = AutomationId::try_from(stem.to_string())?;
|
||||
let bytes = fs::read(&path)
|
||||
.await
|
||||
.map_err(|err| AutomationStoreError::io(&path, err))?;
|
||||
let automation = Automation::from_toml_bytes(id.clone(), &bytes)
|
||||
.map_err(|err| AutomationStoreError::parse(&path, err))?;
|
||||
items.insert(id, automation);
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(err) => return Err(AutomationStoreError::io(&dir, err)),
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
dir,
|
||||
items: RwLock::new(items),
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "Server startup loads automations before a Tokio runtime may be available."
|
||||
)]
|
||||
pub fn load_blocking(dir: impl Into<PathBuf>) -> Result<Self, AutomationStoreError> {
|
||||
let dir = dir.into();
|
||||
let mut items = BTreeMap::new();
|
||||
|
||||
match std::fs::read_dir(&dir) {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|err| AutomationStoreError::io(&dir, err))?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|ext| ext.to_str()) != Some("toml") {
|
||||
continue;
|
||||
}
|
||||
let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
|
||||
return Err(AutomationValidationError::InvalidAutomationId(
|
||||
path.display().to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
let id = AutomationId::try_from(stem.to_string())?;
|
||||
let bytes =
|
||||
std::fs::read(&path).map_err(|err| AutomationStoreError::io(&path, err))?;
|
||||
let automation = Automation::from_toml_bytes(id.clone(), &bytes)
|
||||
.map_err(|err| AutomationStoreError::parse(&path, err))?;
|
||||
items.insert(id, automation);
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(err) => return Err(AutomationStoreError::io(&dir, err)),
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
dir,
|
||||
items: RwLock::new(items),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Vec<Automation> {
|
||||
self.items.read().await.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub async fn get(&self, id: &AutomationId) -> Option<Automation> {
|
||||
self.items.read().await.get(id).cloned()
|
||||
}
|
||||
|
||||
pub async fn create(&self, draft: AutomationDraft) -> Result<Automation, AutomationStoreError> {
|
||||
let id = draft.id.clone();
|
||||
let mut items = self.items.write().await;
|
||||
if items.contains_key(&id) {
|
||||
return Err(AutomationStoreError::AlreadyExists(id));
|
||||
}
|
||||
|
||||
let automation = Automation::from_draft(draft, AutomationRevision::from_bytes(b""))?;
|
||||
let automation = self.persist_with_revision(automation).await?;
|
||||
items.insert(id, automation.clone());
|
||||
Ok(automation)
|
||||
}
|
||||
|
||||
pub async fn replace(
|
||||
&self,
|
||||
id: &AutomationId,
|
||||
expected: &AutomationRevision,
|
||||
draft: AutomationReplace,
|
||||
) -> Result<Automation, AutomationStoreError> {
|
||||
let mut items = self.items.write().await;
|
||||
let current = items
|
||||
.get(id)
|
||||
.ok_or_else(|| AutomationStoreError::NotFound(id.clone()))?;
|
||||
ensure_revision(current, expected)?;
|
||||
|
||||
let automation = draft.into_automation(id.clone(), AutomationRevision::from_bytes(b""))?;
|
||||
let automation = self.persist_with_revision(automation).await?;
|
||||
items.insert(id.clone(), automation.clone());
|
||||
Ok(automation)
|
||||
}
|
||||
|
||||
pub async fn patch(
|
||||
&self,
|
||||
id: &AutomationId,
|
||||
expected: &AutomationRevision,
|
||||
patch: AutomationPatch,
|
||||
) -> Result<Automation, AutomationStoreError> {
|
||||
let mut items = self.items.write().await;
|
||||
let current = items
|
||||
.get(id)
|
||||
.ok_or_else(|| AutomationStoreError::NotFound(id.clone()))?;
|
||||
ensure_revision(current, expected)?;
|
||||
|
||||
let draft = patch.apply_to(current);
|
||||
let automation = draft.into_automation(id.clone(), AutomationRevision::from_bytes(b""))?;
|
||||
let automation = self.persist_with_revision(automation).await?;
|
||||
items.insert(id.clone(), automation.clone());
|
||||
Ok(automation)
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
&self,
|
||||
id: &AutomationId,
|
||||
expected: &AutomationRevision,
|
||||
) -> Result<(), AutomationStoreError> {
|
||||
let mut items = self.items.write().await;
|
||||
let current = items
|
||||
.get(id)
|
||||
.ok_or_else(|| AutomationStoreError::NotFound(id.clone()))?;
|
||||
ensure_revision(current, expected)?;
|
||||
|
||||
let path = self.path_for(id);
|
||||
match fs::remove_file(&path).await {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(err) => return Err(AutomationStoreError::io(&path, err)),
|
||||
}
|
||||
items.remove(id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn persist_with_revision(
|
||||
&self,
|
||||
automation: Automation,
|
||||
) -> Result<Automation, AutomationStoreError> {
|
||||
let bytes = automation
|
||||
.to_toml_bytes()
|
||||
.map_err(|err| AutomationValidationError::InvalidWorkflowSelector(err.to_string()))?;
|
||||
atomic_write(&self.dir, &self.path_for(&automation.id), &bytes).await?;
|
||||
let revision = AutomationRevision::from_bytes(&bytes);
|
||||
Ok(Automation {
|
||||
revision,
|
||||
..automation
|
||||
})
|
||||
}
|
||||
|
||||
fn path_for(&self, id: &AutomationId) -> PathBuf {
|
||||
self.dir.join(format!("{id}.toml"))
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_revision(
|
||||
current: &Automation,
|
||||
expected: &AutomationRevision,
|
||||
) -> Result<(), AutomationStoreError> {
|
||||
if ¤t.revision == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AutomationStoreError::RevisionMismatch {
|
||||
expected: expected.clone(),
|
||||
actual: current.revision.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn atomic_write(
|
||||
dir: &Path,
|
||||
final_path: &Path,
|
||||
bytes: &[u8],
|
||||
) -> Result<(), AutomationStoreError> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.map_err(|err| AutomationStoreError::io(dir, err))?;
|
||||
|
||||
let temp_path = temp_path_for(dir, final_path);
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&temp_path)
|
||||
.await
|
||||
.map_err(|err| AutomationStoreError::io(&temp_path, err))?;
|
||||
let write_result = async {
|
||||
file.write_all(bytes).await?;
|
||||
file.flush().await?;
|
||||
file.sync_all().await
|
||||
}
|
||||
.await;
|
||||
if let Err(err) = write_result {
|
||||
let _ = fs::remove_file(&temp_path).await;
|
||||
return Err(AutomationStoreError::io(&temp_path, err));
|
||||
}
|
||||
drop(file);
|
||||
|
||||
if let Err(err) = fs::rename(&temp_path, final_path).await {
|
||||
let _ = fs::remove_file(&temp_path).await;
|
||||
return Err(AutomationStoreError::io(final_path, err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn temp_path_for(dir: &Path, final_path: &Path) -> PathBuf {
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let stem = final_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("automation.toml");
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_or(0, |duration| duration.as_nanos());
|
||||
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
dir.join(format!(".{stem}.{now}.{counter}.tmp"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokio::fs;
|
||||
|
||||
use super::AutomationStore;
|
||||
use crate::{
|
||||
AutomationDraft, AutomationId, AutomationPatch, AutomationReplace, AutomationRevision,
|
||||
};
|
||||
|
||||
fn draft(id: &str) -> AutomationDraft {
|
||||
toml::from_str(&format!(
|
||||
r#"
|
||||
id = "{id}"
|
||||
name = "Nightly"
|
||||
description = "Runs nightly"
|
||||
|
||||
[target]
|
||||
repository = "fabro-sh/fabro"
|
||||
ref = "main"
|
||||
workflow = "deps"
|
||||
|
||||
[[triggers]]
|
||||
id = "api"
|
||||
type = "api"
|
||||
"#
|
||||
))
|
||||
.expect("draft should deserialize")
|
||||
}
|
||||
|
||||
fn replacement(name: &str) -> AutomationReplace {
|
||||
toml::from_str(&format!(
|
||||
r#"
|
||||
name = "{name}"
|
||||
enabled = true
|
||||
|
||||
[target]
|
||||
repository = "fabro-sh/fabro"
|
||||
ref = "main"
|
||||
workflow = "deps"
|
||||
|
||||
[[triggers]]
|
||||
id = "api"
|
||||
type = "api"
|
||||
"#
|
||||
))
|
||||
.expect("replacement should deserialize")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_directory_loads_empty_store() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
let store = AutomationStore::load(dir.path().join("automations"))
|
||||
.await
|
||||
.expect("store should load");
|
||||
assert!(store.list().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_writes_file() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
let automation_dir = dir.path().join("automations");
|
||||
let store = AutomationStore::load(&automation_dir)
|
||||
.await
|
||||
.expect("store should load");
|
||||
|
||||
let automation = store
|
||||
.create(draft("nightly"))
|
||||
.await
|
||||
.expect("automation should be created");
|
||||
|
||||
let path = automation_dir.join("nightly.toml");
|
||||
let bytes = fs::read(&path).await.expect("file should exist");
|
||||
assert_eq!(automation.revision, AutomationRevision::from_bytes(&bytes));
|
||||
assert!(String::from_utf8_lossy(&bytes).contains("name = \"Nightly\""));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replace_changes_revision() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
let store = AutomationStore::load(dir.path())
|
||||
.await
|
||||
.expect("store should load");
|
||||
let first = store
|
||||
.create(draft("nightly"))
|
||||
.await
|
||||
.expect("automation should be created");
|
||||
|
||||
let second = store
|
||||
.replace(&first.id, &first.revision, replacement("Updated"))
|
||||
.await
|
||||
.expect("automation should be replaced");
|
||||
|
||||
assert_ne!(first.revision, second.revision);
|
||||
assert_eq!(second.name, "Updated");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn patch_keeps_unchanged_fields() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
let store = AutomationStore::load(dir.path())
|
||||
.await
|
||||
.expect("store should load");
|
||||
let first = store
|
||||
.create(draft("nightly"))
|
||||
.await
|
||||
.expect("automation should be created");
|
||||
let patch = AutomationPatch {
|
||||
name: Some("Patched".to_string()),
|
||||
..AutomationPatch::default()
|
||||
};
|
||||
|
||||
let patched = store
|
||||
.patch(&first.id, &first.revision, patch)
|
||||
.await
|
||||
.expect("automation should be patched");
|
||||
|
||||
assert_eq!(patched.name, "Patched");
|
||||
assert_eq!(patched.description.as_deref(), Some("Runs nightly"));
|
||||
assert_eq!(patched.target, first.target);
|
||||
assert_eq!(patched.triggers, first.triggers);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stale_revision_fails() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
let store = AutomationStore::load(dir.path())
|
||||
.await
|
||||
.expect("store should load");
|
||||
let first = store
|
||||
.create(draft("nightly"))
|
||||
.await
|
||||
.expect("automation should be created");
|
||||
|
||||
let result = store
|
||||
.replace(
|
||||
&first.id,
|
||||
&AutomationRevision::from_bytes(b"stale"),
|
||||
replacement("Updated"),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_file() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
let store = AutomationStore::load(dir.path())
|
||||
.await
|
||||
.expect("store should load");
|
||||
let automation = store
|
||||
.create(draft("nightly"))
|
||||
.await
|
||||
.expect("automation should be created");
|
||||
let path = dir.path().join("nightly.toml");
|
||||
|
||||
store
|
||||
.delete(&automation.id, &automation.revision)
|
||||
.await
|
||||
.expect("automation should be deleted");
|
||||
|
||||
assert!(!path.exists());
|
||||
assert!(store.get(&automation.id).await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn startup_fails_on_malformed_toml() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
fs::write(dir.path().join("nightly.toml"), "not = [toml")
|
||||
.await
|
||||
.expect("malformed file should be writable");
|
||||
|
||||
let result = AutomationStore::load(dir.path()).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_filename_fails_load() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
fs::write(
|
||||
dir.path().join("Bad.toml"),
|
||||
r#"
|
||||
name = "Bad"
|
||||
[target]
|
||||
repository = "fabro-sh/fabro"
|
||||
ref = "main"
|
||||
workflow = "deps"
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
.expect("file should be writable");
|
||||
|
||||
let result = AutomationStore::load(dir.path()).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_toml_files_are_ignored() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
fs::write(dir.path().join("README.md"), "ignored")
|
||||
.await
|
||||
.expect("file should be writable");
|
||||
|
||||
let store = AutomationStore::load(dir.path())
|
||||
.await
|
||||
.expect("store should load");
|
||||
|
||||
assert!(store.list().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_returns_created_automation_by_id() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
let store = AutomationStore::load(dir.path())
|
||||
.await
|
||||
.expect("store should load");
|
||||
let created = store
|
||||
.create(draft("nightly"))
|
||||
.await
|
||||
.expect("automation should be created");
|
||||
let id = AutomationId::try_from("nightly".to_string()).expect("id should be valid");
|
||||
|
||||
assert_eq!(store.get(&id).await, Some(created));
|
||||
}
|
||||
}
|
||||
|
|
@ -844,6 +844,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
automation: None,
|
||||
fork_source_ref: None,
|
||||
};
|
||||
serde_json::json!({
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
git: None,
|
||||
automation: None,
|
||||
fork_source_ref: None,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -500,6 +500,7 @@ mod tests {
|
|||
provenance: None,
|
||||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
automation: None,
|
||||
fork_source_ref: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ fabro-validate = { path = "../fabro-validate" }
|
|||
fabro-sandbox = { path = "../fabro-sandbox", features = ["daytona", "docker"] }
|
||||
fabro-github = { path = "../fabro-github" }
|
||||
fabro-agent = { path = "../fabro-agent" }
|
||||
fabro-automation = { path = "../fabro-automation" }
|
||||
fabro-llm = { path = "../fabro-llm" }
|
||||
fabro-manifest = { path = "../fabro-manifest" }
|
||||
fabro-model = { path = "../fabro-model" }
|
||||
|
|
@ -67,6 +68,7 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
serde_yaml = "0.9"
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
clap.workspace = true
|
||||
toml.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
|
|
|||
384
lib/crates/fabro-server/src/automation_materializer.rs
Normal file
384
lib/crates/fabro-server/src/automation_materializer.rs
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fabro_api::types::RunManifest;
|
||||
use fabro_automation::{AutomationId, AutomationTarget};
|
||||
use fabro_config::Storage;
|
||||
use fabro_types::RunId;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::timeout;
|
||||
use tokio::{fs, task};
|
||||
|
||||
pub(crate) struct AutomationRunMaterializeInput {
|
||||
pub automation_id: AutomationId,
|
||||
pub target: AutomationTarget,
|
||||
pub run_id: RunId,
|
||||
pub user_settings_path: PathBuf,
|
||||
pub temp_root: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AutomationRunMaterialized {
|
||||
pub manifest: RunManifest,
|
||||
pub submitted_manifest_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone)]
|
||||
pub(crate) enum AutomationRunMaterializeError {
|
||||
#[error("invalid automation target: {0}")]
|
||||
InvalidTarget(String),
|
||||
#[error("failed to clone automation repository: {0}")]
|
||||
CloneFailed(String),
|
||||
#[error("failed to resolve automation workflow: {0}")]
|
||||
WorkflowNotFound(String),
|
||||
#[error("failed to build run manifest: {0}")]
|
||||
Manifest(String),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub(crate) trait AutomationRunMaterializer: Send + Sync {
|
||||
async fn materialize(
|
||||
&self,
|
||||
input: AutomationRunMaterializeInput,
|
||||
) -> Result<AutomationRunMaterialized, AutomationRunMaterializeError>;
|
||||
}
|
||||
|
||||
pub(crate) struct GitAutomationRunMaterializer {
|
||||
github_credentials: Option<fabro_github::GitHubCredentials>,
|
||||
github_api_base_url: String,
|
||||
http_client: Option<fabro_http::HttpClient>,
|
||||
git_timeout: Duration,
|
||||
}
|
||||
|
||||
impl GitAutomationRunMaterializer {
|
||||
pub(crate) fn new(
|
||||
github_credentials: Option<fabro_github::GitHubCredentials>,
|
||||
github_api_base_url: String,
|
||||
http_client: Option<fabro_http::HttpClient>,
|
||||
) -> Self {
|
||||
Self {
|
||||
github_credentials,
|
||||
github_api_base_url,
|
||||
http_client,
|
||||
git_timeout: Duration::from_mins(2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AutomationRunMaterializer for GitAutomationRunMaterializer {
|
||||
async fn materialize(
|
||||
&self,
|
||||
input: AutomationRunMaterializeInput,
|
||||
) -> Result<AutomationRunMaterialized, AutomationRunMaterializeError> {
|
||||
let (owner, repo) = input.target.repository.owner_repo();
|
||||
if owner.is_empty() || repo.is_empty() {
|
||||
return Err(AutomationRunMaterializeError::InvalidTarget(
|
||||
input.target.repository.to_string(),
|
||||
));
|
||||
}
|
||||
let sanitized_clone_url = github_clone_url(owner, repo);
|
||||
let clone_url = self
|
||||
.authenticated_clone_url(owner, repo, &sanitized_clone_url)
|
||||
.await?;
|
||||
|
||||
fs::create_dir_all(&input.temp_root).await.map_err(|err| {
|
||||
AutomationRunMaterializeError::CloneFailed(format!(
|
||||
"failed to create temp root {}: {err}",
|
||||
input.temp_root.display()
|
||||
))
|
||||
})?;
|
||||
let checkout_dir = input.temp_root.join(input.run_id.to_string());
|
||||
run_git(
|
||||
git_clone_args(&clone_url, &checkout_dir),
|
||||
self.git_timeout,
|
||||
"git clone",
|
||||
)
|
||||
.await?;
|
||||
run_git(
|
||||
git_remote_set_url_args(&checkout_dir, &sanitized_clone_url),
|
||||
self.git_timeout,
|
||||
"git remote set-url origin",
|
||||
)
|
||||
.await?;
|
||||
run_git(
|
||||
git_checkout_args(&checkout_dir, input.target.ref_.as_str()),
|
||||
self.git_timeout,
|
||||
"git checkout",
|
||||
)
|
||||
.await?;
|
||||
|
||||
build_manifest_from_checkout(input, checkout_dir).await
|
||||
}
|
||||
}
|
||||
|
||||
impl GitAutomationRunMaterializer {
|
||||
async fn authenticated_clone_url(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
sanitized_clone_url: &str,
|
||||
) -> Result<String, AutomationRunMaterializeError> {
|
||||
let Some(credentials) = self.github_credentials.as_ref() else {
|
||||
return Ok(sanitized_clone_url.to_string());
|
||||
};
|
||||
let ctx = match self.http_client.clone() {
|
||||
Some(client) => fabro_github::GitHubContext::with_http_client(
|
||||
credentials,
|
||||
&self.github_api_base_url,
|
||||
client,
|
||||
),
|
||||
None => fabro_github::GitHubContext::new(credentials, &self.github_api_base_url),
|
||||
};
|
||||
let (_username, token) = fabro_github::resolve_clone_credentials(&ctx, owner, repo)
|
||||
.await
|
||||
.map_err(|err| AutomationRunMaterializeError::CloneFailed(err.to_string()))?;
|
||||
match token {
|
||||
Some(token) => fabro_github::embed_token_in_url(sanitized_clone_url, &token)
|
||||
.map(|url| url.raw_string())
|
||||
.map_err(|err| AutomationRunMaterializeError::CloneFailed(err.to_string())),
|
||||
None => Ok(sanitized_clone_url.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn automation_temp_root(storage_root: impl Into<PathBuf>) -> PathBuf {
|
||||
Storage::new(storage_root).scratch_dir().join("automations")
|
||||
}
|
||||
|
||||
fn github_clone_url(owner: &str, repo: &str) -> String {
|
||||
format!("https://github.com/{owner}/{repo}.git")
|
||||
}
|
||||
|
||||
fn git_clone_args(clone_url: &str, checkout_path: &Path) -> Vec<OsString> {
|
||||
vec![
|
||||
"clone".into(),
|
||||
"--no-tags".into(),
|
||||
"--".into(),
|
||||
clone_url.into(),
|
||||
checkout_path.as_os_str().to_owned(),
|
||||
]
|
||||
}
|
||||
|
||||
fn git_remote_set_url_args(repo_dir: &Path, sanitized_clone_url: &str) -> Vec<OsString> {
|
||||
vec![
|
||||
"-C".into(),
|
||||
repo_dir.as_os_str().to_owned(),
|
||||
"remote".into(),
|
||||
"set-url".into(),
|
||||
"origin".into(),
|
||||
sanitized_clone_url.into(),
|
||||
]
|
||||
}
|
||||
|
||||
fn git_checkout_args(repo_dir: &Path, ref_: &str) -> Vec<OsString> {
|
||||
vec![
|
||||
"-C".into(),
|
||||
repo_dir.as_os_str().to_owned(),
|
||||
"checkout".into(),
|
||||
"--force".into(),
|
||||
ref_.into(),
|
||||
]
|
||||
}
|
||||
|
||||
async fn run_git(
|
||||
args: Vec<OsString>,
|
||||
git_timeout: Duration,
|
||||
label: &'static str,
|
||||
) -> Result<(), AutomationRunMaterializeError> {
|
||||
let mut command = Command::new("git");
|
||||
command.args(&args);
|
||||
command.env("GIT_TERMINAL_PROMPT", "0");
|
||||
command.kill_on_drop(true);
|
||||
let output = timeout(git_timeout, command.output())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
AutomationRunMaterializeError::CloneFailed(format!(
|
||||
"{label} timed out after {}s",
|
||||
git_timeout.as_secs()
|
||||
))
|
||||
})?
|
||||
.map_err(|err| AutomationRunMaterializeError::CloneFailed(format!("{label}: {err}")))?;
|
||||
if output.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let detail = if stderr.is_empty() { stdout } else { stderr };
|
||||
Err(AutomationRunMaterializeError::CloneFailed(format!(
|
||||
"{label} exited with status {}: {}",
|
||||
output.status,
|
||||
redact_command_output(&detail)
|
||||
)))
|
||||
}
|
||||
|
||||
fn redact_command_output(value: &str) -> String {
|
||||
value
|
||||
.split_whitespace()
|
||||
.map(redact_url_token)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn redact_url_token(value: &str) -> String {
|
||||
fabro_redact::DisplaySafeUrl::parse(value)
|
||||
.map_or_else(|_| value.to_string(), |url| url.redacted_string())
|
||||
}
|
||||
|
||||
async fn build_manifest_from_checkout(
|
||||
input: AutomationRunMaterializeInput,
|
||||
checkout_dir: PathBuf,
|
||||
) -> Result<AutomationRunMaterialized, AutomationRunMaterializeError> {
|
||||
let workflow = PathBuf::from(input.target.workflow.as_str());
|
||||
let user_settings_path = input.user_settings_path;
|
||||
let run_id = input.run_id;
|
||||
let automation_id = input.automation_id.to_string();
|
||||
let built = task::spawn_blocking(move || {
|
||||
fabro_manifest::build_run_manifest(fabro_manifest::ManifestBuildInput {
|
||||
workflow,
|
||||
cwd: checkout_dir,
|
||||
run_id: Some(run_id),
|
||||
user_settings_path: Some(user_settings_path),
|
||||
..fabro_manifest::ManifestBuildInput::default()
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|err| AutomationRunMaterializeError::Manifest(err.to_string()))?
|
||||
.map_err(|err| classify_manifest_error(&automation_id, &err))?;
|
||||
let submitted_manifest_bytes = serde_json::to_vec(&built.manifest)
|
||||
.map_err(|err| AutomationRunMaterializeError::Manifest(err.to_string()))?;
|
||||
Ok(AutomationRunMaterialized {
|
||||
manifest: built.manifest,
|
||||
submitted_manifest_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn classify_manifest_error(
|
||||
automation_id: &str,
|
||||
err: &anyhow::Error,
|
||||
) -> AutomationRunMaterializeError {
|
||||
let message = err.to_string();
|
||||
if err
|
||||
.chain()
|
||||
.any(|cause| cause.to_string().contains("workflow") && cause.to_string().contains("not"))
|
||||
{
|
||||
AutomationRunMaterializeError::WorkflowNotFound(format!("{automation_id}: {message}"))
|
||||
} else {
|
||||
AutomationRunMaterializeError::Manifest(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) struct StaticAutomationRunMaterializer {
|
||||
result: Result<AutomationRunMaterialized, AutomationRunMaterializeError>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl StaticAutomationRunMaterializer {
|
||||
pub(crate) fn ok(
|
||||
manifest: RunManifest,
|
||||
submitted_manifest_bytes: Vec<u8>,
|
||||
) -> std::sync::Arc<Self> {
|
||||
std::sync::Arc::new(Self {
|
||||
result: Ok(AutomationRunMaterialized {
|
||||
manifest,
|
||||
submitted_manifest_bytes,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[async_trait]
|
||||
impl AutomationRunMaterializer for StaticAutomationRunMaterializer {
|
||||
async fn materialize(
|
||||
&self,
|
||||
_input: AutomationRunMaterializeInput,
|
||||
) -> Result<AutomationRunMaterialized, AutomationRunMaterializeError> {
|
||||
self.result.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use fabro_automation::{AutomationId, GitRefSelector, RepositorySlug, WorkflowSlug};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn github_clone_url_uses_sanitized_https_origin() {
|
||||
assert_eq!(
|
||||
github_clone_url("fabro-sh", "fabro"),
|
||||
"https://github.com/fabro-sh/fabro.git"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redact_command_output_strips_credentials() {
|
||||
let redacted = redact_command_output(
|
||||
"fatal: https://x-access-token:ghs_secret@github.com/acme/widgets.git failed",
|
||||
);
|
||||
assert!(redacted.contains("https://x-access-token:***@github.com/acme/widgets.git"));
|
||||
assert!(!redacted.contains("ghs_secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkout_args_pass_ref_as_argv() {
|
||||
let args = git_checkout_args(Path::new("/tmp/repo"), "feature/main");
|
||||
assert_eq!(args[0], OsString::from("-C"));
|
||||
assert_eq!(args[2], OsString::from("checkout"));
|
||||
assert_eq!(args[4], OsString::from("feature/main"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_manifest_from_checkout_resolves_workflow_path() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
let workflow_dir = dir.path().join("flows");
|
||||
fs::create_dir_all(&workflow_dir)
|
||||
.await
|
||||
.expect("workflow dir should be created");
|
||||
fs::write(
|
||||
workflow_dir.join("deps.fabro"),
|
||||
r#"digraph Test {
|
||||
graph [goal="Test"]
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
start -> exit
|
||||
}"#,
|
||||
)
|
||||
.await
|
||||
.expect("workflow should be written");
|
||||
let target = AutomationTarget {
|
||||
repository: RepositorySlug::from_str("fabro-sh/fabro").unwrap(),
|
||||
ref_: GitRefSelector::from_str("main").unwrap(),
|
||||
workflow: WorkflowSlug::from_str("flows/deps").unwrap(),
|
||||
};
|
||||
let run_id = RunId::new();
|
||||
let input = AutomationRunMaterializeInput {
|
||||
automation_id: AutomationId::from_str("nightly").unwrap(),
|
||||
target,
|
||||
run_id,
|
||||
user_settings_path: dir.path().join("settings.toml"),
|
||||
temp_root: dir.path().join("tmp"),
|
||||
};
|
||||
|
||||
let materialized = build_manifest_from_checkout(input, dir.path().to_path_buf())
|
||||
.await
|
||||
.expect("manifest should build");
|
||||
|
||||
assert_eq!(
|
||||
materialized.manifest.run_id.as_deref(),
|
||||
Some(run_id.to_string().as_str())
|
||||
);
|
||||
assert_eq!(materialized.manifest.target.path, "flows/deps.fabro");
|
||||
assert!(
|
||||
std::str::from_utf8(&materialized.submitted_manifest_bytes)
|
||||
.unwrap()
|
||||
.contains("flows/deps.fabro")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
)]
|
||||
|
||||
pub mod auth;
|
||||
mod automation_materializer;
|
||||
mod canonical_host;
|
||||
mod canonical_origin;
|
||||
pub mod csp;
|
||||
|
|
|
|||
|
|
@ -2379,6 +2379,7 @@ index 1111111..2222222 160000
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
},
|
||||
chrono::Utc::now(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ pub(crate) fn create_run_input(
|
|||
title: prepared.title,
|
||||
git: prepared.git,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
parent_id: prepared.parent_id,
|
||||
provenance: None,
|
||||
configured_providers,
|
||||
|
|
|
|||
|
|
@ -805,6 +805,7 @@ where
|
|||
github_api_base_url: None,
|
||||
active_config_path,
|
||||
http_client: None,
|
||||
automation_materializer: None,
|
||||
shutdown: shutdown.clone(),
|
||||
})?;
|
||||
let reconciled = reconcile_incomplete_runs_on_startup(&state).await?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path as StdPath, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
|
@ -45,6 +45,7 @@ pub use fabro_api::types::{
|
|||
SystemRunCounts, TimelineEntryResponse, VncPreviewResponse, WriteBlobResponse,
|
||||
};
|
||||
use fabro_auth::{CredentialSource, VaultCredentialSource, auth_issue_message};
|
||||
use fabro_automation::AutomationStore;
|
||||
#[cfg(test)]
|
||||
use fabro_config::RunSettingsBuilder;
|
||||
use fabro_config::daemon::ServerDaemon;
|
||||
|
|
@ -130,6 +131,7 @@ use tracing::{Instrument, debug, error, info, warn};
|
|||
use ulid::Ulid;
|
||||
|
||||
use crate::auth::{self, GithubEndpoints, auth_translation_middleware, demo_routing_middleware};
|
||||
use crate::automation_materializer::{AutomationRunMaterializer, GitAutomationRunMaterializer};
|
||||
use crate::canonical_origin::resolve_canonical_origin;
|
||||
use crate::error::ApiError;
|
||||
use crate::github_webhooks::{
|
||||
|
|
@ -933,6 +935,8 @@ pub struct AppState {
|
|||
runs: Mutex<HashMap<RunId, ManagedRun>>,
|
||||
aggregate_billing: Mutex<BillingAccumulator>,
|
||||
store: Arc<Database>,
|
||||
automation_store: Arc<AutomationStore>,
|
||||
automation_materializer: Arc<dyn AutomationRunMaterializer>,
|
||||
session_runtimes: SessionRuntimeManager,
|
||||
artifact_store: ArtifactStore,
|
||||
worker_tokens: WorkerTokenKeys,
|
||||
|
|
@ -1059,6 +1063,7 @@ pub(crate) struct AppStateConfig {
|
|||
pub(crate) github_api_base_url: Option<String>,
|
||||
pub(crate) active_config_path: PathBuf,
|
||||
pub(crate) http_client: Option<fabro_http::HttpClient>,
|
||||
pub(crate) automation_materializer: Option<Arc<dyn AutomationRunMaterializer>>,
|
||||
pub(crate) shutdown: CancellationToken,
|
||||
}
|
||||
|
||||
|
|
@ -1263,6 +1268,14 @@ impl AppState {
|
|||
&self.store
|
||||
}
|
||||
|
||||
pub(crate) fn automation_store(&self) -> Arc<AutomationStore> {
|
||||
Arc::clone(&self.automation_store)
|
||||
}
|
||||
|
||||
pub(crate) fn automation_materializer(&self) -> Arc<dyn AutomationRunMaterializer> {
|
||||
Arc::clone(&self.automation_materializer)
|
||||
}
|
||||
|
||||
pub(crate) fn session_runtimes(&self) -> &SessionRuntimeManager {
|
||||
&self.session_runtimes
|
||||
}
|
||||
|
|
@ -1408,6 +1421,53 @@ impl AppState {
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_github_credentials_for_startup(
|
||||
settings: &GithubIntegrationSettings,
|
||||
server_secrets: &ServerSecrets,
|
||||
vault: &Vault,
|
||||
) -> Result<Option<fabro_github::GitHubCredentials>, String> {
|
||||
match settings.strategy {
|
||||
GithubIntegrationStrategy::App => {
|
||||
let Some(app_id) = settings.app_id.as_ref().map(InterpString::as_source) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let raw = server_secrets.get(EnvVars::GITHUB_APP_PRIVATE_KEY);
|
||||
let Some(raw) = raw else {
|
||||
return Ok(None);
|
||||
};
|
||||
let private_key_pem = decode_secret_pem(EnvVars::GITHUB_APP_PRIVATE_KEY, &raw)?;
|
||||
Ok(Some(fabro_github::GitHubCredentials::App(
|
||||
fabro_github::GitHubAppCredentials {
|
||||
app_id,
|
||||
private_key_pem,
|
||||
slug: settings.slug.as_ref().map(InterpString::as_source),
|
||||
},
|
||||
)))
|
||||
}
|
||||
GithubIntegrationStrategy::Token => {
|
||||
let token = process_env_var(EnvVars::GITHUB_TOKEN)
|
||||
.or_else(|| process_env_var(EnvVars::GH_TOKEN))
|
||||
.or_else(|| vault.get(EnvVars::GITHUB_TOKEN).map(str::to_string))
|
||||
.or_else(|| vault.get(EnvVars::GH_TOKEN).map(str::to_string))
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|token| !token.is_empty())
|
||||
.map(str::to_string);
|
||||
match token {
|
||||
Some(token) => {
|
||||
fabro_github::validate_static_github_token(&token)
|
||||
.map_err(|err| err.to_string())?;
|
||||
Ok(Some(fabro_github::GitHubCredentials::Pat(token)))
|
||||
}
|
||||
None => Err(
|
||||
"GITHUB_TOKEN not configured — run fabro install or set GITHUB_TOKEN"
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_llm_client_from_source(
|
||||
source: &dyn CredentialSource,
|
||||
catalog: Arc<Catalog>,
|
||||
|
|
@ -2071,6 +2131,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
github_api_base_url,
|
||||
active_config_path,
|
||||
http_client,
|
||||
automation_materializer,
|
||||
shutdown,
|
||||
} = config;
|
||||
|
||||
|
|
@ -2097,9 +2158,26 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
);
|
||||
}
|
||||
}
|
||||
let vault = Vault::load(vault_path.clone())
|
||||
let current_server_settings = Arc::new(resolved_settings.server_settings);
|
||||
let loaded_vault = Vault::load(vault_path.clone())
|
||||
.with_context(|| format!("load vault {}", vault_path.display()))?;
|
||||
let vault = Arc::new(AsyncRwLock::new(vault));
|
||||
let startup_github_credentials = if automation_materializer.is_none() {
|
||||
resolve_github_credentials_for_startup(
|
||||
¤t_server_settings.server.integrations.github,
|
||||
&server_secrets,
|
||||
&loaded_vault,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!(
|
||||
error = %err,
|
||||
"GitHub credentials unavailable for automation materializer; public clones may still work"
|
||||
);
|
||||
None
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let vault = Arc::new(AsyncRwLock::new(loaded_vault));
|
||||
let llm_source: Arc<dyn CredentialSource> = Arc::new(VaultCredentialSource::with_env_lookup(
|
||||
Arc::clone(&vault),
|
||||
{
|
||||
|
|
@ -2108,7 +2186,6 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
},
|
||||
));
|
||||
let (global_event_tx, _) = broadcast::channel(4096);
|
||||
let current_server_settings = Arc::new(resolved_settings.server_settings);
|
||||
let current_manifest_run_defaults = Arc::new(resolved_settings.manifest_run_defaults);
|
||||
let current_manifest_environment_defaults =
|
||||
Arc::new(resolved_settings.manifest_environment_defaults);
|
||||
|
|
@ -2117,6 +2194,14 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
Catalog::from_builtin_with_overrides(&resolved_settings.llm_catalog_settings)
|
||||
.context("building LLM model catalog")?,
|
||||
);
|
||||
let automation_dir = active_config_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| StdPath::new("."))
|
||||
.join("automations");
|
||||
let automation_store = Arc::new(
|
||||
AutomationStore::load_blocking(&automation_dir)
|
||||
.with_context(|| format!("load automation store {}", automation_dir.display()))?,
|
||||
);
|
||||
let slack_service = {
|
||||
let default_channel = current_server_settings
|
||||
.server
|
||||
|
|
@ -2154,10 +2239,19 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
};
|
||||
let worker_tokens = worker_token_keys_from_server_secrets(&server_secrets)?;
|
||||
let github_api_base_url = github_api_base_url.unwrap_or_else(fabro_github::github_api_base_url);
|
||||
let automation_materializer = automation_materializer.unwrap_or_else(|| {
|
||||
Arc::new(GitAutomationRunMaterializer::new(
|
||||
startup_github_credentials,
|
||||
github_api_base_url.clone(),
|
||||
http_client.clone(),
|
||||
))
|
||||
});
|
||||
Ok(Arc::new(AppState {
|
||||
runs: Mutex::new(HashMap::new()),
|
||||
aggregate_billing: Mutex::new(BillingAccumulator::default()),
|
||||
store,
|
||||
automation_store,
|
||||
automation_materializer,
|
||||
session_runtimes: SessionRuntimeManager::new(),
|
||||
artifact_store,
|
||||
worker_tokens,
|
||||
|
|
|
|||
595
lib/crates/fabro-server/src/server/handler/automations.rs
Normal file
595
lib/crates/fabro-server/src/server/handler/automations.rs
Normal file
|
|
@ -0,0 +1,595 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::Bytes;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::get;
|
||||
use axum::{Json, Router};
|
||||
use axum_extra::extract::Query as ExtraQuery;
|
||||
use chrono::Utc;
|
||||
use fabro_automation::{
|
||||
ApiTrigger, Automation, AutomationDraft, AutomationId, AutomationPatch, AutomationReplace,
|
||||
AutomationRevision, AutomationStoreError, AutomationTarget, AutomationTrigger,
|
||||
AutomationTriggerId, AutomationValidationError, GitRefSelector, RepositorySlug,
|
||||
ScheduleTrigger, WorkflowSlug,
|
||||
};
|
||||
use fabro_types::{AutomationRef, RunId};
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use super::super::{AppState, PaginationParams, paginate_items, resolve_interp_string};
|
||||
use super::runs::{CreateRunFromManifestRequest, create_run_from_manifest};
|
||||
use crate::automation_materializer::{
|
||||
AutomationRunMaterializeError, AutomationRunMaterializeInput, automation_temp_root,
|
||||
};
|
||||
use crate::error::ApiError;
|
||||
use crate::principal_middleware::{RequiredRunManagementActor, RequiredUser};
|
||||
|
||||
pub(super) fn routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/automations",
|
||||
get(list_automations).post(create_automation),
|
||||
)
|
||||
.route(
|
||||
"/automations/{id}",
|
||||
get(get_automation)
|
||||
.put(replace_automation)
|
||||
.patch(patch_automation)
|
||||
.delete(delete_automation),
|
||||
)
|
||||
.route(
|
||||
"/automations/{id}/runs",
|
||||
get(list_automation_runs).post(create_automation_run),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawAutomationTarget {
|
||||
repository: String,
|
||||
#[serde(rename = "ref")]
|
||||
ref_: String,
|
||||
workflow: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawAutomationTrigger {
|
||||
id: String,
|
||||
#[serde(rename = "type")]
|
||||
type_: String,
|
||||
#[serde(default = "default_true")]
|
||||
enabled: bool,
|
||||
#[serde(default)]
|
||||
expression: Option<String>,
|
||||
#[serde(flatten)]
|
||||
extra: BTreeMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawCreateAutomationRequest {
|
||||
id: String,
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default)]
|
||||
enabled: Option<bool>,
|
||||
target: RawAutomationTarget,
|
||||
triggers: Vec<RawAutomationTrigger>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawReplaceAutomationRequest {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
enabled: bool,
|
||||
target: RawAutomationTarget,
|
||||
triggers: Vec<RawAutomationTrigger>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawPatchAutomationRequest {
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_nullable_string_patch")]
|
||||
description: NullableStringPatch,
|
||||
#[serde(default)]
|
||||
enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
target: Option<RawAutomationTarget>,
|
||||
#[serde(default)]
|
||||
triggers: Option<Vec<RawAutomationTrigger>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum NullableStringPatch {
|
||||
#[default]
|
||||
Omitted,
|
||||
Explicit(Option<String>),
|
||||
}
|
||||
|
||||
impl NullableStringPatch {
|
||||
fn apply_to(self, patch: &mut AutomationPatch) {
|
||||
match self {
|
||||
Self::Omitted => {}
|
||||
Self::Explicit(value) => patch.description = Some(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_nullable_string_patch<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<NullableStringPatch, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Option::<String>::deserialize(deserializer).map(NullableStringPatch::Explicit)
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct AutomationListResponse {
|
||||
data: Vec<Automation>,
|
||||
meta: AutomationListMeta,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct AutomationListMeta {
|
||||
total: u64,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn list_automations(_auth: RequiredUser, State(state): State<Arc<AppState>>) -> Response {
|
||||
let mut automations = state.automation_store().list().await;
|
||||
automations.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
let total = automations.len() as u64;
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(AutomationListResponse {
|
||||
data: automations,
|
||||
meta: AutomationListMeta { total },
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn create_automation(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
body: Bytes,
|
||||
) -> Response {
|
||||
let request = match parse_json::<RawCreateAutomationRequest>(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let draft = match request.try_into() {
|
||||
Ok(draft) => draft,
|
||||
Err(err) => return validation_error(&err).into_response(),
|
||||
};
|
||||
match state.automation_store().create(draft).await {
|
||||
Ok(automation) => (StatusCode::CREATED, Json(automation)).into_response(),
|
||||
Err(err) => store_error(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_automation(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
let id = match parse_automation_id(&id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
match state.automation_store().get(&id).await {
|
||||
Some(automation) => with_etag(StatusCode::OK, automation),
|
||||
None => ApiError::not_found("Automation not found.").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn replace_automation(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Response {
|
||||
let id = match parse_automation_id(&id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let expected = match parse_if_match(&headers) {
|
||||
Ok(revision) => revision,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let request = match parse_json::<RawReplaceAutomationRequest>(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let draft = match request.try_into() {
|
||||
Ok(draft) => draft,
|
||||
Err(err) => return validation_error(&err).into_response(),
|
||||
};
|
||||
match state
|
||||
.automation_store()
|
||||
.replace(&id, &expected, draft)
|
||||
.await
|
||||
{
|
||||
Ok(automation) => with_etag(StatusCode::OK, automation),
|
||||
Err(err) => store_error(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn patch_automation(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Response {
|
||||
let id = match parse_automation_id(&id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let expected = match parse_if_match(&headers) {
|
||||
Ok(revision) => revision,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let request = match parse_json::<RawPatchAutomationRequest>(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let patch = match request.try_into() {
|
||||
Ok(patch) => patch,
|
||||
Err(err) => return validation_error(&err).into_response(),
|
||||
};
|
||||
match state.automation_store().patch(&id, &expected, patch).await {
|
||||
Ok(automation) => with_etag(StatusCode::OK, automation),
|
||||
Err(err) => store_error(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_automation(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
let id = match parse_automation_id(&id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let expected = match parse_if_match(&headers) {
|
||||
Ok(revision) => revision,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
match state.automation_store().delete(&id, &expected).await {
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => store_error(err).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_automation_runs(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
ExtraQuery(pagination): ExtraQuery<PaginationParams>,
|
||||
) -> Response {
|
||||
let id = match parse_automation_id(&id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
if state.automation_store().get(&id).await.is_none() {
|
||||
return ApiError::not_found("Automation not found.").into_response();
|
||||
}
|
||||
let entries = match state
|
||||
.store_ref()
|
||||
.list_cached_runs(&fabro_store::ListRunsQuery::default(), Utc::now())
|
||||
.await
|
||||
{
|
||||
Ok(entries) => entries,
|
||||
Err(err) => {
|
||||
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let mut runs = entries
|
||||
.into_iter()
|
||||
.map(|entry| entry.summary)
|
||||
.filter(|run| {
|
||||
run.automation
|
||||
.as_ref()
|
||||
.is_some_and(|automation| automation.id == id.as_str())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
runs.sort_by(|left, right| {
|
||||
right
|
||||
.timestamps
|
||||
.created_at
|
||||
.cmp(&left.timestamps.created_at)
|
||||
.then_with(|| right.id.cmp(&left.id))
|
||||
});
|
||||
let total = runs.len() as u64;
|
||||
let decorated = state.decorate_run_summaries(runs).await;
|
||||
let (data, has_more) = paginate_items(decorated, &pagination);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"data": data,
|
||||
"meta": { "has_more": has_more, "total": total }
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn create_automation_run(
|
||||
RequiredRunManagementActor(actor): RequiredRunManagementActor,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
let id = match parse_automation_id(&id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let Some(automation) = state.automation_store().get(&id).await else {
|
||||
return ApiError::not_found("Automation not found.").into_response();
|
||||
};
|
||||
let Some(api_trigger) = startable_api_trigger(&automation) else {
|
||||
return ApiError::with_code(
|
||||
StatusCode::CONFLICT,
|
||||
"Automation has no enabled API trigger.",
|
||||
"automation_api_trigger_disabled",
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
|
||||
let run_id = RunId::new();
|
||||
let storage_root = match resolve_interp_string(&state.server_settings().server.storage.root) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
return ApiError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to resolve server storage root: {err}"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let materialized = match state
|
||||
.automation_materializer()
|
||||
.materialize(AutomationRunMaterializeInput {
|
||||
automation_id: id.clone(),
|
||||
target: automation.target.clone(),
|
||||
run_id,
|
||||
user_settings_path: state.active_config_path().to_path_buf(),
|
||||
temp_root: automation_temp_root(storage_root),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(materialized) => materialized,
|
||||
Err(err) => return materialize_error(&err).into_response(),
|
||||
};
|
||||
|
||||
let automation_ref = AutomationRef {
|
||||
id: id.to_string(),
|
||||
name: Some(automation.name.clone()),
|
||||
trigger_id: Some(api_trigger.id.to_string()),
|
||||
};
|
||||
Box::pin(create_run_from_manifest(
|
||||
state,
|
||||
CreateRunFromManifestRequest {
|
||||
explicit_title_supplied: materialized.manifest.title.is_some(),
|
||||
manifest: materialized.manifest,
|
||||
submitted_manifest_bytes: materialized.submitted_manifest_bytes,
|
||||
explicit_run_id: Some(run_id),
|
||||
actor,
|
||||
headers,
|
||||
automation: Some(automation_ref),
|
||||
},
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
fn startable_api_trigger(automation: &Automation) -> Option<&ApiTrigger> {
|
||||
if !automation.enabled {
|
||||
return None;
|
||||
}
|
||||
automation
|
||||
.triggers
|
||||
.iter()
|
||||
.find_map(|trigger| match trigger {
|
||||
AutomationTrigger::Api(trigger) if trigger.enabled => Some(trigger),
|
||||
AutomationTrigger::Api(_) | AutomationTrigger::Schedule(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_json<T: DeserializeOwned>(body: &[u8]) -> Result<T, ApiError> {
|
||||
serde_json::from_slice(body).map_err(|err| ApiError::bad_request(err.to_string()))
|
||||
}
|
||||
|
||||
fn parse_automation_id(value: &str) -> Result<AutomationId, ApiError> {
|
||||
AutomationId::try_from(value.to_string()).map_err(|err| ApiError::bad_request(err.to_string()))
|
||||
}
|
||||
|
||||
fn parse_if_match(headers: &HeaderMap) -> Result<AutomationRevision, ApiError> {
|
||||
let Some(value) = headers.get(header::IF_MATCH) else {
|
||||
return Err(ApiError::new(
|
||||
StatusCode::PRECONDITION_REQUIRED,
|
||||
"If-Match header is required.",
|
||||
));
|
||||
};
|
||||
let value = value
|
||||
.to_str()
|
||||
.map_err(|err| ApiError::bad_request(format!("Invalid If-Match header: {err}")))?
|
||||
.trim();
|
||||
let revision = value
|
||||
.strip_prefix('"')
|
||||
.and_then(|value| value.strip_suffix('"'))
|
||||
.unwrap_or(value)
|
||||
.trim();
|
||||
if revision.is_empty() {
|
||||
return Err(ApiError::bad_request(
|
||||
"If-Match revision must not be empty.",
|
||||
));
|
||||
}
|
||||
Ok(AutomationRevision::from_str(revision)
|
||||
.expect("AutomationRevision accepts any non-empty string"))
|
||||
}
|
||||
|
||||
fn with_etag(status: StatusCode, automation: Automation) -> Response {
|
||||
let etag = format!("\"{}\"", automation.revision);
|
||||
let etag = HeaderValue::from_str(&etag).expect("revision etag should be a valid header value");
|
||||
(status, [(header::ETAG, etag)], Json(automation)).into_response()
|
||||
}
|
||||
|
||||
fn validation_error(err: &AutomationValidationError) -> ApiError {
|
||||
ApiError::new(StatusCode::UNPROCESSABLE_ENTITY, err.to_string())
|
||||
}
|
||||
|
||||
fn store_error(err: AutomationStoreError) -> ApiError {
|
||||
match err {
|
||||
AutomationStoreError::NotFound(_) => ApiError::not_found("Automation not found."),
|
||||
AutomationStoreError::AlreadyExists(_) => {
|
||||
ApiError::new(StatusCode::CONFLICT, "Automation already exists.")
|
||||
}
|
||||
AutomationStoreError::MissingRevision => ApiError::new(
|
||||
StatusCode::PRECONDITION_REQUIRED,
|
||||
"If-Match header is required.",
|
||||
),
|
||||
AutomationStoreError::RevisionMismatch { .. } => {
|
||||
ApiError::new(StatusCode::CONFLICT, "Automation revision mismatch.")
|
||||
}
|
||||
AutomationStoreError::Validation(err) => validation_error(&err),
|
||||
AutomationStoreError::Parse { .. } | AutomationStoreError::Io { .. } => {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize_error(err: &AutomationRunMaterializeError) -> ApiError {
|
||||
match err {
|
||||
AutomationRunMaterializeError::InvalidTarget(_)
|
||||
| AutomationRunMaterializeError::CloneFailed(_)
|
||||
| AutomationRunMaterializeError::WorkflowNotFound(_)
|
||||
| AutomationRunMaterializeError::Manifest(_) => {
|
||||
ApiError::new(StatusCode::UNPROCESSABLE_ENTITY, err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RawAutomationTarget> for AutomationTarget {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: RawAutomationTarget) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
repository: RepositorySlug::try_from(value.repository)?,
|
||||
ref_: GitRefSelector::try_from(value.ref_)?,
|
||||
workflow: WorkflowSlug::try_from(value.workflow)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RawAutomationTrigger> for AutomationTrigger {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: RawAutomationTrigger) -> Result<Self, Self::Error> {
|
||||
let id = AutomationTriggerId::try_from(value.id)?;
|
||||
match value.type_.as_str() {
|
||||
"api" => {
|
||||
reject_trigger_shape(
|
||||
value.expression.is_some() || !value.extra.is_empty(),
|
||||
"api trigger only supports id, type, and enabled",
|
||||
)?;
|
||||
Ok(Self::Api(ApiTrigger {
|
||||
id,
|
||||
enabled: value.enabled,
|
||||
}))
|
||||
}
|
||||
"schedule" => {
|
||||
reject_trigger_shape(
|
||||
!value.extra.is_empty(),
|
||||
"schedule trigger only supports id, type, enabled, and expression",
|
||||
)?;
|
||||
Ok(Self::Schedule(ScheduleTrigger {
|
||||
id,
|
||||
enabled: value.enabled,
|
||||
expression: value.expression.unwrap_or_default(),
|
||||
}))
|
||||
}
|
||||
_ => Err(AutomationValidationError::UnknownTriggerType(value.type_)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_trigger_shape(
|
||||
invalid: bool,
|
||||
message: &'static str,
|
||||
) -> Result<(), AutomationValidationError> {
|
||||
if invalid {
|
||||
Err(AutomationValidationError::InvalidTriggerShape(
|
||||
message.to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RawCreateAutomationRequest> for AutomationDraft {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: RawCreateAutomationRequest) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
id: AutomationId::try_from(value.id)?,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
enabled: value.enabled,
|
||||
target: value.target.try_into()?,
|
||||
triggers: convert_triggers(value.triggers)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RawReplaceAutomationRequest> for AutomationReplace {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: RawReplaceAutomationRequest) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
enabled: value.enabled,
|
||||
target: value.target.try_into()?,
|
||||
triggers: convert_triggers(value.triggers)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RawPatchAutomationRequest> for AutomationPatch {
|
||||
type Error = AutomationValidationError;
|
||||
|
||||
fn try_from(value: RawPatchAutomationRequest) -> Result<Self, Self::Error> {
|
||||
let mut patch = Self {
|
||||
name: value.name,
|
||||
description: None,
|
||||
enabled: value.enabled,
|
||||
target: value.target.map(TryInto::try_into).transpose()?,
|
||||
triggers: value.triggers.map(convert_triggers).transpose()?,
|
||||
};
|
||||
value.description.apply_to(&mut patch);
|
||||
Ok(patch)
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_triggers(
|
||||
triggers: Vec<RawAutomationTrigger>,
|
||||
) -> Result<Vec<AutomationTrigger>, AutomationValidationError> {
|
||||
triggers.into_iter().map(TryInto::try_into).collect()
|
||||
}
|
||||
|
|
@ -573,6 +573,7 @@ mod stage_events_tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use axum::routing::{get, post};
|
|||
use super::{ApiError, AppState, IntoResponse, Response, StatusCode, demo};
|
||||
|
||||
mod artifacts;
|
||||
mod automations;
|
||||
mod billing;
|
||||
mod completions;
|
||||
pub(in crate::server) mod events;
|
||||
|
|
@ -148,6 +149,7 @@ pub(super) fn real_routes() -> Router<Arc<AppState>> {
|
|||
.route("/insights/execute", post(not_implemented))
|
||||
.route("/insights/history", get(not_implemented))
|
||||
.merge(runs::routes())
|
||||
.merge(automations::routes())
|
||||
.merge(events::routes())
|
||||
.merge(billing::routes())
|
||||
.merge(pull_requests::routes())
|
||||
|
|
|
|||
|
|
@ -1027,6 +1027,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ use fabro_config::Storage;
|
|||
use fabro_interview::AnswerSubmission;
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_types::{
|
||||
Principal, RunClientProvenance, RunId, RunProvenance, RunServerProvenance, StageContextWindow,
|
||||
StageContextWindowStaleness, StageContextWindowUnavailableReason, StageHandler,
|
||||
StageModelUsage, StageProjection, SystemActorKind, parse_blob_ref,
|
||||
AutomationRef, Principal, RunClientProvenance, RunId, RunProvenance, RunServerProvenance,
|
||||
StageContextWindow, StageContextWindowStaleness, StageContextWindowUnavailableReason,
|
||||
StageHandler, StageModelUsage, StageProjection, SystemActorKind, parse_blob_ref,
|
||||
};
|
||||
use fabro_util::version::FABRO_VERSION;
|
||||
use fabro_workflow::command_log::{command_log_path, read_json_string_blob, read_log_slice};
|
||||
|
|
@ -584,28 +584,68 @@ async fn update_run(
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) struct CreateRunFromManifestRequest {
|
||||
pub(super) manifest: RunManifest,
|
||||
pub(super) submitted_manifest_bytes: Vec<u8>,
|
||||
pub(super) explicit_run_id: Option<RunId>,
|
||||
pub(super) explicit_title_supplied: bool,
|
||||
pub(super) actor: Principal,
|
||||
pub(super) headers: HeaderMap,
|
||||
pub(super) automation: Option<AutomationRef>,
|
||||
}
|
||||
|
||||
async fn create_run(
|
||||
RequiredRunManagementActor(actor): RequiredRunManagementActor,
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Response {
|
||||
let req = match serde_json::from_slice::<RunManifest>(&body) {
|
||||
Ok(req) => req,
|
||||
let manifest = match serde_json::from_slice::<RunManifest>(&body) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
let explicit_title_supplied = req.title.is_some();
|
||||
let explicit_title_supplied = manifest.title.is_some();
|
||||
Box::pin(create_run_from_manifest(
|
||||
state,
|
||||
CreateRunFromManifestRequest {
|
||||
manifest,
|
||||
submitted_manifest_bytes: body.to_vec(),
|
||||
explicit_run_id: None,
|
||||
explicit_title_supplied,
|
||||
actor,
|
||||
headers,
|
||||
automation: None,
|
||||
},
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn create_run_from_manifest(
|
||||
state: Arc<AppState>,
|
||||
request: CreateRunFromManifestRequest,
|
||||
) -> Response {
|
||||
let CreateRunFromManifestRequest {
|
||||
manifest,
|
||||
submitted_manifest_bytes,
|
||||
explicit_run_id,
|
||||
explicit_title_supplied,
|
||||
actor,
|
||||
headers,
|
||||
automation,
|
||||
} = request;
|
||||
let manifest_run_defaults = state.manifest_run_defaults();
|
||||
let manifest_environment_defaults = state.manifest_environment_defaults();
|
||||
let prepared = match run_manifest::prepare_manifest_with_environment_defaults(
|
||||
manifest_run_defaults.as_ref(),
|
||||
manifest_environment_defaults.as_ref(),
|
||||
&req,
|
||||
&manifest,
|
||||
) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
let run_id = prepared.run_id.unwrap_or_else(RunId::new);
|
||||
let run_id = explicit_run_id
|
||||
.or(prepared.run_id)
|
||||
.unwrap_or_else(RunId::new);
|
||||
let provider = run_manifest::effective_sandbox_provider(&prepared.settings.run);
|
||||
if let Some(error) =
|
||||
run_manifest::sandbox_provider_policy_error(&state.server_settings(), provider)
|
||||
|
|
@ -646,7 +686,8 @@ async fn create_run(
|
|||
);
|
||||
create_input.run_id = Some(run_id);
|
||||
create_input.provenance = Some(run_provenance(&headers, &actor));
|
||||
create_input.submitted_manifest_bytes = Some(body.to_vec());
|
||||
create_input.submitted_manifest_bytes = Some(submitted_manifest_bytes);
|
||||
create_input.automation = automation;
|
||||
|
||||
let storage_root = match resolve_interp_string(&state.server_settings().server.storage.root) {
|
||||
Ok(path) => PathBuf::from(path),
|
||||
|
|
|
|||
|
|
@ -1702,6 +1702,7 @@ mod tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
let mut projection = fabro_types::RunProjection::new(String::new(), spec, now);
|
||||
for (index, node_id) in ["start", "plan", "code", "test", "review", "deploy"]
|
||||
|
|
|
|||
|
|
@ -1842,6 +1842,7 @@ methods = ["dev-token"]
|
|||
github_api_base_url: None,
|
||||
active_config_path: tempfile::tempdir().unwrap().path().join("settings.toml"),
|
||||
http_client: Some(fabro_http::test_http_client().expect("test HTTP client should build")),
|
||||
automation_materializer: None,
|
||||
shutdown: tokio_util::sync::CancellationToken::new(),
|
||||
}) else {
|
||||
panic!("build_app_state should require SESSION_SECRET")
|
||||
|
|
@ -1852,6 +1853,17 @@ methods = ["dev-token"]
|
|||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn automation_store_empty_without_directory() {
|
||||
let dir = tempfile::tempdir().expect("tempdir should be created");
|
||||
let state = TestAppStateBuilder::new()
|
||||
.active_config_path(dir.path().join("settings.toml"))
|
||||
.build();
|
||||
|
||||
assert!(!dir.path().join("automations").exists());
|
||||
assert!(state.automation_store().list().await.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_app_state_migrates_legacy_vault_file_on_boot() {
|
||||
let vault_path = test_secret_store_path();
|
||||
|
|
@ -1966,6 +1978,7 @@ fn build_test_app_state_with_vault_path(vault_path: &Path) -> anyhow::Result<Arc
|
|||
github_api_base_url: None,
|
||||
active_config_path: tempfile::tempdir().unwrap().path().join("settings.toml"),
|
||||
http_client: Some(fabro_http::test_http_client().expect("test HTTP client should build")),
|
||||
automation_materializer: None,
|
||||
shutdown: tokio_util::sync::CancellationToken::new(),
|
||||
})
|
||||
}
|
||||
|
|
@ -3150,6 +3163,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -3195,6 +3209,7 @@ async fn create_slack_notification_run(
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -4200,6 +4215,7 @@ async fn list_run_stages_distinguishes_visits() {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -5056,6 +5072,7 @@ fn create_github_token_app_state_with_env_lookup_and_llm_catalog_settings(
|
|||
github_api_base_url,
|
||||
active_config_path: tempfile::tempdir().unwrap().path().join("settings.toml"),
|
||||
http_client: Some(fabro_http::test_http_client().expect("test HTTP client should build")),
|
||||
automation_materializer: None,
|
||||
shutdown: tokio_util::sync::CancellationToken::new(),
|
||||
};
|
||||
let state = build_app_state(config).expect("test app state should build");
|
||||
|
|
@ -5188,6 +5205,7 @@ async fn create_completed_run_ready_for_pull_request(
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
|
||||
create_durable_run_with_events(state, run_id, &[
|
||||
|
|
@ -5207,6 +5225,7 @@ async fn create_completed_run_ready_for_pull_request(
|
|||
manifest_blob: None,
|
||||
git,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -10914,6 +10933,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc<AppState>, run_id: RunId
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -11663,6 +11683,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use axum::middleware::Next;
|
|||
use axum::response::Response;
|
||||
use axum::{Router, middleware};
|
||||
use chrono::Duration as ChronoDuration;
|
||||
use fabro_api::types::RunManifest;
|
||||
use fabro_config::{RunLayer, RunSettingsBuilder, ServerSettingsBuilder, envfile};
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_model::catalog::{LlmCatalogSettings, ProviderCatalogSettings};
|
||||
|
|
@ -27,6 +28,7 @@ use tokio_util::sync::CancellationToken;
|
|||
use ulid::Ulid;
|
||||
|
||||
use crate::auth;
|
||||
use crate::automation_materializer::{AutomationRunMaterializer, StaticAutomationRunMaterializer};
|
||||
use crate::ip_allowlist::IpAllowlistConfig;
|
||||
use crate::jwt_auth::{AuthMode, ConfiguredAuth};
|
||||
#[cfg(test)]
|
||||
|
|
@ -64,6 +66,7 @@ pub struct TestAppStateBuilder {
|
|||
vault_path: Option<PathBuf>,
|
||||
server_env_path: Option<PathBuf>,
|
||||
active_config_path: Option<PathBuf>,
|
||||
automation_materializer: Option<Arc<dyn AutomationRunMaterializer>>,
|
||||
server_secret_env: HashMap<String, String>,
|
||||
env_lookup: EnvLookup,
|
||||
llm_catalog_settings: LlmCatalogSettings,
|
||||
|
|
@ -80,6 +83,7 @@ impl Default for TestAppStateBuilder {
|
|||
vault_path: None,
|
||||
server_env_path: None,
|
||||
active_config_path: None,
|
||||
automation_materializer: None,
|
||||
server_secret_env: HashMap::new(),
|
||||
env_lookup: default_env_lookup(),
|
||||
llm_catalog_settings: LlmCatalogSettings::default(),
|
||||
|
|
@ -170,6 +174,16 @@ impl TestAppStateBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn automation_materializer_manifest(mut self, manifest: RunManifest) -> Self {
|
||||
let submitted_manifest_bytes =
|
||||
serde_json::to_vec(&manifest).expect("test manifest should serialize");
|
||||
self.automation_materializer = Some(StaticAutomationRunMaterializer::ok(
|
||||
manifest,
|
||||
submitted_manifest_bytes,
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Arc<AppState> {
|
||||
let (store, artifact_store) = self.store_bundle.unwrap_or_else(test_store_bundle);
|
||||
let vault_path = self.vault_path.unwrap_or_else(test_secret_store_path);
|
||||
|
|
@ -177,7 +191,9 @@ impl TestAppStateBuilder {
|
|||
.server_env_path
|
||||
.unwrap_or_else(|| vault_path.with_file_name("server.env"));
|
||||
let active_config_path = self.active_config_path.unwrap_or_else(|| {
|
||||
std::env::temp_dir().join(format!("fabro-test-settings-{}.toml", Ulid::new()))
|
||||
std::env::temp_dir()
|
||||
.join(format!("fabro-test-settings-{}", Ulid::new()))
|
||||
.join("settings.toml")
|
||||
});
|
||||
build_app_state(AppStateConfig {
|
||||
resolved_settings: resolved_runtime_settings_for_tests(
|
||||
|
|
@ -197,6 +213,7 @@ impl TestAppStateBuilder {
|
|||
http_client: Some(
|
||||
fabro_http::test_http_client().expect("test HTTP client should build"),
|
||||
),
|
||||
automation_materializer: self.automation_materializer,
|
||||
shutdown: CancellationToken::new(),
|
||||
})
|
||||
.expect("test app state should build")
|
||||
|
|
|
|||
365
lib/crates/fabro-server/tests/it/api/automations.rs
Normal file
365
lib/crates/fabro-server/tests/it/api/automations.rs
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
use axum::body::Body;
|
||||
use axum::http::{Method, Request, StatusCode, header};
|
||||
use fabro_server::test_support::{TestAppStateBuilder, build_test_router};
|
||||
use serde_json::{Value, json};
|
||||
use tower::ServiceExt;
|
||||
|
||||
use crate::helpers::{MINIMAL_DOT, api, minimal_manifest_json, response_json, response_status};
|
||||
|
||||
fn automation_body(id: &str) -> Value {
|
||||
json!({
|
||||
"id": id,
|
||||
"name": "Nightly dependency update",
|
||||
"description": "Open a PR for dependency updates.",
|
||||
"target": {
|
||||
"repository": "fabro-sh/fabro",
|
||||
"ref": "main",
|
||||
"workflow": "dependency-update"
|
||||
},
|
||||
"triggers": [
|
||||
{ "id": "api", "type": "api", "enabled": true },
|
||||
{ "id": "nightly", "type": "schedule", "enabled": true, "expression": "0 3 * * *" }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
fn request_json(method: Method, path: &str, body: &Value) -> Request<Body> {
|
||||
Request::builder()
|
||||
.method(method)
|
||||
.uri(api(path))
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(body.to_string()))
|
||||
.expect("request should build")
|
||||
}
|
||||
|
||||
async fn create_automation(app: &axum::Router, id: &str) -> Value {
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(request_json(
|
||||
Method::POST,
|
||||
"/automations",
|
||||
&automation_body(id),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
response_json(response, StatusCode::CREATED, "POST /automations").await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_list_returns_total_zero() {
|
||||
let app = build_test_router(TestAppStateBuilder::new().build());
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(api("/automations"))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let body = response_json(response, StatusCode::OK, "GET /automations").await;
|
||||
assert_eq!(body, json!({ "data": [], "meta": { "total": 0 } }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_writes_toml_and_duplicate_conflicts() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let state = TestAppStateBuilder::new()
|
||||
.active_config_path(dir.path().join("settings.toml"))
|
||||
.build();
|
||||
let app = build_test_router(state);
|
||||
|
||||
let body = create_automation(&app, "nightly-deps").await;
|
||||
|
||||
assert_eq!(body["id"], "nightly-deps");
|
||||
assert_eq!(body["enabled"], true);
|
||||
assert!(dir.path().join("automations/nightly-deps.toml").exists());
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(request_json(
|
||||
Method::POST,
|
||||
"/automations",
|
||||
&automation_body("nightly-deps"),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
response_status(response, StatusCode::CONFLICT, "duplicate automation").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_replace_patch_and_delete_use_etags() {
|
||||
let app = build_test_router(TestAppStateBuilder::new().build());
|
||||
let created = create_automation(&app, "nightly-deps").await;
|
||||
let revision = created["revision"].as_str().unwrap().to_string();
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(api("/automations/nightly-deps"))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.headers()[header::ETAG], format!("\"{revision}\""));
|
||||
let got = response_json(response, StatusCode::OK, "GET automation").await;
|
||||
assert_eq!(got["id"], "nightly-deps");
|
||||
|
||||
let replace = json!({
|
||||
"name": "Updated automation",
|
||||
"description": "updated",
|
||||
"enabled": true,
|
||||
"target": automation_body("ignored")["target"].clone(),
|
||||
"triggers": [{ "id": "api", "type": "api", "enabled": true }]
|
||||
});
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::PUT)
|
||||
.uri(api("/automations/nightly-deps"))
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.header(header::IF_MATCH, revision.clone())
|
||||
.body(Body::from(replace.to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let replaced = response_json(response, StatusCode::OK, "PUT automation").await;
|
||||
assert_eq!(replaced["name"], "Updated automation");
|
||||
let new_revision = replaced["revision"].as_str().unwrap().to_string();
|
||||
|
||||
let stale_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::PUT)
|
||||
.uri(api("/automations/nightly-deps"))
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.header(header::IF_MATCH, revision)
|
||||
.body(Body::from(replace.to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
response_status(stale_response, StatusCode::CONFLICT, "stale replace").await;
|
||||
|
||||
let missing_if_match = app
|
||||
.clone()
|
||||
.oneshot(request_json(
|
||||
Method::PATCH,
|
||||
"/automations/nightly-deps",
|
||||
&json!({ "description": null }),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
response_status(
|
||||
missing_if_match,
|
||||
StatusCode::PRECONDITION_REQUIRED,
|
||||
"missing if-match",
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::PATCH)
|
||||
.uri(api("/automations/nightly-deps"))
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.header(header::IF_MATCH, format!("\"{new_revision}\""))
|
||||
.body(Body::from(json!({ "description": null }).to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let patched = response_json(response, StatusCode::OK, "PATCH automation").await;
|
||||
assert_eq!(patched["description"], Value::Null);
|
||||
let patched_revision = patched["revision"].as_str().unwrap();
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::DELETE)
|
||||
.uri(api("/automations/nightly-deps"))
|
||||
.header(header::IF_MATCH, patched_revision)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
response_status(response, StatusCode::NO_CONTENT, "DELETE automation").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn validation_errors_return_422() {
|
||||
let app = build_test_router(TestAppStateBuilder::new().build());
|
||||
for (label, triggers) in [
|
||||
(
|
||||
"invalid trigger id",
|
||||
json!([{ "id": "_api", "type": "api", "enabled": true }]),
|
||||
),
|
||||
(
|
||||
"duplicate trigger id",
|
||||
json!([
|
||||
{ "id": "api", "type": "api", "enabled": true },
|
||||
{ "id": "api", "type": "schedule", "enabled": true, "expression": "0 3 * * *" }
|
||||
]),
|
||||
),
|
||||
(
|
||||
"second api trigger",
|
||||
json!([
|
||||
{ "id": "api", "type": "api", "enabled": true },
|
||||
{ "id": "api2", "type": "api", "enabled": true }
|
||||
]),
|
||||
),
|
||||
(
|
||||
"invalid schedule",
|
||||
json!([{ "id": "nightly", "type": "schedule", "enabled": true, "expression": "* * * * * *" }]),
|
||||
),
|
||||
(
|
||||
"unknown trigger",
|
||||
json!([{ "id": "event", "type": "event", "enabled": true }]),
|
||||
),
|
||||
(
|
||||
"unknown trigger future shape",
|
||||
json!([{ "id": "event", "type": "event", "enabled": true, "pattern": "push" }]),
|
||||
),
|
||||
] {
|
||||
let mut body = automation_body(label.replace(' ', "-").as_str());
|
||||
body["triggers"] = triggers;
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(request_json(Method::POST, "/automations", &body))
|
||||
.await
|
||||
.unwrap();
|
||||
response_status(response, StatusCode::UNPROCESSABLE_ENTITY, label).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_or_missing_enabled_api_trigger_cannot_start() {
|
||||
let app = build_test_router(TestAppStateBuilder::new().build());
|
||||
let mut disabled_automation = automation_body("disabled");
|
||||
disabled_automation["enabled"] = json!(false);
|
||||
response_json(
|
||||
app.clone()
|
||||
.oneshot(request_json(
|
||||
Method::POST,
|
||||
"/automations",
|
||||
&disabled_automation,
|
||||
))
|
||||
.await
|
||||
.unwrap(),
|
||||
StatusCode::CREATED,
|
||||
"create disabled automation",
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(api("/automations/disabled/runs"))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let body = response_json(response, StatusCode::CONFLICT, "start disabled automation").await;
|
||||
assert_eq!(body["errors"][0]["code"], "automation_api_trigger_disabled");
|
||||
|
||||
let mut disabled_trigger = automation_body("disabled-trigger");
|
||||
disabled_trigger["triggers"] = json!([{ "id": "api", "type": "api", "enabled": false }]);
|
||||
response_json(
|
||||
app.clone()
|
||||
.oneshot(request_json(
|
||||
Method::POST,
|
||||
"/automations",
|
||||
&disabled_trigger,
|
||||
))
|
||||
.await
|
||||
.unwrap(),
|
||||
StatusCode::CREATED,
|
||||
"create disabled trigger automation",
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(api("/automations/disabled-trigger/runs"))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
response_status(response, StatusCode::CONFLICT, "start disabled trigger").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_triggered_run_persists_automation_and_lists_runs() {
|
||||
let manifest: fabro_api::types::RunManifest =
|
||||
serde_json::from_value(minimal_manifest_json(MINIMAL_DOT))
|
||||
.expect("minimal manifest should deserialize");
|
||||
let state = TestAppStateBuilder::new()
|
||||
.automation_materializer_manifest(manifest)
|
||||
.build();
|
||||
let app = build_test_router(state);
|
||||
create_automation(&app, "nightly-deps").await;
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(api("/automations/nightly-deps/runs"))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let run = response_json(response, StatusCode::CREATED, "POST automation run").await;
|
||||
assert_eq!(run["automation"]["id"], "nightly-deps");
|
||||
assert_eq!(run["automation"]["name"], "Nightly dependency update");
|
||||
assert_eq!(run["automation"]["trigger_id"], "api");
|
||||
|
||||
let run_id = run["id"].as_str().unwrap();
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(api(&format!("/runs/{run_id}")))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let persisted = response_json(response, StatusCode::OK, "GET run").await;
|
||||
assert_eq!(persisted["automation"], run["automation"]);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(api(
|
||||
"/automations/nightly-deps/runs?page[limit]=10&page[offset]=0",
|
||||
))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let runs = response_json(response, StatusCode::OK, "GET automation runs").await;
|
||||
assert_eq!(runs["meta"], json!({ "has_more": false, "total": 1 }));
|
||||
assert_eq!(runs["data"][0]["id"], run_id);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
mod auth_sessions;
|
||||
mod automations;
|
||||
mod cli_auth_token;
|
||||
mod docs;
|
||||
mod events;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ async fn append_completed_run_with_final_patch(
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -784,6 +784,7 @@ fn projection_from_created(event: &EventEnvelope) -> Result<RunProjection> {
|
|||
definition_blob: None,
|
||||
git: props.git.clone(),
|
||||
fork_source_ref: props.fork_source_ref.clone(),
|
||||
automation: props.automation.clone(),
|
||||
};
|
||||
|
||||
let mut projection = RunProjection::new(title, spec, stored.ts);
|
||||
|
|
@ -935,7 +936,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run {
|
|||
edge_count: i64::try_from(state.spec.graph.edges.len())
|
||||
.expect("graph edge count should fit in i64"),
|
||||
},
|
||||
automation: None,
|
||||
automation: state.spec.automation.clone(),
|
||||
repository: Some(RepositoryRef::from_origin_and_source(
|
||||
repo_origin_url,
|
||||
source_directory.as_deref(),
|
||||
|
|
@ -1246,11 +1247,11 @@ mod tests {
|
|||
StagePromptProps, StageRetryingProps, StageStartedProps,
|
||||
};
|
||||
use fabro_types::{
|
||||
AgentBackend, BilledModelUsage, BilledTokenCounts, BlockedReason, Checkpoint,
|
||||
CheckpointRecord, CommandTermination, EventBody, FailureCategory, FailureDetail,
|
||||
FailureReason, Graph, McpServerStatus, Outcome, PendingReason, PermissionLevel,
|
||||
PullRequestLink, QuestionType, ReasoningEffort, RunApprovalState, RunBlobId,
|
||||
RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, RunStatus, Speed,
|
||||
AgentBackend, AutomationRef, BilledModelUsage, BilledTokenCounts, BlockedReason,
|
||||
Checkpoint, CheckpointRecord, CommandTermination, EventBody, FailureCategory,
|
||||
FailureDetail, FailureReason, Graph, McpServerStatus, Outcome, PendingReason,
|
||||
PermissionLevel, PullRequestLink, QuestionType, ReasoningEffort, RunApprovalState,
|
||||
RunBlobId, RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, RunStatus, Speed,
|
||||
StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod,
|
||||
StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning,
|
||||
StageModelUsage, StageOutcome, StageState, SubAgentStatus, SuccessReason, WorkflowSettings,
|
||||
|
|
@ -1340,6 +1341,7 @@ mod tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1392,6 +1394,38 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_created_projects_automation_into_summary() {
|
||||
let event = test_raw_event(
|
||||
1,
|
||||
"run.created",
|
||||
&json!({
|
||||
"settings": WorkflowSettings::default(),
|
||||
"graph": Graph::new("test"),
|
||||
"labels": {},
|
||||
"run_dir": "/tmp/run",
|
||||
"automation": {
|
||||
"id": "nightly-deps",
|
||||
"name": "Nightly dependency update",
|
||||
"trigger_id": "api"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
let projection = RunProjection::apply_events(&[event]).unwrap();
|
||||
let expected = Some(AutomationRef {
|
||||
id: "nightly-deps".to_string(),
|
||||
name: Some("Nightly dependency update".to_string()),
|
||||
trigger_id: Some("api".to_string()),
|
||||
});
|
||||
assert_eq!(projection.spec.automation, expected);
|
||||
assert_eq!(
|
||||
build_summary(&projection, &fixtures::RUN_1).automation,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
fn test_raw_event(
|
||||
seq: u32,
|
||||
event: &str,
|
||||
|
|
@ -2608,6 +2642,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
|
||||
let summary_json = serde_json::to_value(build_summary(&state, &fixtures::RUN_1)).unwrap();
|
||||
|
|
@ -2633,6 +2668,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
|
||||
let summary = build_summary(&state, &fixtures::RUN_1);
|
||||
|
|
|
|||
|
|
@ -552,6 +552,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ fn sample_run_spec() -> RunSpec {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ use std::collections::HashMap;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::WorkflowSettings;
|
||||
use crate::graph::Graph;
|
||||
use crate::principal::Principal;
|
||||
use crate::run_blob_id::RunBlobId;
|
||||
use crate::run_id::RunId;
|
||||
use crate::{AutomationRef, WorkflowSettings};
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RunServerProvenance {
|
||||
|
|
@ -100,6 +100,8 @@ pub struct RunSpec {
|
|||
pub git: Option<GitContext>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub automation: Option<AutomationRef>,
|
||||
}
|
||||
|
||||
impl RunSpec {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
|
|||
use super::{BilledTokenCounts, ExecOutputTail, RunNoticeLevel};
|
||||
use crate::status::{BlockedReason, PendingReason, SuccessReason};
|
||||
use crate::{
|
||||
DiffSummary, ForkSourceRef, GitContext, Graph, PairId, PairTarget, RunBlobId, RunControlAction,
|
||||
RunFailure, RunId, RunProvenance, RunTiming, WorkflowSettings,
|
||||
AutomationRef, DiffSummary, ForkSourceRef, GitContext, Graph, PairId, PairTarget, RunBlobId,
|
||||
RunControlAction, RunFailure, RunId, RunProvenance, RunTiming, WorkflowSettings,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -37,6 +37,8 @@ pub struct RunCreatedProps {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub automation: Option<AutomationRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub retried_from: Option<RunId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parent_id: Option<RunId>,
|
||||
|
|
|
|||
|
|
@ -703,6 +703,7 @@ mod title_tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
RunProjection::new(String::new(), spec, Utc::now())
|
||||
}
|
||||
|
|
@ -772,6 +773,7 @@ mod iter_stages_tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
},
|
||||
Utc::now(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -104,9 +104,11 @@ pub struct WorkflowRef {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AutomationRef {
|
||||
pub id: String,
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
pub name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub trigger_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ fn run_created_props_round_trip_templated_settings() {
|
|||
source_run_id: fixtures::RUN_2,
|
||||
checkpoint_sha: "def456".to_string(),
|
||||
}),
|
||||
automation: None,
|
||||
retried_from: Some(fixtures::RUN_1),
|
||||
parent_id: Some(fixtures::RUN_2),
|
||||
web_url: Some("http://localhost:3000/runs/01JNQVR7M0EJ5GKAT2SC4ERS1Z".to_string()),
|
||||
|
|
@ -93,6 +94,7 @@ fn run_created_props_omits_web_url_when_absent() {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ fn sample_run_spec() -> RunSpec {
|
|||
},
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ fn run_spec_round_trips_templated_settings() {
|
|||
source_run_id: fixtures::RUN_2,
|
||||
checkpoint_sha: "def456".to_string(),
|
||||
}),
|
||||
automation: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&record).expect("record should serialize");
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@ mod tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
manifest_blob,
|
||||
git,
|
||||
fork_source_ref,
|
||||
automation,
|
||||
retried_from,
|
||||
parent_id,
|
||||
web_url,
|
||||
|
|
@ -59,6 +60,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
manifest_blob: *manifest_blob,
|
||||
git: git.clone(),
|
||||
fork_source_ref: fork_source_ref.clone(),
|
||||
automation: automation.clone(),
|
||||
retried_from: *retried_from,
|
||||
parent_id: *parent_id,
|
||||
web_url: web_url.clone(),
|
||||
|
|
@ -2439,6 +2441,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ::fabro_types::{
|
||||
BilledTokenCounts, BlockedReason, CommandTermination, DiffSummary, FailureReason,
|
||||
ForkSourceRef, GitContext, PairId, PairMessageId, PairSystemMessageKind, PairTarget,
|
||||
ParallelBranchId, PendingReason, PermissionLevel, Principal, PullRequestLink, RunBlobId,
|
||||
RunFailure, RunId, RunNoticeLevel, RunPairEndedReason, RunPairFailedReason, RunProvenance,
|
||||
RunRunnableSource, RunTiming, SandboxProvider, StageId, StageTiming, SuccessReason,
|
||||
run_event as fabro_types,
|
||||
AutomationRef, BilledTokenCounts, BlockedReason, CommandTermination, DiffSummary,
|
||||
FailureReason, ForkSourceRef, GitContext, PairId, PairMessageId, PairSystemMessageKind,
|
||||
PairTarget, ParallelBranchId, PendingReason, PermissionLevel, Principal, PullRequestLink,
|
||||
RunBlobId, RunFailure, RunId, RunNoticeLevel, RunPairEndedReason, RunPairFailedReason,
|
||||
RunProvenance, RunRunnableSource, RunTiming, SandboxProvider, StageId, StageTiming,
|
||||
SuccessReason, run_event as fabro_types,
|
||||
};
|
||||
use fabro_agent::{AgentEvent, SandboxEvent};
|
||||
use fabro_model::{ReasoningEffort, Speed};
|
||||
|
|
@ -48,6 +48,8 @@ pub enum Event {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
fork_source_ref: Option<ForkSourceRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
automation: Option<AutomationRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
retried_from: Option<RunId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
parent_id: Option<RunId>,
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -472,6 +472,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -482,6 +482,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -258,6 +258,7 @@ mod tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
},
|
||||
chrono::Utc::now(),
|
||||
))
|
||||
|
|
@ -357,6 +358,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -731,6 +731,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -286,6 +286,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -730,6 +730,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use fabro_graphviz::graph::{AttrValue, Graph};
|
|||
use fabro_model::{Catalog, ProviderId};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{
|
||||
ForkSourceRef, GitContext, ManifestPath, RunId, RunProvenance, WorkflowSettings,
|
||||
AutomationRef, ForkSourceRef, GitContext, ManifestPath, RunId, RunProvenance, WorkflowSettings,
|
||||
};
|
||||
use fabro_util::json::normalize_json_value;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
|
@ -43,6 +43,7 @@ pub struct CreateRunInput {
|
|||
pub title: Option<String>,
|
||||
pub git: Option<GitContext>,
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
pub automation: Option<AutomationRef>,
|
||||
pub parent_id: Option<RunId>,
|
||||
pub provenance: Option<RunProvenance>,
|
||||
pub configured_providers: Vec<ProviderId>,
|
||||
|
|
@ -70,6 +71,7 @@ struct PersistCreateOptions {
|
|||
source_directory: Option<String>,
|
||||
git: Option<GitContext>,
|
||||
fork_source_ref: Option<ForkSourceRef>,
|
||||
automation: Option<AutomationRef>,
|
||||
provenance: Option<RunProvenance>,
|
||||
configured_providers: Vec<ProviderId>,
|
||||
catalog: Arc<Catalog>,
|
||||
|
|
@ -104,6 +106,7 @@ pub async fn create(
|
|||
title,
|
||||
git,
|
||||
fork_source_ref,
|
||||
automation,
|
||||
parent_id,
|
||||
provenance,
|
||||
configured_providers,
|
||||
|
|
@ -146,6 +149,7 @@ pub async fn create(
|
|||
source_directory,
|
||||
git,
|
||||
fork_source_ref,
|
||||
automation,
|
||||
provenance,
|
||||
configured_providers,
|
||||
catalog,
|
||||
|
|
@ -162,6 +166,7 @@ pub async fn create(
|
|||
.workflow_toml_path
|
||||
.as_deref()
|
||||
.and_then(|path| std::fs::read_to_string(path).ok());
|
||||
let automation = persisted.run_spec().automation.clone();
|
||||
persist_created_run(
|
||||
store,
|
||||
&persisted,
|
||||
|
|
@ -171,6 +176,7 @@ pub async fn create(
|
|||
accepted_definition.as_ref(),
|
||||
title,
|
||||
parent_id,
|
||||
automation,
|
||||
web_url,
|
||||
)
|
||||
.await?;
|
||||
|
|
@ -192,6 +198,7 @@ async fn persist_created_run(
|
|||
accepted_definition: Option<&RunDefinition>,
|
||||
explicit_title: Option<String>,
|
||||
parent_id: Option<RunId>,
|
||||
automation: Option<AutomationRef>,
|
||||
web_url: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let record = persisted.run_spec();
|
||||
|
|
@ -245,6 +252,7 @@ async fn persist_created_run(
|
|||
manifest_blob,
|
||||
git: record.git.clone(),
|
||||
fork_source_ref: record.fork_source_ref.clone(),
|
||||
automation,
|
||||
retried_from: None,
|
||||
parent_id,
|
||||
web_url,
|
||||
|
|
@ -358,6 +366,7 @@ fn persist_validated(
|
|||
source_directory,
|
||||
git,
|
||||
fork_source_ref,
|
||||
automation,
|
||||
provenance,
|
||||
configured_providers,
|
||||
catalog,
|
||||
|
|
@ -386,6 +395,7 @@ fn persist_validated(
|
|||
definition_blob: None,
|
||||
git,
|
||||
fork_source_ref,
|
||||
automation,
|
||||
};
|
||||
|
||||
pipeline::persist(validated, PersistOptions { run_dir, run_spec })
|
||||
|
|
@ -1098,6 +1108,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
|
|
@ -1165,6 +1176,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
|
|
@ -1276,6 +1288,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
|
|
@ -1321,6 +1334,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
|
|
@ -1388,6 +1402,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
|
|
@ -1434,6 +1449,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
parent_id: None,
|
||||
provenance: Some(fabro_types::RunProvenance {
|
||||
server: Some(fabro_types::RunServerProvenance {
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ async fn persist_forked_run(
|
|||
manifest_blob: spec.manifest_blob,
|
||||
git: spec.git.clone(),
|
||||
fork_source_ref: spec.fork_source_ref.clone(),
|
||||
automation: spec.automation.clone(),
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -391,6 +392,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ pub async fn retry_run(
|
|||
definition_blob,
|
||||
git,
|
||||
fork_source_ref,
|
||||
automation,
|
||||
} = source.spec;
|
||||
|
||||
let settings = serde_json::to_value(&settings).map_err(|err| Error::engine(err.to_string()))?;
|
||||
|
|
@ -81,6 +82,7 @@ pub async fn retry_run(
|
|||
manifest_blob,
|
||||
git,
|
||||
fork_source_ref,
|
||||
automation,
|
||||
retried_from: Some(source_run_id),
|
||||
parent_id,
|
||||
web_url: input.web_url.clone(),
|
||||
|
|
@ -191,6 +193,7 @@ mod tests {
|
|||
manifest_blob,
|
||||
git: Some(git_context()),
|
||||
fork_source_ref,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -1333,6 +1333,7 @@ reasoning = false
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
|
|
@ -1526,6 +1527,7 @@ reasoning = false
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ mod tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
},
|
||||
Utc::now(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -211,6 +212,7 @@ async fn seed_created_and_starting(
|
|||
manifest_blob: None,
|
||||
git: run_options.pre_run_git.clone(),
|
||||
fork_source_ref: run_options.fork_source_ref.clone(),
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -742,6 +742,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -859,6 +860,7 @@ mod tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
},
|
||||
chrono::Utc::now(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -867,6 +867,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +174,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: record.git.clone(),
|
||||
fork_source_ref: record.fork_source_ref.clone(),
|
||||
automation: record.automation.clone(),
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -827,6 +827,7 @@ mod tests {
|
|||
definition_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
},
|
||||
Utc::now(),
|
||||
)
|
||||
|
|
@ -1150,6 +1151,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
|
||||
run_id: fixtures::RUN_1,
|
||||
|
|
@ -1167,6 +1169,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: run_spec.git.clone(),
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -1219,6 +1222,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
|
||||
run_id: fixtures::RUN_1,
|
||||
|
|
@ -1236,6 +1240,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: run_spec.git.clone(),
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -1573,6 +1578,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
|
||||
run_id: fixtures::RUN_1,
|
||||
|
|
@ -1590,6 +1596,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -1700,6 +1707,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
|
||||
run_id: fixtures::RUN_1,
|
||||
|
|
@ -1717,6 +1725,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
@ -1869,6 +1878,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
};
|
||||
append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated {
|
||||
run_id: fixtures::RUN_1,
|
||||
|
|
@ -1886,6 +1896,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -494,6 +494,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -522,6 +523,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: run_spec.git.clone(),
|
||||
fork_source_ref: run_spec.fork_source_ref.clone(),
|
||||
automation: run_spec.automation.clone(),
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -642,6 +642,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
},
|
||||
chrono::Utc::now(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
definition_blob: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +173,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ async fn initialized(
|
|||
manifest_blob: None,
|
||||
git: run_options.pre_run_git.clone(),
|
||||
fork_source_ref: run_options.fork_source_ref.clone(),
|
||||
automation: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
api.ts
|
||||
api/auth-api.ts
|
||||
api/automations-api.ts
|
||||
api/billing-api.ts
|
||||
api/completions-api.ts
|
||||
api/discovery-api.ts
|
||||
|
|
@ -52,7 +53,14 @@ models/auth-method.ts
|
|||
models/auth-session-user.ts
|
||||
models/auth-session.ts
|
||||
models/auth-sessions-response.ts
|
||||
models/automation-api-trigger.ts
|
||||
models/automation-list-response-meta.ts
|
||||
models/automation-list-response.ts
|
||||
models/automation-ref.ts
|
||||
models/automation-schedule-trigger.ts
|
||||
models/automation-target.ts
|
||||
models/automation-trigger.ts
|
||||
models/automation.ts
|
||||
models/batch-delete-runs-request.ts
|
||||
models/batch-delete-runs-response.ts
|
||||
models/batch-delete-runs-result.ts
|
||||
|
|
@ -82,6 +90,7 @@ models/completion-tool-choice.ts
|
|||
models/completion-tool-definition.ts
|
||||
models/completion-usage.ts
|
||||
models/conclusion.ts
|
||||
models/create-automation-request.ts
|
||||
models/create-completion-request.ts
|
||||
models/create-run-pull-request-request.ts
|
||||
models/create-run-session-request.ts
|
||||
|
|
@ -234,6 +243,7 @@ models/pair-transcript-system-message.ts
|
|||
models/pair-transcript-tool-call.ts
|
||||
models/pair-transcript-user-message.ts
|
||||
models/pair-transcript-warning.ts
|
||||
models/patch-automation-request.ts
|
||||
models/pending-interview-record.ts
|
||||
models/pending-reason.ts
|
||||
models/permission-level.ts
|
||||
|
|
@ -283,6 +293,7 @@ models/related-workflow-diagnostic.ts
|
|||
models/render-workflow-graph-direction.ts
|
||||
models/render-workflow-graph-format.ts
|
||||
models/render-workflow-graph-request.ts
|
||||
models/replace-automation-request.ts
|
||||
models/repo-check-response-permissions.ts
|
||||
models/repo-check-response.ts
|
||||
models/repository-ref.ts
|
||||
|
|
|
|||
1
lib/packages/fabro-api-client/src/api.ts
generated
1
lib/packages/fabro-api-client/src/api.ts
generated
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
|
||||
export * from './api/auth-api';
|
||||
export * from './api/automations-api';
|
||||
export * from './api/billing-api';
|
||||
export * from './api/completions-api';
|
||||
export * from './api/discovery-api';
|
||||
|
|
|
|||
714
lib/packages/fabro-api-client/src/api/automations-api.ts
generated
Normal file
714
lib/packages/fabro-api-client/src/api/automations-api.ts
generated
Normal file
|
|
@ -0,0 +1,714 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from '../configuration';
|
||||
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
|
||||
import globalAxios from 'axios';
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction, replaceWithSerializableTypeIfNeeded } from '../common';
|
||||
// @ts-ignore
|
||||
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
|
||||
// @ts-ignore
|
||||
import type { Automation } from '../models';
|
||||
// @ts-ignore
|
||||
import type { AutomationListResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { CreateAutomationRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { ErrorResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PaginatedRunList } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PatchAutomationRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { ReplaceAutomationRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { Run } from '../models';
|
||||
/**
|
||||
* AutomationsApi - axios parameter creator
|
||||
*/
|
||||
export const AutomationsApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
* Creates an automation and persists it as canonical TOML.
|
||||
* @summary Create Automation
|
||||
* @param {CreateAutomationRequest} createAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createAutomation: async (createAutomationRequest: CreateAutomationRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'createAutomationRequest' is not null or undefined
|
||||
assertParamExists('createAutomation', 'createAutomationRequest', createAutomationRequest)
|
||||
const localVarPath = `/api/v1/automations`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(createAutomationRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Materializes the automation target and creates a run when an enabled `api` trigger is present.
|
||||
* @summary Start Automation Run
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createAutomationRun: async (id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('createAutomationRun', 'id', id)
|
||||
const localVarPath = `/api/v1/automations/{id}/runs`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Delete Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteAutomation: async (ifMatch: string, id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'ifMatch' is not null or undefined
|
||||
assertParamExists('deleteAutomation', 'ifMatch', ifMatch)
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('deleteAutomation', 'id', id)
|
||||
const localVarPath = `/api/v1/automations/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
if (ifMatch != null) {
|
||||
localVarHeaderParameter['If-Match'] = String(ifMatch);
|
||||
}
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Get Automation
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAutomation: async (id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('getAutomation', 'id', id)
|
||||
const localVarPath = `/api/v1/automations/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary List Automation Runs
|
||||
* @param {string} id Automation ID.
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listAutomationRuns: async (id: string, pageLimit?: number, pageOffset?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('listAutomationRuns', 'id', id)
|
||||
const localVarPath = `/api/v1/automations/{id}/runs`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (pageLimit !== undefined) {
|
||||
localVarQueryParameter['page[limit]'] = pageLimit;
|
||||
}
|
||||
|
||||
if (pageOffset !== undefined) {
|
||||
localVarQueryParameter['page[offset]'] = pageOffset;
|
||||
}
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Returns automation definitions sorted by automation ID.
|
||||
* @summary List Automations
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listAutomations: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/v1/automations`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Patch Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {PatchAutomationRequest} patchAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
patchAutomation: async (ifMatch: string, id: string, patchAutomationRequest: PatchAutomationRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'ifMatch' is not null or undefined
|
||||
assertParamExists('patchAutomation', 'ifMatch', ifMatch)
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('patchAutomation', 'id', id)
|
||||
// verify required parameter 'patchAutomationRequest' is not null or undefined
|
||||
assertParamExists('patchAutomation', 'patchAutomationRequest', patchAutomationRequest)
|
||||
const localVarPath = `/api/v1/automations/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
if (ifMatch != null) {
|
||||
localVarHeaderParameter['If-Match'] = String(ifMatch);
|
||||
}
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(patchAutomationRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Replace Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {ReplaceAutomationRequest} replaceAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
replaceAutomation: async (ifMatch: string, id: string, replaceAutomationRequest: ReplaceAutomationRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'ifMatch' is not null or undefined
|
||||
assertParamExists('replaceAutomation', 'ifMatch', ifMatch)
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('replaceAutomation', 'id', id)
|
||||
// verify required parameter 'replaceAutomationRequest' is not null or undefined
|
||||
assertParamExists('replaceAutomation', 'replaceAutomationRequest', replaceAutomationRequest)
|
||||
const localVarPath = `/api/v1/automations/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
if (ifMatch != null) {
|
||||
localVarHeaderParameter['If-Match'] = String(ifMatch);
|
||||
}
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(replaceAutomationRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* AutomationsApi - functional programming interface
|
||||
*/
|
||||
export const AutomationsApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = AutomationsApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
* Creates an automation and persists it as canonical TOML.
|
||||
* @summary Create Automation
|
||||
* @param {CreateAutomationRequest} createAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createAutomation(createAutomationRequest: CreateAutomationRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Automation>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createAutomation(createAutomationRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['AutomationsApi.createAutomation']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Materializes the automation target and creates a run when an enabled `api` trigger is present.
|
||||
* @summary Start Automation Run
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createAutomationRun(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Run>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createAutomationRun(id, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['AutomationsApi.createAutomationRun']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Delete Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteAutomation(ifMatch: string, id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAutomation(ifMatch, id, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['AutomationsApi.deleteAutomation']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Get Automation
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getAutomation(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Automation>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAutomation(id, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['AutomationsApi.getAutomation']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary List Automation Runs
|
||||
* @param {string} id Automation ID.
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listAutomationRuns(id: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listAutomationRuns(id, pageLimit, pageOffset, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['AutomationsApi.listAutomationRuns']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Returns automation definitions sorted by automation ID.
|
||||
* @summary List Automations
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listAutomations(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AutomationListResponse>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listAutomations(options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['AutomationsApi.listAutomations']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Patch Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {PatchAutomationRequest} patchAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async patchAutomation(ifMatch: string, id: string, patchAutomationRequest: PatchAutomationRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Automation>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.patchAutomation(ifMatch, id, patchAutomationRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['AutomationsApi.patchAutomation']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Replace Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {ReplaceAutomationRequest} replaceAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async replaceAutomation(ifMatch: string, id: string, replaceAutomationRequest: ReplaceAutomationRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Automation>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.replaceAutomation(ifMatch, id, replaceAutomationRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['AutomationsApi.replaceAutomation']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* AutomationsApi - factory interface
|
||||
*/
|
||||
export const AutomationsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = AutomationsApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
* Creates an automation and persists it as canonical TOML.
|
||||
* @summary Create Automation
|
||||
* @param {CreateAutomationRequest} createAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createAutomation(createAutomationRequest: CreateAutomationRequest, options?: RawAxiosRequestConfig): AxiosPromise<Automation> {
|
||||
return localVarFp.createAutomation(createAutomationRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Materializes the automation target and creates a run when an enabled `api` trigger is present.
|
||||
* @summary Start Automation Run
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createAutomationRun(id: string, options?: RawAxiosRequestConfig): AxiosPromise<Run> {
|
||||
return localVarFp.createAutomationRun(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Delete Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteAutomation(ifMatch: string, id: string, options?: RawAxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.deleteAutomation(ifMatch, id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Get Automation
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAutomation(id: string, options?: RawAxiosRequestConfig): AxiosPromise<Automation> {
|
||||
return localVarFp.getAutomation(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary List Automation Runs
|
||||
* @param {string} id Automation ID.
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listAutomationRuns(id: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunList> {
|
||||
return localVarFp.listAutomationRuns(id, pageLimit, pageOffset, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns automation definitions sorted by automation ID.
|
||||
* @summary List Automations
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listAutomations(options?: RawAxiosRequestConfig): AxiosPromise<AutomationListResponse> {
|
||||
return localVarFp.listAutomations(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Patch Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {PatchAutomationRequest} patchAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
patchAutomation(ifMatch: string, id: string, patchAutomationRequest: PatchAutomationRequest, options?: RawAxiosRequestConfig): AxiosPromise<Automation> {
|
||||
return localVarFp.patchAutomation(ifMatch, id, patchAutomationRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Replace Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {ReplaceAutomationRequest} replaceAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
replaceAutomation(ifMatch: string, id: string, replaceAutomationRequest: ReplaceAutomationRequest, options?: RawAxiosRequestConfig): AxiosPromise<Automation> {
|
||||
return localVarFp.replaceAutomation(ifMatch, id, replaceAutomationRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* AutomationsApi - object-oriented interface
|
||||
*/
|
||||
export class AutomationsApi extends BaseAPI {
|
||||
/**
|
||||
* Creates an automation and persists it as canonical TOML.
|
||||
* @summary Create Automation
|
||||
* @param {CreateAutomationRequest} createAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public createAutomation(createAutomationRequest: CreateAutomationRequest, options?: RawAxiosRequestConfig) {
|
||||
return AutomationsApiFp(this.configuration).createAutomation(createAutomationRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Materializes the automation target and creates a run when an enabled `api` trigger is present.
|
||||
* @summary Start Automation Run
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public createAutomationRun(id: string, options?: RawAxiosRequestConfig) {
|
||||
return AutomationsApiFp(this.configuration).createAutomationRun(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Delete Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public deleteAutomation(ifMatch: string, id: string, options?: RawAxiosRequestConfig) {
|
||||
return AutomationsApiFp(this.configuration).deleteAutomation(ifMatch, id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Get Automation
|
||||
* @param {string} id Automation ID.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public getAutomation(id: string, options?: RawAxiosRequestConfig) {
|
||||
return AutomationsApiFp(this.configuration).getAutomation(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary List Automation Runs
|
||||
* @param {string} id Automation ID.
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public listAutomationRuns(id: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig) {
|
||||
return AutomationsApiFp(this.configuration).listAutomationRuns(id, pageLimit, pageOffset, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns automation definitions sorted by automation ID.
|
||||
* @summary List Automations
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public listAutomations(options?: RawAxiosRequestConfig) {
|
||||
return AutomationsApiFp(this.configuration).listAutomations(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Patch Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {PatchAutomationRequest} patchAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public patchAutomation(ifMatch: string, id: string, patchAutomationRequest: PatchAutomationRequest, options?: RawAxiosRequestConfig) {
|
||||
return AutomationsApiFp(this.configuration).patchAutomation(ifMatch, id, patchAutomationRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Replace Automation
|
||||
* @param {string} ifMatch Current automation revision, quoted or unquoted.
|
||||
* @param {string} id Automation ID.
|
||||
* @param {ReplaceAutomationRequest} replaceAutomationRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public replaceAutomation(ifMatch: string, id: string, replaceAutomationRequest: ReplaceAutomationRequest, options?: RawAxiosRequestConfig) {
|
||||
return AutomationsApiFp(this.configuration).replaceAutomation(ifMatch, id, replaceAutomationRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
|
@ -1120,7 +1120,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
};
|
||||
},
|
||||
/**
|
||||
* Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable.
|
||||
* Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable.
|
||||
* @summary Retry Run
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
|
|
@ -1868,7 +1868,7 @@ export const RunsApiFp = function(configuration?: Configuration) {
|
|||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable.
|
||||
* Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable.
|
||||
* @summary Retry Run
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
|
|
@ -2264,7 +2264,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
|
|||
return localVarFp.retrieveRunGraphSource(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable.
|
||||
* Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable.
|
||||
* @summary Retry Run
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
|
|
@ -2652,7 +2652,7 @@ export class RunsApi extends BaseAPI {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable.
|
||||
* Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable.
|
||||
* @summary Retry Run
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
|
|
|
|||
27
lib/packages/fabro-api-client/src/models/automation-api-trigger.ts
generated
Normal file
27
lib/packages/fabro-api-client/src/models/automation-api-trigger.ts
generated
Normal 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 AutomationApiTrigger {
|
||||
'id': string;
|
||||
'type': AutomationApiTriggerTypeEnum;
|
||||
'enabled'?: boolean;
|
||||
}
|
||||
|
||||
export const AutomationApiTriggerTypeEnum = {
|
||||
API: 'api'
|
||||
} as const;
|
||||
|
||||
export type AutomationApiTriggerTypeEnum = typeof AutomationApiTriggerTypeEnum[keyof typeof AutomationApiTriggerTypeEnum];
|
||||
19
lib/packages/fabro-api-client/src/models/automation-list-response-meta.ts
generated
Normal file
19
lib/packages/fabro-api-client/src/models/automation-list-response-meta.ts
generated
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* 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 AutomationListResponseMeta {
|
||||
'total': number;
|
||||
}
|
||||
29
lib/packages/fabro-api-client/src/models/automation-list-response.ts
generated
Normal file
29
lib/packages/fabro-api-client/src/models/automation-list-response.ts
generated
Normal 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.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { Automation } from './automation';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationListResponseMeta } from './automation-list-response-meta';
|
||||
|
||||
/**
|
||||
* List of automation definitions.
|
||||
*/
|
||||
export interface AutomationListResponse {
|
||||
'data': Array<Automation>;
|
||||
'meta': AutomationListResponseMeta;
|
||||
}
|
||||
|
|
@ -17,4 +17,5 @@
|
|||
export interface AutomationRef {
|
||||
'id': string;
|
||||
'name': string | null;
|
||||
'trigger_id'?: string;
|
||||
}
|
||||
|
|
|
|||
31
lib/packages/fabro-api-client/src/models/automation-schedule-trigger.ts
generated
Normal file
31
lib/packages/fabro-api-client/src/models/automation-schedule-trigger.ts
generated
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* 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 AutomationScheduleTrigger {
|
||||
'id': string;
|
||||
'type': AutomationScheduleTriggerTypeEnum;
|
||||
'enabled'?: boolean;
|
||||
/**
|
||||
* Five-field cron expression accepted by croner.
|
||||
*/
|
||||
'expression': string;
|
||||
}
|
||||
|
||||
export const AutomationScheduleTriggerTypeEnum = {
|
||||
SCHEDULE: 'schedule'
|
||||
} as const;
|
||||
|
||||
export type AutomationScheduleTriggerTypeEnum = typeof AutomationScheduleTriggerTypeEnum[keyof typeof AutomationScheduleTriggerTypeEnum];
|
||||
33
lib/packages/fabro-api-client/src/models/automation-target.ts
generated
Normal file
33
lib/packages/fabro-api-client/src/models/automation-target.ts
generated
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Repository, ref, and Fabro workflow selector materialized when the automation starts.
|
||||
*/
|
||||
export interface AutomationTarget {
|
||||
/**
|
||||
* GitHub owner/repo slug.
|
||||
*/
|
||||
'repository': string;
|
||||
/**
|
||||
* Branch, tag, or SHA selector to check out.
|
||||
*/
|
||||
'ref': string;
|
||||
/**
|
||||
* Fabro workflow slug or relative workflow path.
|
||||
*/
|
||||
'workflow': string;
|
||||
}
|
||||
27
lib/packages/fabro-api-client/src/models/automation-trigger.ts
generated
Normal file
27
lib/packages/fabro-api-client/src/models/automation-trigger.ts
generated
Normal 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.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationApiTrigger } from './automation-api-trigger';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationScheduleTrigger } from './automation-schedule-trigger';
|
||||
|
||||
/**
|
||||
* @type AutomationTrigger
|
||||
* Automation trigger definition.
|
||||
*/
|
||||
export type AutomationTrigger = { type: 'api' } & AutomationApiTrigger | { type: 'schedule' } & AutomationScheduleTrigger;
|
||||
37
lib/packages/fabro-api-client/src/models/automation.ts
generated
Normal file
37
lib/packages/fabro-api-client/src/models/automation.ts
generated
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* 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 { AutomationTarget } from './automation-target';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationTrigger } from './automation-trigger';
|
||||
|
||||
/**
|
||||
* Server-owned runnable automation binding.
|
||||
*/
|
||||
export interface Automation {
|
||||
'id': string;
|
||||
/**
|
||||
* Lowercase hex SHA-256 revision of the canonical TOML bytes.
|
||||
*/
|
||||
'revision': string;
|
||||
'name': string;
|
||||
'description': string | null;
|
||||
'enabled': boolean;
|
||||
'target': AutomationTarget;
|
||||
'triggers': Array<AutomationTrigger>;
|
||||
}
|
||||
30
lib/packages/fabro-api-client/src/models/create-automation-request.ts
generated
Normal file
30
lib/packages/fabro-api-client/src/models/create-automation-request.ts
generated
Normal 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.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationTarget } from './automation-target';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationTrigger } from './automation-trigger';
|
||||
|
||||
export interface CreateAutomationRequest {
|
||||
'id': string;
|
||||
'name': string;
|
||||
'description'?: string | null;
|
||||
'enabled'?: boolean;
|
||||
'target': AutomationTarget;
|
||||
'triggers': Array<AutomationTrigger>;
|
||||
}
|
||||
10
lib/packages/fabro-api-client/src/models/index.ts
generated
10
lib/packages/fabro-api-client/src/models/index.ts
generated
|
|
@ -29,7 +29,14 @@ export * from './auth-method';
|
|||
export * from './auth-session';
|
||||
export * from './auth-session-user';
|
||||
export * from './auth-sessions-response';
|
||||
export * from './automation';
|
||||
export * from './automation-api-trigger';
|
||||
export * from './automation-list-response';
|
||||
export * from './automation-list-response-meta';
|
||||
export * from './automation-ref';
|
||||
export * from './automation-schedule-trigger';
|
||||
export * from './automation-target';
|
||||
export * from './automation-trigger';
|
||||
export * from './batch-delete-runs-request';
|
||||
export * from './batch-delete-runs-response';
|
||||
export * from './batch-delete-runs-result';
|
||||
|
|
@ -59,6 +66,7 @@ export * from './completion-tool-choice';
|
|||
export * from './completion-tool-definition';
|
||||
export * from './completion-usage';
|
||||
export * from './conclusion';
|
||||
export * from './create-automation-request';
|
||||
export * from './create-completion-request';
|
||||
export * from './create-run-pull-request-request';
|
||||
export * from './create-run-session-request';
|
||||
|
|
@ -210,6 +218,7 @@ export * from './pair-transcript-system-message';
|
|||
export * from './pair-transcript-tool-call';
|
||||
export * from './pair-transcript-user-message';
|
||||
export * from './pair-transcript-warning';
|
||||
export * from './patch-automation-request';
|
||||
export * from './pending-interview-record';
|
||||
export * from './pending-reason';
|
||||
export * from './permission-level';
|
||||
|
|
@ -259,6 +268,7 @@ export * from './related-workflow-diagnostic';
|
|||
export * from './render-workflow-graph-direction';
|
||||
export * from './render-workflow-graph-format';
|
||||
export * from './render-workflow-graph-request';
|
||||
export * from './replace-automation-request';
|
||||
export * from './repo-check-response';
|
||||
export * from './repo-check-response-permissions';
|
||||
export * from './repository-ref';
|
||||
|
|
|
|||
29
lib/packages/fabro-api-client/src/models/patch-automation-request.ts
generated
Normal file
29
lib/packages/fabro-api-client/src/models/patch-automation-request.ts
generated
Normal 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.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationTarget } from './automation-target';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationTrigger } from './automation-trigger';
|
||||
|
||||
export interface PatchAutomationRequest {
|
||||
'name'?: string;
|
||||
'description'?: string | null;
|
||||
'enabled'?: boolean;
|
||||
'target'?: AutomationTarget;
|
||||
'triggers'?: Array<AutomationTrigger>;
|
||||
}
|
||||
29
lib/packages/fabro-api-client/src/models/replace-automation-request.ts
generated
Normal file
29
lib/packages/fabro-api-client/src/models/replace-automation-request.ts
generated
Normal 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.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationTarget } from './automation-target';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AutomationTrigger } from './automation-trigger';
|
||||
|
||||
export interface ReplaceAutomationRequest {
|
||||
'name': string;
|
||||
'description'?: string | null;
|
||||
'enabled': boolean;
|
||||
'target': AutomationTarget;
|
||||
'triggers': Array<AutomationTrigger>;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue