With GitHub Actions, you can create custom workflows for the software development lifecycle directly in your Github repository. These workflows consist of different tasks, called actions, that can be executed automatically when certain events occur.
At Liquibase, we use GitHub Actions for a wide range of tasks involved in the build, test, and release of extensions.
To avoid code duplication of GitHub Actions workflow files across thousands of repositories, we utilize reusable workflows. This allows us to DRY (don't repeat yourself) configurations, so we don't have to copy and paste workflows from one repository to another.
In the calling workflow file, use the uses property to specify the location and version of a
reusable workflow file to run as a job.
name: { Job name }
on:
pull_request:
jobs:
{ workflow }:
uses: liquibase/build-logic/.github/workflows/{workflow}.yml@mainos-extension-test.yml and pro-extension-test.yml are triggered by a workflow call event and runs tests for Liquibase extensions (os/pro) on different Java versions and operating systems. The java input specifies the Java versions to test, and the os input specifies the operating systems to test. Both inputs are required and have default values.
name: { Job name }
on:
pull_request:
jobs:
{ workflow }:
uses: liquibase/build-logic/.github/workflows/{workflow}.yml@main
with:
java: "[17, 18]"
os: '["ubuntu-latest", "windows-latest"]'If inputs are not provided, '[8, 11, 17, 18]' and '["ubuntu-latest", "windows-latest"]' will be used as default values
package-deb.yml is triggered by a workflow call event and runs some Maven goals needed to create and distribute deb packages. It has several inputs:
- groupId: Value from the
groupIdfield in the pom file. i.e.org.liquibase - artifactId: Value from the
artifactIdfield in the pom file. i.e.liquibase - version: Value from the
versionfield in the pom file. i.e4.23.1
To reduce Dependabot noise and centralize SHA management, this repo provides composite action wrappers for high-frequency third-party actions. Instead of every repo pinning SHAs individually, repos reference the wrapper β and SHAs are updated in one place.
| Wrapper | Wraps | Usage |
|---|---|---|
checkout |
actions/checkout |
uses: liquibase/build-logic/.github/actions/checkout@main |
setup-java |
actions/setup-java |
uses: liquibase/build-logic/.github/actions/setup-java@main |
configure-aws-credentials |
aws-actions/configure-aws-credentials |
uses: liquibase/build-logic/.github/actions/configure-aws-credentials@main |
setup-aws-vault |
AWS credentials + Secrets Manager | uses: liquibase/build-logic/.github/actions/setup-aws-vault@main |
setup-google-credentials |
Google Cloud credentials | uses: liquibase/build-logic/.github/actions/setup-google-credentials@main |
Before (every repo pins its own SHA):
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0After (SHA managed centrally in build-logic):
- uses: liquibase/build-logic/.github/actions/checkout@main
with:
fetch-depth: 0- Dependabot monitors each composite action directory in
build-logic - When a new version is released, Dependabot opens one PR in
build-logic - After merge, all calling repos automatically use the updated SHA on next run
- No more N Dependabot PRs across N repos for the same action update! π
- Composite actions can only contain steps, not jobs
- Calling repos reference wrappers with
@mainβ a bad merge tobuild-logicaffects all repos simultaneously - Always test composite action changes thoroughly before merging
- Each wrapper passes through the full set of upstream inputs β see the
action.ymlfor all available parameters
graph LR
A[Feature Branch] -. Open Pull Request .-> B[Build Artifact, Test Matrix] & C[SonarCloud Scan]
B & C -. Close Pull Request .-> D[Attach Artifact to Draft Release]
D -. Push to Main .-> E[Create Draft Release] & F[SonarCloud Scan]
E & F -. Publish Release .-> G[Release to Maven Central]
Please review the below table of reusable workflows and their descriptions:
| Workflow | Description |
|---|---|
build-artifact.yml |
Runs maven build and saves artifacts |
build-extension-jar.yml |
Builds and deploys extension JARs to GitHub Package Manager |
cleanup-individual-artifacts.yml |
Cleans up individual OS-specific artifacts across multiple operating systems |
create-release.yml |
Runs Release Drafter to auto create draft release notes |
dependabot-automerge.yml |
Automatically merges Dependabot PRs for minor and patch updates |
fossa_ai.yml |
Runs FOSSA Scan for AI Generated Code |
ephemeral-cloud-infra.yml |
Creates/Destroys test automation cloud infrastructure |
extension-attach-artifact-release.yml |
Attaches a tested artifact to the draft release. Receives a zip input to upload generated zip files |
extension-release-prepare.yml |
Prepares extension release artifacts |
extension-release-published.yml |
Publishes a release to Maven Central |
extension-release-rollback.yml |
Rolls back a failed extension release |
extension-update-version.yml |
Updates release and development pom.xml versions |
fossa.yml |
Runs FOSSA license compliance checks and uploads reports |
fossa_ai.yml |
Scans code for AI-generated content and runs FOSSA license compliance |
generate-upload-fossa-report.yml |
Generates and uploads license reports to FOSSA |
lth-docker.yml |
Runs Liquibase Test Harness on Docker-based databases |
extension-automated-release.yml |
Unified workflow for OSS and commercial extension releases with automatic publishing to Maven Central |
os-extension-test.yml |
Unit tests across build matrix on previously built artifact |
owasp-scanner.yml |
Runs vulnerability scans using OWASP dependency checker |
package.yml |
Creates and distributes Linux packages (deb, rpm) and updates platform-specific repositories |
package-deb.yml |
Creates and uploads deb packages |
pom-release-published.yml |
Publishes a release pom to Maven Central |
pro-extension-build-for-liquibase.yml |
Builds and tests Pro extensions specifically for Liquibase |
pro-extension-test.yml |
Same as OS job, but with additional Pro-only vars such as License Key |
publish-for-liquibase.yml |
Publishes extensions for Liquibase consumption |
release-notes-aggregate.yml |
Aggregates ## Release note H2 blocks from Done Jira tickets in a Fix Version and renders the release notes (preview-only on dry-run, or appends to the GitHub Release body on publish) |
slack-notification.yml |
Sends notifications to Slack when tests fail |
sonar-scan.yml |
Sonar code coverage scan for PRs and pushes (auto-detects context) |
sonar-coverage-merge.yml |
Merges unit/integration test coverage (JaCoCo) and runs Sonar scan for liquibase/liquibase-pro |
reusable-vulnerability-scan.yml |
Deep vulnerability scanning for Docker images and tarballs (nested JARs + Python packages) |
| various shell scripts | helper scripts for getting the draft release, signing artifacts, and uploading assets |
The reusable-vulnerability-scan.yml workflow provides deep vulnerability scanning for Docker images and tarballs. It extracts nested JARs (Spring Boot BOOT-INF/lib) and Python packages (GraalVM) that standard scanners miss.
Docker Image Scanning:
jobs:
vulnerability-scan:
uses: liquibase/build-logic/.github/workflows/reusable-vulnerability-scan.yml@main
with:
mode: docker
source: "liquibase/liquibase-secure:latest"
image_name: "liquibase-secure"
image_tag: "latest"
fail_on_vulnerabilities: true
upload_sarif: trueTarball Scanning:
jobs:
vulnerability-scan:
uses: liquibase/build-logic/.github/workflows/reusable-vulnerability-scan.yml@main
with:
mode: tarball
source: "distribution-artifact" # artifact name from upload-artifact
version: "5.0.0"
scan_scope: "python-only"
fail_on_vulnerabilities: true| Input | Type | Required | Default | Description |
|---|---|---|---|---|
mode |
string | yes | - | docker or tarball |
source |
string | yes | - | Docker image ref OR artifact name |
image_name |
string | no | '' |
Image name for reporting (docker mode) |
image_tag |
string | no | '' |
Image tag for reporting (docker mode) |
version |
string | no | '' |
Version for reporting (tarball mode) |
scan_scope |
string | no | full |
full or python-only |
fail_on_vulnerabilities |
boolean | no | true |
Fail on HIGH/CRITICAL |
upload_sarif |
boolean | no | false |
Upload to GitHub Security tab |
sarif_category |
string | no | vulnerability-scan |
SARIF category |
generate_sbom |
boolean | no | true |
Generate SBOM (docker only) |
build_logic_ref |
string | no | master |
build-logic branch/tag |
| Output | Description |
|---|---|
total_vulnerabilities |
Total HIGH/CRITICAL count |
surface_vulnerabilities |
Surface scan count (docker only) |
deep_vulnerabilities |
Deep scan count |
grype_vulnerabilities |
Grype scan count (docker only) |
scan_status |
pass or fail |
Two suppression mechanisms exist, applied independently by the reusable vulnerability-scan workflow:
| Mechanism | Where it lives | Who uses it | Why |
|---|---|---|---|
VEX (vex/assessments.yaml) |
liquibase/liquibase-pro |
liquibase-secure scans (vex_enabled: true) |
Cross-scanner (Trivy + Grype + OSV), per-package, structured justifications. Single source of truth for the secure product. |
.trivyignore |
Calling repo (default path docker/.trivyignore) |
Community / OSS Docker scans (TECHOPS-432) | Community workflows cannot reach liquibase-pro/vex/assessments.yaml (TECHOPS-408 forbids dispatch_new_cves). .trivyignore is the documented carve-out for these scans only. |
The reusable workflow fetches the caller's .trivyignore via gh api contents/; missing file is a silent no-op (behavior identical to no input set).
Caller usage:
uses: liquibase/build-logic/.github/workflows/reusable-vulnerability-scan.yml@main
with:
trivyignore_path: docker/.trivyignore # default β override or set to '' to disableFile format & conventions (enforced by review, not CI):
- Plain Trivy-native syntax (one CVE per line,
#comments,exp:YYYY-MM-DDfor expiry β Trivy auto-skips expired entries). - EVERY entry MUST be preceded by a
# reason:comment block explaining the suppression. - Prefer a
review-date(β€ 6 months) over no expiry. - For
liquibase-secure, use VEX..trivyignoreis community-only.
When to use which:
- Multi-scanner suppression needed β VEX (Trivy + Grype + OSV).
- Single-scanner Trivy false positive on a community/OSS image β
.trivyignore. - Pro-secure callers MUST NOT define
.trivyignoreunless explicitly waived; VEX is the canonical path.
The standalone build-logic/.trivyignore file at this repo root is a legacy
copy-pattern artifact and is NOT applied by any current workflow. Cleanup is
tracked separately.
Scripts in scripts/vulnerability-scanning/ can be used directly:
| Script | Description |
|---|---|
extract-nested-deps.sh |
Extract nested JARs and Python packages |
analyze-scan-results.sh |
Combine Trivy/Grype results |
convert-scan-results.sh |
Convert JSON to SARIF |
create-enhanced-report.sh |
Generate detailed markdown report |
append-github-summary.sh |
Append to GitHub Actions summary |
save-grype-results.sh |
Normalize Grype output filenames |
Direct script usage:
# Docker mode - extract nested deps
build-logic/scripts/vulnerability-scanning/extract-nested-deps.sh \
--mode=docker \
--source="my-image:tag" \
--scope=full
# Tarball mode - extract Python only
build-logic/scripts/vulnerability-scanning/extract-nested-deps.sh \
--mode=tarball \
--source="./path/to/tarball.tar.gz" \
--scope=python-onlyThe release-notes-aggregate.yml workflow walks every Done Jira ticket in a Fix Version, extracts the ## Release note H2 block from each ticket's description, and renders a paste-ready markdown release notes block plus a seven-bucket completeness report. Logic lives in scripts/release-notes/ (Python CLI, stdlib only, 70 pytest tests). Per TECHOPS-498.
Today's callers: liquibase/liquibase (Community), liquibase/liquibase-pro (Secure). Future repos (NTT, Platform, MCP) adopt by pasting the ~85-line caller and swapping the product prefix.
| Mode | Trigger | Output | Use case |
|---|---|---|---|
| Dry-run preview | workflow_dispatch with dry_run=true (default) |
Step summary + downloadable release-notes artifact |
Release manager previews before publishing. GitHub Release body untouched. |
| Publish | release: [published] auto-trigger, or manual dispatch with dry_run=false + release_tag |
Step summary + artifact + appended to the GitHub Release body | Production. Existing Release Drafter content is preserved with a --- separator. |
Manual preview (recommended starting point):
jobs:
aggregate:
uses: liquibase/build-logic/.github/workflows/release-notes-aggregate.yml@main
with:
version: "Community 5.0.4" # or Secure / Platform / MCP X.Y.Z
dry_run: true
secrets: inheritAuto-fire on release publish (publishes to release body):
on:
release:
types: [published]
jobs:
derive:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.compute.outputs.version }}
steps:
- id: compute
env:
EVENT_TAG: ${{ github.event.release.tag_name }}
run: |
stripped="${EVENT_TAG#v}"
echo "version=Community ${stripped}" >> "$GITHUB_OUTPUT"
aggregate:
needs: derive
uses: liquibase/build-logic/.github/workflows/release-notes-aggregate.yml@main
with:
version: ${{ needs.derive.outputs.version }}
dry_run: false
release_tag: ${{ github.event.release.tag_name }}
secrets: inheritSwap Community for Secure, Platform, or MCP Changelog when adopting in another product repo.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
version |
string | yes | - | Jira Fix Version name (e.g. Community 5.0.4, Secure 5.2.0, Platform 1.0.0, MCP Changelog 1.0.0) |
dry_run |
boolean | no | true |
If true: preview only in step summary + artifact, GitHub Release body untouched. If false: also append notes to the release body (requires release_tag). |
release_tag |
string | no | "" |
GitHub Release tag to update when dry_run=false (e.g. v5.0.4). Required when publishing, ignored on dry runs. |
build_logic_ref |
string | no | main |
Ref of liquibase/build-logic to check out for the script. Pin to a tag in production callers for stable behaviour. |
Same OIDC pattern as every other jira-* workflow in this repo: GitHub OIDC token β assumes org-level role LIQUIBASE_VAULT_OIDC_ROLE_ARN β pulls ,/vault/liquibase from AWS Secrets Manager β sets JIRA_USER + JIRA_API_TOKEN. Callers just say secrets: inherit.
The aggregator reads ## Release note H2 blocks from each ticket's description. Engineers fill that in per the convention from TECHOPS-479. Tickets without an H2 are flagged in the completeness report (not silently dropped). The skipReleaseNotes label from TECHOPS-478 is honoured as an opt-out.
Every run prints a seven-bucket report; the three mutually-exclusive count buckets satisfy with_note + skipped + flagged == total_done. The script asserts this in code and the report renderer fails loudly if it doesn't hold.
Every user-controllable input (inputs.version, inputs.release_tag, event payload fields) is consumed via env vars in run: blocks. Never interpolated directly into shell. Follows the GitHub workflow-injection guide.
The pom must meet all the requirements from sonatype: https://central.sonatype.org/publish/requirements/#a-complete-example-pom
Jacoco must be configured and exporting test results.
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<configuration>
<fileSets>
<fileSet>
<directory>target</directory>
<includes>
<include>**/jacoco.exec</include>
</includes>
</fileSet>
</fileSets>
</configuration>
</plugin>All unit tests must run and pass with surefire:test. If any test require additional setup, such as docker, they will need to run separately from the reusable build logic.
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
<reportFormat>plain</reportFormat>
</configuration>
</plugin>The following artifacts must be created mvn clean package. If the javadoc and sources should not be public, please copy the contents of the readme for those files. This is based on the recommendation from sonatype: https://central.sonatype.org/publish/requirements/#supply-javadoc-and-sources.
- {artifactId}-{version}.jar
- {artifactId}-{version}.pom
- {artifactId}-{version}-javadoc.jar
- {artifactId}-{version}-sources.jar
<plugin>
<groupId>com.coderplus.maven.plugins</groupId>
<artifactId>copy-rename-maven-plugin</artifactId>
<version>1.0</version>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<fileSets>
<fileSet>
<sourceFile>${project.basedir}/pom.xml</sourceFile>
<destinationFile>${project.basedir}/target/${project.artifactId}-${project.version}.pom</destinationFile>
</fileSet>
<fileSet>
<sourceFile>${project.basedir}/README.md</sourceFile>
<destinationFile>${project.basedir}/target/${project.artifactId}-${project.version}-javadoc.jar</destinationFile>
</fileSet>
<fileSet>
<sourceFile>${project.basedir}/README.md</sourceFile>
<destinationFile>${project.basedir}/target/${project.artifactId}-${project.version}-sources.jar</destinationFile>
</fileSet>
</fileSets>
</configuration>
</execution>
</executions>
</plugin>The Maven release plugin must be configured to allow extensions update pom.xml versions:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>${maven-release-plugin.version}</version>
<configuration>
<scmCommentPrefix>[Version Bumped to ${project.version}]</scmCommentPrefix>
</configuration>
</plugin>
</plugins>
</build>When releasing extensions, a Pull Request is automatically created to update the version in the pom.xml files instead of directly committing to the main branch. This approach provides the following benefits:
- Improved security by requiring reviews before version changes are merged
- Better traceability of version bumps through the PR history
- Opportunity for validation before finalizing the version change
The PR creation is handled by the extension-release-prepare.yml workflow:
- name: Create Pull Request for version bump
uses: peter-evans/create-pull-request@v7.0.8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: update version after release"
title: "Version bump after release"
body: |
This PR updates the POM version after a release.
Automated changes by GitHub Actions.
branch: version-bump-after-release
delete-branch: trueThese version bump PRs are automatically merged through a nightly scheduled workflow (auto_merge_release_prs.yml) in the liquibase-infrastructure repository. This workflow:
- Runs on a daily schedule (midnight UTC) or can be triggered manually
- Identifies all repositories with the
extensiontopic - Finds open PRs with the exact title "Version bump after release"
- Merges these PRs using the squash strategy
This automation ensures that version bumps are consistently applied across all extension repositories without requiring manual intervention, while still maintaining the security benefits of the PR-based approach.
| Workflow | Description |
|---|---|
lth-docker.yml |
Runs Liquibase Test Harness against a docker container |
- Docker Compose file must be located in
src/test/resources/docker-compose.yml
The sonar-coverage-merge.yml reusable workflow merges unit and integration test JaCoCo coverage reports and delivers the aggregated report to SonarCloud. It downloads pre-generated coverage artifacts (liquibase-jacoco-test-results for unit tests, liquibase-integration-jacoco-test-results-* for integration tests), merges them using the JaCoCo CLI, and runs the Sonar scan.
This workflow supports both PR and branch scans, and is used by liquibase and liquibase-pro.
The project has to be configured with the following Maven plugins:
- Maven Surefire Plugin: Runs unit tests
- Maven Failsafe Plugin: Runs integration tests
- Jacoco Plugin: Generates test reports and it also agreggates and merges all of them into a single report
For Maven multimodule projects it is recommended to follow this pattern from SonarSource where there is a specific module to leave the aggregated report:
In the following example we demonstrate how liquibase-pro works:
All modules need to specify where the final report will be generated setting the sonar.coverage.jacoco.xmlReportPaths property. In the parent pom there are 3 profiles to control which tests are executed and the required plugins are configured.
<properties>
<maven-failsafe-plugin.version>3.0.0-M7</maven-failsafe-plugin.version>
<jacoco-maven-plugin.version>0.8.5</jacoco-maven-plugin.version>
<maven-surefire-plugin.version>3.0.0-M7</maven-surefire-plugin.version>
<code.coverage.project.folder>${basedir}/../</code.coverage.project.folder>
<code.coverage.overall.data.folder>${basedir}/target/</code.coverage.overall.data.folder>
<skip.integration.tests>true</skip.integration.tests>
<skip.unit.tests>true</skip.unit.tests>
<itCoverageAgent></itCoverageAgent>
<sonar.coverage.jacoco.xmlReportPaths>liquibase-pro-coverage/target/site/jacoco-aggregate/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<skipTests>${skip.unit.tests}</skipTests>
<excludes>
<exclude>liquibase-pro-integration-tests/**/*IntegrationTest.java</exclude>
</excludes>
<forkCount>1</forkCount>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<executions>
<execution>
<id>integration-tests</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<skipTests>${skip.integration.tests}</skipTests>
<includes>
<include>**/*IntegrationTest.java</include>
</includes>
<reuseForks>true</reuseForks>
<argLine>${itCoverageAgent}</argLine>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin.version}</version>
<executions>
<execution>
<id>prepare-unit-tests</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- prepare agent before integration tests -->
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<phase>pre-integration-test</phase>
<configuration>
<propertyName>itCoverageAgent</propertyName>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1.2184</version>
<executions>
<execution>
<id>sonar</id>
<goals>
<goal>sonar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build> <profile>
<id>unit</id>
<properties>
<skip.integration.tests>true</skip.integration.tests>
<skip.unit.tests>false</skip.unit.tests>
</properties>
</profile>
<profile>
<id>integration-test</id>
<properties>
<skip.integration.tests>false</skip.integration.tests>
<skip.unit.tests>true</skip.unit.tests>
</properties>
</profile>
<profile>
<id>testAll</id>
<properties>
<skip.integration.tests>false</skip.integration.tests>
<skip.unit.tests>false</skip.unit.tests>
</properties>
</profile>maven-surefire-plugin had to be added here because liquibase-pro integration tests are not following the *ITest.java or *IntegrationTest.java naming for integration tests.
<properties>
<sonar.coverage.jacoco.xmlReportPaths>${project.basedir}/../liquibase-pro-coverage/target/site/jacoco-aggregate/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${skip.integration.tests}</skipTests>
<forkCount>1</forkCount>
</configuration>
</plugin>
</plugins>
</build> <properties>
<sonar.coverage.jacoco.xmlReportPaths>${project.basedir}/../liquibase-pro-coverage/target/site/jacoco-aggregate/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
</properties>Here the modules we want to generate and aggregate test reports must be specified as dependencies.
<properties>
<sonar.coverage.jacoco.xmlReportPaths>target/site/jacoco-aggregate/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
<code.coverage.project.folder>${basedir}/../</code.coverage.project.folder>
<code.coverage.overall.data.folder>${basedir}/target/</code.coverage.overall.data.folder>
<sonar.skip>true</sonar.skip>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>liquibase-commercial</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>liquibase-commercial-integration-tests</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>${argLine} -Xms256m -Xmx2048m</argLine>
<forkCount>1</forkCount>
<runOrder>random</runOrder>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>report-aggregate</id>
<phase>verify</phase>
<goals>
<goal>report-aggregate</goal>
</goals>
</execution>
<execution>
<id>merge-results</id>
<phase>verify</phase>
<goals>
<goal>merge</goal>
</goals>
<configuration>
<fileSets>
<fileSet>
<directory>${code.coverage.project.folder}</directory>
<includes>
<include>**/target/jacoco.exec</include>
</includes>
</fileSet>
</fileSets>
<destFile>${code.coverage.overall.data.folder}/aggregate.exec</destFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>When you want to release new version of build-logic, it is important to update all the occurrences of previous version eg: main with the new version eg : main in all the files. As, the code for the new version internally refers to the old version.
Secure authentication and authorization are critical aspects of our CI/CD workflow. We employ different token types for various scenarios to optimize security and minimize GitHub API rate limit issues.
GitHub automatically provides a GITHUB_TOKEN secret that's available during workflow runs. This is a short-lived token that expires when the job completes.
- For single-repository operations
- When no cross-repository access is needed
- For most common GitHub API operations within the same repository
- Automatically rotated for each job
- Limited to the repository where the workflow runs
- Permissions can be explicitly scoped using the
permissionskey
permissions:
id-token: write
contents: write
pull-requests: write- name: Create Pull Request for version bump
uses: peter-evans/create-pull-request@v7.0.8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: update version after release"
title: "Version bump after release"For operations that require cross-repository access or elevated permissions without using personal credentials, we use GitHub App tokens.
- For cross-repository operations (cloning, checking out)
- When you need specific permissions across multiple repositories
- For operations that would exceed rate limits with
GITHUB_TOKEN
- Fine-grained permissions control
- No personal credentials involved
- Short-lived by default (can be configured)
- Auditable through GitHub App activity. (Search filter:
action:oauth_application)
- name: Get GitHub App token
id: get-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ env.LIQUIBASE_GITHUB_APP_ID }}
private-key: ${{ env.LIQUIBASE_GITHUB_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
permission-contents: read
permission-packages: write
- name: Checkout code
uses: actions/checkout@v4
with:
repository: ${{ inputs.repository }}
token: ${{ steps.get-token.outputs.token }}While we minimize their use, PATs are sometimes necessary for specific scenarios where GitHub Apps or GITHUB_TOKEN aren't sufficient.
- When GitHub App tokens cannot provide the necessary access
- For GitHub Package Manager (
GPM) cross-repository access - For legacy integrations that don't support other authentication methods
- Store as encrypted repository secrets
- Use fine-grained PATs with minimum required scopes (
GPMcross-repo deploys requires classicPAT) - Regularly rotate tokens
- Consider using organization-level PATs instead of personal tokens
- name: Maven settings with GPM access
uses: whelk-io/maven-settings-xml-action@v22
with:
repositories: |
[
{
"id": "github",
"url": "https://maven.pkg.github.com/liquibase/*",
"releases": {
"enabled": "true"
},
"snapshots": {
"enabled": "true"
}
}
]
servers: |
[
{
"id": "github",
"username": "${{ github.actor }}",
"password": "${{ env.LIQUIBOT_PAT_GPM_ACCESS }}"
}
]Our approach to selecting tokens follows these principles:
- Default to GITHUB_TOKEN when possible for its security and simplicity
- Use GitHub App tokens for cross-repository operations
- Limit PAT usage to only specialized cases where other token types don't suffice
- Scope permissions explicitly for all token types
- Separate tokens for different types of operations (deployment vs. reading)
This strategy helps us maintain security while ensuring our CI/CD workflows operate smoothly with appropriate permissions.
The extension-automated-release.yml workflow is a unified release automation for both OSS and commercial/secure Liquibase extensions. It automatically publishes extensions to Maven Central.
Supports:
- OSS Extensions: BigQuery, Cache, Cassandra, CosmosDB, DB2i, FileChangelog, NoChangeLock, HanaDB, MaxDB, Modify Column, MSSQL, Oracle, PostgreSQL, Redshift, SQLFire, Teradata, Vertica, YugabyteDB, Hibernate, Parent POM
- Commercial Extensions: BigQuery, Databricks, MongoDB, Azure, AWS
The workflow is called from orchestrator workflows in different repositories:
For OSS Extensions (liquibase repository):
name: Release Extensions
on:
workflow_dispatch:
inputs:
version:
description: 'Liquibase Version'
required: true
jobs:
automated-os-extensions-release:
uses: liquibase/build-logic/.github/workflows/extension-automated-release.yml@main
secrets: inherit
with:
version: ${{ inputs.version }}
# Uses default OSS extension listFor Commercial/Secure Extensions (liquibase-pro repository):
name: Release Secure Extensions
on:
workflow_dispatch:
inputs:
version:
description: 'Liquibase Secure Version (e.g., 5.0.0, 5.0.1)'
required: true
jobs:
automated-secure-extensions-release:
uses: liquibase/build-logic/.github/workflows/extension-automated-release.yml@main
secrets: inherit
with:
version: ${{ inputs.version }}
repositories: '["liquibase-commercial-bigquery","liquibase-commercial-databricks","liquibase-commercial-mongodb","liquibase-azure-extension","liquibase-aws-extension"]'To manually trigger a release:
For OSS Extensions:
- Navigate to the
liquibaserepository on GitHub - Go to Actions β Release Extensions
- Click Run workflow
- Enter the version number (e.g.,
4.26.0) - Click Run workflow to start
For Commercial Extensions:
- Navigate to the
liquibase-prorepository on GitHub - Go to Actions β Release Secure Extensions
- Click Run workflow
- Enter the version number (e.g.,
4.26.0) - Click Run workflow to start
- version (required): Version to release in semantic versioning format (e.g.,
4.26.0,4.26.1) - repositories (optional): JSON array of repository names to release
- Default:
["liquibase-bigquery", "liquibase-databricks", "liquibase-mongodb", "liquibase-azure", "liquibase-aws"] - Can be customized to release specific extensions only
- Default:
The workflow executes the following jobs in sequence:
- Runs in parallel across all specified extension repositories
- Checks for open Dependabot security alerts using GitHub API
- Fails if any open security vulnerabilities are found
- Uses matrix strategy with
fail-fast: falseto check all repositories
Log Messages:
- INFO: "Checking repository: {repository}"
- INFO: "Security vulnerabilities for {repository} are addressed."
- ERROR: "Security vulnerabilities for {repository} are not addressed."
- Depends on:
check-security-vulnerabilities - Installs and runs Dependabot CLI on each extension repository
- Updates Maven dependencies automatically
- Runs in parallel across all repositories
- Depends on:
check-security-vulnerabilities - Updates extension versions to next SNAPSHOT
- Updates
liquibase.versionproperty in pom.xml - Commits and pushes changes if modified
- Configures Maven settings for both
liquibaseandliquibase-prorepositories
Important: Maven settings include access to both OSS and Pro packages for commercial extension dependencies.
- Depends on:
update-pom - Waits 180 seconds for artifacts to be available
- Finds draft releases containing the specified version
- Publishes draft releases (sets
draft: false) - Tracks published releases
- Depends on:
release-draft-releases - Waits 120 seconds for releases to be fully available
- Downloads artifacts from published GitHub releases (jar, pom, asc, md5, sha1)
- Creates Maven repository layout bundles
- Uploads to Sonatype Central Portal with AUTOMATIC publishing
- Publishes to Maven Central (same as OSS extensions)
Artifact Types Downloaded:
.jar- Extension JAR file.pom- Maven POM file.asc- GPG signatures (code signing).md5and.sha1- Checksums
Each commercial extension repository must:
- Have a LICENSE file (LICENSE, LICENSE.md, or LICENSE.txt) in the root
- Include FSL license headers in Java source files
- Follow Maven project structure with pom.xml
- Have draft releases ready with signed artifacts
The following secrets must be available in AWS Secrets Manager (/vault/liquibase):
LIQUIBASE_GITHUB_APP_ID- GitHub App ID for repository accessLIQUIBASE_GITHUB_APP_PRIVATE_KEY- GitHub App private keyLIQUIBOT_PAT_GPM_ACCESS- GitHub PAT for Maven Package accessSONATYPE_USERNAME- Sonatype Central Portal usernameSONATYPE_TOKEN- Sonatype Central Portal token
The workflow requires:
contents: write- For releasing and updating repositoriessecurity-events: read- For checking Dependabot alertspackages: write- For Maven package operationsid-token: write- For OIDC authentication with AWS
Issue: Security vulnerabilities not addressed
- Cause: Open Dependabot alerts in repository
- Solution: Review and fix Dependabot alerts before releasing
- Check: GitHub repository β Security β Dependabot alerts
Issue: No artifacts found in release
- Cause: Draft release doesn't contain artifacts for the specified version
- Solution: Ensure draft release is created with proper version tag
- Check: GitHub repository β Releases β Draft releases
Issue: Maven Central upload failed
- Cause: Invalid credentials or artifact bundle structure
- Solution: Verify SONATYPE_USERNAME and SONATYPE_TOKEN in vault
- Check: Sonatype Central Portal deployment logs
Issue: POM update failed
- Cause: Maven settings not configured or missing dependencies
- Solution: Verify both liquibase and liquibase-pro repository access
- Check: Ensure LIQUIBOT_PAT_GPM_ACCESS is valid
graph TB
A[Manual Trigger from liquibase or liquibase-pro] --> B[Check Security Vulnerabilities]
B --> C{All Extensions Clean?}
C -->|No| D[Fail Workflow]
C -->|Yes| E[Run Dependabot]
E --> F[Update POMs]
F --> G[Release Draft Releases]
G --> H[Wait for Artifacts]
H --> I[Download Artifacts]
I --> J[Create Maven Bundles]
J --> K[Upload to Central Portal]
K --> L[Publish to Maven Central]
L --> M[Archive Results]
style A fill:#e1f5ff
style L fill:#e8f5e9
A successful release completes when:
- β All security vulnerabilities addressed across all extensions
- β POM versions updated and committed
- β Draft releases published to GitHub
- β Artifacts uploaded to Maven Central via Sonatype Central Portal
- β Deployment IDs logged for tracking
- GitHub Actions: View workflow runs in
liquibase(OSS) orliquibase-pro(commercial) repositories - Maven Central: Track deployment status at https://central.sonatype.com/publishing/deployments
- Artifacts: Published extensions artifact uploaded with deployment IDs
- Logs: All jobs log INFO/WARNING/ERROR messages for easy debugging
