Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to `devloop` will be recorded in this file.

## [Unreleased]

### Added

- Added transactional artifact generations through the single
`publish_artifact` workflow action. Devloop now builds in isolated candidate
directories, switches declared consumers only after build success, requires
exact-generation HTTP readiness, rolls back failed switches, cleans crash
remnants, and bounds retained generations.
- Added `devloop docs artifacts` with an agent-oriented configuration contract
and complete lifecycle guidance.

## [0.10.5] - 2026-09-02

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Built-in reference docs are also available from the CLI:
```bash
devloop docs config
devloop docs behavior
devloop docs artifacts
devloop docs development
devloop docs security
```
Expand Down Expand Up @@ -197,6 +198,11 @@ For the runtime behavior reference, see
For the full configuration reference, see
[`docs/configuration.md`](docs/configuration.md).

For servers that consume directories replaced by a build, see
[`docs/artifacts.md`](docs/artifacts.md). It defines the agent-safe
`publish_artifact` workflow, exact-generation readiness, rollback, and bounded
retention.

For local contributor workflow details, including the opt-in watch
flake smoke test, see [`docs/development.md`](docs/development.md).

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- [Behavior Reference](behavior.md)
- [Configuration Reference](configuration.md)
- [Transactional Artifact Generations](artifacts.md)
- [Development Guide](development.md)
- [Security Notes](security.md)

Expand Down
107 changes: 107 additions & 0 deletions docs/artifacts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Transactional Artifact Generations

Use artifact generations when a build command replaces a directory that a
long-running HTTP server is already serving. A status-only readiness probe can
observe the server while its asset manifest still points at deleted files.
`publish_artifact` prevents that mixed state.

This guide is also available in the CLI:

```bash
devloop docs artifacts
```

## Agent rule

If a rebuild deletes or replaces served output, configure an artifact. Do not
teach a workflow to delete the live directory, restart the server, or poll `/`
with a status-only probe. Devloop must own the candidate directory, generation
switch, exact readiness check, rollback, and retention.

## Complete configuration

```toml
root = "."
startup_workflows = ["build_site"]

[watch.site]
paths = ["src/**", "public/"]
workflow = "build_site"

[hook.build_site]
command = ["./scripts/build-site.sh"]

[process.site]
command = ["./scripts/serve-site.sh"]
autostart = false

[process.site.readiness]
kind = "http"
url = "http://127.0.0.1:8787/__devloop_generation"
expect_body = "{{ artifact.site.generation }}"
interval_ms = 250
timeout_ms = 30000

[artifact.site]
build_hook = "build_site"
consumers = ["site"]
retain = 2

[workflow.build_site]
steps = [{ action = "publish_artifact", artifact = "site" }]
triggers = ["browser_reload"]

[workflow.browser_reload]
steps = [{ action = "notify_reload" }]
```

The build hook writes only to `DEVLOOP_ARTIFACT_CANDIDATE`. The consumer reads
`DEVLOOP_ARTIFACT_SITE_DIR` and `DEVLOOP_ARTIFACT_SITE_GENERATION` from its
environment, serves that directory, and returns the generation value as the
complete response body of `/__devloop_generation`. Artifact names are converted
to uppercase environment components; non-alphanumeric characters become `_`.
Artifact names must start with a lowercase letter and contain only lowercase
letters, digits, and underscores.

Set artifact consumers to `autostart = false` and put the publication workflow
in `startup_workflows`. The publish action starts each stopped consumer after a
successful initial build.

## Guarantees

For each publication, devloop:

1. removes incomplete candidate directories left by an interrupted build
2. creates a private candidate directory
3. runs the build hook with candidate and generation environment variables
4. preserves the live process and active generation if the build fails
5. makes the completed directory immutable by generation name
6. switches session state and restarts the declared consumers
7. accepts readiness only when the response body matches the expected generation
8. restores and restarts the previous generation if switching fails
9. removes old generations beyond `retain`
10. runs downstream triggers, including browser reload, only after success

The workflow API deliberately exposes only `publish_artifact`. Partial
`prepare_artifact` or `promote_artifact` steps do not exist because their order
would be easy to misconfigure.

Once consumers pass exact-generation readiness, the switch is committed.
Failure to remove an older retained directory is logged but cannot turn that
successful switch into a failed workflow. An interrupted, unverified switch is
marked in session state and conservatively restored on the next publication.

## Environment

During the build hook:

