Skip to Content
Mélodium 0.10.1 is now available!
DocsExemplesPipeline CI

Pipeline CI

Source: 16_ci_pipeline

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"
Note

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 written

Fonctionnement

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.trigger

package 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

main treatment diagram Voir dans Compositeur Studio

É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.trigger

package 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