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 tomain, it startsapp-deploy.app-deploybuilds onlymain, 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.
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-ciandapp-deploylog in with it.db-url: the URL of the target database. Onlyapp-deployreads it, asDATABASE_URL, whichatlas.hclreads withgetenv("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.
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.
- Versioned
- Declarative
The migration files are in migrations/.
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 fromDATABASE_URL, and applies the pending migrations of theapprepository.
The desired schema is in schema.sql.
env "ci" {
dev = "docker://postgres/17/dev?search_path=public"
schema {
src = "file://schema.sql"
repo {
name = "app"
}
}
}
env "deploy" {
url = getenv("DATABASE_URL")
dev = "docker://postgres/17/dev?search_path=public"
schema {
src = "atlas://app"
repo {
name = "app"
}
}
}
atlas schema push app --env ci
ci:atlas schema planplans the change from the latest schema pushed to the registry toschema.sql. See pre-planning schema migrations.deploy: reads the database URL fromDATABASE_URL, and applies the latest schema pushed to theapprepository with the plan approved for it.
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:
- Versioned
- Declarative
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://appcompares the directory with the latest state pushed toapp, 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 startsapp-deploy.
pipeline {
agent any
environment {
ATLAS_TOKEN = credentials('atlas-token')
}
stages {
stage('Log in') {
steps {
sh 'atlas login --token "$ATLAS_TOKEN"'
}
}
stage('Plan') {
when { changeRequest() }
steps {
sh 'atlas schema plan --env ci --dry-run'
}
}
stage('Deploy') {
when { branch 'main' }
steps {
build job: '/app/deploy/app-deploy', wait: false
}
}
}
}
- Plan runs on pull request builds. It prints the planned SQL and its lint findings in the build log, without saving the plan.
- Deploy runs on
main, and startsapp-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.
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:
- Versioned
- Declarative
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.
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 schema plan --env ci --auto-approve'
sh 'atlas schema push app --env ci'
}
}
stage('Deploy') {
steps {
sh 'atlas schema apply --env deploy'
}
}
}
}
Push saves the plan for the merged change in the registry as approved, and pushes the new schema.
atlas schema apply runs the approved plan when the database is in the state the plan starts from. In
any other state, it plans the change again and waits for an approval that a build can't give, so the
build fails before any SQL runs.
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.
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
- Versioned
- Declarative
- On a new branch, add a migration with
atlas migrate new --edit, and open a pull request.app-cibuilds the pull request, and the Lint stage prints its findings. - Merge the pull request.
app-cibuildsmainand startsapp-deploy, which pushes the new migration to the registry and applies it to the database.
- On a new branch, change
schema.sql, and open a pull request.app-cibuilds the pull request, and the Plan stage prints the planned SQL and its findings. - Merge the pull request.
app-cibuildsmainand startsapp-deploy, which approves the plan, pushes the schema, and applies the plan to the database.
Get Started with a Coding Agent
With a coding agent, install the Atlas agent skills. One command installs them:
- Claude Code
- OpenAI Codex
- Cursor
- GitHub Copilot
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
Install the skills from a terminal. Run it from the repository root. It installs every Atlas skill in .agents/skills/.
npx skills add ariga/atlas -a codex -y
Then start the onboarding skill in OpenAI Codex. See OpenAI Codex with Atlas for the other ways to use the skills.
$atlas-onboard
Install the skills from a terminal. Run it from the repository root. It installs every Atlas skill in .agents/skills/.
npx skills add ariga/atlas -a cursor -y
Then start the onboarding skill in Cursor. See Cursor with Atlas for the other ways to use the skills.
/atlas-onboard
Install the skills from a terminal. Run it from the repository root. It installs every Atlas skill in .agents/skills/.
npx skills add ariga/atlas -a github-copilot -y
Then start the onboarding skill in GitHub Copilot. See GitHub Copilot with Atlas for the other ways to use the skills.
/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
Setup CI/CD
The steps of a versioned migrations pipeline, from lint to deployment
Schema Plans
Plan, review, and approve declarative migrations before they run
Migration Linting
Analyzers, lint policies, and how lint picks the files to analyze
Bot Tokens
Create bot tokens and keep CI working when Atlas Cloud is unreachable
Working with Secrets
Read database credentials from secret stores in atlas.hcl