Files
oracle__skills/db/sqlcl/sqlcl-cicd.md
Gustavo Evangelista 1e627937fa docs(sqlcl): keep passwords off the SQLcl command line
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>
2026-05-10 11:27:14 -03:00

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 + a CONNECT on stdin. See Keep Credentials Out of the Command Line.
  • Always use WHENEVER SQLERROR EXIT SQL.SQLCODE ROLLBACK at 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 tag before 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