Pipeline CI
Un pipeline CI en trois étapes s’exécutant entièrement sur des conteneurs cloud provisionnés : build (binaire Rust en release), test (cargo test avec un sidecar Postgres), et package (archive tar.gz). build et test s’exécutent en parallèle ; package ne démarre qu’après la fin des deux.
Exécution
melodium run 16_ci_pipeline/Compo.toml \
--api_token "my-api-token" \
--repo_url "https://github.com/my-org/my-project.git"api_token authentifie ici auprès d’une API Mélodium Services, comme Cadence.CI .
[…] info: ci: pipeline started
[…] info: build: build succeeded
[…] info: test: all tests passed
[…] info: ci: pipeline complete, artifact writtenFonctionnement
Un seul modèle CicdDispatchEngine gère toute l’infrastructure, encapsulant le provisionnement DistantEngine et DistributionEngine sous une seule API :
model Dispatcher(const api_token: string) : CicdDispatchEngine {
api_token = |wrap<string>(api_token)
}Chaque étape est un sous-traitement encapsulant simpleStep ou simpleStepWithInput, qui abstraient le provisionnement, l’exécution, les E/S de fichiers et le nettoyage en un seul appel. Aucun câblage manuel de distant / start / distribute n’est nécessaire.
Étapes concurrentes avec fork-join
build et test démarrent tous les deux depuis startup.trigger :
startup.trigger -> build.trigger
startup.trigger -> test.triggerpackage attend que les deux se terminent grâce à flock<void>(), qui collecte deux signaux Block<void> et émet un Stream<void> une fois les deux reçus. Un trigger<void>() convertit ce flux en un seul Block<void> pour déclencher 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.dataÉtape 1 : build
build encapsule simpleStep dans un conteneur rust:1.82-slim, clone le dépôt et compile un binaire en release. out_file = "binary" indique à simpleStep de renvoyer /mnt/data/binary en flux via 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, // millicœurs
memory = 4096, // Mo
storage = 8192, // Mo
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
}Étape 2 : test, avec un sidecar de service
test utilise la même image, mais y attache un |service_container Postgres 16 accessible par le nom d’hôte "postgres". DATABASE_URL dans les variables du conteneur principal pointe vers celui-ci :
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, // millicœurs
memory = 4096, // Mo
storage = 8192, // Mo
max_duration = 1800,
service_containers = [
|service_container(
"postgres",
512, // mémoire, Mo
1000, // cpu, millicœurs
2048, // stockage, Mo
|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
}Étape 3 : package, alimentée par le binaire de l’étape 1
package encapsule simpleStepWithInput, recevant les octets du binaire en tant que data: Stream<byte> (transmis directement depuis build.data, sans passer par le disque local), les écrivant dans /mnt/data/binary via in_file = "binary" sur un conteneur debian:bookworm-slim, puis les archivant :
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, // millicœurs
memory = 512, // Mo
storage = 2048, // Mo
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 transmet en flux le binaire compilé directement du conteneur de build vers l’entrée du conteneur de package.
Fusion des erreurs
Deux one<void>() chaînés fusionnent les quatre signaux d’échec/erreur (build.failed, test.failed, build.error, test.error) en un seul signal oneAnyFailed pour la journalisation :
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 se déclenche quand même sur bothFinished, indépendamment du résultat de build/test ; si build a échoué, package produira une erreur lorsque simpleStepWithInput tentera d’extraire le in_file manquant.
Dépendances
[dependencies]
std = "0.10.1" # flux de base, journalisation, structures de données
fs = "0.10.1" # lecture/écriture de fichiers locaux
process = "0.10.1" # exécution de commandes shell
work = "0.10.1" # provisionnement de runners cloud
distrib = "0.10.1" # distribution de flux entre runners
cicd = "0.10.1" # dispatch et orchestration d'étapes CI/CD