Fleet fan-out

Run the same pipeline shape across N repos with per-repo overrides. One forma, one apply.

For each service repo: a Target bound to that repo, a typed Repo::Workflow for the deploy pipeline, repo variables for project name and region, and a production environment with AWS OIDC secret and branch policy.

Prerequisites

  • Quick start complete, agent running
  • GitHub Actions plugin installed (install)
  • GITHUB_TOKEN available (env var or gh auth token)

The class

Inputs are the per-repo knobs. The resource listing plus the target are emitted as resources.

// service_pipeline.pkl
module service_pipeline

import "@formae/formae.pkl"
import "@gha/gha.pkl"
import "@gha/repo/repovariable.pkl" as repovar
import "@gha/repo/repoworkflow.pkl" as workflow
import "@gha/env/environment.pkl"
import "@gha/env/envsecret.pkl"
import "@com.github.actions/Workflow.pkl" as GHAWorkflow

class ServicePipeline {
    orgName: String
    repoName: String
    awsRegion: String = "us-east-1"
    prodRoleArn: String
    prodBranchPattern: String = "main"

    hidden target: formae.Target = new formae.Target {
        label = "gha-\(repoName)"
        namespace = "GHA"
        config = new gha.Config {
            owner = orgName
            repo = repoName
        }
    }

    hidden projectVar: repovar.Variable = new {
        label = "\(repoName)-project"
        target = target.res
        name = "PROJECT_NAME"
        value = repoName
    }

    hidden regionVar: repovar.Variable = new {
        label = "\(repoName)-region"
        target = target.res
        name = "AWS_DEFAULT_REGION"
        value = awsRegion
    }

    hidden prodEnv: environment.Environment = new {
        label = "\(repoName)-production"
        target = target.res
        name = "production"
        preventSelfReview = true
    }

    hidden prodRoleSecret: envsecret.Secret = new {
        label = "\(repoName)-prod-role"
        target = target.res
        environment = prodEnv.res.name
        name = "AWS_ROLE_ARN"
        value = prodRoleArn
    }

    hidden deployWorkflow: workflow.Workflow = new {
        label = "\(repoName)-deploy"
        target = target.res
        path = ".github/workflows/deploy.yml"
        name = "Deploy"
        on = new GHAWorkflow.On {
            push { branches { prodBranchPattern } }
        }
        permissions = new GHAWorkflow.Permissions {
            `id-token` = "write"
            contents = "read"
        }
        jobs {
            ["deploy"] {
                `runs-on` = "ubuntu-latest"
                environment { name = "production" }
                steps {
                    new { uses = "actions/checkout@v4" }
                    new {
                        name = "Configure AWS"
                        uses = "aws-actions/configure-aws-credentials@v4"
                        `with` {
                            ["role-to-assume"] = "${{ secrets.AWS_ROLE_ARN }}"
                            ["aws-region"] = "${{ vars.AWS_DEFAULT_REGION }}"
                        }
                    }
                    new { name = "Deploy"; run = "./deploy.sh" }
                }
            }
        }
    }

    resources: Listing<formae.Resource|formae.Target> = new {
        target
        projectVar
        regionVar
        prodEnv
        prodRoleSecret
        deployWorkflow
    }
}

The fleet manifest

Three services shown. Scale up by adding listing entries.

// main.pkl
amends "@formae/forma.pkl"

import "@formae/formae.pkl"
import "./service_pipeline.pkl" as sp

local stack = new formae.Stack {
    label = "service-fleet"
    description = "Deploy pipelines for all service repos"
}

local orgName = read("env:GHA_OWNER")

local services: Listing<sp.ServicePipeline> = new {
    new {
        orgName = orgName
        repoName = "orders-api"
        prodRoleArn = "arn:aws:iam::111111111111:role/orders-deploy"
    }
    new {
        orgName = orgName
        repoName = "payments-api"
        awsRegion = "us-west-2"
        prodRoleArn = "arn:aws:iam::111111111111:role/payments-deploy"
    }
    new {
        orgName = orgName
        repoName = "catalog-api"
        prodRoleArn = "arn:aws:iam::111111111111:role/catalog-deploy"
        prodBranchPattern = "release/*"
    }
}

forma {
    stack
    for (svc in services) {
        ...svc.resources
    }
}

Each ServicePipeline carries its own Target. Resources inside bind to it via target.res.

Apply

export GHA_OWNER=my-org
formae apply --mode reconcile --watch main.pkl

Reconcile removes anything in the fleet that isn't declared. This is the Classic GitOps posture.

Update the fleet

Add a step to the class:

steps {
    new { uses = "actions/checkout@v4" }
    new {
        name = "Security scan"
        uses = "aquasecurity/trivy-action@master"
    }
    // ... rest of steps
}

Re-apply. Every repo picks it up.

Add a repo

Append to the listing:

new {
    orgName = orgName
    repoName = "inventory-api"
    prodRoleArn = "arn:aws:iam::111111111111:role/inventory-deploy"
}

Re-apply.

Per-repo override

Override a single field on one spec; class defaults carry the rest.

new {
    orgName = orgName
    repoName = "legacy-service"
    awsRegion = "eu-central-1"
    prodBranchPattern = "stable"
    prodRoleArn = "arn:aws:iam::222222222222:role/legacy-deploy"
}

What's next