CI/CD Integration
Put the cost of every infrastructure change in the pull request, where it is still a review comment rather than an invoice. On GitHub, the C3X Action does it in one step: it installs c3x, estimates the pull request and its base branch, posts the change as a comment, and fails the check when the change is over budget. GitLab, Bitbucket, Azure DevOps, Atlantis, and Spacelift use the same CLI, with recipes further down.
GitHub MarketplaceC3X Cost Estimation c3xdev/c3x@v0GitHub Actions
The Action is published on GitHub Marketplace as C3X Cost Estimation and referenced as c3xdev/c3x@v0. No API key and no secrets: it uses the job's own token.
Quick start
Save this as .github/workflows/cost.yml:
name: Cost estimate
on: [pull_request]
permissions:
contents: read
pull-requests: write # post the comment
id-token: write # optional: comment as c3x-cloud[bot]
jobs:
c3x:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: c3xdev/c3x@v0
with:
path: .
budget-delta: "50" # fail the check if this PR adds more than $50/mo@v0 is moved to every stable release, so the workflow picks up fixes and new resources without edits. To pin one release of the binary, set the version input. Drop budget-delta if you only want the comment, and drop id-token: write if you do not want branded comments.
What the comment looks like
Estimate details
On a pull request the comment shows the cost change versus the base branch: the Action checks out the base commit, estimates it, and diffs the two. It leads with the dollar change, then the baseline, new, and change figures, with the per-resource breakdown collapsed under Estimate details so a large change does not flood the thread. Later pushes update the same comment in place.
When path is a plan JSON, the plan's own prior state is the baseline, so no base-branch checkout is involved. If the path did not exist on the base branch, the baseline is $0 and the whole total counts as added. If the base branch cannot be estimated, the comment falls back to the absolute total and the workflow log says why. Every comment ends with a short footer noting that the figures are public list prices before discounts, with a link to how to read them.
Inputs
| Input | Description |
|---|---|
pathdefault . | Terraform or OpenTofu directory, plan JSON, or CloudFormation template to estimate. |
commentdefault true | Post the cost comment on the pull request, and update it in place on later pushes. Needs pull-requests: write. |
budget-deltadefault 0 | Fail the check (after the comment is posted) when this pull request raises the monthly total by more than this amount versus the base branch. 0 disables it. Independent of budget. |
budgetdefault 0 | Fail the check (after the comment is posted) when the monthly total exceeds this amount. 0 disables it. |
strictdefault false | Fail (exit code 3) when any line of the estimate rests on an assumption: a price from another region, usage not provided, an attribute that could not be evaluated. The comment is posted first, with the caveats listed. |
branded-commentsdefault auto | Post as c3x-cloud[bot] instead of github-actions. auto does so when the C3X Cloud app is installed and the workflow grants id-token: write, and otherwise posts as github-actions quietly. true warns when it cannot; false never tries and makes no request to token.c3x.dev. |
untrusteddefault auto | Untrusted-input mode: no remote module fetches, no file functions, and the repository's .c3x.toml cannot redirect pricing or file access. auto turns it on for pull requests from forks; true or false force it. |
formatdefault text | Output format for the workflow log: text, markdown, json, junit, html, csv, or sarif. The PR comment is always Markdown. |
currencydefault unset | Display currency (USD, EUR, GBP, ...). Unset means USD. |
versiondefault latest | c3x release to install, for example a tag from the releases page. latest resolves the newest release. |
install-onlydefault false | Only install the binary and put it on PATH; skip the estimate, comment, and gates so you can run c3x yourself. |
token-service | Token service used for branded comments. Advanced; leave the default, https://token.c3x.dev. |
Cost gates
Three inputs turn the estimate into a required check. The comment is always posted; then the check fails if a gate is exceeded, so reviewers see the numbers that failed it. Mark the job as a required status check in branch protection to block the merge.
budget-deltacaps what one pull request may add per month, measured against the base branch. It catches an expensive change even when the project total is still under budget, which makes it the gate most teams want.budgetcaps the absolute monthly total of the project after the change.strictfails with exit code 3 when any line rests on an assumption; the comment lists which lines are uncertain.
- uses: c3xdev/c3x@v0
with:
path: .
budget-delta: "50" # fail if this PR adds more than $50/mo
budget: "1000" # fail if the total exceeds $1000/mo
strict: true # fail if any line is an assumptionThe same gates work from the CLI anywhere else: c3x estimate --budget 1000 for the total, and c3x diff --budget-delta 50 for the increase (against a plan JSON, or a baseline saved with c3x estimate --save-baseline base.json).
Estimate from the plan JSON
A directory is evaluated statically, with no cloud credentials. That is convenient, but values Terraform only knows at plan time, such as data sources and attributes computed from them, are estimated. If the workflow already runs terraform plan, give c3x the plan instead: it has the real values, so it is the most accurate input.
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false # keep plan.json clean JSON
- run: terraform init -input=false
- run: terraform plan -out=tfplan && terraform show -json tfplan > plan.json
- uses: c3xdev/c3x@v0
with:
path: plan.json
budget-delta: "50" # the plan's prior state is the baselineThe plan step needs whatever credentials your plan already uses. OpenTofu works the same way with tofu plan and tofu show -json.
Branded comments
Every comment carries the c3x mark and footer. By default it is posted by the github-actions bot. To post it as c3x-cloud[bot], with the c3x avatar, install the C3X Cloud GitHub App on the repository and keep id-token: write in the workflow permissions, as in the quick start. Nothing else changes: branded-comments defaults to auto.
- With the app installed and
id-token: writegranted, the Action exchanges the job's GitHub OIDC token attoken.c3x.devfor a short-lived token scoped to that one repository. The service verifies the OIDC token and stores nothing. There are no secrets to create. - Without the app, or without
id-token: write, the comment is posted asgithub-actions. Withoutid-token: writeno request is made totoken.c3x.devat all. branded-comments: falsenever makes the request;truebehaves likeautobut logs a warning when it has to fall back.
Pull requests from forks
A fork's Terraform is code you do not control, so with the default untrusted: auto the Action estimates fork pull requests in untrusted-input mode: no remote module fetches, no file functions, and the repository's .c3x.toml cannot redirect pricing or file access. The mode applies to later c3x calls in the job as well. Set untrusted: true to use it on every run. Note that GitHub gives pull_request workflows from forks a read-only token, so the comment step cannot post on those.
Install only, run c3x yourself
With install-only: true the Action just installs the binary and adds it to PATH. That is the way to run several estimates in one job, for example one comment per environment, kept apart with --comment-tag:
- uses: c3xdev/c3x@v0
with:
install-only: true
- run: |
c3x comment github --path envs/prod --comment-tag prod
c3x comment github --path envs/staging --comment-tag staging
env:
GITHUB_TOKEN: ${{ github.token }}Run this way, each comment shows the absolute total for its directory; pass a plan JSON as --path, or --baseline with a saved estimate, to show the change. See the CLI reference for every c3x comment flag.
How the Action installs c3x
The binary is downloaded from the GitHub release, checked against the release's published SHA-256 checksums before it runs (a mismatch or a missing entry fails the step), and kept in the runner tool cache, so self-hosted runners download each version once. The release is resolved with the job's token, so busy shared runners do not hit GitHub's anonymous API rate limit.
GitLab CI
Add a cost-estimation stage to your .gitlab-ci.yml.
stages:
- validate
- cost-estimation
- plan
- apply
cost-estimation:
stage: cost-estimation
image: alpine:latest
before_script:
- apk add --no-cache curl tar
- curl -fsSL https://c3x.dev/install.sh | sh
script:
# Posts the comment; project / MR / API URL auto-detect from GitLab CI
- c3x comment gitlab --path .
- c3x estimate --path . --budget 5000
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"The token is read from $GITLAB_TOKEN (or $CI_JOB_TOKEN). Set a GITLAB_TOKEN variable holding a project access token with the api scope in Settings > CI/CD > Variables to write merge request comments. Self-managed GitLab is detected from CI_API_V4_URL.
Bitbucket Pipelines
Add a cost estimation step to your bitbucket-pipelines.yml.
pipelines:
pull-requests:
'**':
- step:
name: Cost Estimation
image: alpine:latest
script:
# Install C3X
- apk add --no-cache curl tar
- curl -fsSL https://c3x.dev/install.sh | sh
# Post PR comment (workspace / repo / PR auto-detect in CI)
- c3x comment bitbucket --path .
# Budget gate
- c3x estimate --path . --budget 5000Set BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD as repository or workspace variables (with pull request read and write access). c3x reads them automatically.
Azure Pipelines
Add a cost estimation job to your azure-pipelines.yml.
trigger: none
pr:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
- script: |
curl -fsSL https://c3x.dev/install.sh | sh
displayName: Install C3X
- script: |
c3x comment azuredevops --path .
displayName: Post PR comment
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
- script: |
c3x estimate --path . --budget 5000
displayName: Budget gatec3x reads the token from SYSTEM_ACCESSTOKEN (or AZURE_DEVOPS_TOKEN); organisation, project, repository, and PR auto-detect from the Azure Pipelines environment. Ensure the build service account has Contribute to pull requests permission. c3x comment ado is a shorter alias.
Atlantis
Atlantis already plans every pull request, so price that plan: add a custom workflow in your atlantis.yaml. Output from run steps appears in the plan comment Atlantis posts, and a failing step blocks the apply.
version: 3
projects:
- dir: .
workflow: c3x
workflows:
c3x:
plan:
steps:
- init
- plan
# c3x reads plan JSON; $PLANFILE is Terraform's binary plan
- run: terraform show -json $PLANFILE > $SHOWFILE
- run: c3x estimate --path $SHOWFILE --budget 5000
- run: c3x diff --path $SHOWFILE --budget-delta 200Install C3X on the Atlantis server, or use a custom image that includes both Terraform and C3X. Repo-level workflows need allowed_overrides: [workflow] and allow_custom_workflows: true in the server-side repo config; alternatively define the workflow server-side. With a plan JSON, c3x diff needs no baseline: the plan's prior state is the baseline.
Spacelift
Use C3X in Spacelift hooks: after_plan to report the cost and before_apply to gate the deployment on it. Hooks run in the stack's runner image, so add c3x to that image:
FROM public.ecr.aws/spacelift/runner-terraform:latest
COPY --from=ghcr.io/c3xdev/c3x:latest /usr/local/bin/c3x /usr/local/bin/c3x# .spacelift/config.yml
version: "1"
stack_defaults:
runner_image: registry.example.com/spacelift-runner-c3x:latest
after_plan:
- c3x estimate --path . --format text
before_apply:
- c3x estimate --path . --budget 5000The before_apply hook acts as a budget gate: if the estimate exceeds the threshold, C3X exits with code 1 and the apply is blocked. The same hooks can also be set in the stack settings instead of the runtime config file.
Gating the change on other platforms
The recipes above gate the absolute total with --budget. To gate what a merge request adds, as budget-delta does on GitHub, estimate a plan JSON: c3x diff --path plan.json --budget-delta 200 takes its baseline from the plan, and c3x comment gitlab --path plan.json (or bitbucket, azuredevops) posts the change rather than the total. Without a plan, save a baseline from the target branch with c3x estimate --save-baseline base.json and pass it as --baseline base.json to both commands.
Scheduled Teardown of Expensive Environments
A common way to cut development spend is to shut the costly parts of an environment down overnight or over the weekend. c3x top --format targets produces the list of destroy targets, ranked by monthly cost, so a scheduled job can tear down the few resources that account for most of the bill while leaving the rest of the environment intact.
# GitHub Actions: shut down the five costliest resources each weeknight
name: nightly-teardown
on:
schedule:
- cron: "0 20 * * 1-5" # 20:00 UTC, Monday to Friday
workflow_dispatch:
jobs:
teardown:
runs-on: ubuntu-latest
environment: dev # require an approval/environment guard
steps:
- uses: actions/checkout@v5
- uses: opentofu/setup-opentofu@v1
- run: curl -fsSL https://c3x.dev/install.sh | sh
- name: Show what will be destroyed
run: c3x top --path envs/dev --limit 5
- name: Destroy the five costliest resources
working-directory: envs/dev
run: |
tofu init -input=false
tofu destroy -auto-approve $(c3x top --limit 5 --format targets)The same pattern works in GitLab CI on a pipeline schedule:
nightly-teardown:
stage: teardown
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
script:
- c3x top --path envs/dev --limit 5
- cd envs/dev && tofu init -input=false
- tofu destroy -auto-approve $(c3x top --limit 5 --format targets)C3X only produces the list, it never destroys anything itself. Treat the destroy step with the same care as any other automated teardown: scope it to a non-production environment, put an approval or protected environment in front of it, and remember that -target is a blunt instrument, since removing a resource that others depend on can break the rest of the stack. Running c3x top on its own first, as in the example above, records in the job log exactly what the destroy will act on.
Tips
- Use
--format json > c3x.jsonto save the estimate for downstream steps or artifact storage. - Use
--budgetfor absolute cost gates andc3x diff --budget-deltafor incremental cost gates. - Provide a usage file for accurate estimates of variable-cost resources.
- For air-gapped CI environments, see the Self-Hosted & Offline guide.
- See the CLI Reference for the full list of flags available on each command.