Closes #32 Connection strings passed as argv (user/password@service) are visible to other users via ps and /proc, recorded in shell history, and captured by terminal scrollback and CI job log retention. Restructure the SQLcl basics and CI/CD skills so the primary examples never put a password on the command line. sqlcl-basics.md - Lead the Connecting section with a security warning and replace the first credentialed examples with prompted-password forms. - Replace the literal admin/MyPassword123@myadb_high example. - Add a Discouraged: Passwords on the Command Line subsection that enumerates the leak surfaces. - Link SEPS as the password-less option from the Cloud Wallet section. sqlcl-cicd.md - Replace sql -S user/pass@service patterns with sql -S /nolog plus a CONNECT issued from stdin via heredoc, so credentials reach SQLcl on fd 0 rather than as process arguments. Document SEPS (/@alias) as the password-less alternative. - Update the GitHub Actions and GitLab CI examples to inject masked CI secrets through the step env: block and the heredoc. - Add a Keep Credentials Out of the Command Line section with side- by-side SEPS, /nolog, and avoid examples, and a warning against set -x in steps that handle the stdin connect. - Add Oracle Database Security Guide, GitHub Actions secrets, and GitLab CI variables references to Sources. Signed-off-by: Gustavo Evangelista <gustavoborges2@gmail.com>
23 KiB
SQLcl in CI/CD Pipelines
Overview
SQLcl is well-suited for CI/CD pipelines because it is a standalone Java executable with no Oracle Client installation required, supports non-interactive (headless) execution, can connect to Oracle Cloud Autonomous Database via wallet, and returns meaningful exit codes that CI/CD systems can act on. Combined with its built-in Liquibase support, SQLcl can serve as the single tool for schema migrations, data seeding, DDL extraction, and validation checks in automated deployment pipelines.
This guide covers:
- Running SQLcl non-interactively
- Handling exit codes
- Connecting to cloud databases without interactive prompts
- Integrating with GitHub Actions and GitLab CI
- Environment variable substitution
- Logging and error capture patterns
Running SQLcl Non-Interactively
Do not pass passwords on the SQLcl command line in CI/CD. Connect from a SEPS wallet (
sql -S /@alias) or from stdin via/nolog+CONNECT. See Keep Credentials Out of the Command Line for the rationale and the full Good/Bad comparison.
Basic Headless Execution
The -S (silent) flag suppresses the SQLcl banner and all interactive prompts.
The recommended pattern for CI is to start SQLcl with /nolog and issue CONNECT from stdin so credentials are not exposed as command arguments. The CI runtime injects DB_USER, DB_PASS, and DB_SERVICE from masked secret variables.
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
WHENEVER SQLERROR EXIT SQL.SQLCODE
@deploy.sql
EXIT 0
EOF
If the runner has access to a SEPS (Secure External Password Store) wallet, prefer the password-less form — the wallet stores the credential keyed to the TNS alias:
export TNS_ADMIN=/path/to/seps-wallet
sql -S /@DB_ALIAS @deploy.sql
The script (@deploy.sql or anything inside the heredoc) is executed and SQLcl exits when the script completes or when an EXIT command is reached.
Passing Commands via stdin
Combine /nolog connect with the inline commands:
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
SET FEEDBACK ON
SELECT COUNT(*) FROM employees;
EXIT
EOF
Or with SEPS (no credentials in the script at all):
sql -S /@DB_ALIAS <<'EOF'
SET FEEDBACK ON
SELECT COUNT(*) FROM employees;
EXIT
EOF
Note: use <<EOF (unquoted) when the heredoc must expand shell variables such as ${DB_USER}; use <<'EOF' (quoted) when the body should be passed to SQLcl literally, for example to keep &substitution variables intact.
Running a Script File
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
@/path/to/script.sql
EXIT 0
EOF
Or with SEPS:
sql -S /@DB_ALIAS @/path/to/script.sql
Command-line -c Flag (Inline Command)
The official SQLcl startup flags documentation does not include a -c option for inline SQL commands. Use stdin or a script file with the patterns above.
Exit Code Handling
SQLcl exits with code 0 on success and a non-zero code on failure. However, to ensure failures in SQL scripts cause non-zero exits, you must use WHENEVER SQLERROR at the top of your scripts.
Essential Exit Code Pattern
Every CI/CD SQL script should begin with:
-- deploy.sql
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK
WHENEVER OSERROR EXIT 9 ROLLBACK
SET FEEDBACK ON
SET ECHO ON
-- Your SQL statements here
ALTER TABLE employees ADD (middle_name VARCHAR2(30));
COMMIT;
EXIT 0
| Statement | Meaning |
|---|---|
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK |
On any SQL error, exit with the Oracle error code and roll back uncommitted changes |
WHENEVER OSERROR EXIT 9 ROLLBACK |
On any OS error, exit with code 9 and roll back |
EXIT 0 |
Explicit success exit at the end |
EXIT 1 |
Explicit failure exit (use for validation failures) |
Checking Exit Code in Shell
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
@deploy.sql
EOF
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "ERROR: SQL deployment failed with exit code $EXIT_CODE"
exit $EXIT_CODE
fi
echo "Deployment successful"
Exit Codes for Validation Scripts
-- validate_schema.sql
WHENEVER SQLERROR EXIT SQL.SQLCODE
-- Check required tables exist
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count
FROM user_tables
WHERE table_name IN ('EMPLOYEES','DEPARTMENTS','JOBS');
IF v_count < 3 THEN
RAISE_APPLICATION_ERROR(-20001, 'Required tables missing. Expected 3, found ' || v_count);
END IF;
END;
/
-- Check no invalid objects
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM user_objects WHERE status = 'INVALID';
IF v_count > 0 THEN
RAISE_APPLICATION_ERROR(-20002, v_count || ' invalid objects found after deployment');
END IF;
END;
/
EXIT 0
Connecting with Oracle Cloud Wallet in Headless Mode
Wallet Setup
# Unzip the wallet to a directory accessible by the CI runner
mkdir -p /tmp/wallet
echo "$WALLET_ZIP_BASE64" | base64 -d > /tmp/wallet.zip
unzip -q /tmp/wallet.zip -d /tmp/wallet
chmod 600 /tmp/wallet/*
Setting TNS_ADMIN
export TNS_ADMIN=/tmp/wallet
The sqlnet.ora in the wallet directory contains:
WALLET_LOCATION=(SOURCE=(METHOD=file)(METHOD_DATA=(DIRECTORY=/tmp/wallet)))
SSL_SERVER_DN_MATCH=yes
Connecting
# Use the TNS alias defined in the wallet's tnsnames.ora; password comes from CI secret
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE_NAME}
@deploy.sql
EOF
Where DB_SERVICE_NAME is one of the aliases defined in the wallet's tnsnames.ora (e.g., myatp_high, myatp_medium, myatp_low).
For fully password-less automation, configure a Secure External Password Store (SEPS) and connect with /@alias:
export TNS_ADMIN=/tmp/wallet
sql -S /@${DB_SERVICE_NAME} @deploy.sql
Full Cloud Connection Example
With SEPS the heredoc can stay quoted, no shell-side escapes needed:
export TNS_ADMIN=/tmp/wallet
sql -S /@myatp_high <<'EOF'
WHENEVER SQLERROR EXIT SQL.SQLCODE
SELECT instance_name, status FROM v$instance;
EXIT 0
EOF
Environment Variable Substitution
Using Shell Variables in SQL Scripts
SQLcl supports SQL*Plus-style substitution variables (&variable_name). You can pass values through the environment by defining them before the script runs:
-- deploy_env.sql
DEFINE ENV = &1
DEFINE APP_VER = &2
PROMPT Deploying version &APP_VER to environment &ENV
SELECT 'Deploying to: ' || '&ENV' AS info FROM DUAL;
Pass arguments from the command line (credentials still go through /nolog + CONNECT, not argv):
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
@deploy_env.sql PROD v2.5.1
EOF
Using Shell Variable Expansion
For environment variables from the shell, use the shell's own variable substitution in the heredoc. Keep credentials out of the sql command line by issuing CONNECT from inside the heredoc:
export APP_VERSION="2.5.1"
export DEPLOY_ENV="production"
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
WHENEVER SQLERROR EXIT SQL.SQLCODE
INSERT INTO deployment_log (version, environment, deployed_at)
VALUES ('${APP_VERSION}', '${DEPLOY_ENV}', SYSDATE);
COMMIT;
EXIT 0
EOF
Note: Use <<EOF (not <<'EOF') to allow shell variable expansion inside the heredoc.
DEFINE Variables for Script-internal Configuration
-- parameters.sql (sourced at start of pipeline scripts)
DEFINE SCHEMA_NAME = HR
DEFINE APP_VERSION = 2.5.1
DEFINE ROLLBACK_TAG = v2.4.0
PROMPT Schema: &SCHEMA_NAME
PROMPT Version: &APP_VERSION
-- deploy.sql
@parameters.sql
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK
lb tag -tag &APP_VERSION
lb update -changelog-file controller.xml
EXIT 0
GitHub Actions Integration
Basic Workflow
# .github/workflows/deploy-db.yml
name: Deploy Database Changes
on:
push:
branches: [main]
paths:
- 'db/**'
pull_request:
branches: [main]
paths:
- 'db/**'
env:
TNS_ADMIN: /tmp/wallet
jobs:
validate:
name: Validate SQL Changes
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Install SQLcl
run: |
curl -sL https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-latest.zip -o sqlcl.zip
unzip -q sqlcl.zip -d /opt
echo "/opt/sqlcl/bin" >> $GITHUB_PATH
- name: Set up Oracle wallet
run: |
mkdir -p /tmp/wallet
echo "${{ secrets.WALLET_ZIP_B64 }}" | base64 -d | unzip -q -d /tmp/wallet -
- name: Validate changelog status
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_SERVICE: ${{ secrets.DB_SERVICE }}
run: |
cd db
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
WHENEVER SQLERROR EXIT SQL.SQLCODE
lb status -changelog-file controller.xml
EXIT 0
EOF
deploy:
name: Deploy to Test
runs-on: ubuntu-latest
needs: validate
if: github.ref == 'refs/heads/main'
environment: test
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Install SQLcl
run: |
curl -sL https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-latest.zip -o sqlcl.zip
unzip -q sqlcl.zip -d /opt
echo "/opt/sqlcl/bin" >> $GITHUB_PATH
- name: Set up Oracle wallet
run: |
mkdir -p /tmp/wallet
echo "${{ secrets.WALLET_ZIP_B64 }}" | base64 -d | unzip -q -d /tmp/wallet -
- name: Tag pre-deployment state
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_SERVICE: ${{ secrets.DB_SERVICE }}
run: |
cd db
VERSION="${{ github.sha }}"
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
WHENEVER SQLERROR EXIT SQL.SQLCODE
lb tag -tag pre-${VERSION}
EXIT 0
EOF
- name: Apply Liquibase changes
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_SERVICE: ${{ secrets.DB_SERVICE }}
run: |
cd db
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK
SET ECHO ON
lb update -changelog-file controller.xml
EXIT 0
EOF
- name: Run post-deployment validation
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_SERVICE: ${{ secrets.DB_SERVICE }}
run: |
cd db
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
@validate_schema.sql
EOF
- name: Upload deployment log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: deployment-log
path: /tmp/sqlcl-deploy.log
Reusable Action for SQLcl Operations
# .github/workflows/sqlcl-action.yml (reusable workflow)
on:
workflow_call:
inputs:
script:
required: true
type: string
environment:
required: true
type: string
secrets:
DB_USER:
required: true
DB_PASS:
required: true
DB_SERVICE:
required: true
WALLET_ZIP_B64:
required: true
jobs:
run-sqlcl:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- name: Install SQLcl
run: |
curl -sL https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-latest.zip -o sqlcl.zip
unzip -q sqlcl.zip -d /opt
echo "/opt/sqlcl/bin" >> $GITHUB_PATH
- name: Configure wallet
run: |
mkdir -p /tmp/wallet
echo "${{ secrets.WALLET_ZIP_B64 }}" | base64 -d | unzip -q -d /tmp/wallet -
echo "TNS_ADMIN=/tmp/wallet" >> $GITHUB_ENV
- name: Execute SQLcl script
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_SERVICE: ${{ secrets.DB_SERVICE }}
run: |
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
@${{ inputs.script }}
EOF
GitLab CI Integration
# .gitlab-ci.yml
variables:
TNS_ADMIN: "/tmp/wallet"
stages:
- validate
- deploy
- verify
.sqlcl_setup: &sqlcl_setup
before_script:
- apt-get update -qq && apt-get install -y -qq unzip curl
- curl -sL https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-latest.zip -o sqlcl.zip
- unzip -q sqlcl.zip -d /opt
- export PATH="/opt/sqlcl/bin:$PATH"
- mkdir -p /tmp/wallet
- echo "$WALLET_ZIP_B64" | base64 -d | unzip -q -d /tmp/wallet -
validate_changes:
stage: validate
<<: *sqlcl_setup
script:
- cd db
- |
sql -S /nolog <<SQLEOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
WHENEVER SQLERROR EXIT SQL.SQLCODE
lb status -changelog-file controller.xml
EXIT 0
SQLEOF
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
deploy_db:
stage: deploy
<<: *sqlcl_setup
script:
- cd db
- |
sql -S /nolog <<SQLEOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK
SET ECHO ON
lb tag -tag pre-${CI_COMMIT_SHORT_SHA}
lb update -changelog-file controller.xml
EXIT 0
SQLEOF
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
environment:
name: production
action: start
verify_deployment:
stage: verify
<<: *sqlcl_setup
script:
- cd db
- |
sql -S /nolog <<SQLEOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
@validate_schema.sql
SQLEOF
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
DB_USER, DB_PASS, and DB_SERVICE should be defined as masked, protected CI/CD variables in GitLab so they are injected into the runner environment but never echoed to the job log. Avoid set -x or set -v in pipeline steps that handle credentials, even via stdin.
Logging and Error Capture
Capturing All SQLcl Output
# Redirect both stdout and stderr to a log file. Credentials come from masked
# CI variables (${DB_USER}, ${DB_PASS}, ${DB_SERVICE}) and are passed via stdin
# so they never appear in the process list.
sql -S /nolog <<EOF 2>&1 | tee /tmp/deploy.log
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
@deploy.sql
EOF
# Check exit code (tee preserves the pipeline, PIPESTATUS captures it)
EXIT_CODE=${PIPESTATUS[0]}
if [ $EXIT_CODE -ne 0 ]; then
echo "Deployment failed. Log:"
cat /tmp/deploy.log
exit $EXIT_CODE
fi
SPOOL for Detailed SQL-side Logging
Add SPOOL to your SQL script to capture database-side output including row counts and timing:
-- deploy.sql
SPOOL /tmp/deploy_output.log
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK
SET ECHO ON
SET FEEDBACK ON
SET TIMING ON
SET SERVEROUTPUT ON SIZE UNLIMITED
-- Your deployment statements
lb update -changelog-file controller.xml
SPOOL OFF
EXIT 0
Structured Log Output for CI Parsing
-- deploy_structured.sql
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK
SET ECHO OFF
SET FEEDBACK OFF
PROMPT [INFO] Starting deployment at &_DATE
PROMPT [INFO] Connected as: &_USER to &_CONNECT_IDENTIFIER
lb update -changelog-file controller.xml
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM databasechangelog
WHERE dateexecuted > SYSDATE - 1/24;
DBMS_OUTPUT.PUT_LINE('[INFO] Changesets applied in last hour: ' || v_count);
END;
/
PROMPT [INFO] Deployment completed successfully
EXIT 0
Rollback on Failure Pattern
-- deploy_with_rollback.sql
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK
-- Tag pre-deployment state for rollback target
lb tag -tag pre-deploy-&1
-- Apply changes
lb update -changelog-file controller.xml
-- Verify deployment
DECLARE
v_invalid NUMBER;
BEGIN
SELECT COUNT(*) INTO v_invalid FROM user_objects WHERE status = 'INVALID';
IF v_invalid > 0 THEN
-- Trigger SQLERROR path → ROLLBACK and EXIT with error
RAISE_APPLICATION_ERROR(-20001,
'Deployment produced ' || v_invalid || ' invalid objects. Rolling back.');
END IF;
END;
/
EXIT 0
If validation fails, the WHENEVER SQLERROR EXIT ... ROLLBACK triggers DML rollback. To roll back Liquibase schema changes, add a shell-level rollback step:
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
@deploy_with_rollback.sql "$CI_COMMIT_SHORT_SHA"
EOF
if [ $? -ne 0 ]; then
echo "Deployment failed, rolling back Liquibase changes..."
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
lb rollback -tag pre-deploy-${CI_COMMIT_SHORT_SHA} -changelog-file controller.xml
EXIT
EOF
exit 1
fi
Security Best Practices in CI/CD
Keep Credentials Out of the Command Line
Pass credentials through /nolog + stdin or a SEPS wallet so they never appear as process arguments. With SEPS the password is not present at all; with /nolog it travels on fd 0 instead of argv.
# SEPS wallet — no password on the command line or in stdin
export TNS_ADMIN=/path/to/seps-wallet
sql -S /@DB_ALIAS @deploy.sql
# /nolog + stdin — password from a masked CI variable, not visible in `ps`
sql -S /nolog <<EOF
CONNECT ${DB_USER}/"${DB_PASS}"@${DB_SERVICE}
@deploy.sql
EOF
# Avoid: credentials interpolated as process arguments
sql -S "${DB_USER}/${DB_PASS}@${DB_SERVICE}" @deploy.sql
Do not enable shell tracing (set -x, set -v, bash -x) in steps that handle the stdin connect — tracing prints the expanded CONNECT line to the job log.
Wallet as Base64 Secret
Store the entire wallet ZIP as a base64-encoded CI/CD secret:
# Convert wallet to base64 for storage as a secret
base64 -i Wallet_MyATP.zip > wallet_b64.txt
# Copy the contents of wallet_b64.txt into your CI/CD secret variable
In the pipeline, decode and use it:
mkdir -p /tmp/wallet
echo "$WALLET_ZIP_B64" | base64 -d > /tmp/wallet.zip
unzip -q /tmp/wallet.zip -d /tmp/wallet
chmod 600 /tmp/wallet/*
export TNS_ADMIN=/tmp/wallet
Least-Privilege Deployment Account
Use a dedicated deployment schema or user with only the privileges required for deployment:
-- Create a deployment user with only necessary privileges
CREATE USER deploy_user IDENTIFIED BY "SecurePass123!";
GRANT CREATE SESSION TO deploy_user;
GRANT CREATE TABLE, CREATE VIEW, CREATE PROCEDURE, CREATE SEQUENCE TO deploy_user;
GRANT UNLIMITED TABLESPACE TO deploy_user;
-- Do NOT grant DBA unless strictly necessary
Audit Deployment Activity
-- Log each pipeline run to an audit table
INSERT INTO deployment_audit (
pipeline_id, commit_sha, deployed_by, deploy_time, status
) VALUES (
'${CI_PIPELINE_ID}', '${CI_COMMIT_SHA}', '${GITLAB_USER_LOGIN}', SYSDATE, 'STARTED'
);
COMMIT;
Best Practices
- Never put credentials on the SQLcl command line. Use SEPS (
/@alias) or/nolog+ aCONNECTon stdin. See Keep Credentials Out of the Command Line. - Always use
WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACKat the top of every CI/CD SQL script. Without it, SQLcl will continue executing after a SQL error and exit with code 0 even if statements failed. - Use the
-S(silent) flag for all CI/CD invocations. Without it, the SQLcl banner and connection messages will appear in your pipeline log and may confuse log parsers. - Keep the wallet directory out of your repository. Store it as a base64-encoded CI/CD secret and decode it at pipeline runtime. Never commit wallet files (
.sso,.jks,.p12,ewallet.p12) to version control. - Use
lb tagbefore every deployment to create a rollback target. Always include the commit SHA or pipeline ID in the tag name so you can identify exactly what state the database was in. - Separate validation from deployment in your pipeline stages. The validate stage (checking
lb status, running lint checks) should run on pull requests; the deploy stage should run only on merges to main/master. - Capture SPOOL output and upload it as a CI artifact on failure. Raw SQLcl output alone may not be sufficient to diagnose what went wrong.
Common Mistakes and How to Avoid Them
Mistake: SQL errors are silently ignored and the pipeline succeeds
Without WHENEVER SQLERROR EXIT SQL.SQLCODE, SQLcl ignores SQL errors by default and exits with code 0. Always add the WHENEVER directive as the very first statement in CI scripts.
Mistake: TNS_ADMIN not set before connecting
If TNS_ADMIN is not set, SQLcl cannot find the wallet and the connection fails. Set TNS_ADMIN as an environment variable in the pipeline step or export it in the CI step's before_script. Verify with echo $TNS_ADMIN before running SQLcl.
Mistake: Wallet files have incorrect permissions
On Linux, Oracle requires wallet files to be readable only by the owning user (chmod 600). CI runners may unzip files with permissions that are too open, causing SSL handshake failures. Always run chmod 600 /tmp/wallet/* after unzipping.
Mistake: Heredoc with variable expansion in quotes kills substitution
Using <<'EOF' (with quotes around EOF) prevents shell variable expansion inside the heredoc. Use <<EOF (without quotes) when you need shell variables expanded, and <<'EOF' when you want to pass &variable substitutions through to SQLcl literally.
Mistake: Pipeline uses DBA account for all operations Using a DBA or ADMIN account for routine deployments is a security risk and makes it hard to audit what changes came from the pipeline versus manual intervention. Use a dedicated deployment account with only the minimum required privileges.
Mistake: Liquibase changes not rolled back on pipeline failure
WHENEVER SQLERROR EXIT ... ROLLBACK only rolls back uncommitted DML transactions. Liquibase DDL changes (CREATE TABLE, ALTER TABLE) are auto-committed by Oracle and cannot be rolled back transactionally. Always use lb rollback -tag for schema rollbacks after a failed deployment.
Sources
- Starting and Leaving SQLcl — startup flags reference
- Oracle SQLcl 25.2 User's Guide
- SQLcl Release Notes 25.2
- Oracle SQLcl Releases index
- Oracle Database Security Guide 19c — Configuring Authentication (Secure External Password Store)
- GitHub Docs — Using secrets in GitHub Actions
- GitLab Docs — CI/CD variables, masking, and protected variables