mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
feat(run): add managed branch controls (#243)
## Summary Adds run-level controls for clone behavior, managed run branch setup/pushes, and metadata branch writes/pushes so workflows can opt out of Fabro-managed Git behavior without relying on provider-specific `skip_clone` settings. This closes fabro-sh/fabro#240. ## What Changed - Introduced `[run.clone]`, `[run.run_branch]`, and `[run.meta_branch]` settings with defaults that preserve current behavior. - Removed user-facing `skip_clone` from Docker/Daytona config while mapping the new run-level clone setting into the internal sandbox runtime options. - Gated run branch setup/push, metadata branch writer creation/push, and PR branch output on the new settings. - Enforced invalid combinations: pull requests require an enabled pushed run branch, and disabling the run branch also disables metadata branch behavior. - Updated OpenAPI, the generated TypeScript API client, frontend fixture data, and docs for the new configuration shape. ## Testing - `cargo nextest run -p fabro-config -p fabro-types -p fabro-workflow -p fabro-server` - `cargo build -p fabro-api` - `cd lib/packages/fabro-api-client && bun run generate` - `cd lib/packages/fabro-api-client && bun run typecheck` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun test` - `cargo build --workspace` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` - `cargo insta pending-snapshots` - `git diff --check` ## Post-Deploy Monitoring & Validation - Validation window: first 24 hours after release; owner: release owner/on-call engineer. - Log queries/search terms: `run_branch`, `meta_branch`, `clone.enabled`, `skip_clone`, `pull request requires an enabled pushed run branch`, `metadata branch`. - Healthy signals: runs without custom branch config continue creating and pushing run/meta branches; runs with `[run.clone] enabled = false` start provider sandboxes without cloning; runs with branch pushes disabled complete without Git push errors. - Failure signals: increased run startup failures for Docker/Daytona, unexpected PR creation conflicts, missing metadata for default-config runs, or validation errors for configurations that previously used default settings. - Mitigation trigger: if default-config runs stop producing expected branch/metadata artifacts or sandbox startup failures increase, roll back the release or temporarily restore previous defaults while investigating the run-level setting resolution path. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) Co-authored-by: Haroldo Olivieri <6575718+haroldolivieri@users.noreply.github.com>
This commit is contained in:
parent
762ac19649
commit
a33d17c88d
30 changed files with 811 additions and 149 deletions
|
|
@ -62,6 +62,9 @@ function sampleSettings({
|
|||
prepare: { commands: prepareCommands, timeout_ms: 120_000 },
|
||||
execution: { mode: "normal", approval: "prompt" },
|
||||
checkpoint: { exclude_globs: [] },
|
||||
clone: { enabled: true },
|
||||
run_branch: { enabled: true, push: true },
|
||||
meta_branch: { enabled: true, push: true },
|
||||
sandbox: {
|
||||
provider: "daytona",
|
||||
preserve: false,
|
||||
|
|
@ -80,7 +83,6 @@ function sampleSettings({
|
|||
dockerfile: null,
|
||||
},
|
||||
network: null,
|
||||
skip_clone: false,
|
||||
},
|
||||
},
|
||||
notifications: {},
|
||||
|
|
|
|||
|
|
@ -8654,6 +8654,9 @@ components:
|
|||
- prepare
|
||||
- execution
|
||||
- checkpoint
|
||||
- clone
|
||||
- run_branch
|
||||
- meta_branch
|
||||
- sandbox
|
||||
- notifications
|
||||
- interviews
|
||||
|
|
@ -8688,6 +8691,12 @@ components:
|
|||
$ref: "#/components/schemas/RunExecutionSettings"
|
||||
checkpoint:
|
||||
$ref: "#/components/schemas/RunCheckpointSettings"
|
||||
clone:
|
||||
$ref: "#/components/schemas/RunCloneSettings"
|
||||
run_branch:
|
||||
$ref: "#/components/schemas/RunBranchSettings"
|
||||
meta_branch:
|
||||
$ref: "#/components/schemas/RunMetaBranchSettings"
|
||||
sandbox:
|
||||
$ref: "#/components/schemas/RunSandboxSettings"
|
||||
notifications:
|
||||
|
|
@ -8826,6 +8835,31 @@ components:
|
|||
items:
|
||||
type: string
|
||||
|
||||
RunCloneSettings:
|
||||
type: object
|
||||
required: [enabled]
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
|
||||
RunBranchSettings:
|
||||
type: object
|
||||
required: [enabled, push]
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
push:
|
||||
type: boolean
|
||||
|
||||
RunMetaBranchSettings:
|
||||
type: object
|
||||
required: [enabled, push]
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
push:
|
||||
type: boolean
|
||||
|
||||
RunSandboxSettings:
|
||||
type: object
|
||||
required: [provider, preserve, stop_on_terminal, devcontainer, env, docker, daytona]
|
||||
|
|
@ -8853,7 +8887,7 @@ components:
|
|||
|
||||
DockerSettings:
|
||||
type: object
|
||||
required: [image, network_mode, memory_limit, cpu_quota, env_vars, skip_clone]
|
||||
required: [image, network_mode, memory_limit, cpu_quota, env_vars]
|
||||
properties:
|
||||
image:
|
||||
type: string
|
||||
|
|
@ -8869,12 +8903,10 @@ components:
|
|||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/InterpString"
|
||||
skip_clone:
|
||||
type: boolean
|
||||
|
||||
DaytonaSettings:
|
||||
type: object
|
||||
required: [auto_stop_interval, labels, snapshot, network, skip_clone]
|
||||
required: [auto_stop_interval, labels, snapshot, network]
|
||||
properties:
|
||||
auto_stop_interval:
|
||||
type: ["integer", "null"]
|
||||
|
|
@ -8889,8 +8921,6 @@ components:
|
|||
oneOf:
|
||||
- $ref: "#/components/schemas/DaytonaNetworkLayer"
|
||||
- type: "null"
|
||||
skip_clone:
|
||||
type: boolean
|
||||
|
||||
DaytonaSnapshotSettings:
|
||||
type: object
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ date: "2026-04-26"
|
|||
|
||||
To migrate:
|
||||
1. Use `--sandbox local` or `[run.sandbox] provider = "local"` when a run must operate directly on the host working tree.
|
||||
2. For Docker or Daytona runs without a GitHub origin, set `skip_clone = true` under the provider-specific sandbox section.
|
||||
2. For Docker or Daytona runs without a GitHub origin, set `[run.clone] enabled = false`.
|
||||
3. Make sure private GitHub repositories have GitHub App credentials configured before running clone-based sandboxes.
|
||||
</Warning>
|
||||
|
||||
|
|
@ -20,8 +20,8 @@ This is a breaking sandbox change:
|
|||
|
||||
- Docker no longer bind-mounts host workspaces.
|
||||
- Docker and Daytona accept GitHub origins only when cloning.
|
||||
- Present non-GitHub origins fail unless `skip_clone = true`.
|
||||
- Absent origins and `skip_clone = true` runs create empty workspaces without repository files.
|
||||
- Present non-GitHub origins fail unless `[run.clone] enabled = false`.
|
||||
- Absent origins and `[run.clone] enabled = false` runs create empty workspaces without repository files.
|
||||
|
||||
The packaged Docker Compose service mounts the host Docker socket so the server can create sibling run containers. Operators remain responsible for Docker socket permissions, Docker Desktop behavior, `DOCKER_HOST`, TLS, and remote daemon security.
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ The Docker sandbox runs all tool operations inside a Docker container. Docker is
|
|||
|
||||
- **Container lifecycle** — On `initialize()`, Fabro pulls the image (if needed), creates a container with `sleep infinity`, and starts it. On `cleanup()`, Fabro stops and removes the container.
|
||||
- **Working directory** — Docker runs use a provider-owned workspace inside the container. When a run has a GitHub origin, Fabro clones that repository into the workspace instead of bind-mounting the host source tree.
|
||||
- **Git clone** — Docker and Daytona accept GitHub origins for automatic cloning. If the run has a present non-GitHub origin, set `skip_clone = true` or switch to `local`.
|
||||
- **Git clone** — Docker and Daytona accept GitHub origins for automatic cloning. If the run has a present non-GitHub origin, set `[run.clone] enabled = false` or switch to `local`.
|
||||
- **Commands** — Executed via `docker exec` with `/bin/bash -c` inside the container. Timeout and cancellation are supported.
|
||||
- **File writes** — Use the Docker API's tar upload to avoid shell escaping issues with special characters.
|
||||
- **Platform detection** — The container's `uname -r` is cached at startup.
|
||||
|
|
@ -83,7 +83,6 @@ image = "buildpack-deps:noble"
|
|||
network_mode = "bridge"
|
||||
memory_limit = "4GB"
|
||||
cpu_quota = 200000
|
||||
skip_clone = false
|
||||
```
|
||||
|
||||
| Setting | Default | Description |
|
||||
|
|
@ -92,9 +91,8 @@ skip_clone = false
|
|||
| `network_mode` | `bridge` | Docker network mode |
|
||||
| `memory_limit` | `4GB` | Memory limit |
|
||||
| `cpu_quota` | `200000` | CPU quota (microseconds per 100ms period) |
|
||||
| `skip_clone` | `false` | Create an empty workspace instead of cloning the run's GitHub origin |
|
||||
|
||||
When `skip_clone = true`, the sandbox starts with an empty provider workspace. Use [prepare steps](/execution/run-configuration#runprepare) to clone or create any files the workflow needs.
|
||||
Set `[run.clone] enabled = false` to start with an empty provider workspace instead of cloning the run's GitHub origin. Use [prepare steps](/execution/run-configuration#runprepare) to clone or create any files the workflow needs.
|
||||
|
||||
### Preserving the container
|
||||
|
||||
|
|
@ -129,7 +127,7 @@ The Daytona API key must include `write:snapshots`, `delete:snapshots`, `write:s
|
|||
|
||||
- **Sandbox lifecycle** — On `initialize()`, Fabro creates a Daytona sandbox (from an image or a snapshot), clones the run's GitHub origin into it when one is available, and waits until it's ready. On `cleanup()`, the sandbox is deleted.
|
||||
- **Working directory** — Fixed at `/home/daytona/workspace`. GitHub-origin runs clone the repository there automatically.
|
||||
- **Git clone** — Fabro detects the run manifest's GitHub origin URL and branch, converts SSH URLs to HTTPS, and clones into the sandbox. For private repositories, Fabro uses a GitHub App Installation Access Token scoped to the specific repository. Public repositories are cloned without credentials. Set `skip_clone = true` to create an empty workspace instead.
|
||||
- **Git clone** — Fabro detects the run manifest's GitHub origin URL and branch, converts SSH URLs to HTTPS, and clones into the sandbox. For private repositories, Fabro uses a GitHub App Installation Access Token scoped to the specific repository. Public repositories are cloned without credentials. Set `[run.clone] enabled = false` to create an empty workspace instead.
|
||||
- **Commands** — Executed via the Daytona process API. Commands are base64-encoded and piped through `sh` to support pipes, environment variables, and other shell features.
|
||||
- **Ephemeral** — Sandboxes are created with `ephemeral: true` and a unique timestamped name (e.g. `fabro-20260305-142301-a3f2`).
|
||||
|
||||
|
|
@ -143,7 +141,6 @@ provider = "daytona"
|
|||
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
skip_clone = false
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "rust-dev"
|
||||
|
|
|
|||
|
|
@ -157,6 +157,47 @@ script = "npm install"
|
|||
|
||||
Each step must exit with status 0. If any step fails, the run aborts before the workflow starts. Prepare steps replace across layers — the higher-precedence layer wins wholesale.
|
||||
|
||||
### `[run.clone]`
|
||||
|
||||
Configure whether clone-based sandboxes clone the run's GitHub origin before execution.
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.clone]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
Set `enabled = false` to start Docker and Daytona runs with an empty provider workspace. Use [prepare steps](#runprepare) to clone or create any files the workflow needs.
|
||||
|
||||
### `[run.run_branch]`
|
||||
|
||||
Configure Fabro's managed `fabro/run/<id>` checkpoint branch.
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.run_branch]
|
||||
enabled = true
|
||||
push = true
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `enabled` | When `false`, Fabro does not create the managed run branch or checkpoint commits. This also disables metadata branch writes. |
|
||||
| `push` | When `false`, Fabro creates local checkpoint commits but does not push `fabro/run/<id>` to the remote. |
|
||||
|
||||
### `[run.meta_branch]`
|
||||
|
||||
Configure Fabro's managed `fabro/meta/<id>` metadata branch.
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.meta_branch]
|
||||
enabled = true
|
||||
push = true
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `enabled` | When `false`, Fabro skips metadata branch snapshots. |
|
||||
| `push` | When `false`, Fabro writes metadata snapshots locally but does not push `fabro/meta/<id>` to the remote. |
|
||||
|
||||
### `[run.sandbox]`
|
||||
|
||||
Configure how agent tools (bash, file edits) are executed.
|
||||
|
|
@ -186,7 +227,6 @@ image = "buildpack-deps:noble"
|
|||
network_mode = "bridge"
|
||||
memory_limit = "4GB"
|
||||
cpu_quota = 200000
|
||||
skip_clone = false
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|
|
@ -196,7 +236,6 @@ skip_clone = false
|
|||
| `memory_limit` | Memory limit using human-readable units such as `"4GB"` or `"512MiB"`. |
|
||||
| `cpu_quota` | Docker CPU quota in microseconds per 100ms period. Defaults to `200000`. |
|
||||
| `env_vars` | Provider-level environment variables passed to the Docker container. For workflow tool execution, prefer `[run.sandbox.env]`. |
|
||||
| `skip_clone` | When `true`, start with an empty container workspace instead of cloning the run's GitHub origin. |
|
||||
|
||||
#### `[run.sandbox.daytona]`
|
||||
|
||||
|
|
@ -230,7 +269,6 @@ dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update"
|
|||
| `snapshot.disk` | Disk size using the same units as `memory`. |
|
||||
| `snapshot.dockerfile` | Dockerfile content (inline string) or path (`{ path = "..." }`) for building the snapshot image. Paths are resolved relative to the TOML file's directory. |
|
||||
| `network` | Network access mode: `"allow_all"` (default), `"block"`, or `{ allow_list = ["..."] }`. See [Sandboxing](/administration/sandboxing#network-access-control). |
|
||||
| `skip_clone` | When `true`, start with an empty Daytona workspace instead of cloning the run's GitHub origin. |
|
||||
|
||||
#### Local sandbox
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ preserve = false
|
|||
|
||||
[run.sandbox.daytona]
|
||||
auto_stop_interval = 60
|
||||
skip_clone = false
|
||||
|
||||
[run.sandbox.daytona.labels]
|
||||
project = "fabro"
|
||||
|
|
@ -104,14 +103,14 @@ If a snapshot is configured by name but doesn't exist and no `dockerfile` is pro
|
|||
|
||||
Fabro automatically clones the run's GitHub origin into the sandbox at `/home/daytona/workspace`. Public repositories work without extra configuration. Private repositories require GitHub access. In `token` mode, Fabro uses the stored token directly. In `app` mode, Fabro uses short-lived Installation Access Tokens scoped to the specific repository.
|
||||
|
||||
Set `skip_clone = true` in `[run.sandbox.daytona]` when a workflow should start with an empty Daytona workspace instead of cloning the run origin:
|
||||
Set `[run.clone] enabled = false` when a workflow should start with an empty Daytona workspace instead of cloning the run origin:
|
||||
|
||||
```toml title="run.toml"
|
||||
[run.sandbox]
|
||||
provider = "daytona"
|
||||
|
||||
[run.sandbox.daytona]
|
||||
skip_clone = true
|
||||
[run.clone]
|
||||
enabled = false
|
||||
```
|
||||
|
||||
If the clone fails without GitHub access configured, Fabro suggests running the setup flow:
|
||||
|
|
|
|||
|
|
@ -926,6 +926,9 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"checkpoint": {
|
||||
"exclude_globs": []
|
||||
},
|
||||
"clone": {
|
||||
"enabled": true
|
||||
},
|
||||
"execution": {
|
||||
"approval": "prompt",
|
||||
"mode": "normal"
|
||||
|
|
@ -948,6 +951,10 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"provider": null,
|
||||
"slack": null
|
||||
},
|
||||
"meta_branch": {
|
||||
"enabled": true,
|
||||
"push": true
|
||||
},
|
||||
"metadata": {},
|
||||
"model": {
|
||||
"controls": {
|
||||
|
|
@ -964,6 +971,10 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"timeout_ms": 300000
|
||||
},
|
||||
"pull_request": null,
|
||||
"run_branch": {
|
||||
"enabled": true,
|
||||
"push": true
|
||||
},
|
||||
"sandbox": {
|
||||
"daytona": null,
|
||||
"devcontainer": false,
|
||||
|
|
@ -972,8 +983,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"env_vars": {},
|
||||
"image": "buildpack-deps:noble",
|
||||
"memory_limit": 4000000000,
|
||||
"network_mode": null,
|
||||
"skip_clone": false
|
||||
"network_mode": null
|
||||
},
|
||||
"env": {},
|
||||
"preserve": false,
|
||||
|
|
|
|||
|
|
@ -148,6 +148,17 @@ fn inspect_resolves_selector_via_server_endpoint() {
|
|||
"checkpoint": {
|
||||
"exclude_globs": []
|
||||
},
|
||||
"clone": {
|
||||
"enabled": true
|
||||
},
|
||||
"run_branch": {
|
||||
"enabled": true,
|
||||
"push": true
|
||||
},
|
||||
"meta_branch": {
|
||||
"enabled": true,
|
||||
"push": true
|
||||
},
|
||||
"sandbox": {
|
||||
"provider": "local",
|
||||
"preserve": false,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ approval = "prompt"
|
|||
[run.prepare]
|
||||
timeout = "5m"
|
||||
|
||||
[run.clone]
|
||||
enabled = true
|
||||
|
||||
[run.run_branch]
|
||||
enabled = true
|
||||
push = true
|
||||
|
||||
[run.meta_branch]
|
||||
enabled = true
|
||||
push = true
|
||||
|
||||
[run.sandbox]
|
||||
provider = "docker"
|
||||
preserve = false
|
||||
|
|
@ -22,7 +33,6 @@ devcontainer = false
|
|||
image = "buildpack-deps:noble"
|
||||
memory_limit = "4GB"
|
||||
cpu_quota = 200000
|
||||
skip_clone = false
|
||||
|
||||
[cli.output]
|
||||
format = "text"
|
||||
|
|
|
|||
|
|
@ -30,9 +30,10 @@ pub use run::{
|
|||
GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer,
|
||||
InterviewsLayer, McpEntryLayer, ModelRefOrSplice, NotificationProviderLayer,
|
||||
NotificationRouteLayer, PrepareStep, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer,
|
||||
RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer,
|
||||
RunLayer, RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer,
|
||||
RunSandboxLayer, RunScmLayer, ScmGitHubLayer, StringOrSplice,
|
||||
RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer,
|
||||
RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer,
|
||||
RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, RunSandboxLayer, RunScmLayer,
|
||||
ScmGitHubLayer, StringOrSplice,
|
||||
};
|
||||
pub use server::{
|
||||
GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ pub struct RunLayer {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub checkpoint: Option<RunCheckpointLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub clone: Option<RunCloneLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub run_branch: Option<RunRunBranchLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta_branch: Option<RunMetaBranchLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sandbox: Option<RunSandboxLayer>,
|
||||
#[serde(default, skip_serializing_if = "MergeMap::is_empty")]
|
||||
pub notifications: MergeMap<NotificationRouteLayer>,
|
||||
|
|
@ -278,6 +284,34 @@ pub struct RunCheckpointLayer {
|
|||
pub exclude_globs: Vec<String>,
|
||||
}
|
||||
|
||||
/// `[run.clone]` — source workspace clone policy.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RunCloneLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// `[run.run_branch]` — Fabro-managed checkpoint branch policy.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RunRunBranchLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub push: Option<bool>,
|
||||
}
|
||||
|
||||
/// `[run.meta_branch]` — Fabro-managed checkpoint metadata branch policy.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RunMetaBranchLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub push: Option<bool>,
|
||||
}
|
||||
|
||||
/// `[run.sandbox]` — sandbox selection and execution-environment surface.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
|
@ -312,8 +346,6 @@ pub struct DockerSandboxLayer {
|
|||
pub cpu_quota: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "StickyMap::is_empty")]
|
||||
pub env_vars: StickyMap<InterpString>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub skip_clone: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
|
|
@ -328,8 +360,6 @@ pub struct DaytonaSandboxLayer {
|
|||
pub snapshot: Option<DaytonaSnapshotLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub network: Option<DaytonaNetworkLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub skip_clone: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -47,13 +47,14 @@ pub use layers::{
|
|||
ModelCostTable, ModelRefOrSplice, ModelSettings, NotificationProviderLayer,
|
||||
NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, PrepareStep, ProjectLayer,
|
||||
ProviderSettings, ReplaceMap, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer,
|
||||
RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer,
|
||||
RunLayer, RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer,
|
||||
RunSandboxLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer,
|
||||
ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer,
|
||||
ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
|
||||
ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer,
|
||||
SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer,
|
||||
RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer,
|
||||
RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer,
|
||||
RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, RunSandboxLayer, RunScmLayer,
|
||||
ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer,
|
||||
ServerIntegrationsLayer, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerLayer,
|
||||
ServerListenLayer, ServerLoggingLayer, ServerSchedulerLayer, ServerSlateDbLayer,
|
||||
ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer, StickyMap, StringOrSplice,
|
||||
WorkflowLayer,
|
||||
};
|
||||
pub(crate) use layers::{Combine, SettingsLayer};
|
||||
pub use logging::{resolve_log_destination, resolve_log_destination_with_env};
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ use fabro_types::settings::run::{
|
|||
ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DockerSettings, DockerfileSource,
|
||||
GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings,
|
||||
McpTransport, MergeStrategy, NotificationProviderSettings, NotificationRouteSettings,
|
||||
PullRequestSettings, RunAgentSettings, RunCheckpointSettings, RunExecutionSettings,
|
||||
RunGitSettings, RunGoal, RunIntegrationsGithubSettings, RunIntegrationsSettings,
|
||||
RunInterviewsSettings, RunModelControls, RunModelSettings, RunNamespace, RunPrepareSettings,
|
||||
RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode,
|
||||
PullRequestSettings, RunAgentSettings, RunBranchSettings, RunCheckpointSettings,
|
||||
RunCloneSettings, RunExecutionSettings, RunGitSettings, RunGoal, RunIntegrationsGithubSettings,
|
||||
RunIntegrationsSettings, RunInterviewsSettings, RunMetaBranchSettings, RunModelControls,
|
||||
RunModelSettings, RunNamespace, RunPrepareSettings, RunSandboxSettings, RunScmSettings,
|
||||
ScmGitHubSettings, TlsMode,
|
||||
};
|
||||
|
||||
use super::ResolveError;
|
||||
|
|
@ -14,40 +15,68 @@ use crate::{
|
|||
DaytonaDockerfileLayer, DaytonaSandboxLayer, HookAgentMarker, HookEntry, HookTlsMode,
|
||||
InterviewProviderLayer, InterviewsLayer, McpEntryLayer, ModelRefOrSplice,
|
||||
NotificationProviderLayer, NotificationRouteLayer, RunAgentLayer, RunArtifactsLayer,
|
||||
RunCheckpointLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsLayer,
|
||||
RunLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunSandboxLayer, RunScmLayer,
|
||||
StringOrSplice,
|
||||
RunCheckpointLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer,
|
||||
RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelLayer, RunPrepareLayer,
|
||||
RunPullRequestLayer, RunRunBranchLayer, RunSandboxLayer, RunScmLayer, StringOrSplice,
|
||||
};
|
||||
|
||||
pub fn resolve_run(layer: &RunLayer, errors: &mut Vec<ResolveError>) -> RunNamespace {
|
||||
let clone = resolve_clone(layer.clone.as_ref());
|
||||
let run_branch = resolve_run_branch(layer.run_branch.as_ref());
|
||||
let mut meta_branch = resolve_meta_branch(layer.meta_branch.as_ref());
|
||||
if !run_branch.enabled {
|
||||
if meta_branch.enabled || meta_branch.push {
|
||||
tracing::debug!(
|
||||
run_branch_enabled = run_branch.enabled,
|
||||
"Disabling metadata branch because run branch is disabled"
|
||||
);
|
||||
}
|
||||
meta_branch = RunMetaBranchSettings {
|
||||
enabled: false,
|
||||
push: false,
|
||||
};
|
||||
}
|
||||
let pull_request = resolve_pull_request(layer.pull_request.as_ref());
|
||||
if pull_request.is_some() && (!run_branch.enabled || !run_branch.push) {
|
||||
errors.push(ResolveError::Invalid {
|
||||
path: "run.pull_request".to_string(),
|
||||
reason: "run.pull_request.enabled requires run.run_branch.enabled and \
|
||||
run.run_branch.push"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
RunNamespace {
|
||||
goal: resolve_goal(layer.goal.as_ref()),
|
||||
working_dir: layer.working_dir.clone(),
|
||||
metadata: layer.metadata.clone().into_inner(),
|
||||
inputs: layer.inputs.clone().unwrap_or_default(),
|
||||
model: resolve_model(layer.model.as_ref()),
|
||||
git: resolve_git(layer.git.as_ref()),
|
||||
prepare: resolve_prepare(layer.prepare.as_ref(), errors),
|
||||
execution: resolve_execution(layer.execution.as_ref()),
|
||||
checkpoint: resolve_checkpoint(layer.checkpoint.as_ref()),
|
||||
sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors),
|
||||
goal: resolve_goal(layer.goal.as_ref()),
|
||||
working_dir: layer.working_dir.clone(),
|
||||
metadata: layer.metadata.clone().into_inner(),
|
||||
inputs: layer.inputs.clone().unwrap_or_default(),
|
||||
model: resolve_model(layer.model.as_ref()),
|
||||
git: resolve_git(layer.git.as_ref()),
|
||||
prepare: resolve_prepare(layer.prepare.as_ref(), errors),
|
||||
execution: resolve_execution(layer.execution.as_ref()),
|
||||
checkpoint: resolve_checkpoint(layer.checkpoint.as_ref()),
|
||||
clone,
|
||||
run_branch,
|
||||
meta_branch,
|
||||
sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors),
|
||||
notifications: layer
|
||||
.notifications
|
||||
.iter()
|
||||
.map(|(name, route)| (name.clone(), resolve_notification_route(route)))
|
||||
.collect(),
|
||||
interviews: resolve_interviews(layer.interviews.as_ref()),
|
||||
agent: resolve_agent(layer.agent.as_ref()),
|
||||
hooks: layer
|
||||
interviews: resolve_interviews(layer.interviews.as_ref()),
|
||||
agent: resolve_agent(layer.agent.as_ref()),
|
||||
hooks: layer
|
||||
.hooks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, hook)| resolve_hook(hook, index, errors))
|
||||
.collect(),
|
||||
scm: resolve_scm(layer.scm.as_ref()),
|
||||
pull_request: resolve_pull_request(layer.pull_request.as_ref()),
|
||||
artifacts: resolve_artifacts(layer.artifacts.as_ref()),
|
||||
integrations: resolve_integrations(layer.integrations.as_ref()),
|
||||
scm: resolve_scm(layer.scm.as_ref()),
|
||||
pull_request,
|
||||
artifacts: resolve_artifacts(layer.artifacts.as_ref()),
|
||||
integrations: resolve_integrations(layer.integrations.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,6 +190,34 @@ fn resolve_checkpoint(checkpoint: Option<&RunCheckpointLayer>) -> RunCheckpointS
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_clone(clone: Option<&RunCloneLayer>) -> RunCloneSettings {
|
||||
RunCloneSettings {
|
||||
enabled: clone.and_then(|clone| clone.enabled).unwrap_or(true),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_run_branch(run_branch: Option<&RunRunBranchLayer>) -> RunBranchSettings {
|
||||
RunBranchSettings {
|
||||
enabled: run_branch
|
||||
.and_then(|run_branch| run_branch.enabled)
|
||||
.unwrap_or(true),
|
||||
push: run_branch
|
||||
.and_then(|run_branch| run_branch.push)
|
||||
.unwrap_or(true),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_meta_branch(meta_branch: Option<&RunMetaBranchLayer>) -> RunMetaBranchSettings {
|
||||
RunMetaBranchSettings {
|
||||
enabled: meta_branch
|
||||
.and_then(|meta_branch| meta_branch.enabled)
|
||||
.unwrap_or(true),
|
||||
push: meta_branch
|
||||
.and_then(|meta_branch| meta_branch.push)
|
||||
.unwrap_or(true),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_sandbox(
|
||||
sandbox: Option<&RunSandboxLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
|
|
@ -205,7 +262,6 @@ fn resolve_docker(docker: &crate::DockerSandboxLayer) -> DockerSettings {
|
|||
.and_then(|size| i64::try_from(size.as_bytes()).ok()),
|
||||
cpu_quota: docker.cpu_quota,
|
||||
env_vars: docker.env_vars.clone().into_inner(),
|
||||
skip_clone: docker.skip_clone.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +289,6 @@ fn resolve_daytona(daytona: &DaytonaSandboxLayer) -> DaytonaSettings {
|
|||
})
|
||||
}),
|
||||
network: daytona.network.clone(),
|
||||
skip_clone: daytona.skip_clone.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,10 +53,120 @@ fn resolves_run_defaults_from_empty_settings() {
|
|||
assert_eq!(docker.image, "buildpack-deps:noble");
|
||||
assert_eq!(docker.memory_limit, Some(4_000_000_000));
|
||||
assert_eq!(docker.cpu_quota, Some(200_000));
|
||||
assert!(!docker.skip_clone);
|
||||
assert!(settings.clone.enabled);
|
||||
assert!(settings.run_branch.enabled);
|
||||
assert!(settings.run_branch.push);
|
||||
assert!(settings.meta_branch.enabled);
|
||||
assert!(settings.meta_branch.push);
|
||||
assert!(settings.pull_request.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_run_level_clone_branch_controls() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[run.clone]
|
||||
enabled = false
|
||||
|
||||
[run.run_branch]
|
||||
enabled = true
|
||||
push = false
|
||||
|
||||
[run.meta_branch]
|
||||
enabled = true
|
||||
push = false
|
||||
",
|
||||
)
|
||||
.expect("run branch controls should resolve")
|
||||
.run;
|
||||
|
||||
assert!(!settings.clone.enabled);
|
||||
assert!(settings.run_branch.enabled);
|
||||
assert!(!settings.run_branch.push);
|
||||
assert!(settings.meta_branch.enabled);
|
||||
assert!(!settings.meta_branch.push);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabling_run_branch_forces_meta_branch_off() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[run.run_branch]
|
||||
enabled = false
|
||||
|
||||
[run.meta_branch]
|
||||
enabled = true
|
||||
push = true
|
||||
",
|
||||
)
|
||||
.expect("run branch disabled should resolve")
|
||||
.run;
|
||||
|
||||
assert!(!settings.run_branch.enabled);
|
||||
assert!(!settings.meta_branch.enabled);
|
||||
assert!(!settings.meta_branch.push);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_requires_pushed_run_branch() {
|
||||
let disabled_branch = WorkflowSettingsBuilder::from_toml(
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[run.run_branch]
|
||||
enabled = false
|
||||
|
||||
[run.pull_request]
|
||||
enabled = true
|
||||
",
|
||||
)
|
||||
.expect_err("pull requests require an enabled pushed run branch");
|
||||
let message = disabled_branch.to_string();
|
||||
assert!(
|
||||
message.contains("run.pull_request.enabled requires run.run_branch.enabled"),
|
||||
"expected run branch validation error, got: {message}"
|
||||
);
|
||||
|
||||
let disabled_push = WorkflowSettingsBuilder::from_toml(
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[run.run_branch]
|
||||
push = false
|
||||
|
||||
[run.pull_request]
|
||||
enabled = true
|
||||
",
|
||||
)
|
||||
.expect_err("pull requests require run branch push");
|
||||
let message = disabled_push.to_string();
|
||||
assert!(
|
||||
message.contains("run.pull_request.enabled requires run.run_branch.enabled"),
|
||||
"expected run branch push validation error, got: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_skip_clone_is_rejected() {
|
||||
let err = r"
|
||||
_version = 1
|
||||
|
||||
[run.sandbox.docker]
|
||||
skip_clone = true
|
||||
"
|
||||
.parse::<SettingsLayer>()
|
||||
.expect_err("provider-level skip_clone should be unknown");
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("skip_clone") || message.contains("unknown field"),
|
||||
"expected unknown-field error mentioning skip_clone, got: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_run_chat_surfaces_are_slack_only() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
|
|
|
|||
|
|
@ -1675,7 +1675,6 @@ mod runs {
|
|||
dockerfile: None,
|
||||
}),
|
||||
network: None,
|
||||
skip_clone: false,
|
||||
}),
|
||||
},
|
||||
..RunNamespace::default()
|
||||
|
|
|
|||
|
|
@ -564,16 +564,26 @@ fn resolve_sandbox_provider(settings: &RunNamespace) -> Result<SandboxProvider>
|
|||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
fn resolve_daytona_config(settings: &RunNamespace) -> Option<DaytonaConfig> {
|
||||
settings
|
||||
fn resolve_daytona_config(settings: &RunNamespace) -> DaytonaConfig {
|
||||
let mut config = settings
|
||||
.sandbox
|
||||
.daytona
|
||||
.as_ref()
|
||||
.map(runtime_daytona_config)
|
||||
.map(|daytona| runtime_daytona_config(daytona, !settings.clone.enabled))
|
||||
.unwrap_or_default();
|
||||
config.skip_clone = !settings.clone.enabled;
|
||||
config
|
||||
}
|
||||
|
||||
fn resolve_docker_config(settings: &RunNamespace) -> Option<DockerSandboxOptions> {
|
||||
settings.sandbox.docker.as_ref().map(runtime_docker_config)
|
||||
fn resolve_docker_config(settings: &RunNamespace) -> DockerSandboxOptions {
|
||||
let mut config = settings
|
||||
.sandbox
|
||||
.docker
|
||||
.as_ref()
|
||||
.map(|docker| runtime_docker_config(docker, !settings.clone.enabled))
|
||||
.unwrap_or_default();
|
||||
config.skip_clone = !settings.clone.enabled;
|
||||
config
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -584,16 +594,7 @@ struct GitRemoteRefCheck {
|
|||
|
||||
fn clone_disabled_for_provider(provider: SandboxProvider, resolved_run: &RunNamespace) -> bool {
|
||||
match provider {
|
||||
SandboxProvider::Docker => resolved_run
|
||||
.sandbox
|
||||
.docker
|
||||
.as_ref()
|
||||
.is_some_and(|docker| docker.skip_clone),
|
||||
SandboxProvider::Daytona => resolved_run
|
||||
.sandbox
|
||||
.daytona
|
||||
.as_ref()
|
||||
.is_some_and(|daytona| daytona.skip_clone),
|
||||
SandboxProvider::Docker | SandboxProvider::Daytona => !resolved_run.clone.enabled,
|
||||
SandboxProvider::Local => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -759,7 +760,7 @@ fn preflight_sandbox_spec(
|
|||
working_directory: prepared.source_directory.clone(),
|
||||
},
|
||||
SandboxProvider::Docker => {
|
||||
let mut config = resolve_docker_config(resolved_run).unwrap_or_default();
|
||||
let mut config = resolve_docker_config(resolved_run);
|
||||
config.skip_clone = true;
|
||||
SandboxSpec::Docker {
|
||||
config,
|
||||
|
|
@ -770,7 +771,7 @@ fn preflight_sandbox_spec(
|
|||
}
|
||||
}
|
||||
SandboxProvider::Daytona => {
|
||||
let mut config = resolve_daytona_config(resolved_run).unwrap_or_default();
|
||||
let mut config = resolve_daytona_config(resolved_run);
|
||||
config.skip_clone = true;
|
||||
SandboxSpec::Daytona {
|
||||
config: Box::new(config),
|
||||
|
|
@ -1083,11 +1084,11 @@ fn resolve_model_provider(
|
|||
}
|
||||
}
|
||||
|
||||
fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig {
|
||||
fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> DaytonaConfig {
|
||||
DaytonaConfig {
|
||||
auto_stop_interval: settings.auto_stop_interval,
|
||||
labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()),
|
||||
snapshot: settings
|
||||
labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()),
|
||||
snapshot: settings
|
||||
.snapshot
|
||||
.as_ref()
|
||||
.map(|snapshot| DaytonaSnapshotSettings {
|
||||
|
|
@ -1107,18 +1108,18 @@ fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig {
|
|||
}
|
||||
}),
|
||||
}),
|
||||
network: settings.network.as_ref().map(|network| match network {
|
||||
network: settings.network.as_ref().map(|network| match network {
|
||||
DaytonaNetworkLayer::Block => DaytonaNetwork::Block,
|
||||
DaytonaNetworkLayer::AllowAll => DaytonaNetwork::AllowAll,
|
||||
DaytonaNetworkLayer::AllowList { allow_list } => {
|
||||
DaytonaNetwork::AllowList(allow_list.clone())
|
||||
}
|
||||
}),
|
||||
skip_clone: settings.skip_clone,
|
||||
skip_clone,
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_docker_config(settings: &DockerSettings) -> DockerSandboxOptions {
|
||||
fn runtime_docker_config(settings: &DockerSettings, skip_clone: bool) -> DockerSandboxOptions {
|
||||
let mut env_vars = settings
|
||||
.env_vars
|
||||
.iter()
|
||||
|
|
@ -1132,7 +1133,7 @@ fn runtime_docker_config(settings: &DockerSettings) -> DockerSandboxOptions {
|
|||
memory_limit: settings.memory_limit,
|
||||
cpu_quota: settings.cpu_quota,
|
||||
env_vars,
|
||||
skip_clone: settings.skip_clone,
|
||||
skip_clone,
|
||||
..DockerSandboxOptions::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -1377,7 +1378,7 @@ mod tests {
|
|||
|
||||
fn prepared_and_resolved_for_sandbox(
|
||||
provider: SandboxProvider,
|
||||
skip_clone: bool,
|
||||
clone_enabled: bool,
|
||||
git: Option<types::GitContext>,
|
||||
) -> (PreparedManifest, RunNamespace) {
|
||||
let mut manifest = minimal_manifest();
|
||||
|
|
@ -1391,8 +1392,8 @@ _version = 1
|
|||
[run.sandbox]
|
||||
provider = "{provider}"
|
||||
|
||||
[run.sandbox.{provider}]
|
||||
skip_clone = {skip_clone}
|
||||
[run.clone]
|
||||
enabled = {clone_enabled}
|
||||
"#
|
||||
)),
|
||||
type_: types::ManifestConfigType::Project,
|
||||
|
|
@ -1419,7 +1420,7 @@ skip_clone = {skip_clone}
|
|||
async fn repository_access_check_skips_when_clone_is_disabled() {
|
||||
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
|
||||
SandboxProvider::Docker,
|
||||
true,
|
||||
false,
|
||||
Some(git_context("https://github.com/acme/widgets", "main")),
|
||||
);
|
||||
let calls = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
|
|
@ -1448,7 +1449,7 @@ skip_clone = {skip_clone}
|
|||
async fn repository_access_check_rejects_non_github_origins_before_remote_probe() {
|
||||
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
|
||||
SandboxProvider::Docker,
|
||||
false,
|
||||
true,
|
||||
Some(git_context("https://gitlab.com/acme/widgets", "main")),
|
||||
);
|
||||
let calls = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
|
|
@ -1486,7 +1487,7 @@ skip_clone = {skip_clone}
|
|||
async fn repository_access_check_probes_normalized_github_branch() {
|
||||
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
|
||||
SandboxProvider::Docker,
|
||||
false,
|
||||
true,
|
||||
Some(git_context(
|
||||
"git@github.com:acme/widgets.git",
|
||||
"feature/demo",
|
||||
|
|
@ -1523,7 +1524,7 @@ skip_clone = {skip_clone}
|
|||
async fn repository_access_check_surfaces_remote_probe_failure() {
|
||||
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
|
||||
SandboxProvider::Docker,
|
||||
false,
|
||||
true,
|
||||
Some(git_context("https://github.com/acme/widgets", "missing")),
|
||||
);
|
||||
let mut checks = Vec::new();
|
||||
|
|
@ -1555,7 +1556,7 @@ skip_clone = {skip_clone}
|
|||
fn preflight_sandbox_spec_disables_docker_clone_but_preserves_clone_metadata() {
|
||||
let (prepared, resolved) = prepared_and_resolved_for_sandbox(
|
||||
SandboxProvider::Docker,
|
||||
false,
|
||||
true,
|
||||
Some(git_context("https://github.com/acme/widgets", "main")),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use super::interp::InterpString;
|
|||
use super::model_ref::ModelRef;
|
||||
|
||||
/// A structurally resolved `[run]` view for consumers.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunNamespace {
|
||||
pub goal: Option<RunGoal>,
|
||||
pub working_dir: Option<InterpString>,
|
||||
|
|
@ -28,6 +28,9 @@ pub struct RunNamespace {
|
|||
pub prepare: RunPrepareSettings,
|
||||
pub execution: RunExecutionSettings,
|
||||
pub checkpoint: RunCheckpointSettings,
|
||||
pub clone: RunCloneSettings,
|
||||
pub run_branch: RunBranchSettings,
|
||||
pub meta_branch: RunMetaBranchSettings,
|
||||
pub sandbox: RunSandboxSettings,
|
||||
pub notifications: HashMap<String, NotificationRouteSettings>,
|
||||
pub interviews: RunInterviewsSettings,
|
||||
|
|
@ -39,6 +42,38 @@ pub struct RunNamespace {
|
|||
pub integrations: RunIntegrationsSettings,
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::derivable_impls,
|
||||
reason = "run defaults are product policy; keep the true-valued branch defaults visible here"
|
||||
)]
|
||||
impl Default for RunNamespace {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
goal: None,
|
||||
working_dir: None,
|
||||
metadata: HashMap::new(),
|
||||
inputs: HashMap::new(),
|
||||
model: RunModelSettings::default(),
|
||||
git: RunGitSettings::default(),
|
||||
prepare: RunPrepareSettings::default(),
|
||||
execution: RunExecutionSettings::default(),
|
||||
checkpoint: RunCheckpointSettings::default(),
|
||||
clone: RunCloneSettings::default(),
|
||||
run_branch: RunBranchSettings::default(),
|
||||
meta_branch: RunMetaBranchSettings::default(),
|
||||
sandbox: RunSandboxSettings::default(),
|
||||
notifications: HashMap::new(),
|
||||
interviews: RunInterviewsSettings::default(),
|
||||
agent: RunAgentSettings::default(),
|
||||
hooks: Vec::new(),
|
||||
scm: RunScmSettings::default(),
|
||||
pull_request: None,
|
||||
artifacts: ArtifactsSettings::default(),
|
||||
integrations: RunIntegrationsSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `[run.integrations]` — run-level integration knobs.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunIntegrationsSettings {
|
||||
|
|
@ -205,6 +240,47 @@ pub struct RunCheckpointSettings {
|
|||
pub exclude_globs: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunCloneSettings {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for RunCloneSettings {
|
||||
fn default() -> Self {
|
||||
Self { enabled: true }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunBranchSettings {
|
||||
pub enabled: bool,
|
||||
pub push: bool,
|
||||
}
|
||||
|
||||
impl Default for RunBranchSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
push: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunMetaBranchSettings {
|
||||
pub enabled: bool,
|
||||
pub push: bool,
|
||||
}
|
||||
|
||||
impl Default for RunMetaBranchSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
push: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunSandboxSettings {
|
||||
pub provider: String,
|
||||
|
|
@ -242,7 +318,6 @@ pub struct DockerSettings {
|
|||
pub memory_limit: Option<i64>,
|
||||
pub cpu_quota: Option<i64>,
|
||||
pub env_vars: HashMap<String, InterpString>,
|
||||
pub skip_clone: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -251,7 +326,6 @@ pub struct DaytonaSettings {
|
|||
pub labels: HashMap<String, String>,
|
||||
pub snapshot: Option<DaytonaSnapshotSettings>,
|
||||
pub network: Option<DaytonaNetworkLayer>,
|
||||
pub skip_clone: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -290,7 +290,9 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
|
|||
};
|
||||
|
||||
// Push run branch (skip in dry-run mode)
|
||||
if !self.run_options.dry_run_enabled() {
|
||||
if !self.run_options.dry_run_enabled()
|
||||
&& self.run_options.settings.run.run_branch.push
|
||||
{
|
||||
if let Some(branch) = self
|
||||
.run_options
|
||||
.git
|
||||
|
|
@ -1095,6 +1097,51 @@ mod tests {
|
|||
assert_eq!(diff_summary.deletions, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkpoint_git_result_omits_push_when_run_branch_push_disabled() {
|
||||
let repo_dir = tempfile::tempdir().unwrap();
|
||||
let repo = repo_dir.path();
|
||||
init_git_repo(repo);
|
||||
tokio::fs::write(repo.join("notes.txt"), "checkpoint\n")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut options = run_options(repo, "fabro/metadata/run").as_ref().clone();
|
||||
options.settings.run.run_branch.push = false;
|
||||
options.git = Some(GitCheckpointOptions {
|
||||
base_sha: None,
|
||||
run_branch: Some("fabro/run/test".to_string()),
|
||||
meta_branch: None,
|
||||
});
|
||||
let lifecycle = git_lifecycle_with_writer(
|
||||
repo,
|
||||
Arc::new(Emitter::new(fixtures::RUN_1)),
|
||||
RunStoreHandle::local(run_store(fixtures::RUN_1).await),
|
||||
Arc::new(options),
|
||||
Arc::new(RunMetadataRuntime::new()),
|
||||
None,
|
||||
);
|
||||
let graph = workflow_graph();
|
||||
let node = graph.get_node("build").unwrap();
|
||||
let mut state = ExecutionState::new(&graph).unwrap();
|
||||
state.increment_visits("build");
|
||||
let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1);
|
||||
|
||||
lifecycle
|
||||
.on_checkpoint(&node, &result, Some("exit"), &state)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let git_result = lifecycle
|
||||
.checkpoint_git_result
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap();
|
||||
assert!(git_result.commit_sha.is_some());
|
||||
assert!(git_result.push_results.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn degraded_metadata_runtime_skips_snapshot_events() {
|
||||
let repo_dir = tempfile::tempdir().unwrap();
|
||||
|
|
|
|||
|
|
@ -287,13 +287,7 @@ impl RunSession {
|
|||
.state()
|
||||
.await
|
||||
.map_err(|err| Error::engine(err.to_string()))?;
|
||||
let git = state.start.and_then(|start| {
|
||||
start.run_branch.as_ref().map(|_| GitCheckpointOptions {
|
||||
base_sha: start.base_sha.clone(),
|
||||
run_branch: start.run_branch.clone(),
|
||||
meta_branch: Some(metadata_branch_name(&record.run_id.to_string())),
|
||||
})
|
||||
});
|
||||
let git = git_checkpoint_options_from_start(settings, &record.run_id, state.start);
|
||||
let definition_blob = state.spec.definition_blob;
|
||||
let accepted_definition = match definition_blob {
|
||||
Some(blob_id) => {
|
||||
|
|
@ -353,7 +347,7 @@ impl RunSession {
|
|||
working_directory: working_directory.clone(),
|
||||
},
|
||||
SandboxProvider::Docker => SandboxSpec::Docker {
|
||||
config: resolve_docker_config(resolved).unwrap_or_default(),
|
||||
config: resolve_docker_config(resolved),
|
||||
github_app: services.github_app.clone(),
|
||||
run_id: Some(record.run_id),
|
||||
clone_origin_url: record.repo_origin_url().map(str::to_string),
|
||||
|
|
@ -369,7 +363,7 @@ impl RunSession {
|
|||
None => None,
|
||||
};
|
||||
SandboxSpec::Daytona {
|
||||
config: Box::new(resolve_daytona_config(resolved).unwrap_or_default()),
|
||||
config: Box::new(resolve_daytona_config(resolved)),
|
||||
github_app: services.github_app.clone(),
|
||||
run_id: Some(record.run_id),
|
||||
clone_origin_url: record.repo_origin_url().map(str::to_string),
|
||||
|
|
@ -459,6 +453,27 @@ fn resolve_interp(value: &InterpString) -> String {
|
|||
.map_or_else(|_| value.as_source(), |resolved| resolved.value)
|
||||
}
|
||||
|
||||
fn git_checkpoint_options_from_start(
|
||||
settings: &fabro_types::WorkflowSettings,
|
||||
run_id: &RunId,
|
||||
start: Option<fabro_types::StartRecord>,
|
||||
) -> Option<GitCheckpointOptions> {
|
||||
if !settings.run.run_branch.enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start = start?;
|
||||
start.run_branch.as_ref().map(|_| GitCheckpointOptions {
|
||||
base_sha: start.base_sha.clone(),
|
||||
run_branch: start.run_branch.clone(),
|
||||
meta_branch: settings
|
||||
.run
|
||||
.meta_branch
|
||||
.enabled
|
||||
.then(|| metadata_branch_name(&run_id.to_string())),
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "Run startup interpolation owns a process-env lookup facade for {{ env.* }} values."
|
||||
|
|
@ -492,16 +507,26 @@ fn resolve_sandbox_provider(settings: &ResolvedRunSettings) -> Result<SandboxPro
|
|||
.map_or_else(|| Ok(SandboxProvider::default()), Ok)
|
||||
}
|
||||
|
||||
fn resolve_daytona_config(settings: &ResolvedRunSettings) -> Option<DaytonaConfig> {
|
||||
settings
|
||||
fn resolve_daytona_config(settings: &ResolvedRunSettings) -> DaytonaConfig {
|
||||
let mut config = settings
|
||||
.sandbox
|
||||
.daytona
|
||||
.as_ref()
|
||||
.map(runtime_daytona_config)
|
||||
.map(|daytona| runtime_daytona_config(daytona, !settings.clone.enabled))
|
||||
.unwrap_or_default();
|
||||
config.skip_clone = !settings.clone.enabled;
|
||||
config
|
||||
}
|
||||
|
||||
fn resolve_docker_config(settings: &ResolvedRunSettings) -> Option<DockerSandboxOptions> {
|
||||
settings.sandbox.docker.as_ref().map(runtime_docker_config)
|
||||
fn resolve_docker_config(settings: &ResolvedRunSettings) -> DockerSandboxOptions {
|
||||
let mut config = settings
|
||||
.sandbox
|
||||
.docker
|
||||
.as_ref()
|
||||
.map(|docker| runtime_docker_config(docker, !settings.clone.enabled))
|
||||
.unwrap_or_default();
|
||||
config.skip_clone = !settings.clone.enabled;
|
||||
config
|
||||
}
|
||||
|
||||
fn resolve_fallback_chain(
|
||||
|
|
@ -554,11 +579,11 @@ fn runtime_mcp_server(settings: &ResolvedMcpServerSettings) -> McpServerSettings
|
|||
}
|
||||
}
|
||||
|
||||
fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig {
|
||||
fn runtime_daytona_config(settings: &DaytonaSettings, skip_clone: bool) -> DaytonaConfig {
|
||||
DaytonaConfig {
|
||||
auto_stop_interval: settings.auto_stop_interval,
|
||||
labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()),
|
||||
snapshot: settings
|
||||
labels: (!settings.labels.is_empty()).then_some(settings.labels.clone()),
|
||||
snapshot: settings
|
||||
.snapshot
|
||||
.as_ref()
|
||||
.map(|snapshot| DaytonaSnapshotSettings {
|
||||
|
|
@ -578,18 +603,18 @@ fn runtime_daytona_config(settings: &DaytonaSettings) -> DaytonaConfig {
|
|||
}
|
||||
}),
|
||||
}),
|
||||
network: settings.network.as_ref().map(|network| match network {
|
||||
network: settings.network.as_ref().map(|network| match network {
|
||||
DaytonaNetworkLayer::Block => DaytonaNetwork::Block,
|
||||
DaytonaNetworkLayer::AllowAll => DaytonaNetwork::AllowAll,
|
||||
DaytonaNetworkLayer::AllowList { allow_list } => {
|
||||
DaytonaNetwork::AllowList(allow_list.clone())
|
||||
}
|
||||
}),
|
||||
skip_clone: settings.skip_clone,
|
||||
skip_clone,
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_docker_config(settings: &DockerSettings) -> DockerSandboxOptions {
|
||||
fn runtime_docker_config(settings: &DockerSettings, skip_clone: bool) -> DockerSandboxOptions {
|
||||
let mut env_vars = settings
|
||||
.env_vars
|
||||
.iter()
|
||||
|
|
@ -603,7 +628,7 @@ fn runtime_docker_config(settings: &DockerSettings) -> DockerSandboxOptions {
|
|||
memory_limit: settings.memory_limit,
|
||||
cpu_quota: settings.cpu_quota,
|
||||
env_vars,
|
||||
skip_clone: settings.skip_clone,
|
||||
skip_clone,
|
||||
..DockerSandboxOptions::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -992,7 +1017,7 @@ mod tests {
|
|||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_config::{RunExecutionLayer, RunLayer, WorkflowSettingsBuilder};
|
||||
use fabro_config::{RunCloneLayer, RunExecutionLayer, RunLayer, WorkflowSettingsBuilder};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::settings::run::RunMode;
|
||||
use fabro_types::{WorkflowSettings, fixtures};
|
||||
|
|
@ -1043,6 +1068,52 @@ mod tests {
|
|||
.expect("settings should resolve")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_clone_config_uses_run_level_clone_policy() {
|
||||
let settings = settings_from_run_layer(RunLayer {
|
||||
clone: Some(RunCloneLayer {
|
||||
enabled: Some(false),
|
||||
}),
|
||||
..RunLayer::default()
|
||||
});
|
||||
|
||||
assert!(resolve_docker_config(&settings.run).skip_clone);
|
||||
assert!(resolve_daytona_config(&settings.run).skip_clone);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_record_git_options_honor_disabled_run_branch() {
|
||||
let mut settings = WorkflowSettings::default();
|
||||
settings.run.run_branch.enabled = false;
|
||||
let start = fabro_types::StartRecord {
|
||||
start_time: Utc::now(),
|
||||
run_branch: Some("fabro/run/test".to_string()),
|
||||
base_sha: Some("abc123".to_string()),
|
||||
};
|
||||
|
||||
assert!(
|
||||
git_checkpoint_options_from_start(&settings, &fixtures::RUN_1, Some(start)).is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_record_git_options_honor_disabled_meta_branch() {
|
||||
let mut settings = WorkflowSettings::default();
|
||||
settings.run.meta_branch.enabled = false;
|
||||
let start = fabro_types::StartRecord {
|
||||
start_time: Utc::now(),
|
||||
run_branch: Some("fabro/run/test".to_string()),
|
||||
base_sha: Some("abc123".to_string()),
|
||||
};
|
||||
|
||||
let git = git_checkpoint_options_from_start(&settings, &fixtures::RUN_1, Some(start))
|
||||
.expect("run branch should remain enabled");
|
||||
|
||||
assert_eq!(git.run_branch.as_deref(), Some("fabro/run/test"));
|
||||
assert_eq!(git.base_sha.as_deref(), Some("abc123"));
|
||||
assert_eq!(git.meta_branch, None);
|
||||
}
|
||||
|
||||
async fn persisted_workflow(dot: &str, storage_root: &Path) -> (Persisted, Arc<Database>) {
|
||||
let store = memory_store();
|
||||
let created = crate::operations::create(
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ pub async fn initialize(
|
|||
.as_ref()
|
||||
.and_then(|g| g.run_branch.as_ref())
|
||||
.is_some();
|
||||
if !has_run_branch {
|
||||
if options.run_options.settings.run.run_branch.enabled && !has_run_branch {
|
||||
let intent = git_setup_intent(&options.run_options);
|
||||
let sandbox_has_origin = sandbox.origin_url().is_some();
|
||||
if sandbox_has_origin {
|
||||
|
|
@ -540,9 +540,13 @@ pub async fn initialize(
|
|||
options.run_options.git = Some(GitCheckpointOptions {
|
||||
base_sha,
|
||||
run_branch: Some(info.run_branch.clone()),
|
||||
meta_branch: Some(metadata_branch_name(
|
||||
&options.run_options.run_id.to_string(),
|
||||
)),
|
||||
meta_branch: options
|
||||
.run_options
|
||||
.settings
|
||||
.run
|
||||
.meta_branch
|
||||
.enabled
|
||||
.then(|| metadata_branch_name(&options.run_options.run_id.to_string())),
|
||||
});
|
||||
if options.run_options.base_branch.is_none() {
|
||||
options.run_options.base_branch = info.base_branch;
|
||||
|
|
|
|||
|
|
@ -634,7 +634,13 @@ pub async fn pull_request(concluded: Concluded, options: &PullRequestOptions) ->
|
|||
run_id: run_options.run_id,
|
||||
outcome,
|
||||
conclusion,
|
||||
pushed_branch: run_options.run_branch().map(str::to_string),
|
||||
pushed_branch: run_options
|
||||
.settings
|
||||
.run
|
||||
.run_branch
|
||||
.push
|
||||
.then(|| run_options.run_branch().map(str::to_string))
|
||||
.flatten(),
|
||||
pr_url,
|
||||
}
|
||||
}
|
||||
|
|
@ -665,10 +671,14 @@ mod tests {
|
|||
use httpmock::MockServer;
|
||||
use object_store::memory::InMemory;
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::*;
|
||||
use crate::event::{Event, append_event};
|
||||
use crate::outcome::Outcome;
|
||||
use crate::records::StageSummary;
|
||||
use crate::run_options::{GitCheckpointOptions, RunOptions};
|
||||
use crate::services::EngineServices;
|
||||
|
||||
struct MockProvider {
|
||||
name: String,
|
||||
|
|
@ -869,6 +879,48 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pull_request_omits_pushed_branch_when_run_branch_push_disabled() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let mut settings = WorkflowSettings::default();
|
||||
settings.run.run_branch.push = false;
|
||||
let run_options = RunOptions {
|
||||
settings,
|
||||
run_dir: temp.path().to_path_buf(),
|
||||
cancel_token: CancellationToken::new(),
|
||||
run_id: fixtures::RUN_1,
|
||||
labels: HashMap::new(),
|
||||
workflow_slug: None,
|
||||
github_app: None,
|
||||
pre_run_git: None,
|
||||
fork_source_ref: None,
|
||||
base_branch: None,
|
||||
display_base_sha: None,
|
||||
git: Some(GitCheckpointOptions {
|
||||
base_sha: None,
|
||||
run_branch: Some("fabro/run/test".to_string()),
|
||||
meta_branch: None,
|
||||
}),
|
||||
};
|
||||
let concluded = Concluded {
|
||||
outcome: Ok(Outcome::success()),
|
||||
conclusion: make_test_conclusion(),
|
||||
graph: Graph::new("test"),
|
||||
run_options,
|
||||
services: EngineServices::test_default().run,
|
||||
};
|
||||
|
||||
let finalized = pull_request(concluded, &PullRequestOptions {
|
||||
pr_config: None,
|
||||
github_app: None,
|
||||
origin_url: None,
|
||||
model: "test-model".to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(finalized.pushed_branch, None);
|
||||
}
|
||||
|
||||
// ── format_arc_details_section tests ────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ impl RunMetadataWriterHandle {
|
|||
author: GitAuthor,
|
||||
fetch_depth: Option<i32>,
|
||||
) -> Result<Self, RunMetadataError> {
|
||||
let writer = RunMetadataWriter::new(remote_url, branch, author, fetch_depth)?;
|
||||
let writer = RunMetadataWriter::new(remote_url, branch, author, fetch_depth, true)?;
|
||||
Ok(Self::new(writer, Arc::new(NoAuth)))
|
||||
}
|
||||
|
||||
|
|
@ -208,6 +208,9 @@ impl RunMetadataWriterHandle {
|
|||
pub(crate) fn build_metadata_writer(
|
||||
run_options: &RunOptions,
|
||||
) -> Result<Option<RunMetadataWriterHandle>, RunMetadataError> {
|
||||
if !run_options.settings.run.meta_branch.enabled {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(git) = run_options.pre_run_git.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
|
@ -239,6 +242,7 @@ pub(crate) fn build_metadata_writer(
|
|||
meta_branch.clone(),
|
||||
run_options.git_author(),
|
||||
Some(1),
|
||||
run_options.settings.run.meta_branch.push,
|
||||
)?;
|
||||
Ok(Some(RunMetadataWriterHandle::new(writer, auth)))
|
||||
}
|
||||
|
|
@ -266,14 +270,15 @@ pub(crate) async fn mint_token(
|
|||
}
|
||||
|
||||
pub(crate) struct RunMetadataWriter {
|
||||
store: Store,
|
||||
tempdir: tempfile::TempDir,
|
||||
remote_url: String,
|
||||
branch: String,
|
||||
author: GitAuthor,
|
||||
fetch_depth: Option<i32>,
|
||||
parent_oid: Option<Oid>,
|
||||
discovered: bool,
|
||||
store: Store,
|
||||
tempdir: tempfile::TempDir,
|
||||
remote_url: String,
|
||||
branch: String,
|
||||
author: GitAuthor,
|
||||
fetch_depth: Option<i32>,
|
||||
push_enabled: bool,
|
||||
parent_oid: Option<Oid>,
|
||||
discovered: bool,
|
||||
}
|
||||
|
||||
impl RunMetadataWriter {
|
||||
|
|
@ -282,6 +287,7 @@ impl RunMetadataWriter {
|
|||
branch: String,
|
||||
author: GitAuthor,
|
||||
fetch_depth: Option<i32>,
|
||||
push_enabled: bool,
|
||||
) -> Result<Self, RunMetadataError> {
|
||||
let tempdir = tempfile::tempdir()
|
||||
.map_err(|_| RunMetadataError::Init("failed to create writer tempdir".to_string()))?;
|
||||
|
|
@ -298,6 +304,7 @@ impl RunMetadataWriter {
|
|||
branch,
|
||||
author,
|
||||
fetch_depth,
|
||||
push_enabled,
|
||||
parent_oid: None,
|
||||
discovered: false,
|
||||
})
|
||||
|
|
@ -341,7 +348,11 @@ impl RunMetadataWriter {
|
|||
.map_err(|err| RunMetadataError::Commit(self.redact_checkpoint(err)))?;
|
||||
self.parent_oid = Some(commit_oid);
|
||||
|
||||
let push_error = self.push(token).err();
|
||||
let push_error = if self.push_enabled {
|
||||
self.push(token).err()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(MetadataSnapshot {
|
||||
commit_sha: commit_oid.to_string(),
|
||||
push_error,
|
||||
|
|
@ -689,6 +700,7 @@ mod tests {
|
|||
branch.to_string(),
|
||||
GitAuthor::default(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let handle = RunMetadataWriterHandle::new(writer, Arc::new(NoAuth));
|
||||
|
|
@ -838,6 +850,34 @@ mod tests {
|
|||
assert!(push_error.contains("non-bare repos"), "{push_error}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn metadata_writer_can_commit_without_pushing() {
|
||||
let remote = tempfile::tempdir().unwrap();
|
||||
init_git_repo(remote.path());
|
||||
let branch_before = run_git(remote.path(), &["rev-parse", "main"]);
|
||||
let writer = RunMetadataWriter::new(
|
||||
file_url(remote.path()),
|
||||
"main".to_string(),
|
||||
GitAuthor::default(),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
let handle = RunMetadataWriterHandle::new(writer, Arc::new(NoAuth));
|
||||
|
||||
let snapshot = handle
|
||||
.write_snapshot(&metadata_dump(), "checkpoint")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!snapshot.commit_sha.is_empty());
|
||||
assert_eq!(snapshot.push_error, None);
|
||||
assert_eq!(
|
||||
run_git(remote.path(), &["rev-parse", "main"]),
|
||||
branch_before
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
|
|
@ -983,4 +1023,12 @@ mod tests {
|
|||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_writer_factory_skips_disabled_meta_branch() {
|
||||
let mut options = run_options_for_origin("https://github.com/owner/repo.git");
|
||||
options.settings.run.meta_branch.enabled = false;
|
||||
|
||||
assert!(build_metadata_writer(&options).unwrap().is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,9 +238,11 @@ models/run-billing-stage.ts
|
|||
models/run-billing-summary.ts
|
||||
models/run-billing-totals.ts
|
||||
models/run-billing.ts
|
||||
models/run-branch-settings.ts
|
||||
models/run-checkpoint-settings.ts
|
||||
models/run-checkpoint.ts
|
||||
models/run-client-provenance.ts
|
||||
models/run-clone-settings.ts
|
||||
models/run-commit-parent.ts
|
||||
models/run-commit-person.ts
|
||||
models/run-commit.ts
|
||||
|
|
@ -261,6 +263,7 @@ models/run-interviews-settings.ts
|
|||
models/run-lifecycle.ts
|
||||
models/run-links.ts
|
||||
models/run-manifest.ts
|
||||
models/run-meta-branch-settings.ts
|
||||
models/run-mode.ts
|
||||
models/run-model-settings.ts
|
||||
models/run-model.ts
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* 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
|
||||
|
|
@ -25,6 +25,4 @@ export interface DaytonaSettings {
|
|||
'labels': { [key: string]: string; };
|
||||
'snapshot': DaytonaSnapshotSettings | null;
|
||||
'network': DaytonaNetworkLayer | null;
|
||||
'skip_clone': boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* 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
|
||||
|
|
@ -20,6 +20,4 @@ export interface DockerSettings {
|
|||
'memory_limit': number | null;
|
||||
'cpu_quota': number | null;
|
||||
'env_vars': { [key: string]: string; };
|
||||
'skip_clone': boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -216,9 +216,11 @@ export * from './run-billing';
|
|||
export * from './run-billing-stage';
|
||||
export * from './run-billing-summary';
|
||||
export * from './run-billing-totals';
|
||||
export * from './run-branch-settings';
|
||||
export * from './run-checkpoint';
|
||||
export * from './run-checkpoint-settings';
|
||||
export * from './run-client-provenance';
|
||||
export * from './run-clone-settings';
|
||||
export * from './run-commit';
|
||||
export * from './run-commit-parent';
|
||||
export * from './run-commit-person';
|
||||
|
|
@ -239,6 +241,7 @@ export * from './run-interviews-settings';
|
|||
export * from './run-lifecycle';
|
||||
export * from './run-links';
|
||||
export * from './run-manifest';
|
||||
export * from './run-meta-branch-settings';
|
||||
export * from './run-mode';
|
||||
export * from './run-model';
|
||||
export * from './run-model-settings';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
/* 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 RunBranchSettings {
|
||||
'enabled': boolean;
|
||||
'push': boolean;
|
||||
}
|
||||
|
|
@ -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 RunCloneSettings {
|
||||
'enabled': boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/* 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 RunMetaBranchSettings {
|
||||
'enabled': boolean;
|
||||
'push': boolean;
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* 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
|
||||
|
|
@ -30,9 +30,15 @@ import type { PullRequestSettings } from './pull-request-settings';
|
|||
import type { RunAgentSettings } from './run-agent-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunBranchSettings } from './run-branch-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunCheckpointSettings } from './run-checkpoint-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunCloneSettings } from './run-clone-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunExecutionSettings } from './run-execution-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
|
|
@ -48,6 +54,9 @@ import type { RunIntegrationsSettings } from './run-integrations-settings';
|
|||
import type { RunInterviewsSettings } from './run-interviews-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunMetaBranchSettings } from './run-meta-branch-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunModelSettings } from './run-model-settings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
|
|
@ -72,6 +81,9 @@ export interface RunNamespace {
|
|||
'prepare': RunPrepareSettings;
|
||||
'execution': RunExecutionSettings;
|
||||
'checkpoint': RunCheckpointSettings;
|
||||
'clone': RunCloneSettings;
|
||||
'run_branch': RunBranchSettings;
|
||||
'meta_branch': RunMetaBranchSettings;
|
||||
'sandbox': RunSandboxSettings;
|
||||
'notifications': { [key: string]: NotificationRouteSettings; };
|
||||
'interviews': RunInterviewsSettings;
|
||||
|
|
@ -82,4 +94,3 @@ export interface RunNamespace {
|
|||
'artifacts': ArtifactsSettings;
|
||||
'integrations': RunIntegrationsSettings;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue