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_TOKENavailable (env var orgh 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
- Modular infrastructure for the class pattern
- Apply modes for reconcile vs patch
- Pipeline stages when each repo's pipeline has multiple stages