# CI/CD Integration

> Gate a pull request on cloud cost with l4 estimate and l4 diff, using ready-made workflows for GitHub Actions and GitLab CI.

Source: https://docs.levelfour.ai/guides/ci-cd


The LevelFour CLI (`l4`) prices the Terraform and CloudFormation in a workspace inside a CI job, so a cost regression fails the pull request instead of arriving on next month's bill.

* `l4 estimate` prices everything the workspace defines.
* `l4 diff` prices the change against a baseline.

Both take `--fail-above`. Over the threshold they exit `2`, and that exit code is the gate.

> **Note**
>
> The two thresholds measure different things. On `l4 estimate` the threshold applies to the absolute monthly cost of the whole workspace, so a repository that is already expensive trips it on every pull request. On `l4 diff` it applies only to the change.

Store the API token as a CI secret and expose it as `LEVELFOUR_TOKEN`, as the configurations below do. A cost gate only reads, so a `read` key is enough. [CLI authentication](/cli/authentication) has the resolution order and the read versus read-write split.

## Estimating a workspace

```bash
l4 estimate ./infra/
```

Example output:

```text
Name                                          Quantity  Unit       Monthly Cost

 aws_instance.bastion
 ├─ Instance usage (Linux, t4g.micro)              730  hours            $6.13
 └─ root_block_device
     └─ Storage (General Purpose, gp3)              30  GB               $2.40

 aws_ecs_service.api
 └─ Fargate vCPU                                  0.25  vCPU             $7.39

 aws_db_instance.this
 └─ Instance usage (db.t4g.micro)                  730  hours           $12.41

 Total                                                                 $52.29
```

### Gating on the total

Exit `2` when the estimated monthly cost is above a threshold:

```bash
l4 estimate ./infra/ --fail-above 500 -q
echo $?
```

```text
0
```

With a lower threshold:

```bash
l4 estimate ./infra/ --fail-above 1 -q
echo $?
```

```text
Cost delta $52.29 exceeds threshold $1.00
2
```

> **Success**
>
> Under the threshold the command exits `0`, over it `2`. The `-q` (quiet) flag suppresses the table output and communicates only via exit code, so a runner has one thing to read.

### Formatting for a pull request comment

`--format github-comment` emits a Markdown table you can pipe straight into a comment:

```markdown
## Cost Estimate

| Module | Resource | Type | Cost/mo | Delta |
|--------|----------|------|---------|-------|
|  | + bastion | aws_instance | $8.53 | +$8.53 |
|  | + api | aws_ecs_service | $7.39 | +$7.39 |
| `module.db` | + this | aws_db_instance | $13.98 | +$13.98 |
```

`--out-file baseline.json` writes the resource snapshot instead, for `l4 diff` to compare against later. [`l4 estimate`](/cli/commands/estimate) covers both, along with the `table` and `json` formats.

## Diffing a change

`l4 diff` reports the monthly delta between the workspace and a baseline. Pick the baseline:

| Baseline                                                | Command                           |
| ------------------------------------------------------- | --------------------------------- |
| The git merge-base with `main` or `master`, the default | `l4 diff ./infra/`                |
| A specific git ref                                      | `l4 diff --base develop ./infra/` |
| A snapshot saved by `l4 estimate --out-file`            | `l4 diff baseline.json ./infra/`  |

> **Warning**
>
> The default baseline needs git history. Both runners below check out a shallow clone by default, which leaves no merge-base to compare against, so the configurations set `fetch-depth: 0` on GitHub Actions and `GIT_DEPTH: 0` on GitLab CI. Where a runner has no git history at all, diff against a snapshot.

The same flag gates the delta:

```bash
l4 diff ./infra/ --fail-above 100 --format github-comment
```

## Wiring it into a pipeline

