Skip to main content

Jenkins

Run Infracost in Jenkins to post cloud cost estimates and FinOps best practices on every pull request.

Jenkins is VCS-agnostic: it names no host, so the provider and repository URL are yours to set. Everything else comes from a multibranch pipeline's CHANGE_* variables.

1. Add the credentials​

Add three Jenkins credentials:

Credential IDValue
infracost-cli-tokenYour Infracost CLI token
github-tokenSecret text. A token for the VCS your repository lives on
git-credentialsUsername with password. The same token as the password, for the git fetch

Swap the last two for GitLab, Bitbucket or Azure DevOps credentials to match where the repository lives.

The last two hold the same token in different shapes, because they are consumed differently: infracost-ci reads a token from the environment, and git wants a username and a password. On a public repository you only need the first two.

2. Add the pipeline​

pipeline {
agent {
docker {
image 'ghcr.io/infracost/ci:2'
// The plugin holds the container open with `cat`, so clear the entrypoint.
args '--entrypoint='
}
}

environment {
INFRACOST_CLI_AUTHENTICATION_TOKEN = credentials('infracost-cli-token')
GITHUB_TOKEN = credentials('github-token')
INFRACOST_VCS_PROVIDER = 'github'
INFRACOST_VCS_REPOSITORY_URL = 'https://github.com/ORG/REPO'
}

stages {
stage('Infracost') {
when { changeRequest() }
steps {
// An `sh` step does not inherit the checkout's credentials, so the
// fetch below needs them supplied.
withCredentials([gitUsernamePassword(credentialsId: 'git-credentials')]) {
sh '''
# The workspace is reused between builds, so clear old worktrees.
git worktree remove --force base || true
git worktree remove --force head || true
git worktree prune

git fetch origin "$CHANGE_TARGET"
git worktree add base FETCH_HEAD
git worktree add head HEAD
infracost-ci diff --base-path base --head-path head
'''
}
}
}
}
}

diff takes two checkouts of the same repository, computes the cost difference, posts or updates the pull request comment, and uploads the run to Infracost Cloud. No Terraform binary and no plan file: the parsers read your infrastructure code directly.

Set INFRACOST_VCS_PROVIDER to github, gitlab, bitbucket or azure_repos, and the token variable to GITHUB_TOKEN, GITLAB_TOKEN, BITBUCKET_TOKEN or AZURE_DEVOPS_EXT_PAT to match.

caution

Bitbucket users must also set INFRACOST_CI_PLATFORM = 'jenkins'. Any variable starting BITBUCKET_ makes detection guess Bitbucket Pipelines, which drops every CHANGE_* value, so the pull request number and branches come out empty and diff fails.

Four things are specific to Jenkins:

  • The fetch needs credentials of its own. An sh step does not inherit the ones the SCM checkout used, so on a private repository git fetch origin fails with fatal: could not read Username for 'https://github.com'. withCredentials([gitUsernamePassword(...)]) supplies them for the length of the block, using the git-credentials entry above. It needs the Git plugin. On a public repository, drop the wrapper and keep the bare sh.
  • The base worktree points at FETCH_HEAD. Jenkins checks out the pull request ref, so there is no origin/$CHANGE_TARGET to point at.
  • Worktrees are cleared first. The workspace is reused between builds.
  • INFRACOST_VCS_REPOSITORY_URL is the web URL. GIT_URL is a clone URL and may carry credentials, so it is not a safe substitute.
caution

If you set other INFRACOST_VCS_* variables on an earlier version, delete all but the two above. An environment value always beats an inferred one, and Groovy renders an unset env.CHANGE_BRANCH as the literal string null, which becomes the branch, or fails the run when INFRACOST_VCS_PULL_REQUEST_ID gets it.

Detection keys on JENKINS_URL, JENKINS_NODE_COOKIE or JENKINS_HOME, whichever the build has. JENKINS_URL is exported only when the Jenkins URL is set under Manage Jenkins → System, and JENKINS_HOME is controller state that a Docker agent or Kubernetes pod template does not see. Set INFRACOST_CI_PLATFORM = 'jenkins' to pin it when none of the three reach the build.

3. Track your default branch​

Add a stage so Infracost Cloud keeps baseline costs and issues current. This populates Issue Explorer and Visibility → Repos.

stage('Infracost baseline') {
when { branch 'main' }
steps {
sh 'infracost-ci scan --path .'
}
}

scan takes a single directory, unlike diff.

4. Report merged pull requests​

Infracost Cloud calculates cost prevention (money saved by changes made before merging) from merged pull requests. Read the merge commit on the default branch:

stage('Infracost merged status') {
when { branch 'main' }
steps {
sh '''
PR=$(git log -1 --pretty=%B | grep -oE 'Merge pull request #[0-9]+' | grep -oE '[0-9]+$') || true
[ -n "$PR" ] && infracost-ci status --status MERGED --pr-number "$PR" || echo "no pull request in the commit message"
'''
}
}

Adjust the pattern to match your VCS: GitLab uses See merge request .+!([0-9]+), Azure Repos Merged PR ([0-9]+): and Bitbucket pull request #([0-9]+).

caution

Fast-forward merges and customized merge commit templates break the pattern match. The pull request simply won't get merged status; nothing else fails.

Guardrails and blocking​

Guardrails and policies evaluate on every run. When one blocks, the comment posts first and then diff exits 1, so the build goes red with the reason already on the pull request. An unblock from the dashboard takes effect on the next run.

A red build does not stop a merge on its own. Mark the check required on the VCS — on GitHub, Settings → Branches → the branch ruleset → Require status checks to pass.

Your code stays local​

Code is parsed inside the agent. Only the cost estimate and the pull request metadata reach Infracost Cloud.