CI / CD Integration
strata is CI-friendly by design. It provides deterministic exit codes, machine-readable JSON output, and environment variable configuration — everything you need to integrate infrastructure validation and deployment into automated pipelines.
Exit Codes
Every strata command returns one of five exit codes. Use these to control pipeline flow:
Code |
Meaning |
CI Behaviour |
|---|---|---|
|
Success |
Continue pipeline |
|
System / execution failure (crash, missing file, init error) |
Fail the build |
|
Usage error — invalid CLI arguments |
Fail the build (fix your pipeline script) |
|
Validation failure — file processed but schema-invalid |
Fail the PR gate; block merge |
|
Lock conflict — deployment locked by another process |
Retry deployment after delay (use backoff) |
Tip: Exit code
3is the most actionable in CI — it means the YAML was parsed but failed validation. Display the JSON output as a PR comment so authors can fix issues without digging through logs.
Machine-Readable Output
Pass --output json to any command to get structured JSON on stdout:
{
"success": true,
"data": {},
"errors": [],
"messages": []
}
When success is false, the errors array contains structured error objects:
{
"success": false,
"data": null,
"errors": [
{
"code": "VALIDATION_ERROR",
"message": "Field 'spec.provider' is required",
"file": "deploy/deploy-prd.yaml",
"line": 12
}
],
"messages": []
}
In CI scripts, parse this with jq, PowerShell ConvertFrom-Json, or your language’s JSON library.
Environment Variables
Set these once in your pipeline environment to avoid repeating flags on every command:
Variable |
Equivalent Flag |
Description |
|---|---|---|
|
|
Default deployment file path |
|
|
Workspace root directory |
|
|
Output format ( |
# GitHub Actions example
env:
STRATA_FILE: deploy/deploy-prd.yaml
STRATA_WORK_PATH: ${{ github.workspace }}
GitHub Actions
A complete workflow that validates on PRs, posts diff output as a comment, and deploys on merge to main:
name: Infrastructure CI
on:
pull_request:
paths: ['deploy/**', 'envs/**', 'stack/**']
push:
branches: [main]
env:
STRATA_FILE: deploy/deploy-prd.yaml
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv tool install xyz-strata
- run: strata validate --file $STRATA_FILE --output json
id: validate
- run: strata build plan --file $STRATA_FILE --output json > plan.json
if: github.event_name == 'pull_request'
- uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
const fs = require('fs');
const diff = fs.readFileSync('plan.json', 'utf8');
const body = `### 🔍 strata build plan\n\`\`\`json\n${diff}\n\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: validate
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write # required for OIDC; harmless when using client-secret mode
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv tool install xyz-strata
- uses: huybrechtsxyz/strata/.github/actions/azure-login@v0
with:
azure_tenant_id: ${{ vars.AZURE_TENANT_ID }}
azure_subscription_id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
azure_client_id: ${{ vars.AZURE_CLIENT_ID }}
# azure_client_secret: ${{ secrets.AZURE_CLIENT_SECRET }} # omit for OIDC
- run: strata build run --file $STRATA_FILE
- run: strata deploy run --file $STRATA_FILE --force
Azure authentication
The azure-login composite action supports two authentication modes, selected automatically from the inputs you provide. In OIDC mode (preferred), no secret is stored in GitHub — Azure issues a short-lived token scoped to that specific run, which is auditable in Azure Entra ID sign-in logs; this requires permissions: id-token: write on the calling job and a federated credential configured in your App Registration (see the Setting Up Azure OIDC guide). In client-secret mode (fallback), pass azure_client_secret: ${{ secrets.AZURE_CLIENT_SECRET }} — the action detects the input and switches modes automatically, with no other changes required. If no Azure stores are configured for your deployment, omit all azure_* inputs and remove the login step entirely.
Tip: Start with client-secret mode to validate the role assignments and pipeline wiring, then switch to OIDC once everything works — it requires only removing the secret and adding the federated credential in Azure.
Azure Pipelines
An equivalent pipeline for Azure DevOps:
trigger:
branches:
include: [main]
paths:
include:
- deploy/*
- envs/*
- stack/*
pr:
paths:
include:
- deploy/*
- envs/*
- stack/*
variables:
STRATA_FILE: deploy/deploy-prd.yaml
stages:
- stage: Validate
jobs:
- job: ValidateConfig
pool:
vmImage: ubuntu-latest
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.13'
- script: |
pip install uv
uv tool install xyz-strata
displayName: install xyz-strata
- script: strata validate --file $(STRATA_FILE) --output json
displayName: Validate deployment file
- script: strata build plan --file $(STRATA_FILE) --output json > $(Build.ArtifactStagingDirectory)/plan.json
displayName: Generate plan
condition: eq(variables['Build.Reason'], 'PullRequest')
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: $(Build.ArtifactStagingDirectory)/plan.json
ArtifactName: plan
condition: eq(variables['Build.Reason'], 'PullRequest')
displayName: Publish plan artifact
- stage: Deploy
dependsOn: Validate
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployProduction
pool:
vmImage: ubuntu-latest
environment: production
strategy:
runOnce:
deploy:
steps:
- checkout: self
- script: |
pip install uv
uv tool install xyz-strata
displayName: install xyz-strata
- script: strata build run --file $(STRATA_FILE)
displayName: Build artifacts
- script: strata deploy run --file $(STRATA_FILE) --force
displayName: Deploy to production
Tips
Cache uv packages
Speed up pipeline runs by caching the uv tool directory:
# GitHub Actions
- uses: actions/cache@v4
with:
path: ~/.local/share/uv
key: uv-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
# Azure Pipelines
- task: Cache@2
inputs:
key: 'uv | "$(Agent.OS)" | **/pyproject.toml'
path: $(HOME)/.local/share/uv
Run in Docker
If you publish a strata CLI image, skip the install step entirely:
# GitHub Actions
jobs:
validate:
runs-on: ubuntu-latest
container: ghcr.io/org/strata-cli:latest
steps:
- uses: actions/checkout@v4
- run: strata validate --file $STRATA_FILE --output json
Parallelise validation across multiple files
When your workspace contains multiple deployment files, validate them in parallel:
# GitHub Actions — matrix strategy
jobs:
validate:
runs-on: ubuntu-latest
strategy:
matrix:
file:
- deploy/deploy-prd.yaml
- deploy/deploy-stg.yaml
- deploy/deploy-dev.yaml
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv tool install xyz-strata
- run: strata validate --file ${{ matrix.file }} --output json
Suppress console noise
Use --quiet in deploy steps where you only care about the exit code:
strata deploy run --file $STRATA_FILE --force --quiet
Or combine --output json with --quiet to get only the JSON envelope with no progress messages on stderr.
State Locking in CI
Locking prevents two pipelines from deploying or destroying the same environment at the
same time. Enable it by adding a locking block to your deployment file:
spec:
locking:
enabled: true
strategy: wrap # strata acquires the lock (default)
wait_timeout: "10m" # how long a second pipeline waits before failing
When a second pipeline starts while a deploy is in progress, strata exits with code 1 and prints:
🔒 Could not acquire lock — held by 'ci-bot@runner-07'.
Run `strata deploy lock status` for details.
Recovering from a crashed pipeline
If a pipeline is killed (OOM, timeout, node reboot) the lock may stay held. Use
--force-lock on the next run to break the stale lock and proceed:
# GitHub Actions
strata deploy run --file $STRATA_FILE --force --force-lock
# Azure Pipelines
strata deploy run --file $(STRATA_FILE) --force --force-lock
Or use the standalone release command (useful from a maintenance step):
strata deploy lock release --file $STRATA_FILE --force
GitHub Actions — concurrent run guard
Combine concurrency: with strata locking for defence-in-depth:
# .github/workflows/deploy.yml
jobs:
deploy:
concurrency:
group: deploy-production
cancel-in-progress: false # queue, not cancel
steps:
- run: strata deploy run --file deploy/deploy-prd.yaml --force
The concurrency group ensures only one GitHub Actions run is active at a time.
strata’s lock provides a second layer that protects against runs from different
workflow files, manual triggers, or external CI systems hitting the same environment.
Azure Pipelines — exclusive lock
Use an Azure DevOps Exclusive Lock on the environment resource in combination
with strata locking:
- stage: Deploy
jobs:
- deployment: DeployProduction
environment:
name: production
resourceType: VirtualMachine # or Kubernetes, etc.
# ADO environments support an "Exclusive Lock" check in the UI —
# enable it to prevent concurrent deployments within this pipeline.
strategy:
runOnce:
deploy:
steps:
- script: strata deploy run --file deploy/deploy-prd.yaml --force
displayName: Deploy to production
Checking lock status from CI
# Exit 0 if locked, 1 if not
strata deploy lock status --file $STRATA_FILE --output json
# Show history (useful in post-failure diagnostics)
strata deploy lock history --file $STRATA_FILE --last 5
For a full walk-through of lock backends, strategies, and manual management see How Deployment Locking Works.