- `DEVLOOP_ARTIFACT`: configured artifact name
- `DEVLOOP_ARTIFACT_GENERATION`: candidate generation identifier
- `DEVLOOP_ARTIFACT_CANDIDATE`: absolute candidate directory
- `DEVLOOP_ARTIFACT_<NAME>_GENERATION`: same candidate identifier
- `DEVLOOP_ARTIFACT_<NAME>_DIR`: same candidate directory

After promotion, every hook and managed process receives the named `DIR` and
`GENERATION` variables for every active artifact. Project code remains
responsible only for writing to the supplied directory, serving the supplied
directory, and returning the supplied generation from its readiness endpoint.
20 changes: 20 additions & 0 deletions docs/behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,32 @@ Workflows run step by step, in order.
emitting output.
- `notify_reload` broadcasts a generic `reload` event to browser
listeners connected to `devloop`'s browser reload event stream.
- `publish_artifact` is one atomic workflow effect. The engine does not expose
partial preparation or promotion steps.

If any step fails, that workflow fails immediately and logs the error
loudly, but `devloop` itself keeps running so later file changes or
external events can retry the workflow without restarting the
supervisor.

## Artifact publication

Artifact publication isolates destructive builds from live consumers. A build
failure deletes only its private candidate. A successful build becomes a named
generation, then devloop changes the active session state and restarts the
declared consumers. HTTP readiness succeeds only when its response body equals
the active generation. A mismatch or consumer failure restores the previous
state and process generation before the workflow reports failure.

Interrupted candidates are removed at the next publication. Successful
generations are retained newest-first according to the artifact's `retain`
limit. Cleanup failure after a ready switch is logged without failing the
already committed publication. Browser reload belongs in a triggered workflow, so clients are notified
only after exact-generation readiness succeeds.

See [Transactional Artifact Generations](artifacts.md) for the agent-facing
configuration and environment contract.

## Processes

Managed processes are long-running child commands.
Expand Down
41 changes: 41 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,13 @@ kind = "http"
url = "http://127.0.0.1:$CONTAINER_PORT/"
interval_ms = 500
timeout_ms = 30000
expect_body = "{{ artifact.site.generation }}"
```

- `expect_body`: optional exact response-body match after trimming the response.
Templates read current session state. Artifact consumers must match their
`artifact.<name>.generation`; a status-only probe is rejected for them.

### State-key probe

```toml
Expand Down Expand Up @@ -275,6 +280,31 @@ Hooks default to dimmed inherited output because they are typically
short-lived helper commands whose output is useful context but not the
primary long-running log stream.

## Artifacts

Artifacts describe build outputs that long-running processes consume.

```toml
[artifact.site]
build_hook = "build_site"
consumers = ["site"]
retain = 2
```

- `build_hook`: hook that writes to `DEVLOOP_ARTIFACT_CANDIDATE`.
- `consumers`: managed processes restarted against a completed generation.
- `retain`: total successful generation directories to keep. Default: `2`;
must be greater than zero.

Artifact table names must start with a lowercase letter and contain only
lowercase letters, digits, and underscores.

Each consumer must have HTTP readiness whose `expect_body` is exactly
`{{ artifact.<name>.generation }}`. Use the artifact only through
`publish_artifact`; lifecycle fragments are intentionally not configurable.
See [Transactional Artifact Generations](artifacts.md) for the complete
contract and environment variables.

### Observed hooks

```toml
Expand Down Expand Up @@ -422,6 +452,7 @@ would make ordering and duplication ambiguous.
- `restart_process`
- `wait_for_process`
- `run_hook`
- `publish_artifact`
- `run_workflow`
- `sleep_ms`
- `write_state`
Expand All @@ -442,6 +473,16 @@ would make ordering and duplication ambiguous.
`value` supports `{{state_key}}` interpolation from the current session
state.

### `publish_artifact`

```toml
{ action = "publish_artifact", artifact = "site" }
```

Builds a private candidate, switches all declared consumers, verifies that
they serve the selected generation, rolls back a failed switch, and cleans old
generations. Downstream triggers run only after publication succeeds.

### `log`

```toml
Expand Down
1 change: 1 addition & 0 deletions src/browser_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ mod tests {
watch: BTreeMap::new(),
process: BTreeMap::new(),
hook: BTreeMap::new(),
artifact: BTreeMap::new(),
event_server: EventServerConfig::default(),
browser_reload_server: BrowserReloadServerConfig::default(),
event: BTreeMap::new(),
Expand Down
Loading