Skip to main content

CI/CD for Databases on Jenkins (Versioned and Declarative Migrations)

Jenkins pipelines run the Atlas CLI in sh steps. This guide sets up two pipelines, for either workflow:

  • app-ci, a Multibranch Pipeline, checks the schema change on every pull request: it lints new migration files in the versioned workflow, or plans the change in the declarative workflow. When the change merges to main, it starts app-deploy.
  • app-deploy builds only main, one build at a time. It publishes the change to the Atlas Registry, and deploys it to the database.

The database URL is stored where only app-deploy can read it, so pull request builds never receive it. Each step below has a tab per workflow.

Prerequisites​

  • A Jenkins controller with the Pipeline, Folders, and Credentials Binding plugins, and the branch source plugin for your Git host, such as GitHub Branch Source.
  • An agent with the Atlas CLI and Docker. Atlas starts a dev database from a docker:// URL, which needs Docker on the agent (FAQ).
  • An Atlas Cloud bot token, created in Atlas Cloud under Settings > Bots. See Bots.
  • Optional, for coding agents: the Atlas agent skills. See Get Started with a Coding Agent.
note

Migration linting and schema plans are Atlas Pro features. Run atlas login to start a free trial. CI jobs log in with the bot token.

Store the Credentials​

The pipelines read two secrets. Store them as Secret text credentials, or in the secret store your Jenkins reads from, with these IDs:

  • atlas-token: the Atlas Cloud bot token. app-ci and app-deploy log in with it.
  • db-url: the URL of the target database. Only app-deploy reads it, as DATABASE_URL, which atlas.hcl reads with getenv("DATABASE_URL").

Make each secret readable only by the jobs that use it, and db-url by app-deploy alone: app-ci builds pull requests, which must not receive the database URL. In Jenkins, credentials stored in a folder are available only to the jobs in that folder, so this guide uses two: app holds atlas-token and both jobs, and app/deploy holds db-url and app-deploy.

The URL names the public schema with search_path=public, the same scope as the dev database in atlas.hcl below. See URLs for the format of each engine.

Protected flows

Pull request builds log in with atlas-token too. To keep them from pushing to the registry or approving plans, turn on protected flows for the app repository, and give the Writer role only to a second bot, whose token you store in app/deploy for app-deploy to log in with. Protected flows are an Atlas Pro feature.

Configure Atlas​

Add an atlas.hcl with one env per pipeline, then push to the registry once, so the repository exists before the first pull request build. Run the push from the directory of atlas.hcl.

The migration files are in migrations/.

atlas.hcl
env "ci" {
dev = "docker://postgres/17/dev?search_path=public"
migration {
dir = "file://migrations"
}
}

env "deploy" {
url = getenv("DATABASE_URL")
migration {
dir = "atlas://app"
}
}
atlas migrate push app --env ci
  • ci: the migration directory and the dev database that lint and push use.
  • deploy: reads the database URL from DATABASE_URL, and applies the pending migrations of the app repository.

Replace the dev database URLs with ones for your database. See dev database for the URL of each engine.

Check Pull Requests​

app-ci runs this Jenkinsfile for every branch and pull request:

Jenkinsfile
pipeline {
agent any
environment {
ATLAS_TOKEN = credentials('atlas-token')
}
stages {
stage('Log in') {
steps {
sh 'atlas login --token "$ATLAS_TOKEN"'
}
}
stage('Lint') {
when { changeRequest() }
steps {
sh 'atlas migrate lint --env ci --base atlas://app'
}
}
stage('Deploy') {
when { branch 'main' }
steps {
build job: '/app/deploy/app-deploy', wait: false
}
}
}
}
  • Lint runs on pull request builds. --base atlas://app compares the directory with the latest state pushed to app, and lint analyzes only the new files. See changeset detection. Findings print in the build log, and the stage fails when a check reports an error.
  • Deploy runs on main, and starts app-deploy.

credentials('atlas-token') binds the token to ATLAS_TOKEN, and Jenkins masks it in the build log. The sh steps use single quotes, so the shell expands the variable and the token stays out of the Groovy string.

To create the job, add a Multibranch Pipeline named app-ci in the app folder, set your repository as its branch source, and keep the default script path, Jenkinsfile.

Pull requests from forks

A pull request build runs the Jenkinsfile and atlas.hcl from the pull request, and atlas.hcl can run programs, so the build can read every credential the job gets. Configure the branch source to discover pull requests from the repository itself, not from forks.

Publish and Deploy​

app-deploy runs Jenkinsfile.deploy from main:

Jenkinsfile.deploy
pipeline {
agent any
options {
disableConcurrentBuilds()
}
environment {
ATLAS_TOKEN = credentials('atlas-token')
DATABASE_URL = credentials('db-url')
}
stages {
stage('Log in') {
steps {
sh 'atlas login --token "$ATLAS_TOKEN"'
}
}
stage('Push') {
steps {
sh 'atlas migrate push app --env ci'
}
}
stage('Deploy') {
steps {
sh 'atlas migrate apply --env deploy'
}
}
}
}

Push updates the app repository in the registry with the merged migration files, and Deploy applies the pending ones.

disableConcurrentBuilds() runs one build at a time, so each build deploys the change it pushed before the next build pushes another.

To create the job, add a Pipeline named app-deploy in the app/deploy folder. Under Pipeline, choose Pipeline script from SCM, set the branch specifier to */main, and set the script path to Jenkinsfile.deploy.

app-deploy is the only job that can read db-url. It checks out only main, so the code that runs with the database URL is code that was reviewed and merged.

Approve before apply

To review the SQL before it runs, add a stage before Deploy that runs the deploy command with --dry-run to print the pending statements, followed by an input step that waits for approval.

Test the Workflow​

  1. On a new branch, add a migration with atlas migrate new --edit, and open a pull request. app-ci builds the pull request, and the Lint stage prints its findings.
  2. Merge the pull request. app-ci builds main and starts app-deploy, which pushes the new migration to the registry and applies it to the database.

Get Started with a Coding Agent​

With a coding agent, install the Atlas agent skills. One command installs them:

Install the skills from a terminal. It installs the Atlas plugin, with all its skills, for every project on this machine.

claude plugin marketplace add ariga/atlas
claude plugin install atlas@ariga

Then start the onboarding skill in Claude Code. See Claude Code with Atlas for the other ways to use the skills.

/atlas:atlas-onboard

The atlas-action skill covers pipelines that run Atlas, and the agent loads it when it writes or fixes one. To set up Atlas in a new project, start the atlas-onboard skill.

Next Steps​