GitLab CI

Same pattern as GitHub Actions: provision with formae apply, capture a property from the new resource, deploy, verify. The whole pipeline is a GitLab::Project::Pipeline resource.

GitLab CI passes values between jobs with dotenv artifacts instead of $GITHUB_OUTPUT. The bridge mechanism differs; the pattern is identical.

The pipeline as a forma

import "@gitlab/project/projectpipeline.pkl" as pipeline
import "@gitlab/ci/pipeline.pkl" as CI

local deployPipeline = new pipeline.Pipeline {
    label = "deploy"
    path = ".gitlab-ci.yml"
    stages = new { "provision"; "deploy"; "verify" }

    jobs {
        ["provision"] = new CI.Job {
            stage = "provision"
            image = "ubuntu:24.04"
            script = new {
                #"curl -fsSL https://hub.platform.engineering/setup/formae.sh | bash -s -- -y"#
                "export PATH=/opt/pel/formae/bin:$PATH"
                "formae apply --mode reconcile --yes --watch infra/database.pkl"
                #"""
                DB_HOST=$(formae inventory resources \
                  --query='label:pg-server' \
                  --output-consumer=machine \
                  | jq -r '.Resources[0].ReadOnlyProperties.fullyQualifiedDomainName')
                echo "DB_HOST=$DB_HOST" >> deploy.env
                """#
            }
            artifacts = new CI.Artifacts {
                reports = new CI.Reports { dotenv = "deploy.env" }
            }
        }
        ["deploy"] = new CI.Job {
            stage = "deploy"
            needs = new { "provision" }
            script = new {
                #"deploy_cmd --db-host=$DB_HOST"#
            }
        }
        ["verify"] = new CI.Job {
            stage = "verify"
            needs = new { "deploy" }
            script = new { "./run-smoke-tests.sh" }
        }
    }
}

The capture step writes DB_HOST=... to a deploy.env file, and the job exposes that file as a dotenv artifact report. Downstream jobs that needs this one get $DB_HOST injected into their environment automatically.

Apply

export GITLAB_TOKEN=glpat-...
formae apply --mode reconcile --watch main.pkl

The pipeline file lands at .gitlab-ci.yml. Trigger it from the GitLab UI.

Full working example

infra-to-app provisions Azure PostgreSQL, deploys Miniflux, and verifies feeds end to end. Same pattern, complete Pkl.

What's next