> **Error**
>
> A job with no token exits `4`, not `2`. A gate that branches on any non-zero exit reports a cost breach when the real problem is an unset secret, so branch on `2`. [Exit codes](/cli#exit-codes) has the full set.

Each configuration downloads the release binary in the job. [Installation](/cli/installation) covers Homebrew, `go install`, and pinning a version inside a CI image.

**GitHub Actions**

Comment the estimate on every pull request that touches `infra/`:

```yaml title=".github/workflows/cost-estimate.yml"
name: Cost Estimate
on:
  pull_request:
    paths:
      - 'infra/**'

jobs:
  cost-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install LevelFour CLI
        run: |
          curl -sSL https://github.com/LevelFourAI/levelfour-cli/releases/latest/download/l4_linux_amd64.tar.gz | tar xz
          sudo mv l4 /usr/local/bin/

      - name: Estimate costs
        id: estimate
        env:
          LEVELFOUR_TOKEN: ${{ secrets.LEVELFOUR_TOKEN }}
        run: |
          l4 estimate ./infra/ --format github-comment > cost-estimate.md

      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const body = fs.readFileSync('cost-estimate.md', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body,
            });
```

Gate the delta, and comment either way:

```yaml title=".github/workflows/cost-diff.yml"
name: Cost Diff
on:
  pull_request:
    paths:
      - 'infra/**'

jobs:
  cost-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install LevelFour CLI
        run: |
          curl -sSL https://github.com/LevelFourAI/levelfour-cli/releases/latest/download/l4_linux_amd64.tar.gz | tar xz
          sudo mv l4 /usr/local/bin/

      - name: Cost diff
        id: diff
        env:
          LEVELFOUR_TOKEN: ${{ secrets.LEVELFOUR_TOKEN }}
        run: |
          l4 diff ./infra/ --format github-comment --fail-above 100 > cost-diff.md || echo "exit_code=$?" >> $GITHUB_OUTPUT

      - name: Comment on PR
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const body = fs.readFileSync('cost-diff.md', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body,
            });

      - name: Fail if over threshold
        if: steps.diff.outputs.exit_code == '2'
        run: exit 1
```

**GitLab CI**

Two jobs in one file. The first gates the absolute cost, the second gates the delta.

```yaml title=".gitlab-ci.yml"
cost-estimate:
  stage: validate
  image: ubuntu:latest
  before_script:
    - curl -sSL https://github.com/LevelFourAI/levelfour-cli/releases/latest/download/l4_linux_amd64.tar.gz | tar xz
    - mv l4 /usr/local/bin/
  script:
    - l4 estimate ./infra/ --fail-above 500 --format table
  variables:
    LEVELFOUR_TOKEN: $LEVELFOUR_TOKEN
  rules:
    - changes:
        - infra/**

cost-diff:
  stage: validate
  image: ubuntu:latest
  before_script:
    - curl -sSL https://github.com/LevelFourAI/levelfour-cli/releases/latest/download/l4_linux_amd64.tar.gz | tar xz
    - mv l4 /usr/local/bin/
  script:
    - l4 diff ./infra/ --fail-above 100 --format table
  variables:
    LEVELFOUR_TOKEN: $LEVELFOUR_TOKEN
    GIT_DEPTH: 0
  rules:
    - changes:
        - infra/**
```

## Flags

The defaults that matter in a pipeline:

| Flag              | Default | Description                                                                                            |
| ----------------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `--fail-above`    | `0`     | Exit code 2 if the monthly cost (`l4 estimate`) or the monthly delta (`l4 diff`) exceeds the threshold |
| `--max-resources` | `500`   | Maximum number of resources to include                                                                 |

[`l4 estimate`](/cli/commands/estimate#flags) and [`l4 diff`](/cli/commands/diff#flags) carry the rest: pricing regions, Terraform variables, CloudFormation parameters, remote module downloads and output format.

## Next

* [`l4 estimate`](/cli/commands/estimate) prices a workspace, with every flag and default
* [`l4 diff`](/cli/commands/diff) does the same for a change against a baseline
* [Exit codes](/cli#exit-codes) are what a job should branch on
* [Installation](/cli/installation) covers Homebrew, `go install`, and pinning a version inside a CI image
* [CLI authentication](/cli/authentication) has the token resolution order and the read versus read-write split
* [Output formats](/cli/output-formats) covers `--json`, `--jq`, `--template` and `--csv`
* [GitHub Actions](/guides/github-actions) posts cost reports through the API and SDKs rather than the CLI
