CI/CD setup
Automate SteadyCron deployments with GitHub Actions — plan diffs on pull requests, apply on merge. Works with both YAML manifests and Terraform.
Running a plan on every pull request and applying on merge turns your IaC configuration into a GitOps workflow: reviewers see exactly what cron changes a PR introduces, and the main branch is always the source of truth.
The steadycron/action GitHub Action supports
both IaC tools via the tool input:
yaml(default) — YAML manifest managed by the SteadyCron CLIterraform— HCL resources managed by the Terraform provider
Step 1 — add the API key as a secret
In your GitHub repository, go to Settings → Secrets and variables → Actions and add:
| Name | Value |
|---|---|
STEADYCRON_API_KEY | Your SteadyCron API key (sc_...) |
Add any other environment-specific secrets your manifest references (e.g. SLACK_WEBHOOK_URL).
For plan-only pipelines (read operations), you can create a separate read-only API key and use it exclusively in the PR workflow.
YAML manifest
Workflow 1 — plan diff on pull request
Post the plan output as a PR comment whenever a manifest file changes:
# .github/workflows/cron-plan.yml
name: Cron plan
on:
pull_request:
paths:
- 'manifests/**'
jobs:
plan:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: steadycron/action@v1
with:
command: plan
manifest: manifests/production.yaml
comment-on-pr: 'true'
env:
STEADYCRON_API_KEY: ${{ secrets.STEADYCRON_API_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
The action posts a comment like:
SteadyCron plan — manifests/production.yaml
~ weekly-digest update retries: 2 → 3
+ invoice-cron create http 0 17 * * 5
1 to create · 1 to update · 0 to delete
Workflow 2 — apply on merge
Apply the manifest (with --prune) whenever the main branch is updated:
# .github/workflows/cron-apply.yml
name: Cron apply
on:
push:
branches:
- main
paths:
- 'manifests/**'
jobs:
apply:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: steadycron/action@v1
with:
command: apply
manifest: manifests/production.yaml
prune: 'true'
env:
STEADYCRON_API_KEY: ${{ secrets.STEADYCRON_API_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Multiple environments
Manage separate staging and production manifests with a matrix:
jobs:
apply:
runs-on: ubuntu-latest
strategy:
matrix:
env: [staging, production]
steps:
- uses: actions/checkout@v4
- uses: steadycron/action@v1
with:
command: apply
manifest: manifests/${{ matrix.env }}.yaml
prune: 'true'
env:
STEADYCRON_API_KEY: ${{ secrets[format('STEADYCRON_API_KEY_{0}', matrix.env)] }}
Validate on every push
Add a validate step to catch schema errors before plan runs:
- uses: steadycron/action@v1
with:
command: validate
manifest: manifests/production.yaml
env:
STEADYCRON_API_KEY: ${{ secrets.STEADYCRON_API_KEY }}
validate exits non-zero on schema errors and does not require an API key, but passing
one allows the server to validate against your account’s current limits.
Terraform
The same action handles Terraform workflows. Set tool: terraform and point
working-directory at your .tf files — the action installs Terraform,
runs terraform init, then plan or apply.
Workflow 1 — plan diff on pull request
# .github/workflows/cron-tf-plan.yml
name: Cron Terraform plan
on:
pull_request:
paths:
- 'infra/steadycron/**'
jobs:
plan:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: steadycron/action@v1
with:
tool: terraform
command: plan
working-directory: infra/steadycron
comment-on-pr: 'true'
env:
STEADYCRON_API_KEY: ${{ secrets.STEADYCRON_API_KEY }}
The action posts a sticky comment summarising resource adds, changes, and destroys,
with the full plan output collapsed in a <details> block.
Workflow 2 — apply on merge
# .github/workflows/cron-tf-apply.yml
name: Cron Terraform apply
on:
push:
branches:
- main
paths:
- 'infra/steadycron/**'
jobs:
apply:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: steadycron/action@v1
with:
tool: terraform
command: apply
working-directory: infra/steadycron
env:
STEADYCRON_API_KEY: ${{ secrets.STEADYCRON_API_KEY }}
Plan then apply in one job
Running plan and apply as separate steps in the same job ensures apply uses exactly the plan that was reviewed:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: steadycron/action@v1
id: plan
with:
tool: terraform
command: plan
working-directory: infra/steadycron
env:
STEADYCRON_API_KEY: ${{ secrets.STEADYCRON_API_KEY }}
- if: steps.plan.outputs.has-drift == 'true'
uses: steadycron/action@v1
with:
tool: terraform
command: apply
working-directory: infra/steadycron
env:
STEADYCRON_API_KEY: ${{ secrets.STEADYCRON_API_KEY }}
When a tfplan file is present from a prior plan step in the same job, apply uses
it directly — no second plan is run and no -auto-approve flag is needed.
Remote state backend
Pass -backend-config lines (one per line) for runtime backend configuration:
- uses: steadycron/action@v1
with:
tool: terraform
command: plan
working-directory: infra/steadycron
backend-config: |
bucket=my-tfstate-bucket
key=steadycron/terraform.tfstate
region=eu-central-1
env:
STEADYCRON_API_KEY: ${{ secrets.STEADYCRON_API_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Action inputs
Shared
| Input | Description | Default |
|---|---|---|
tool | yaml or terraform | yaml |
command | plan or apply (both tools); validate, sync (yaml only) | plan |
comment-on-pr | Post the plan output as a sticky PR comment | false |
YAML tool
| Input | Description | Default |
|---|---|---|
manifest | Path to the manifest file or directory | . |
prune | Delete server resources not in the manifest | false |
namespace | Account namespace (required with --prune across multiple namespaces) | — |
cli-version | Pin a specific CLI version, e.g. 1.4.0 | latest |
env-file | Path to a .env file for ${VAR} substitution | — |
All ${ENV_VAR} placeholders in your manifest are read from the job’s environment —
pass them via the env: block in your workflow step.
Terraform tool
| Input | Description | Default |
|---|---|---|
working-directory | Directory containing .tf files | . |
terraform-version | Pin a specific Terraform version, e.g. 1.8.0 | latest |
backend-config | Newline-separated -backend-config=key=value lines | — |
var-file | Path to a .tfvars file passed to plan/apply | — |
Action outputs
| Output | Description |
|---|---|
has-drift | "true" when the plan detected changes (both tools) |
plan-json | Raw plan JSON — SteadyCron format (yaml) or terraform show -json (terraform) |
plan-output | Human-readable plan text (terraform only) |
Least-privilege API keys
For plan-only workflows (PR comments), use a read-only API key that cannot make changes.
Create a separate key in the dashboard with read scope only and store it as a separate
secret (e.g. STEADYCRON_API_KEY_RO).
This limits the blast radius if the PR workflow secret is ever compromised.
Related
- YAML & CLI — all CLI commands explained
- Terraform provider — managing jobs with HCL
- From dashboard to code — adopt an existing account into YAML IaC
- Manifest reference — full YAML field reference