Skip to content

CI/CD Integration

Run migrations and database operations in CI/CD pipelines with dbward’s approval workflow.

  • Exit code 2 = approval pending (pipeline can wait or notify)
  • --idempotency-key = prevent duplicate requests on retry
  • --format json = machine-readable JSON envelope on stdout
  • --format quiet = JSON output with zero stderr (log-free)
  • API tokens = no interactive login needed
  • --yes = skip confirmation prompts (required in non-interactive mode)
name: Deploy
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dbward
run: |
VERSION=$(curl -s https://api.github.com/repos/dbward-dev/dbward/releases/latest | grep -o '"tag_name": "v[^"]*"' | cut -d'"' -f4 | sed 's/^v//')
curl -sL "https://github.com/dbward-dev/dbward/releases/latest/download/dbward-v${VERSION}-x86_64-unknown-linux-gnu.tar.gz" | tar xz
chmod +x dbward
sudo mv dbward /usr/local/bin/
- name: Run migrations
env:
DBWARD_TOKEN: ${{ secrets.DBWARD_TOKEN }}
run: |
cat > dbward.toml << EOF
default_database = "app"
[server]
url = "${{ vars.DBWARD_SERVER_URL }}"
token = "${DBWARD_TOKEN}"
[databases.app]
EOF
dbward --environment production migrate up \
--idempotency-key "deploy-${{ github.sha }}" \
--ticket "${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}" \
--repo "${{ github.repository }}"
- name: Run migrations
id: migrate
continue-on-error: true
run: dbward --environment production migrate up --idempotency-key "deploy-${{ github.sha }}"
- name: Wait for approval if pending
if: steps.migrate.outcome == 'failure'
run: |
# Exit code 2 = pending approval
REQUEST_ID=$(dbward --format json --environment production migrate status | jq -r '.data.pending_request_id // empty')
if [ -n "$REQUEST_ID" ]; then
echo "⏳ Waiting for approval: $REQUEST_ID"
echo "Approve with: dbward request approve $REQUEST_ID"
# Optionally: poll until approved (with timeout)
# dbward request resume $REQUEST_ID
fi

Create a dedicated CI token with appropriate permissions:

Terminal window
dbward token create \
--subject "github-actions" \
--scope-roles requester \
--expires 90d

Store the token as a repository secret (DBWARD_TOKEN).

CodeMeaningCI action
0SuccessContinue pipeline
1Error (auth failure, network, validation)Fail pipeline
2Pending approval / issues foundWait or notify
124Timeout (agent did not respond)Retry or alert
130Interrupted (SIGINT)Re-run job

Use --idempotency-key to safely retry failed CI jobs:

Terminal window
dbward --environment production migrate up --idempotency-key "deploy-${GITHUB_SHA}"

If the request already exists (same key), dbward returns the existing request status instead of creating a duplicate.

Use --format json for machine-readable output. All commands produce a consistent JSON envelope on stdout:

Terminal window
dbward --format json --environment production migrate status
{"ok": true, "data": {"migrations": [{"name": "20260501_create_users", "status": "applied"}, {"name": "20260502_add_index", "status": "pending"}]}}

Error responses follow the same structure:

{"ok": false, "data": null, "error": {"code": "network_error", "message": "connection refused"}}
ModestdoutstderrBest for
--format jsonJSON envelopeError message onlyScripts that parse output with jq
--format quietJSON envelopeNothing (0 bytes)Log-free pipelines, health checks

Use --format quiet when stderr noise would clutter CI logs:

Terminal window
RESULT=$(dbward --format quiet -y execute "SELECT version()" 2>/dev/null)
echo "$RESULT" | jq -r '.data.result_data.rows[0]'
Terminal window
# Check success
dbward --format json -y execute "SELECT 1" | jq -e '.ok'
# Extract data field
TOKEN=$(dbward --format json -y token create --scope-roles requester | jq -r '.data.token')
# Handle errors
RESULT=$(dbward --format json -y -e production execute "SELECT 1" 2>/dev/null)
if echo "$RESULT" | jq -e '.ok' > /dev/null 2>&1; then
echo "Success"
else
echo "Error: $(echo "$RESULT" | jq -r '.error.message')"
fi

Configure workflows to auto-approve non-production environments:

dbward-server.toml
[[workflows]]
database = "*"
environment = "staging"
[workflows.auto_approve]
mode = "always"

This lets CI deploy to staging without waiting, while production still requires human approval.

Combine with webhooks so approvers are notified immediately:

[[webhooks]]
url = "${SLACK_WEBHOOK_URL}"
events = ["request.created"]
format = "slack"

When CI creates a migration request, the team gets a Slack message with the SQL and an approve prompt.

  • Migrations — Migration file management
  • Workflows — Configure auto-approve for specific environments