mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc3aa5421b | ||
|
|
530913856d | ||
|
|
612371337b | ||
|
|
9e06254062 | ||
|
|
f18a8c197d | ||
|
|
1829e372df | ||
|
|
68001b86fe | ||
|
|
dfe70b91eb | ||
|
|
f38ebbeed5 | ||
|
|
afe4e3e392 | ||
|
|
670a003fff | ||
|
|
819a3af44c | ||
|
|
ac31f6fa98 | ||
|
|
d814ce3e6c | ||
|
|
a6db3171d4 | ||
|
|
7a6af5600f |
@@ -0,0 +1,191 @@
|
||||
name: Auto Main Deployment
|
||||
|
||||
# Rebuilds and redeploys the always-on demo from main, using the same image,
|
||||
# host and licence plumbing as the PR previews.
|
||||
#
|
||||
# Port choice: PR previews publish on their bare PR number, so the demo has to
|
||||
# sit below every PR number that will ever be open. Open PRs are in the
|
||||
# thousands and only climb, so 900 is permanently clear of them and leaves
|
||||
# 901+ free for any other static service. The VPS proxy maps
|
||||
# <port>.ssl.stirlingpdf.cloud, so this lands on 900.ssl.stirlingpdf.cloud.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
DEMO_PORT: "900"
|
||||
DEMO_DIR: /stirling/V2-MAIN
|
||||
BUILD_PORTAL: "true"
|
||||
|
||||
jobs:
|
||||
deploy-main:
|
||||
runs-on: ubuntu-latest
|
||||
# Never let two pushes to main race for the same directory on the VPS.
|
||||
concurrency:
|
||||
group: v2-deploy-main
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # needed for the commit-hash image tag
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Get commit hash for app
|
||||
id: commit-hash
|
||||
run: |
|
||||
APP_HASH=$(git log -1 --format="%H" -- . 2>/dev/null || echo "")
|
||||
if [ -z "$APP_HASH" ]; then
|
||||
APP_HASH="no-changes"
|
||||
echo "app_short=no-changes" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "app_short=${APP_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "App hash: $APP_HASH"
|
||||
|
||||
# Same tag scheme as the PR previews, so a commit already built for a PR
|
||||
# is reused verbatim when it lands on main.
|
||||
- name: Check if image exists
|
||||
id: check-image
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
if: steps.check-image.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-latest
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-main-latest
|
||||
build-args: |
|
||||
VERSION_TAG=v2-alpha
|
||||
BUILD_PORTAL=${{ env.BUILD_PORTAL }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Stage seed database
|
||||
id: assets
|
||||
env:
|
||||
SSH_KEY: ../private.key
|
||||
VPS_USER: ${{ secrets.NEW_VPS_USERNAME }}
|
||||
VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
REMOTE_DIR: ${{ env.DEMO_DIR }}
|
||||
SEED_DB: testing/seed-databases/main-demo.mv.db
|
||||
# A demo is only useful if it looks the same every time.
|
||||
RESET_SEED: "true"
|
||||
run: bash scripts/deploy/stage-seed-database.sh
|
||||
|
||||
- name: Deploy to VPS
|
||||
id: deploy
|
||||
env:
|
||||
# Same enterprise key the enterprise Playwright suite runs on.
|
||||
ENTERPRISE_LICENSE_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
|
||||
run: |
|
||||
if [ -n "$ENTERPRISE_LICENSE_KEY" ]; then
|
||||
PREMIUM_ENABLED="true"
|
||||
echo "license_enabled=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
PREMIUM_ENABLED="false"
|
||||
echo "license_enabled=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf-main:
|
||||
container_name: stirling-pdf-v2-main
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
ports:
|
||||
- "${{ env.DEMO_PORT }}:8080"
|
||||
volumes:
|
||||
- ${{ env.DEMO_DIR }}/data:/usr/share/tessdata:rw
|
||||
- ${{ env.DEMO_DIR }}/config:/configs:rw
|
||||
- ${{ env.DEMO_DIR }}/logs:/logs:rw
|
||||
- ${{ env.DEMO_DIR }}/storage:/storage:rw
|
||||
environment:
|
||||
PREMIUM_ENABLED: "${PREMIUM_ENABLED}"
|
||||
PREMIUM_KEY: "${ENTERPRISE_LICENSE_KEY}"
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: "Stirling-PDF Demo (main)"
|
||||
UI_HOMEDESCRIPTION: "Always-on demo built from the main branch"
|
||||
UI_APPNAMENAVBAR: "Demo (main)"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "https://${{ env.DEMO_PORT }}.ssl.stirlingpdf.cloud"
|
||||
baseUrl: "https://${{ env.DEMO_PORT }}.ssl.stirlingpdf.cloud"
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose-main.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T \
|
||||
${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
|
||||
set -e
|
||||
mv /tmp/docker-compose-main.yml ${{ env.DEMO_DIR }}/docker-compose.yml
|
||||
cd ${{ env.DEMO_DIR }}
|
||||
docker-compose down --remove-orphans 2>/dev/null || true
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
# Prune images only - --volumes would take the PR previews' data with it.
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
- name: Wait for the demo to answer
|
||||
run: |
|
||||
URL="http://${{ secrets.NEW_VPS_HOST }}:${{ env.DEMO_PORT }}/api/v1/info/status"
|
||||
for i in $(seq 1 60); do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$URL" || true)
|
||||
if [ "$code" = "200" ]; then
|
||||
echo "Demo healthy after $((i * 10))s"
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "::error::Demo did not become healthy within 10 minutes"
|
||||
exit 1
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: rm -f ../private.key
|
||||
continue-on-error: true
|
||||
@@ -248,20 +248,55 @@ jobs:
|
||||
BUILD_PORTAL=${{ env.BUILD_PORTAL }}
|
||||
platforms: linux/amd64
|
||||
|
||||
# Staging runs with the VPS SSH key in scope, so the script comes from
|
||||
# main, not the PR branch. After the build: the Dockerfile does `COPY . .`
|
||||
# and an earlier checkout would thrash the image layer cache.
|
||||
- name: Checkout deploy scripts from main
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: main
|
||||
path: .deploy-main
|
||||
sparse-checkout: scripts/deploy
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${NEW_VPS_SSH_KEY}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
env:
|
||||
NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
|
||||
|
||||
- name: Stage seed database
|
||||
id: assets
|
||||
env:
|
||||
SSH_KEY: ../private.key
|
||||
VPS_USER: ${{ secrets.NEW_VPS_USERNAME }}
|
||||
VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
REMOTE_DIR: /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}
|
||||
# Seed data comes from the PR branch so a PR that changes the fixture
|
||||
# can be exercised in its own preview.
|
||||
SEED_DB: testing/seed-databases/pr-preview.mv.db
|
||||
# Previews are throwaway; every deploy starts from the known-good
|
||||
# fixture rather than whatever the last tester left behind.
|
||||
RESET_SEED: "true"
|
||||
run: bash .deploy-main/scripts/deploy/stage-seed-database.sh
|
||||
|
||||
- name: Deploy V2 to VPS
|
||||
id: deploy
|
||||
run: |
|
||||
# Use same port strategy as regular PRs - just the PR number
|
||||
V2_PORT=${{ needs.check-pr.outputs.pr_number }}
|
||||
|
||||
# Absent on fork PRs, where secrets are not exposed.
|
||||
if [ -n "$ENTERPRISE_LICENSE_KEY" ]; then
|
||||
PREMIUM_ENABLED="true"
|
||||
echo "license_enabled=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
PREMIUM_ENABLED="false"
|
||||
echo "license_enabled=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Create docker-compose for V2 with unified embedded image
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
@@ -277,6 +312,8 @@ jobs:
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/storage:/storage:rw
|
||||
environment:
|
||||
PREMIUM_ENABLED: "${PREMIUM_ENABLED}"
|
||||
PREMIUM_KEY: "${ENTERPRISE_LICENSE_KEY}"
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
@@ -328,6 +365,8 @@ jobs:
|
||||
TEST_LOGIN_PASSWORD: ${{ secrets.TEST_LOGIN_PASSWORD }}
|
||||
NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }}
|
||||
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
# Same enterprise key the enterprise Playwright suite runs on.
|
||||
ENTERPRISE_LICENSE_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
|
||||
|
||||
# ---- Storybook preview (only when this PR touches stories/.storybook) ----
|
||||
# Runs inside the same approved-contributor-gated deploy job, so it deploys
|
||||
@@ -393,6 +432,8 @@ jobs:
|
||||
env:
|
||||
SB_URL: ${{ steps.storybook.outputs.url }}
|
||||
SB_FILES: ${{ steps.sb-changes.outputs.storybook_files }}
|
||||
LICENSE_ENABLED: ${{ steps.deploy.outputs.license_enabled }}
|
||||
SEED_STAGED: ${{ steps.assets.outputs.seed_staged }}
|
||||
NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }}
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
@@ -447,8 +488,18 @@ jobs:
|
||||
`</details>\n\n`;
|
||||
}
|
||||
|
||||
const licenseNote = process.env.LICENSE_ENABLED === "true"
|
||||
? `🏢 **Enterprise licence active** - audit, teams and portal analytics unlocked.\n\n`
|
||||
: `⚠️ **No enterprise licence** - premium features are locked.\n\n`;
|
||||
|
||||
const seedNote = process.env.SEED_STAGED === "true"
|
||||
? `👥 **Seeded teams, users and policies** - credentials are not public; ask if you need them.\n\n`
|
||||
: ``;
|
||||
|
||||
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
||||
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
licenseNote +
|
||||
seedNote +
|
||||
portalNote +
|
||||
storybookNote +
|
||||
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
||||
@@ -516,7 +567,6 @@ jobs:
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${NEW_VPS_SSH_KEY}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
env:
|
||||
NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }}
|
||||
- name: Cleanup V2 deployment
|
||||
|
||||
@@ -4,6 +4,8 @@ name: DB migration smoke test
|
||||
# releases (v2.0.0 / v2.5.0 / v2.10.0) and verifies admin login still works.
|
||||
# Catches schema changes that would break existing user databases under
|
||||
# Hibernate's `ddl-auto=update` upgrade path.
|
||||
#
|
||||
# Also covers the seed databases in testing/seed-databases/.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -181,6 +181,10 @@ app/core/src/main/resources/static/images/google-drive.svg
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.db
|
||||
# Deliberately committed H2 fixtures: past-release captures for the migration
|
||||
# smoke test, and the seed databases shipped into the preview deployments.
|
||||
!app/proprietary/src/test/resources/db-migration-fixtures/*.mv.db
|
||||
!testing/seed-databases/*.mv.db
|
||||
build
|
||||
app/core/build
|
||||
app/common/build
|
||||
|
||||
@@ -20,6 +20,9 @@ set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
FIXTURE_DIR="${FIXTURE_DIR:-$REPO_ROOT/app/proprietary/src/test/resources/db-migration-fixtures}"
|
||||
# Seed databases are checked too: a schema change that breaks one would
|
||||
# otherwise only surface as a broken deployment.
|
||||
SEED_DIR="${SEED_DIR:-$REPO_ROOT/testing/seed-databases}"
|
||||
STIRLING_JAR="${STIRLING_JAR:-}"
|
||||
JAVA_BIN="${JAVA_BIN:-${MIGRATION_TEST_JAVA:-}}"
|
||||
ADMIN_USERNAME="${ADMIN_USERNAME:-admin}"
|
||||
@@ -108,6 +111,9 @@ wait_for_url() {
|
||||
|
||||
test_fixture() {
|
||||
local fixture_path="$1"
|
||||
# Seed databases ship deliberately non-default admin credentials, so only
|
||||
# the boot and schema checks apply to them.
|
||||
local skip_login="${2:-false}"
|
||||
local label
|
||||
label=$(basename "$fixture_path" .mv.db)
|
||||
log "=== $label ==="
|
||||
@@ -162,7 +168,12 @@ test_fixture() {
|
||||
rc=1
|
||||
fi
|
||||
|
||||
if (( rc == 0 )); then
|
||||
if (( rc == 0 )) && [[ "$skip_login" == "true" ]]; then
|
||||
local status_code
|
||||
status_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$base_url/api/v1/info/status" || echo "000")
|
||||
log " GET /api/v1/info/status -> HTTP $status_code (login check skipped)"
|
||||
if [[ "$status_code" != "200" ]]; then rc=1; else log " PASS: $label migrated"; fi
|
||||
elif (( rc == 0 )); then
|
||||
local login_body; login_body=$(printf '{"username":"%s","password":"%s"}' "$ADMIN_USERNAME" "$ADMIN_PASSWORD")
|
||||
local resp_file="$workdir/login.resp"
|
||||
local code
|
||||
@@ -228,12 +239,23 @@ main() {
|
||||
mapfile -t fixtures < <(find "$FIXTURE_DIR" -maxdepth 1 -name '*.mv.db' | sort)
|
||||
[[ ${#fixtures[@]} -gt 0 ]] || fail "No fixtures under $FIXTURE_DIR"
|
||||
|
||||
if [[ -d "$SEED_DIR" ]]; then
|
||||
local seeds
|
||||
mapfile -t seeds < <(find "$SEED_DIR" -maxdepth 1 -name '*.mv.db' | sort)
|
||||
if [[ ${#seeds[@]} -gt 0 ]]; then
|
||||
log "Including ${#seeds[@]} deployment seed database(s) from $SEED_DIR"
|
||||
fixtures+=("${seeds[@]}")
|
||||
fi
|
||||
fi
|
||||
|
||||
local failed=0
|
||||
local failed_names=()
|
||||
for f in "${fixtures[@]}"; do
|
||||
local skip_login=false
|
||||
[[ "$f" == "$SEED_DIR"/* ]] && skip_login=true
|
||||
# set -e would abort the whole run on the first failure; we want to
|
||||
# report every fixture's status, so guard with ||.
|
||||
if ! test_fixture "$f"; then
|
||||
if ! test_fixture "$f" "$skip_login"; then
|
||||
failed=$(( failed + 1 ))
|
||||
failed_names+=("$(basename "$f" .mv.db)")
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copies the seed H2 database onto the VPS before `docker-compose up`, so a
|
||||
# preview deployment boots with teams, users and policies already present.
|
||||
# Shared by the PR preview and main demo workflows.
|
||||
#
|
||||
# Inputs (env): SSH_KEY, VPS_USER, VPS_HOST, REMOTE_DIR (all required),
|
||||
# SEED_DB (optional path), RESET_SEED ("true" reseeds and wipes existing data).
|
||||
# Outputs: appends seed_staged=true|false to $GITHUB_OUTPUT when set.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf '[stage-seed] %s\n' "$*" >&2; }
|
||||
die() { printf '[stage-seed][error] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
: "${SSH_KEY:?SSH_KEY is required}"
|
||||
: "${VPS_USER:?VPS_USER is required}"
|
||||
: "${VPS_HOST:?VPS_HOST is required}"
|
||||
: "${REMOTE_DIR:?REMOTE_DIR is required}"
|
||||
|
||||
SEED_DB="${SEED_DB:-}"
|
||||
RESET_SEED="${RESET_SEED:-false}"
|
||||
|
||||
SSH_OPTS=(-i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null)
|
||||
remote_sh() { ssh "${SSH_OPTS[@]}" -T "$VPS_USER@$VPS_HOST" "$@"; }
|
||||
push() { scp "${SSH_OPTS[@]}" "$1" "$VPS_USER@$VPS_HOST:$2"; }
|
||||
|
||||
emit() {
|
||||
[[ -n "${GITHUB_OUTPUT:-}" ]] && printf '%s\n' "$1" >> "$GITHUB_OUTPUT"
|
||||
log "$1"
|
||||
}
|
||||
|
||||
# Namespaced by the deployment dir so parallel PR deploys never race on /tmp.
|
||||
STAGING="/tmp/stirling-stage-$(basename "$REMOTE_DIR")-$$"
|
||||
|
||||
remote_sh "mkdir -p '$REMOTE_DIR'/{config,logs,data,storage} '$STAGING'"
|
||||
# Clean up on any exit path: a mid-run failure would otherwise leave a copy of
|
||||
# the database on the VPS every retry.
|
||||
cleanup_staging() { remote_sh "rm -rf '$STAGING'" 2>/dev/null || true; }
|
||||
trap cleanup_staging EXIT
|
||||
|
||||
# Must stop first: a running app holds the H2 file open and flushes on shutdown,
|
||||
# so swapping the file underneath it loses the seed.
|
||||
if remote_sh "test -f '$REMOTE_DIR/docker-compose.yml'"; then
|
||||
log "Stopping the running deployment before staging..."
|
||||
remote_sh "cd '$REMOTE_DIR' && docker-compose down --remove-orphans 2>/dev/null || true"
|
||||
fi
|
||||
|
||||
# Matches spring.datasource.url; the container's base path is /.
|
||||
DB_NAME="stirling-pdf-DB-2.3.232.mv.db"
|
||||
|
||||
seed_staged=false
|
||||
if [[ -n "$SEED_DB" && ! -f "$SEED_DB" ]]; then
|
||||
# Expected on PR branches cut before the fixtures landed; still deploy.
|
||||
log "WARNING: SEED_DB '$SEED_DB' not found in this checkout - skipping seed."
|
||||
SEED_DB=""
|
||||
fi
|
||||
|
||||
if [[ -n "$SEED_DB" ]]; then
|
||||
if [[ "$RESET_SEED" == "true" ]]; then
|
||||
should_seed=true
|
||||
elif remote_sh "test -f '$REMOTE_DIR/config/$DB_NAME'"; then
|
||||
should_seed=false
|
||||
else
|
||||
should_seed=true
|
||||
fi
|
||||
|
||||
if [[ "$should_seed" == true ]]; then
|
||||
log "Seeding database from $(basename "$SEED_DB") (reset=$RESET_SEED)..."
|
||||
push "$SEED_DB" "$STAGING/$DB_NAME"
|
||||
# Stale .trace.db/.lock.db beside a replaced .mv.db blocks H2 opening it.
|
||||
remote_sh "rm -f '$REMOTE_DIR/config/stirling-pdf-DB-'*.mv.db \
|
||||
'$REMOTE_DIR/config/stirling-pdf-DB-'*.trace.db \
|
||||
'$REMOTE_DIR/config/stirling-pdf-DB-'*.lock.db; \
|
||||
install -m 644 '$STAGING/$DB_NAME' '$REMOTE_DIR/config/$DB_NAME'"
|
||||
seed_staged=true
|
||||
else
|
||||
log "Existing database found and RESET_SEED is not true - leaving data alone."
|
||||
fi
|
||||
fi
|
||||
emit "seed_staged=$seed_staged"
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Builds a seed H2 database for the CI preview deployments: boots the app once
|
||||
# against an empty database so Hibernate creates the schema and bootstrap rows,
|
||||
# then applies a plain SQL script to the shut-down file.
|
||||
#
|
||||
# Driving the REST API instead would need a live enterprise licence just to
|
||||
# build a fixture - the team endpoints are premium-gated and saveUser stops at
|
||||
# the unlicensed 5-seat limit. SQL against the app's own schema needs nothing.
|
||||
#
|
||||
# Usage:
|
||||
# ./gradlew :stirling-pdf:bootJar -PnoSpotless
|
||||
# scripts/seed-db/build-seed-db.sh --sql testing/seed-databases/pr-preview.sql \
|
||||
# --out testing/seed-databases/pr-preview.mv.db
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
SEED_SQL=""
|
||||
OUT_DB=""
|
||||
JAR=""
|
||||
STARTUP_TIMEOUT_SEC="${STARTUP_TIMEOUT_SEC:-300}"
|
||||
|
||||
# Must match spring.datasource.url in application.properties - the app derives
|
||||
# the file name, so the fixture has to carry the same one.
|
||||
DB_BASENAME="stirling-pdf-DB-2.3.232"
|
||||
|
||||
log() { printf '[seed-db] %s\n' "$*" >&2; }
|
||||
die() { printf '[seed-db][error] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--sql) SEED_SQL="$2"; shift 2 ;;
|
||||
--out) OUT_DB="$2"; shift 2 ;;
|
||||
--jar) JAR="$2"; shift 2 ;;
|
||||
-h|--help) sed -n '2,14p' "$0"; exit 0 ;;
|
||||
*) die "Unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$SEED_SQL" ]] || die "--sql is required"
|
||||
[[ -n "$OUT_DB" ]] || die "--out is required"
|
||||
[[ -f "$SEED_SQL" ]] || die "Seed script not found: $SEED_SQL"
|
||||
|
||||
find_java() {
|
||||
if [[ -n "${JAVA_HOME:-}" && -x "$JAVA_HOME/bin/java" ]]; then
|
||||
printf '%s' "$JAVA_HOME/bin/java"
|
||||
else
|
||||
command -v java || die "No java on PATH and JAVA_HOME is unset"
|
||||
fi
|
||||
}
|
||||
|
||||
JAVA_BIN=$(find_java)
|
||||
major=$("$JAVA_BIN" -XshowSettings:properties -version 2>&1 \
|
||||
| awk -F'= ' '/java.specification.version =/ { print $2; exit }')
|
||||
[[ -n "$major" && "$major" -ge 25 ]] \
|
||||
|| die "The built jar needs JDK 25+, but '$JAVA_BIN' is Java ${major:-unknown}"
|
||||
|
||||
if [[ -z "$JAR" ]]; then
|
||||
JAR=$(find "$REPO_ROOT/app/core/build/libs" -maxdepth 1 \
|
||||
\( -name 'Stirling-PDF*.jar' -o -name 'stirling-pdf*.jar' \) 2>/dev/null \
|
||||
| grep -vE '(-plain|-sources)\.jar$' | head -n 1 || true)
|
||||
[[ -n "$JAR" ]] || die "No jar under app/core/build/libs - run './gradlew :stirling-pdf:bootJar' first"
|
||||
fi
|
||||
JAR=$(realpath "$JAR")
|
||||
|
||||
# The H2 jar ships in the build's dependency cache; RunScript comes from it.
|
||||
H2_JAR=$(find "${GRADLE_USER_HOME:-$HOME/.gradle}/caches/modules-2" -name 'h2-*.jar' 2>/dev/null \
|
||||
| grep -vE '(sources|javadoc)' | sort | tail -n 1 || true)
|
||||
[[ -n "$H2_JAR" ]] || die "Could not find an h2-*.jar in the Gradle cache"
|
||||
|
||||
# JVM needs native paths: under Git Bash, Java resolves /tmp/xyz against the
|
||||
# drive root and silently creates a new empty database instead.
|
||||
to_native() {
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
cygpath -m "$1" # mixed mode: C:/like/this, valid in JDBC URLs and file args
|
||||
else
|
||||
printf '%s' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
free_port() {
|
||||
python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
|
||||
}
|
||||
|
||||
wait_for_url() {
|
||||
local url="$1" deadline=$(( $(date +%s) + STARTUP_TIMEOUT_SEC ))
|
||||
while (( $(date +%s) < deadline )); do
|
||||
local code
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$url" || true)
|
||||
[[ "$code" =~ ^[1-5][0-9][0-9]$ ]] && return 0
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
WORKDIR=$(mktemp -d)
|
||||
cleanup() { rm -rf "$WORKDIR"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$WORKDIR/configs"
|
||||
DB_URL="jdbc:h2:file:./configs/$DB_BASENAME;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE;MODE=PostgreSQL"
|
||||
PORT=$(free_port)
|
||||
LOG_FILE="$WORKDIR/app.log"
|
||||
|
||||
log "jar=$JAR"
|
||||
log "h2=$H2_JAR"
|
||||
log "workdir=$WORKDIR port=$PORT"
|
||||
log "Phase 1/3: booting to generate the schema..."
|
||||
|
||||
pushd "$WORKDIR" >/dev/null
|
||||
# DISABLE_ADDITIONAL_FEATURES=false pulls in the proprietary module so the
|
||||
# teams/policies/audit tables exist. The licence only gates endpoints.
|
||||
DISABLE_ADDITIONAL_FEATURES=false \
|
||||
SECURITY_ENABLELOGIN=true \
|
||||
SECURITY_INITIALLOGIN_USERNAME=admin \
|
||||
SECURITY_INITIALLOGIN_PASSWORD=stirling \
|
||||
"$JAVA_BIN" -Xmx1g -jar "$JAR" \
|
||||
"--server.port=$PORT" \
|
||||
"--spring.datasource.url=$DB_URL" \
|
||||
"--spring.jpa.show-sql=false" \
|
||||
"--logging.level.root=WARN" \
|
||||
"--logging.level.stirling=INFO" \
|
||||
> "$LOG_FILE" 2>&1 &
|
||||
APP_PID=$!
|
||||
popd >/dev/null
|
||||
|
||||
if ! wait_for_url "http://127.0.0.1:$PORT/api/v1/info/status"; then
|
||||
log "App did not start within ${STARTUP_TIMEOUT_SEC}s; last 60 log lines:"
|
||||
tail -n 60 "$LOG_FILE" >&2
|
||||
kill -KILL "$APP_PID" 2>/dev/null || true
|
||||
die "Boot failed"
|
||||
fi
|
||||
log " app up"
|
||||
|
||||
# Graceful stop so H2 checkpoints; DB_CLOSE_ON_EXIT=TRUE covers a hard kill.
|
||||
kill -TERM "$APP_PID" 2>/dev/null || true
|
||||
for _ in $(seq 1 30); do kill -0 "$APP_PID" 2>/dev/null || break; sleep 1; done
|
||||
kill -KILL "$APP_PID" 2>/dev/null || true
|
||||
wait "$APP_PID" 2>/dev/null || true
|
||||
log " app stopped, schema captured"
|
||||
|
||||
DB_FILE="$WORKDIR/configs/$DB_BASENAME.mv.db"
|
||||
[[ -f "$DB_FILE" ]] || die "No database produced at $DB_FILE"
|
||||
|
||||
OFFLINE_URL="jdbc:h2:file:$(to_native "$WORKDIR/configs/$DB_BASENAME");MODE=PostgreSQL"
|
||||
|
||||
# Guard the above: H2 creates a blank database rather than failing, and every
|
||||
# later step would then "succeed" on nothing.
|
||||
table_count=$("$JAVA_BIN" -cp "$H2_JAR" org.h2.tools.Shell \
|
||||
-url "$OFFLINE_URL" -user sa -sql \
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='PUBLIC'" 2>/dev/null \
|
||||
| grep -oE '^[0-9]+$' | head -n 1 || true)
|
||||
[[ -n "$table_count" && "$table_count" -gt 0 ]] \
|
||||
|| die "Opened database has no tables - the JDBC URL is not pointing at the generated file"
|
||||
log " schema has $table_count tables"
|
||||
|
||||
log "Phase 2/3: applying $(basename "$SEED_SQL")..."
|
||||
# RunScript exits non-zero on the first failing statement, so a drifted seed
|
||||
# fails the build instead of half-populating the fixture.
|
||||
"$JAVA_BIN" -cp "$H2_JAR" org.h2.tools.RunScript \
|
||||
-url "$OFFLINE_URL" -user sa -script "$(to_native "$(realpath "$SEED_SQL")")" -showResults \
|
||||
|| die "Seed script failed - see the error above"
|
||||
|
||||
log "Phase 3/3: writing $OUT_DB"
|
||||
mkdir -p "$(dirname "$OUT_DB")"
|
||||
cp "$DB_FILE" "$OUT_DB"
|
||||
|
||||
size=$(wc -c < "$OUT_DB" | tr -d ' ')
|
||||
log "Done: $OUT_DB (${size} bytes)"
|
||||
Binary file not shown.
@@ -0,0 +1,125 @@
|
||||
-- Teams, users and saved policies for the main demo deployment. Applied by
|
||||
-- scripts/seed-db/build-seed-db.sh on top of a freshly booted database.
|
||||
--
|
||||
-- Only bcrypt hashes are committed; the demo-account plaintext is a 32-char
|
||||
-- random string held outside the repo. Rotate by re-hashing and rebuilding.
|
||||
-- Seeding users means SECURITY_INITIALLOGIN_* is ignored: the app only
|
||||
-- bootstraps an admin when the user table is empty.
|
||||
|
||||
INSERT INTO teams (team_id, name) VALUES
|
||||
(100, 'Engineering'),
|
||||
(101, 'Finance'),
|
||||
(102, 'Legal'),
|
||||
(103, 'Marketing'),
|
||||
(104, 'Operations'),
|
||||
(105, 'Support');
|
||||
|
||||
-- Reset the bootstrap admin to the seeded credentials rather than leaving the
|
||||
-- generator's well-known admin/stirling in place.
|
||||
UPDATE users
|
||||
SET password = '$2a$10$yUh3LFp9Vp6Av5jkYm/L.uQZlkd61zUIJ6oARXIl9crydZjtHWSWa',
|
||||
is_first_login = FALSE,
|
||||
has_completed_initial_setup = TRUE
|
||||
WHERE username = 'admin';
|
||||
|
||||
-- role_name stays null: the effective role lives in authorities, matching how
|
||||
-- the app writes the bootstrap admin. new.starter is mid first-login,
|
||||
-- former.staff is disabled.
|
||||
INSERT INTO users (user_id, username, password, enabled, authenticationtype,
|
||||
is_first_login, has_completed_initial_setup,
|
||||
force_password_change, oauth_grandfathered, team_id,
|
||||
email, created_at, updated_at) VALUES
|
||||
(100, 'eng.lead', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 100, 'eng.lead@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(101, 'eng.dev', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 100, 'eng.dev@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(102, 'eng.qa', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 100, 'eng.qa@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(103, 'finance.lead', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 101, 'finance.lead@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(104, 'finance.ap', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 101, 'finance.ap@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(105, 'finance.audit', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 101, 'finance.audit@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(106, 'legal.counsel', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 102, 'legal.counsel@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(107, 'legal.para', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 102, 'legal.para@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(108, 'marketing.lead', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 103, 'marketing.lead@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(109, 'marketing.des', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 103, 'marketing.des@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(110, 'ops.lead', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 104, 'ops.lead@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(111, 'ops.automation', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 104, 'ops.automation@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(112, 'support.lead', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 105, 'support.lead@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(113, 'support.agent', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 105, 'support.agent@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(114, 'new.starter', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', TRUE, FALSE, TRUE, FALSE, 100, 'new.starter@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(115, 'former.staff', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', FALSE, 'web', FALSE, TRUE, FALSE, FALSE, 102, 'former.staff@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
||||
|
||||
INSERT INTO authorities (id, authority, user_id) VALUES
|
||||
(100, 'ROLE_ADMIN', 100),
|
||||
(101, 'ROLE_USER', 101),
|
||||
(102, 'ROLE_USER', 102),
|
||||
(103, 'ROLE_ADMIN', 103),
|
||||
(104, 'ROLE_WEB_ONLY_USER', 104),
|
||||
(105, 'ROLE_USER', 105),
|
||||
(106, 'ROLE_LIMITED_API_USER', 106),
|
||||
(107, 'ROLE_WEB_ONLY_USER', 107),
|
||||
(108, 'ROLE_USER', 108),
|
||||
(109, 'ROLE_USER', 109),
|
||||
(110, 'ROLE_ADMIN', 110),
|
||||
(111, 'ROLE_LIMITED_API_USER', 111),
|
||||
(112, 'ROLE_USER', 112),
|
||||
(113, 'ROLE_EXTRA_LIMITED_API_USER', 113),
|
||||
(114, 'ROLE_USER', 114),
|
||||
(115, 'ROLE_USER', 115);
|
||||
|
||||
-- former.staff has a null accepted_at: exercises the pending-membership state.
|
||||
INSERT INTO team_memberships (membership_id, team_id, user_id, role,
|
||||
invited_at, accepted_at, created_at, updated_at) VALUES
|
||||
(100, 100, 100, 'LEADER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(101, 100, 101, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(102, 100, 102, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(103, 101, 103, 'LEADER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(104, 101, 104, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(105, 101, 105, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(106, 102, 106, 'LEADER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(107, 102, 107, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(108, 103, 108, 'LEADER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(109, 103, 109, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(110, 104, 110, 'LEADER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(111, 104, 111, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(112, 105, 112, 'LEADER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(113, 105, 113, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(114, 100, 114, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(115, 102, 115, 'MEMBER', CURRENT_TIMESTAMP, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
||||
|
||||
INSERT INTO user_settings (user_id, setting_key, setting_value) VALUES
|
||||
(100, 'language', 'en-GB'),
|
||||
(100, 'theme', 'dark'),
|
||||
(103, 'language', 'en-US'),
|
||||
(106, 'language', 'de-DE'),
|
||||
(108, 'theme', 'dark');
|
||||
|
||||
-- policy_json is the serialised Policy record; the columns beside it are
|
||||
-- denormalised copies the store uses for listing and ordering, so they must
|
||||
-- agree. GET /api/v1/policies is team-scoped, so the first two sit in team 1
|
||||
-- (Default) where admin lands; the rest need their team's lead to be visible.
|
||||
INSERT INTO policies (id, name, owner, enabled, team_id, sort_order, policy_json) VALUES
|
||||
('demo-shrink-and-clean', 'Shrink and clean up', 'admin', TRUE, 1, 1,
|
||||
'{"id":"demo-shrink-and-clean","name":"Shrink and clean up","owner":"admin","enabled":true,"inputs":[],"steps":[{"operation":"/api/v1/misc/compress-pdf","parameters":{"optimizeLevel":3},"fileParameters":{}},{"operation":"/api/v1/misc/sanitize-pdf","parameters":{"removeJavaScript":true,"removeEmbeddedFiles":true},"fileParameters":{}}],"output":{"type":"inline","options":{}},"outputIds":[],"teamId":1}'),
|
||||
|
||||
('demo-watermark-draft', 'Watermark as draft', 'admin', TRUE, 1, 2,
|
||||
'{"id":"demo-watermark-draft","name":"Watermark as draft","owner":"admin","enabled":true,"inputs":[],"steps":[{"operation":"/api/v1/security/add-watermark","parameters":{"watermarkType":"text","watermarkText":"DRAFT","fontSize":40,"rotation":45,"opacity":0.3,"widthSpacer":50,"heightSpacer":50},"fileParameters":{}}],"output":{"type":"inline","options":{}},"outputIds":[],"teamId":1}'),
|
||||
|
||||
('demo-compress-archive', 'Compress for archive', 'eng.lead', TRUE, 100, 3,
|
||||
'{"id":"demo-compress-archive","name":"Compress for archive","owner":"eng.lead","enabled":true,"inputs":[],"steps":[{"operation":"/api/v1/misc/compress-pdf","parameters":{"optimizeLevel":4},"fileParameters":{}}],"output":{"type":"inline","options":{}},"outputIds":[],"teamId":100}'),
|
||||
|
||||
('demo-invoice-flatten', 'Flatten and stamp invoices', 'finance.lead', TRUE, 101, 4,
|
||||
'{"id":"demo-invoice-flatten","name":"Flatten and stamp invoices","owner":"finance.lead","enabled":true,"inputs":[],"steps":[{"operation":"/api/v1/misc/flatten","parameters":{"flattenOnlyForms":false},"fileParameters":{}},{"operation":"/api/v1/misc/add-stamp","parameters":{"stampType":"text","stampText":"PAID","fontSize":30,"rotation":45,"opacity":0.4,"position":5},"fileParameters":{}}],"output":{"type":"inline","options":{}},"outputIds":[],"teamId":101}'),
|
||||
|
||||
('demo-contract-redact', 'Redact and protect contracts', 'legal.counsel', TRUE, 102, 5,
|
||||
'{"id":"demo-contract-redact","name":"Redact and protect contracts","owner":"legal.counsel","enabled":true,"inputs":[],"steps":[{"operation":"/api/v1/security/auto-redact","parameters":{"listOfText":"Confidential\nInternal Only","useRegex":false,"wholeWordSearch":false,"convertPDFToImage":true},"fileParameters":{}},{"operation":"/api/v1/security/add-password","parameters":{"ownerPassword":"demo-owner","preventPrinting":true},"fileParameters":{}}],"output":{"type":"inline","options":{}},"outputIds":[],"teamId":102}'),
|
||||
|
||||
('demo-brochure-merge', 'Merge brochure pages', 'marketing.lead', TRUE, 103, 6,
|
||||
'{"id":"demo-brochure-merge","name":"Merge brochure pages","owner":"marketing.lead","enabled":true,"inputs":[],"steps":[{"operation":"/api/v1/general/merge-pdfs","parameters":{"sortType":"orderProvided"},"fileParameters":{}}],"output":{"type":"inline","options":{}},"outputIds":[],"teamId":103}'),
|
||||
|
||||
('demo-ocr-scans', 'OCR scanned intake', 'ops.lead', FALSE, 104, 7,
|
||||
'{"id":"demo-ocr-scans","name":"OCR scanned intake","owner":"ops.lead","enabled":false,"inputs":[],"steps":[{"operation":"/api/v1/misc/ocr-pdf","parameters":{"languages":["eng"],"ocrType":"skip-text"},"fileParameters":{}}],"output":{"type":"inline","options":{}},"outputIds":[],"teamId":104}');
|
||||
|
||||
-- Counters still sit low after bootstrap; without this the first row the app
|
||||
-- inserts collides with a seeded id.
|
||||
ALTER TABLE teams ALTER COLUMN team_id RESTART WITH 200;
|
||||
ALTER TABLE users ALTER COLUMN user_id RESTART WITH 200;
|
||||
ALTER TABLE authorities ALTER COLUMN id RESTART WITH 200;
|
||||
ALTER TABLE team_memberships ALTER COLUMN membership_id RESTART WITH 200;
|
||||
Binary file not shown.
@@ -0,0 +1,59 @@
|
||||
-- Teams and users for PR preview deployments. Applied by
|
||||
-- scripts/seed-db/build-seed-db.sh on top of a freshly booted database.
|
||||
--
|
||||
-- Only bcrypt hashes are committed; the demo-account plaintext is a 32-char
|
||||
-- random string held outside the repo. Rotate by re-hashing and rebuilding.
|
||||
-- Seeding users means SECURITY_INITIALLOGIN_* is ignored: the app only
|
||||
-- bootstraps an admin when the user table is empty.
|
||||
|
||||
INSERT INTO teams (team_id, name) VALUES
|
||||
(100, 'Engineering'),
|
||||
(101, 'Finance'),
|
||||
(102, 'Legal');
|
||||
|
||||
-- Reset the bootstrap admin to the seeded credentials rather than leaving the
|
||||
-- generator's well-known admin/stirling in place.
|
||||
UPDATE users
|
||||
SET password = '$2a$10$yUh3LFp9Vp6Av5jkYm/L.uQZlkd61zUIJ6oARXIl9crydZjtHWSWa',
|
||||
is_first_login = FALSE,
|
||||
has_completed_initial_setup = TRUE
|
||||
WHERE username = 'admin';
|
||||
|
||||
-- role_name stays null: the effective role lives in authorities, matching how
|
||||
-- the app writes the bootstrap admin.
|
||||
INSERT INTO users (user_id, username, password, enabled, authenticationtype,
|
||||
is_first_login, has_completed_initial_setup,
|
||||
force_password_change, oauth_grandfathered, team_id,
|
||||
email, created_at, updated_at) VALUES
|
||||
(100, 'eng.lead', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 100, 'eng.lead@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(101, 'eng.dev', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 100, 'eng.dev@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(102, 'finance.lead', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 101, 'finance.lead@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(103, 'finance.ap', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 101, 'finance.ap@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(104, 'legal.counsel', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', TRUE, 'web', FALSE, TRUE, FALSE, FALSE, 102, 'legal.counsel@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
-- Disabled: covers the deactivated-account rendering and login rejection.
|
||||
(105, 'former.staff', '$2a$10$Rw/dExUcJO5OM6Ijj09L0.MQqXzqUsWPfxO1lQne7QO.K.2ryWWai', FALSE, 'web', FALSE, TRUE, FALSE, FALSE, 102, 'former.staff@example.com', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
||||
|
||||
INSERT INTO authorities (id, authority, user_id) VALUES
|
||||
(100, 'ROLE_ADMIN', 100),
|
||||
(101, 'ROLE_USER', 101),
|
||||
(102, 'ROLE_USER', 102),
|
||||
(103, 'ROLE_WEB_ONLY_USER', 103),
|
||||
(104, 'ROLE_LIMITED_API_USER', 104),
|
||||
(105, 'ROLE_USER', 105);
|
||||
|
||||
INSERT INTO team_memberships (membership_id, team_id, user_id, role,
|
||||
invited_at, accepted_at, created_at, updated_at) VALUES
|
||||
(100, 100, 100, 'LEADER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(101, 100, 101, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(102, 101, 102, 'LEADER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(103, 101, 103, 'MEMBER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
(104, 102, 104, 'LEADER', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
-- Invited but never accepted: exercises the pending-membership state.
|
||||
(105, 102, 105, 'MEMBER', CURRENT_TIMESTAMP, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
||||
|
||||
-- Counters still sit low after bootstrap; without this the first row the app
|
||||
-- inserts collides with a seeded id.
|
||||
ALTER TABLE teams ALTER COLUMN team_id RESTART WITH 200;
|
||||
ALTER TABLE users ALTER COLUMN user_id RESTART WITH 200;
|
||||
ALTER TABLE authorities ALTER COLUMN id RESTART WITH 200;
|
||||
ALTER TABLE team_memberships ALTER COLUMN membership_id RESTART WITH 200;
|
||||
Reference in New Issue
Block a user