Files
wfe/workflows.yaml
Sienna Meridian Satterwhite 6cc1437f0c feat(workflows.yaml): display names + restructure for shallow-merge
Two scope changes that together get the self-hosted wfe CI pipeline
passing against builds.sunbeam.pt.

1. Add `name:` display names to all 12 workflow definitions
   (Continuous Integration, Unit Tests, Build Image, etc.) so the
   new wfectl tables and UIs have human-friendly labels alongside
   the slug ids.

2. Restructure step references from the old `<<: *ci_step` / `<<:
   *ci_long` anchors to inner-config merges of the form:

       - name: foo
         type: kubernetes
         config:
           <<: *ci_config
           run: |
             ...

   YAML 1.1 merge keys are *shallow*. The old anchors put `config:`
   on the top-level step, then the step's own `config:` block
   replaced it wholesale — image, memory, cpu, env all vanished.
   The new pattern merges at the `config:` level so step-specific
   fields (`run:`, `outputs:`, etc.) sit alongside the inherited
   `image:`, `memory:`, `cpu:`, `env:`.

3. Secret env vars (GITEA_TOKEN, TEA_TOKEN, CARGO_REGISTRIES_*,
   BUILDKIT_*) moved into the shared `ci_env` anchor. Individual
   steps used to declare their own `env:` blocks which — again due
   to shallow merge — would replace the whole inherited env map.
2026-04-07 19:11:50 +01:00

821 lines
23 KiB
YAML

