Skip to content

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@v0

GitHub 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

c3x-cloudbotcommented
c3xC3X cost estimate
Monthly cost increased by $280.32 (+27.0%)
Baseline
New
Change
$1038.32/mo
$1318.64/mo
🔺 +$280.32
Estimate details
🟡 Modified
Resource
Baseline
Current
Δ
aws_instance.web
$285.32
$565.64
🔺 +$280.32
Cost estimate by c3x, free and open source. Public list prices before discounts; how to read it.

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

InputDescription
pathdefault .Terraform or OpenTofu directory, plan JSON, or CloudFormation template to estimate.
commentdefault truePost the cost comment on the pull request, and update it in place on later pushes. Needs pull-requests: write.
budget-deltadefault 0Fail 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 0Fail the check (after the comment is posted) when the monthly total exceeds this amount. 0 disables it.
strictdefault falseFail (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 autoPost 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 autoUntrusted-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 textOutput format for the workflow log: text, markdown, json, junit, html, csv, or sarif. The PR comment is always Markdown.
currencydefault unsetDisplay currency (USD, EUR, GBP, ...). Unset means USD.
versiondefault latestc3x release to install, for example a tag from the releases page. latest resolves the newest release.
install-onlydefault falseOnly install the binary and put it on PATH; skip the estimate, comment, and gates so you can run c3x yourself.
token-serviceToken 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-delta caps 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.
  • budget caps the absolute monthly total of the project after the change.
  • strict fails 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 assumption

The 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 baseline

The 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: write granted, the Action exchanges the job's GitHub OIDC token at token.c3x.dev for 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 as github-actions. Without id-token: write no request is made to token.c3x.dev at all.
  • branded-comments: false never makes the request; true behaves like auto but 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 5000

Set 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 gate

c3x 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 200

Install 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 5000

The 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.json to save the estimate for downstream steps or artifact storage.
  • Use --budget for absolute cost gates and c3x diff --budget-delta for 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.