Use whenever a schema is managed with Liquibase: changelogs and changesets in XML, YAML, JSON or SQL, update, status, rollback and tag, preconditions, contexts and labels, and the changelog lock tables, for portable migrations across databases with fine rollback control.
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Liquibase's distinguishing feature vs Flyway: changesets are format-portable (XML / YAML / JSON / SQL) and rollback-aware - each changeset declares its own rollback action, enabling deterministic per-changeset rollback rather than versioned-only migration replay.
State tracked in two tables (per Liquibase docs):
DATABASECHANGELOG - applied changeset history (id + author +
filename + dateExecuted + checksum)DATABASECHANGELOGLOCK - distributed lock to serialize concurrent
update runsdb.changelog-master.xml / .yaml / .json.Pick one runner and pin its version:
# CLI in a container (CI and local)
docker run --rm -v "$PWD/changelog:/liquibase/changelog" \
-e LIQUIBASE_COMMAND_PASSWORD \
liquibase/liquibase:5.0.4 \
--url=jdbc:postgresql://host/db --username=app \
--changelog-file=changelog/db.changelog-master.yaml \
status --verboseKeep the password off the command line: any command option can be set
as LIQUIBASE_COMMAND_<option-name> (environment variables),
and -e LIQUIBASE_COMMAND_PASSWORD with no value forwards the host's
variable into the container (docker run).
Maven (Maven integration):
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>5.0.4</version>
<configuration>
<changeLogFile>src/main/resources/db/changelog/db.changelog-master.yaml</changeLogFile>
<url>${env.DB_URL}</url>
<username>${env.DB_USER}</username>
<password>${env.DB_PASSWORD}</password>
</configuration>
</plugin>then mvn liquibase:validate liquibase:update. Spring Boot: with
spring-boot-starter-liquibase on the classpath, migrations run at
startup and before tests, reading db/changelog/db.changelog-master.yaml
unless spring.liquibase.change-log says otherwise (Spring Boot).
A changelog is a file (or set of included files) containing one or
more changeset entries. Each changeset has an id + author (these
are the identity for the DATABASECHANGELOG row).
YAML example:
databaseChangeLog:
- changeSet:
id: 1
author: alice
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: bigint
autoIncrement: true
constraints: { primaryKey: true, nullable: false }
- column:
name: email
type: varchar(255)
constraints: { unique: true, nullable: false }
rollback:
- dropTable:
tableName: users
- changeSet:
id: 2
author: alice
changes:
- addColumn:
tableName: users
columns:
- column: { name: created_at, type: timestamp }
rollback:
- dropColumn: { tableName: users, columnName: created_at }Equivalent forms in XML / JSON / SQL exist - pick by team preference; they're functionally equivalent at apply time. (Per lb-docs for format-specific syntax.)
updateRun these in order; stop at the first failure:
liquibase validate # 1. syntax, duplicate id/author/file, checksums
liquibase status --verbose # 2. which changesets are pending
liquibase update-sql > pending.sql # 3. read the SQL that update would run
liquibase update # 4. applyvalidate checks that the changelog parses, that no id / author /
file combination repeats, and that no applied changeset's checksum
changed (validate). update-sql prints the SQL update
would run so it can be reviewed; it does not check that SQL for
correctness (update-sql).
On a checksum error, someone edited a changeset that already ran.
Revert the edit and put the change in a new changeset. Only if the edit
is deliberately harmless (a comment, whitespace), add that changeset's
new checksum to a validCheckSum entry and say why in the PR; never
clear checksums to make the gate pass.
On a held lock after an interrupted run, confirm no other
Liquibase process is running, then liquibase release-locks
(release-locks).
Other commands:
| Command | Use |
|---|---|
liquibase history | Show executed changesets |
liquibase tag <tag> / rollback <tag> | Mark a state; roll back to it |
liquibase rollback-count <n> | Roll back the last N changesets |
liquibase update-testing-rollback | Update, roll back the same changesets, update again: proves every rollback works (update-testing-rollback) |
liquibase changelog-sync | Mark pending changesets as applied without running them (adopting an existing schema) |
Changesets can declare preconditions that must hold before they're applied (or the changeset is skipped / failed):
- changeSet:
id: 3
author: alice
preConditions:
- onFail: MARK_RAN # if precondition fails, mark as run without executing
- onError: MARK_RAN
- tableExists: { tableName: users }
changes:
- addColumn: { tableName: users, columns: [{ column: { name: status, type: varchar(20) }}] }Common precondition predicates: tableExists, columnExists,
viewExists, indexExists, foreignKeyConstraintExists,
changeSetExecuted, dbms (e.g., only-on-postgresql),
primaryKeyExists, sqlCheck (custom SQL returning a single value).
onFail / onError actions: HALT (default - fails the run),
CONTINUE, MARK_RAN, WARN.
Selective changeset execution per environment / feature:
- changeSet:
id: 4
author: alice
contexts: dev,test # only run when --contexts dev or test
labels: experimental # filterable by --labels selector
changes:
- createTable: { tableName: feature_x_audit, columns: [...] }Run contexts at apply time:
liquibase update --contexts="dev,test"
liquibase update --label-filter="!experimental" # exclude experimentalMaster changelog references file groups (per docs.liquibase.com/concepts/changelogs):
databaseChangeLog:
- include: { file: changelog/v1.0/users.yaml }
- include: { file: changelog/v1.0/orders.yaml }
- includeAll: { path: changelog/v2.0/, errorIfMissingOrEmpty: true }The includeAll orders files alphabetically; use prefixes
(001-users.yaml, 002-orders.yaml) to enforce order.
services:
postgres:
image: postgres:17
env: { POSTGRES_PASSWORD: ci }
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- name: Liquibase gate on a throwaway database
env:
LIQUIBASE_COMMAND_URL: jdbc:postgresql://localhost:5432/postgres
LIQUIBASE_COMMAND_USERNAME: postgres
LIQUIBASE_COMMAND_PASSWORD: ci
LIQUIBASE_COMMAND_CHANGELOG_FILE: changelog/db.changelog-master.yaml
run: |
lb() { docker run --rm --network=host -v "$PWD/changelog:/liquibase/changelog" \
-e LIQUIBASE_COMMAND_URL -e LIQUIBASE_COMMAND_USERNAME \
-e LIQUIBASE_COMMAND_PASSWORD -e LIQUIBASE_COMMAND_CHANGELOG_FILE \
liquibase/liquibase:5.0.4 "$@"; }
lb validate
lb update-sql
lb update-testing-rollbackThe job fails on the first failing command (bash -e is the default
shell on GitHub-hosted runners), so a checksum error stops it before
anything is applied. Run the same validate step in the deploy job
before update against each real environment.
Run the migration-blast-radius-reviewer agent on new changesets and
classify each with migration-operation-taxonomy (additive, breaking,
data-loss, locking) before merge.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Reuse a changeset id after it's been deployed | Checksum mismatch on next update; deploys fail | New id per change; never edit deployed changesets |
| Rollback section omitted | liquibase rollback fails silently; downtime | Author rollback per changeset (Step 2) |
runAlways: true on schema or data changes | Re-runs on every deploy | Reserve it for idempotent statements (grants, view refreshes) |
| Editing an applied changeset, then clearing checksums | History no longer matches the database | New changeset; validCheckSum only for harmless edits (Step 3) |
| Mix XML + YAML + JSON in one repo | Reviewer cognitive load; sloppy diffs | Pick one format per repo |
| Skip preconditions on conditional changes | Apply fails on environments where preconditions don't hold | Use dbms/tableExists preconditions (Step 4) |
dbms precondition
DATABASECHANGELOGLOCK locked;
liquibase release-locks clears it (Step 3).LIQUIBASE_COMMAND_<option-name> environment variablesdocker run -e VAR forwards the host valueflyway-migrations, atlas-migrations - sister tools (Flyway =
SQL-first versioned; Atlas = declarative HCL / SQL with lint);
migration-operation-taxonomy and migration-blast-radius-reviewer
for risk review