https://github.com/adiled/orch
https://github.com/adiled/orch
Last synced: 4 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/adiled/orch
- Owner: adiled
- License: apache-2.0
- Created: 2026-02-28T07:02:00.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-03-07T17:03:40.000Z (5 months ago)
- Last Synced: 2026-03-07T22:49:18.139Z (5 months ago)
- Language: Rust
- Size: 249 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# orch
A parser for Orchfiles -- declarative, platform-agnostic service orchestration specifications.
`orch` reads an Orchfile and outputs structured JSON. It validates all constraints, expands variables, and detects dependency cycles. The JSON output is designed for consumption by platform-specific generators (launchd, systemd, etc.) or shell scripts.
## Install
```sh
cargo build --release
cp target/release/orch /usr/local/bin/
```
## Usage
```
orch parse [ ...] [--arg name=value ...]
orch validate [ ...] [--arg name=value ...]
```
### Commands
| Command | Output | Exit Code |
|------------|----------|------------------------------------|
| `parse` | JSON to stdout | 0 on success, 1 on errors |
| `validate` | "valid" to stderr | 0 if valid, 1 if errors |
Exit code 2 indicates usage errors (bad arguments, missing file).
### Multi-file Composition
Multiple files are merged left-to-right using a systemd drop-in overlay model:
```sh
orch parse base.orch staging.orch personal.orch
```
Merge rules:
- **Scalars** (FROM, MEMORY, CPUS, ...): last wins
- **Keyed lists** (ENV, PUBLISH, VOLUME): merge by key, overlay wins on conflict
- **Positional lists** (REQUIRES, AFTER, ENV_FILE): append + dedup
- **CLEAR directive**: resets list fields before applying overlay values
See [SPEC.md](SPEC.md) for full composition semantics.
### ARG Overrides
Override Orchfile `ARG` defaults via CLI flags or environment variables:
```sh
# CLI flag (highest priority)
orch parse Orchfile --arg postgres_port=9999
# Multiple overrides
orch parse Orchfile --arg postgres_port=9999 --arg memory=8G
# Environment variable
ORCH_ARG_postgres_port=9999 orch parse Orchfile
```
**Priority order:** `--arg` flag > `ORCH_ARG_*` env var > Orchfile default.
## Example Orchfile
```
ARG postgres_port=5433
ARG django_port=9090
SERVICE postgres
FROM pgvector/pgvector:pg15
MEMORY 4G
CPUS 2
PUBLISH ${postgres_port}:5432
VOLUME postgres-data:/var/lib/postgresql/data
ENV POSTGRES_USER=postgres
HEALTHCHECK pg_isready -h localhost -p ${postgres_port}
RESTART on-failure
SERVICE django
RUN python manage.py runserver 0.0.0.0:${django_port}
WORKDIR backend/canary
REQUIRES postgres
HEALTHCHECK http://localhost:${django_port}/health
```
## JSON Output
```sh
orch parse Orchfile
```
```json
{
"version": "0.1.0",
"args": {
"postgres_port": "5433",
"django_port": "9090"
},
"services": [
{
"name": "postgres",
"mode": "container",
"image": "pgvector/pgvector:pg15",
"publish": [{ "host": 5433, "container": 5432 }],
"volumes": [{ "source": "postgres-data", "destination": "/var/lib/postgresql/data", "is_named": true }],
"env": { "POSTGRES_USER": "postgres" },
"healthcheck": "pg_isready -h localhost -p 5433",
"oneshot": false,
"disabled": false,
"recreate": "never",
"restart": { "policy": "on_failure" },
"timeouts": {},
"resources": { "memory": "4G", "cpus": 2.0 },
"logging": {}
},
{
"name": "django",
"mode": "host",
"run_command": "python manage.py runserver 0.0.0.0:9090",
"workdir": "backend/canary",
"requires": ["postgres"],
"healthcheck": "http://localhost:9090/health",
"oneshot": false,
"disabled": false,
"recreate": "never",
"restart": { "policy": "no" },
"timeouts": {},
"resources": {},
"logging": {}
}
]
}
```
## Bash Script Integration
The JSON output is designed for `jq`-based consumption in shell scripts. Here are practical examples:
### List all service names
```sh
orch parse Orchfile | jq -r '.services[].name'
```
### Get port mappings for a service
```sh
orch parse Orchfile | jq '.services[] | select(.name == "postgres") | .publish'
```
### Generate a container run command
```sh
#!/bin/bash
set -euo pipefail
ORCH_JSON=$(orch parse Orchfile)
# Iterate over container services
echo "$ORCH_JSON" | jq -c '.services[] | select(.mode == "container")' | while read -r svc; do
name=$(echo "$svc" | jq -r '.name')
image=$(echo "$svc" | jq -r '.image')
# Build port flags
ports=$(echo "$svc" | jq -r '.publish[]? | "-p \(.host):\(.container)"' | tr '\n' ' ')
# Build volume flags
vols=$(echo "$svc" | jq -r '.volumes[]? | "-v \(.source):\(.destination)"' | tr '\n' ' ')
# Build env flags
envs=$(echo "$svc" | jq -r '.env // {} | to_entries[] | "-e \(.key)=\(.value)"' | tr '\n' ' ')
# Build memory flag
mem=$(echo "$svc" | jq -r '.resources.memory // empty' | sed 's/^/--memory /')
echo "docker run -d --name $name $ports $vols $envs $mem $image"
done
```
### Check if an Orchfile is valid in CI
```sh
if orch validate Orchfile 2>/dev/null; then
echo "Orchfile is valid"
else
echo "Orchfile has errors:" >&2
orch validate Orchfile
exit 1
fi
```
### Extract all required dependencies for a service
```sh
orch parse Orchfile | jq -r '.services[] | select(.name == "django") | .requires[]'
```
### Build a dependency-ordered start list
```sh
#!/bin/bash
# Start services in dependency order using the parsed requires/after fields
set -euo pipefail
ORCH_JSON=$(orch parse Orchfile)
started=()
start_service() {
local name=$1
# Skip if already started
for s in "${started[@]:-}"; do
[[ "$s" == "$name" ]] && return
done
# Start required dependencies first
for dep in $(echo "$ORCH_JSON" | jq -r --arg n "$name" '.services[] | select(.name == $n) | .requires[]? // empty'); do
start_service "$dep"
done
echo "Starting: $name"
started+=("$name")
}
# Start all non-disabled services
for svc in $(echo "$ORCH_JSON" | jq -r '.services[] | select(.disabled == false) | .name'); do
start_service "$svc"
done
```
### Filter services by mode
```sh
# Container services only
orch parse Orchfile | jq '[.services[] | select(.mode == "container")]'
# Host services only
orch parse Orchfile | jq '[.services[] | select(.mode == "host")]'
```
### Get the healthcheck for a specific service
```sh
orch parse Orchfile | jq -r '.services[] | select(.name == "postgres") | .healthcheck // "none"'
```
## Error Messages
Errors include line numbers (for parse errors) or service names (for validation errors):
```
$ orch validate bad.orch
error: parse error: line 5: cannot specify both FROM and RUN
error: validation error: service 'app': ENTRYPOINT is only valid with FROM (container mode) (C2)
error: validation error: service 'a': dependency cycle detected: a -> b -> a
```
## Grammar
The formal EBNF grammar is in [`grammar.ebnf`](grammar.ebnf). A typeset version with railroad diagrams is in [`docs/grammar.pdf`](docs/grammar.pdf).
## Specification
See [SPEC.md](SPEC.md) for the complete Orchfile specification.
## Constraints Enforced
| Constraint | Description |
|------------|-------------|
| C1 | Each service must have exactly one of `FROM` or `RUN` |
| C2 | `ENTRYPOINT`, `CMD`, `PUBLISH`, `VOLUME` only with `FROM` |
| C3 | `USER`, `STOP`, `RELOAD` only with `RUN` |
| C4 | `REQUIRES` + `AFTER` must form a DAG (no cycles) |
Additional validations:
- Service names: lowercase alphanumeric + hyphens, starts with letter, max 63 chars
- No duplicate service names
- `REQUIRES` must reference defined services
- `AFTER` is soft -- undefined targets are allowed (per spec)
- Numeric fields validated (`CPUS`, `LIMIT_NOFILE`, `IO_WEIGHT` range 10-1000, etc.)
## Tests
```sh
cargo test
```
140 tests covering all directives, all constraints (C1-C4), variable expansion, multi-file composition, merge semantics, CLEAR directive, error cases, edge cases, and the full spec example.
## License
Apache 2.0