CI Pipeline
A three-stage CI pipeline running entirely on provisioned cloud containers: build (Rust release binary), test (cargo test with a Postgres sidecar), and package (tar.gz archive). build and test run concurrently; package starts only after both complete.
Running
melodium run 16_ci_pipeline/Compo.toml \
--api_token "my-api-token" \
--repo_url "https://github.com/my-org/my-project.git"api_token here authenticates against a Mélodium Services API, such as Cadence.CI .
[…] info: ci: pipeline started
[…] info: build: build succeeded
[…] info: test: all tests passed
[…] info: ci: pipeline complete, artifact writtenHow it works
A single CicdDispatchEngine model handles all infrastructure concerns, wrapping DistantEngine and DistributionEngine provisioning under one API:
model Dispatcher(const api_token: string) : CicdDispatchEngine {
api_token = |wrap<string>(api_token)
}Each stage is a sub-treatment wrapping simpleStep or simpleStepWithInput, which abstract provisioning, execution, file I/O, and cleanup into a single call. No manual distant / start / distribute wiring is needed.
Concurrent stages with fork-join
build and test both start from startup.trigger:
startup.trigger -> build.trigger
startup.trigger -> test.triggerpackage waits for both to finish using flock<void>(), which collects two Block<void> signals and emits a Stream<void> once both arrive. A trigger<void>() converts that stream to a single Block<void> gate for package:
bothFinished: flock<void>()
build.finished -> bothFinished.a
test.finished -> bothFinished.b
bothTrigger: trigger<void>()
bothFinished.stream -> bothTrigger.stream
package[dispatcher=dispatcher](output=output)
bothTrigger.start -> package.trigger
build.data -> package.dataStage 1: build
build wraps simpleStep in a rust:1.82-slim container, cloning the repo and compiling a release binary. out_file = "binary" tells simpleStep to stream /mnt/data/binary back as data: Stream<byte>:
treatment build[dispatcher: CicdDispatchEngine](const repo_url: string)
input trigger: Block<void>
output started: Block<void>
output success: Block<void>
output error: Block<void>
output failed: Block<void>
output finished: Block<void>
output data: Stream<byte>
{
simpleStep[dispatcher=dispatcher](
name = "build",
image = "rust:1.82-slim",
cpu = 2000, // millicores
memory = 4096, // MB
storage = 8192, // MB
max_duration = 1800,
commands = |raw_commands([
"apt-get update -qq && apt-get install -y --no-install-recommends git",
"git clone --depth 1 ${REPO_URL} /workspace",
"cd /workspace && cargo build --release 2>&1",
"cp /workspace/target/release/app /mnt/data/binary"
]),
variables = |wrap<StringMap>(|map([|entry<string>("REPO_URL", repo_url)])),
out_file = "binary"
)
Self.trigger -> simpleStep.trigger
simpleStep.data -> Self.data
simpleStep.finished -> Self.finished
}Stage 2: test, with a service container sidecar
test runs the same image, but attaches a Postgres 16 |service_container reachable by the hostname "postgres". DATABASE_URL in the main container’s variables points at it:
treatment test[dispatcher: CicdDispatchEngine](const repo_url: string)
input trigger: Block<void>
output started: Block<void>
output success: Block<void>
output error: Block<void>
output failed: Block<void>
output finished: Block<void>
{
simpleStep[dispatcher=dispatcher](
name = "test",
image = "rust:1.82-slim",
cpu = 2000, // millicores
memory = 4096, // MB
storage = 8192, // MB
max_duration = 1800,
service_containers = [
|service_container(
"postgres",
512, // memory, MB
1000, // cpu, millicores
2048, // storage, MB
|amd64(),
[],
"postgres:16",
_,
|wrap<StringMap>(|map([
|entry<string>("POSTGRES_USER", "ci"),
|entry<string>("POSTGRES_PASSWORD", "ci"),
|entry<string>("POSTGRES_DB", "ci_test")
])),
_
)
],
commands = |raw_commands([
"apt-get update -qq && apt-get install -y --no-install-recommends git",
"git clone --depth 1 ${REPO_URL} /workspace",
"cd /workspace && cargo test 2>&1"
]),
variables = |wrap<StringMap>(|map([
|entry<string>("REPO_URL", repo_url),
|entry<string>("DATABASE_URL", "postgres://ci:ci@postgres/ci_test")
]))
)
Self.trigger -> simpleStep.trigger
simpleStep.finished -> Self.finished
}Stage 3: package, fed by stage 1’s binary
package wraps simpleStepWithInput, receiving the binary bytes as data: Stream<byte> (streamed straight from build.data, no local disk involved), writing them to /mnt/data/binary via in_file = "binary" on a debian:bookworm-slim container, then archiving:
treatment package[dispatcher: CicdDispatchEngine](const output: string)
input trigger: Block<void>
input data: Stream<byte>
output started: Block<void>
output success: Block<void>
output error: Block<void>
output failed: Block<void>
output finished: Block<void>
output data: Stream<byte>
{
simpleStepWithInput[dispatcher=dispatcher](
name = "package",
image = "debian:bookworm-slim",
cpu = 500, // millicores
memory = 512, // MB
storage = 2048, // MB
max_duration = 300,
in_file = "binary",
commands = |raw_commands([
"chmod +x /mnt/data/binary",
"tar -czf /mnt/data/artifact.tar.gz -C /mnt/data binary"
]),
out_file = "artifact.tar.gz"
)
Self.trigger -> simpleStepWithInput.trigger
Self.data -> simpleStepWithInput.data
simpleStepWithInput.data -> Self.data
}build.data -> package.data streams the compiled binary directly from the build container into the package container’s input.
Error merging
Two chained one<void>() merge the four failure/error signals (build.failed, test.failed, build.error, test.error) into a single oneAnyFailed signal for logging:
failedOrError1: one<void>()
failedOrError2: one<void>()
build.failed -> failedOrError1.a
test.failed -> failedOrError1.b
build.error -> failedOrError2.a
test.error -> failedOrError2.b
oneAnyFailed: one<void>()
failedOrError1.value -> oneAnyFailed.a
failedOrError2.value -> oneAnyFailed.b
logAbort: logErrorMessage(label="ci", message="pipeline aborted — skipping package stage")
oneAnyFailed.value -> logAbort.triggerpackage still triggers on bothFinished regardless of build/test outcome; if build failed, package will error when simpleStepWithInput tries to extract the missing in_file.
Dependencies
[dependencies]
std = "0.10.1" # core flows, logging, data structures
fs = "0.10.1" # local file I/O
process = "0.10.1" # shell command execution
work = "0.10.1" # cloud runner provisioning
distrib = "0.10.1" # stream distribution across runners
cicd = "0.10.1" # CI/CD step dispatch and orchestration