# workflows.yaml -- WFE self-hosting CI pipeline (Kubernetes-native)
#
# Designed to run on every push via wfe-server webhook → ci workflow.
# Every step runs as a Kubernetes Job in the wfe namespace; every service
# (postgres, valkey, opensearch) is a K8s Pod + Service in the workflow's
# scoped namespace.
#
# === Prerequisites ===
#
# 1. wfe-server deployed to the K8s cluster (see ../sbbb/base/wfe/)
# Reads webhooks at /webhooks/gitea, persists to postgres, locks via valkey.
#
# 2. wfe-credentials Secret in the wfe namespace (via Vault Secrets Operator).
# Required keys:
# sccache-s3-endpoint: S3-compatible endpoint for sccache
# sccache-bucket: S3 bucket name
# sccache-region: S3 region
# aws-access-key-id: S3 credentials for sccache
# aws-secret-access-key: S3 credentials for sccache
# cargo-registry-token: sunbeam registry publish token
# gitea-token: tea CLI token for releases
# buildkit-ca-cert: PEM-encoded CA cert (mTLS to buildkitd)
# buildkit-client-cert: PEM client cert
# buildkit-client-key: PEM client key
#
# 3. wfe-ci image at src.sunbeam.pt/studio/wfe-ci:latest
# Built from Dockerfile.ci -- contains rust, nextest, llvm-cov, sccache,
# buildctl, kubectl, tea, git.
#
# 4. buildkitd running in the build namespace at buildkitd.build.svc:1234
# Uses mTLS; client cert mounted from wfe-credentials secret.
# --- Shared Templates ---
_templates:
# Shared CI environment variables for the prebuilt wfe-ci image.
# Pulls secrets from wfe-credentials so sccache hits the shared S3 cache.
ci_env: &ci_env
SCCACHE_BUCKET: ${WFE_SCCACHE_BUCKET}
SCCACHE_REGION: ${WFE_SCCACHE_REGION}
SCCACHE_ENDPOINT: ${WFE_SCCACHE_S3_ENDPOINT}
SCCACHE_S3_USE_SSL: "true"
AWS_ACCESS_KEY_ID: ${WFE_AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${WFE_AWS_SECRET_ACCESS_KEY}
CARGO_HOME: /workspace/.cargo
RUSTC_WRAPPER: /usr/local/cargo/bin/sccache
CARGO_INCREMENTAL: "0"
# Secrets used by individual steps (publish, image, release). Defined here
# so the shared *ci_env / *ci_config anchors stay flat — YAML 1.1 merge
# keys are shallow, so a step can't override `env:` and still inherit.
GITEA_TOKEN: ${WFE_GITEA_TOKEN}
TEA_TOKEN: ${WFE_GITEA_TOKEN}
CARGO_REGISTRIES_SUNBEAM_TOKEN: ${WFE_CARGO_REGISTRY_TOKEN}
BUILDKIT_CA_CERT: ${WFE_BUILDKIT_CA_CERT}
BUILDKIT_CLIENT_CERT: ${WFE_BUILDKIT_CLIENT_CERT}
BUILDKIT_CLIENT_KEY: ${WFE_BUILDKIT_CLIENT_KEY}
# Default config for short CI steps (4Gi memory, 30min timeout).
ci_config: &ci_config
image: src.sunbeam.pt/studio/wfe-ci:latest
memory: 4Gi
cpu: "2"
timeout: 30m
env: *ci_env
# Default config for long-running CI steps (8Gi memory, 60min timeout).
ci_long_config: &ci_long_config
image: src.sunbeam.pt/studio/wfe-ci:latest
memory: 8Gi
cpu: "4"
timeout: 60m
env: *ci_env
# --- Workflows ---
workflows:
# === checkout: clone the repo into a shared workspace ===
- id: checkout
name: Checkout
version: 1
inputs:
repo_url: string
commit_sha: string
outputs:
checkout_ok: bool
commit: string
steps:
- name: clone
type: kubernetes
outputs:
- name: checkout_ok
- name: commit
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace
if [ ! -d wfe ]; then
git clone "$REPO_URL" wfe
fi
cd wfe
git fetch --all
git checkout "$COMMIT_SHA"
COMMIT=$(git rev-parse HEAD)
echo "Checked out: $COMMIT"
echo "##wfe[output checkout_ok=true]"
echo "##wfe[output commit=$COMMIT]"
# === lint: fmt + clippy ===
- id: lint
name: Lint
version: 1
outputs:
fmt_ok: bool
clippy_ok: bool
steps:
- name: fmt-check
type: kubernetes
outputs:
- name: fmt_ok
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
cargo fmt --all -- --check
echo "##wfe[output fmt_ok=true]"
- name: clippy
type: kubernetes
outputs:
- name: clippy_ok
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
cargo clippy --workspace --all-features -- -D warnings
echo "##wfe[output clippy_ok=true]"
# === test-unit: pure unit tests, no external dependencies ===
- id: test-unit
name: Unit Tests
version: 1
outputs:
core_ok: bool
yaml_ok: bool
deno_ok: bool
kubernetes_ok: bool
rustlang_ok: bool
steps:
- name: core-tests
type: kubernetes
outputs:
- name: core_ok
config:
<<: *ci_long_config
run: |
set -euo pipefail
cd /workspace/wfe
cargo nextest run -p wfe-core -p wfe -P ci
echo "##wfe[output core_ok=true]"
- name: yaml-tests
type: kubernetes
outputs:
- name: yaml_ok
config:
<<: *ci_long_config
run: |
set -euo pipefail
cd /workspace/wfe
cargo nextest run -p wfe-yaml \
--features deno,buildkit,containerd,rustlang,kubernetes -P ci
echo "##wfe[output yaml_ok=true]"
- name: deno-tests
type: kubernetes
outputs:
- name: deno_ok
config:
<<: *ci_long_config
run: |
set -euo pipefail
cd /workspace/wfe
cargo nextest run -p wfe-deno -P ci
echo "##wfe[output deno_ok=true]"
- name: kubernetes-unit-tests
type: kubernetes
outputs:
- name: kubernetes_ok
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
cargo nextest run -p wfe-kubernetes --lib -P ci
echo "##wfe[output kubernetes_ok=true]"
- name: rustlang-tests
type: kubernetes
outputs:
- name: rustlang_ok
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
cargo nextest run -p wfe-rustlang -P ci
echo "##wfe[output rustlang_ok=true]"
# === test-integration: postgres + valkey + opensearch via K8s services ===
#
# The wfe ServiceProvider creates Pods + K8s Services in the workflow's
# scoped namespace. Step containers reach services via in-cluster DNS:
# postgres → postgres.<namespace>.svc.cluster.local
- id: test-integration
name: Integration Tests
version: 1
outputs:
postgres_ok: bool
valkey_ok: bool
opensearch_ok: bool
services:
postgres:
image: postgres:17
ports: [5432]
env:
POSTGRES_USER: wfe
POSTGRES_PASSWORD: wfe
POSTGRES_DB: wfe_test
readiness:
exec: ["pg_isready", "-U", "wfe"]
interval: 2s
timeout: 60s
retries: 30
memory: 512Mi
cpu: 500m
valkey:
image: valkey/valkey:8
ports: [6379]
readiness:
tcp: 6379
interval: 2s
timeout: 30s
retries: 15
memory: 256Mi
cpu: 250m
opensearch:
image: opensearchproject/opensearch:2
ports: [9200]
env:
discovery.type: single-node
DISABLE_SECURITY_PLUGIN: "true"
OPENSEARCH_INITIAL_ADMIN_PASSWORD: admin
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
readiness:
http: { port: 9200, path: / }
interval: 5s
timeout: 120s
retries: 24
memory: 1536Mi
cpu: "1"
steps:
- name: postgres-tests
type: kubernetes
outputs:
- name: postgres_ok
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
export DATABASE_URL="postgres://wfe:wfe@postgres:5432/wfe_test"
cargo nextest run -p wfe-postgres -P ci
echo "##wfe[output postgres_ok=true]"
- name: valkey-tests
type: kubernetes
outputs:
- name: valkey_ok
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
export VALKEY_URL="redis://valkey:6379"
cargo nextest run -p wfe-valkey -P ci
echo "##wfe[output valkey_ok=true]"
- name: opensearch-tests
type: kubernetes
outputs:
- name: opensearch_ok
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
export OPENSEARCH_URL="http://opensearch:9200"
cargo nextest run -p wfe-opensearch -P ci
echo "##wfe[output opensearch_ok=true]"
# === test-kubernetes: K8s executor + service provisioner ===
#
# These tests need cluster access to create namespaces, pods, jobs, services.
# The wfe ServiceAccount must have RBAC for the necessary verbs.
- id: test-kubernetes
name: Kubernetes Tests
version: 1
outputs:
k8s_integration_ok: bool
steps:
- name: k8s-integration-tests
type: kubernetes
outputs:
- name: k8s_integration_ok
config:
<<: *ci_long_config
run: |
set -euo pipefail
cd /workspace/wfe
# In-cluster auth via the pod's ServiceAccount.
cargo nextest run -p wfe-kubernetes --test integration -P ci
echo "##wfe[output k8s_integration_ok=true]"
# === test (orchestrator) ===
- id: test
name: Tests
version: 1
outputs:
all_passed: bool
steps:
- name: run-unit
type: workflow
outputs:
- name: core_ok
- name: yaml_ok
- name: deno_ok
- name: kubernetes_ok
- name: rustlang_ok
config:
workflow: test-unit
version: 1
- name: run-integration
type: workflow
outputs:
- name: postgres_ok
- name: valkey_ok
- name: opensearch_ok
config:
workflow: test-integration
version: 1
- name: run-kubernetes
type: workflow
outputs:
- name: k8s_integration_ok
config:
workflow: test-kubernetes
version: 1
- name: mark-passed
type: kubernetes
outputs:
- name: all_passed
config:
<<: *ci_config
run: |
echo "All test workflows completed"
echo "##wfe[output all_passed=true]"
# === cover: coverage report with threshold gate ===
- id: cover
name: Coverage
version: 1
inputs:
coverage_threshold: number?
outputs:
line_coverage: number
meets_threshold: bool
services:
postgres:
image: postgres:17
ports: [5432]
env:
POSTGRES_USER: wfe
POSTGRES_PASSWORD: wfe
POSTGRES_DB: wfe_test
readiness:
exec: ["pg_isready", "-U", "wfe"]
timeout: 60s
memory: 512Mi
valkey:
image: valkey/valkey:8
ports: [6379]
readiness:
tcp: 6379
memory: 256Mi
opensearch:
image: opensearchproject/opensearch:2
ports: [9200]
env:
discovery.type: single-node
DISABLE_SECURITY_PLUGIN: "true"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
readiness:
http: { port: 9200, path: / }
timeout: 120s
memory: 1536Mi
steps:
- name: run-coverage
type: kubernetes
outputs:
- name: coverage_json
config:
<<: *ci_long_config
run: |
set -euo pipefail
cd /workspace/wfe
export DATABASE_URL="postgres://wfe:wfe@postgres:5432/wfe_test"
export VALKEY_URL="redis://valkey:6379"
export OPENSEARCH_URL="http://opensearch:9200"
cargo llvm-cov nextest --workspace -P cover --json > /tmp/wfe-coverage.json
echo "##wfe[output coverage_json=/tmp/wfe-coverage.json]"
- name: assert-threshold
type: deno
outputs:
- name: line_coverage
- name: meets_threshold
config:
script: |
const data = inputs();
const threshold = data.coverage_threshold || 85;
const text = await readFile("/tmp/wfe-coverage.json");
const report = JSON.parse(text);
const totals = report.data[0].totals;
const lineCov = (totals.lines.covered / totals.lines.count * 100).toFixed(1);
log(`Line coverage: ${lineCov}% (threshold: ${threshold}%)`);
output("line_coverage", parseFloat(lineCov));
output("meets_threshold", parseFloat(lineCov) >= threshold);
if (parseFloat(lineCov) < threshold) {
throw new Error(`Coverage ${lineCov}% is below threshold ${threshold}%`);
}
permissions:
read: ["/tmp"]
# === tag: read version, create git tag (only on mainline) ===
- id: tag
name: Tag Release
version: 1
outputs:
version: string
tag_created: bool
tag_already_existed: bool
steps:
- name: read-version
type: kubernetes
outputs:
- name: version
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')
echo "Version: $VERSION"
echo "##wfe[output version=$VERSION]"
- name: check-tag-exists
type: kubernetes
outputs:
- name: tag_already_existed
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
git fetch --tags
TAG="v${VERSION}"
if git tag -l "$TAG" | grep -q "$TAG"; then
echo "Tag $TAG already exists"
echo "##wfe[output tag_already_existed=true]"
else
echo "Tag $TAG does not exist"
echo "##wfe[output tag_already_existed=false]"
fi
- name: create-tag
type: kubernetes
when:
field: .outputs.tag_already_existed
equals: false
outputs:
- name: tag_created
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
TAG="v${VERSION}"
git config user.email "wfe-ci@sunbeam.pt"
git config user.name "wfe-ci"
git tag -a "$TAG" -m "$TAG"
git push origin "$TAG"
echo "##wfe[output tag_created=true]"
# === publish: publish all crates to sunbeam registry ===
- id: publish
name: Publish Crates
version: 1
outputs:
all_published: bool
steps:
- name: publish-tier-1
type: kubernetes
config:
<<: *ci_long_config
run: |
set -euo pipefail
cd /workspace/wfe
for crate in wfe-core wfe-containerd-protos wfe-buildkit-protos wfe-server-protos; do
echo "--- Publishing $crate ---"
cargo publish -p "$crate" --registry sunbeam 2>&1 || echo "Already published: $crate"
done
error_behavior:
type: retry
interval: 10s
max_retries: 2
- name: publish-tier-2
type: kubernetes
config:
<<: *ci_long_config
run: |
set -euo pipefail
cd /workspace/wfe
for crate in wfe-sqlite wfe-postgres wfe-opensearch wfe-valkey \
wfe-buildkit wfe-containerd wfe-rustlang wfe-kubernetes; do
echo "--- Publishing $crate ---"
cargo publish -p "$crate" --registry sunbeam 2>&1 || echo "Already published: $crate"
done
error_behavior:
type: retry
interval: 10s
max_retries: 2
- name: publish-tier-3
type: kubernetes
config:
<<: *ci_long_config
run: |
set -euo pipefail
cd /workspace/wfe
sleep 10
for crate in wfe wfe-yaml; do
echo "--- Publishing $crate ---"
cargo publish -p "$crate" --registry sunbeam 2>&1 || echo "Already published: $crate"
done
error_behavior:
type: retry
interval: 10s
max_retries: 2
- name: publish-tier-4
type: kubernetes
outputs:
- name: all_published
config:
<<: *ci_long_config
run: |
set -euo pipefail
cd /workspace/wfe
sleep 10
for crate in wfe-server wfe-deno; do
echo "--- Publishing $crate ---"
cargo publish -p "$crate" --registry sunbeam 2>&1 || echo "Already published: $crate"
done
echo "##wfe[output all_published=true]"
error_behavior:
type: retry
interval: 10s
max_retries: 2
# === image: build wfe-server Docker image via in-cluster buildkitd ===
#
# Connects to buildkitd.build.svc:1234 over mTLS using certs mounted from
# the wfe-credentials secret. The wfe-buildkit step type handles all the
# client-side details.
- id: image
name: Build Image
version: 1
inputs:
version: string
outputs:
image_pushed: bool
image_digest: string
steps:
- name: write-buildkit-certs
type: kubernetes
config:
<<: *ci_config
run: |
set -euo pipefail
mkdir -p /workspace/buildkit-certs
echo "$BUILDKIT_CA_CERT" > /workspace/buildkit-certs/ca.pem
echo "$BUILDKIT_CLIENT_CERT" > /workspace/buildkit-certs/client.pem
echo "$BUILDKIT_CLIENT_KEY" > /workspace/buildkit-certs/client-key.pem
chmod 600 /workspace/buildkit-certs/client-key.pem
- name: build-and-push
type: buildkit
outputs:
- name: image_pushed
- name: image_digest
config:
dockerfile: /workspace/wfe/Dockerfile
context: /workspace/wfe
tags:
- "src.sunbeam.pt/studio/wfe:${VERSION}"
- "src.sunbeam.pt/studio/wfe:latest"
push: true
buildkit_addr: tcp://buildkitd.build.svc:1234
tls:
ca: /workspace/buildkit-certs/ca.pem
cert: /workspace/buildkit-certs/client.pem
key: /workspace/buildkit-certs/client-key.pem
timeout: 30m
# === release: create Gitea release with changelog notes ===
- id: release
name: Gitea Release
version: 1
inputs:
version: string
outputs:
release_created: bool
release_url: string
steps:
- name: extract-changelog
type: deno
outputs:
- name: notes
config:
script: |
const data = inputs();
const version = data.version;
const text = await readFile("/workspace/wfe/CHANGELOG.md");
const lines = text.split("\n");
const notes = [];
let inSection = false;
for (const line of lines) {
if (line.startsWith("## [")) {
if (inSection) break;
if (line.includes(`[${version}]`)) inSection = true;
continue;
}
if (inSection) notes.push(line);
}
const body = notes.join("\n").trim();
log(`Extracted ${body.length} chars of changelog notes`);
output("notes", body);
permissions:
read: ["/workspace/wfe"]
- name: create-gitea-release
type: kubernetes
outputs:
- name: release_created
- name: release_url
config:
<<: *ci_config
run: |
set -euo pipefail
cd /workspace/wfe
TAG="v${VERSION}"
# tea login from env
tea login add --name sunbeam --url https://src.sunbeam.pt --token "$TEA_TOKEN" 2>/dev/null || true
if tea release create --tag "$TAG" --title "$TAG" --note "$NOTES" 2>&1; then
echo "##wfe[output release_created=true]"
else
echo "Release may already exist"
echo "##wfe[output release_created=false]"
fi
echo "##wfe[output release_url=https://src.sunbeam.pt/studio/wfe/releases/tag/${TAG}]"
# === ci: top-level orchestrator -- runs on every push ===
#
# Triggered by Gitea webhook → /webhooks/gitea on wfe-server.
# The webhook handler maps push events to this workflow with:
# inputs: { repo_url, commit_sha, branch }
- id: ci
name: Continuous Integration
version: 1
inputs:
repo_url: string
commit_sha: string
branch: string?
coverage_threshold: number?
outputs:
version: string
all_tests_passed: bool
coverage: number
published: bool
image_pushed: bool
released: bool
steps:
- name: run-checkout
type: workflow
outputs:
- name: commit
config:
workflow: checkout
version: 1
- name: run-lint
type: workflow
outputs:
- name: fmt_ok
- name: clippy_ok
config:
workflow: lint
version: 1
- name: run-tests
type: workflow
outputs:
- name: all_tests_passed
config:
workflow: test
version: 1
- name: run-coverage
type: workflow
outputs:
- name: coverage
config:
workflow: cover
version: 1
# Below this line: only run on mainline (release branch).
- name: run-tag
type: workflow
when:
field: .inputs.branch
equals: mainline
outputs:
- name: version
- name: tag_created
config:
workflow: tag
version: 1
- name: run-publish
type: workflow
when:
field: .outputs.tag_created
equals: true
outputs:
- name: published
config:
workflow: publish
version: 1
- name: run-image
type: workflow
when:
field: .outputs.tag_created
equals: true
outputs:
- name: image_pushed
config:
workflow: image
version: 1
- name: run-release
type: workflow
when:
field: .outputs.tag_created
equals: true
outputs:
- name: released
- name: release_url
config:
workflow: release
version: 1