Skip to content
Open
54 changes: 54 additions & 0 deletions .github/actions/setup-runtime/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Setup Runtime
description: Install (if needed), authenticate, and verify Docker or Podman tooling.

inputs:
runtime:
description: Runtime to prepare (docker or podman)
required: true
ghcr-username:
description: Username for GHCR login
required: true
ghcr-token:
description: Token for GHCR login
required: true

runs:
using: composite
steps:
- name: Install podman and compose support
if: inputs.runtime == 'podman'
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y podman podman-compose

- name: Log in to GHCR (Docker)
if: inputs.runtime == 'docker'
shell: bash
run: |
set -euo pipefail
echo "${{ inputs.ghcr-token }}" | docker login ghcr.io -u "${{ inputs.ghcr-username }}" --password-stdin

- name: Log in to GHCR (Podman)
if: inputs.runtime == 'podman'
shell: bash
run: |
set -euo pipefail
echo "${{ inputs.ghcr-token }}" | podman login ghcr.io -u "${{ inputs.ghcr-username }}" --password-stdin

- name: Verify runtime tooling
shell: bash
run: |
set -euo pipefail
if [ "${{ inputs.runtime }}" = "docker" ]; then
command -v docker
docker version
docker compose version
else
command -v podman
podman --version
command -v podman-compose
podman-compose version
PODMAN_COMPOSE_PROVIDER=podman-compose podman compose version
fi
168 changes: 168 additions & 0 deletions .github/scripts/runtime-flow.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/usr/bin/env bash
set -euo pipefail

RUNTIME="${1:-}"
SCENARIO="${2:-}"

if [[ "$RUNTIME" != "docker" && "$RUNTIME" != "podman" ]]; then
echo "usage: $0 <docker|podman> <manual|installer>" >&2
exit 1
fi

if [[ "$SCENARIO" != "manual" && "$SCENARIO" != "installer" ]]; then
echo "usage: $0 <docker|podman> <manual|installer>" >&2
exit 1
fi

if [[ "$RUNTIME" == "podman" ]]; then
export PODMAN_COMPOSE_PROVIDER=podman-compose
fi

compose_cmd() {
"$RUNTIME" compose "$@"
}

resolve_db_container() {
local name

if "$RUNTIME" ps -a --format '{{.Names}}' | grep -Fxq "postgres"; then
echo "postgres"
return 0
fi

name=$("$RUNTIME" ps -a --filter "label=com.docker.compose.service=db" --format '{{.Names}}' | head -n1)
if [[ -n "$name" ]]; then
echo "$name"
return 0
fi

name=$("$RUNTIME" ps -a --filter "label=io.podman.compose.service=db" --format '{{.Names}}' | head -n1)
if [[ -n "$name" ]]; then
echo "$name"
return 0
fi

return 1
}

wait_for_postgres() {
local db_container
db_container=$(resolve_db_container || true)
if [[ -z "$db_container" ]]; then
echo "could not find db container after compose up" >&2
exit 1
fi

for i in $(seq 1 30); do
if "$RUNTIME" exec "$db_container" pg_isready -U synkronus_user -d postgres -q 2>/dev/null; then
echo "postgres ready after ${i}s"
return 0
fi
if [[ "$i" -eq 30 ]]; then
echo "postgres did not become ready" >&2
exit 1
fi
sleep 1
done
}

wait_for_health() {
local url="$1"
local fail_msg="$2"

local healthy=0
for i in $(seq 1 40); do
code=$(curl -sS -o /tmp/synk-health-body.txt -w '%{http_code}' --max-time 10 "$url" || true)
if [[ "$code" == "200" ]]; then
echo "health_status=$code"
cat /tmp/synk-health-body.txt
healthy=1
break
fi
sleep 2
done

if [[ "$healthy" -ne 1 ]]; then
echo "$fail_msg" >&2
compose_cmd logs
exit 1
fi
}

seed_attachment_probe() {
local seeded=0
for i in $(seq 1 20); do
if "$RUNTIME" exec synkronus sh -c 'mkdir -p /app/data/attachments/ci && echo ci-probe > /app/data/attachments/ci/probe.txt'; then
seeded=1
break
fi
sleep 2
done

if [[ "$seeded" -ne 1 ]]; then
echo "failed to seed attachments after service became healthy" >&2
compose_cmd logs
exit 1
fi
}

run_backup_assertions() {
chmod +x ./utilities/backup-db.sh ./utilities/backup-attachments.sh

SYNK_RUNTIME="$RUNTIME" ./utilities/backup-db.sh -o /tmp/synk-db-backup.sql
test -s /tmp/synk-db-backup.sql

SYNK_RUNTIME="$RUNTIME" ./utilities/backup-attachments.sh -o /tmp/synk-attachments-backup
test -f /tmp/synk-attachments-backup/app-data-attachments/ci/probe.txt
}

cleanup() {
compose_cmd down -v || true
}
trap cleanup EXIT

if [[ "$SCENARIO" == "manual" ]]; then
echo "=== STEP 1: start db ==="
compose_cmd up db -d
"$RUNTIME" ps -a

echo "=== STEP 2: wait for postgres ==="
wait_for_postgres

echo "=== STEP 3: create org database ==="
chmod +x ./create_sync_db.sh
SYNK_RUNTIME="$RUNTIME" ./create_sync_db.sh myorg

echo "=== STEP 4: start full stack ==="
compose_cmd up -d

echo "=== STEP 5: wait for health ==="
wait_for_health "http://localhost:8080/health" "service did not become healthy"

echo "=== STEP 5b: seed attachment data ==="
seed_attachment_probe

echo "=== STEP 6: test backup scripts ==="
run_backup_assertions
exit 0
fi

echo "=== STEP 1: run installer (no domain, localhost) ==="
chmod +x ./install.sh
printf 'n\nlocalhost\n' | ./install.sh

test -f Caddyfile
test -f docker-compose.override.yml

echo "=== STEP 2: start stack ==="
compose_cmd up -d
compose_cmd ps

echo "=== STEP 3: wait for caddy health endpoint ==="
wait_for_health "http://localhost:8081/health" "installer flow did not become healthy via caddy"

echo "=== STEP 3b: seed attachment data ==="
seed_attachment_probe

echo "=== STEP 4: test backup scripts ==="
run_backup_assertions
96 changes: 96 additions & 0 deletions .github/scripts/upgrade-path-flow.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail

RUNTIME="${1:-}"
PROJECT_NAME="${2:-upgrade-path}"

if [[ "$RUNTIME" != "docker" && "$RUNTIME" != "podman" ]]; then
echo "usage: $0 <docker|podman> [project_name]" >&2
exit 1
fi

if [[ "$RUNTIME" == "podman" ]]; then
export PODMAN_COMPOSE_PROVIDER=podman-compose
VOL_SUFFIX=":Z"
SCRIPT_MOUNT_SUFFIX=":ro,Z"
else
VOL_SUFFIX=""
SCRIPT_MOUNT_SUFFIX=":ro"
fi

compose_cmd() {
"$RUNTIME" compose -p "$PROJECT_NAME" "$@"
}

cleanup() {
compose_cmd down -v || true
}
trap cleanup EXIT

VOLUME_NAME="${PROJECT_NAME}_appdata"

echo "=== STEP 1: create compose volumes ==="
compose_cmd up db -d
compose_cmd down

echo "=== STEP 2: seed legacy layout ==="
"$RUNTIME" run --rm -v "${VOLUME_NAME}:/data${VOL_SUFFIX}" docker.io/library/alpine:3.21 sh -eu -c '
mkdir -p /data/app-bundles/forms
mkdir -p /data/app-bundle-versions/0001
printf "legacy-active" > /data/app-bundles/forms/index.txt
printf "legacy-version" > /data/app-bundle-versions/0001/bundle.zip
'

echo "=== STEP 3: dry-run migration ==="
"$RUNTIME" run --rm \
-v "${VOLUME_NAME}:/data${VOL_SUFFIX}" \
-v "$PWD/utilities/migrate-synkronus-data.sh:/migrate.sh${SCRIPT_MOUNT_SUFFIX}" \
docker.io/library/alpine:3.21 \
sh /migrate.sh --dry-run /data | tee /tmp/migrate-dry-run.log

grep -q "Migrating app-bundles/ -> app-bundle/active/" /tmp/migrate-dry-run.log
grep -q "Migrating app-bundle-versions/ -> app-bundle/versions/" /tmp/migrate-dry-run.log

echo "=== STEP 4: apply migration ==="
"$RUNTIME" run --rm \
-v "${VOLUME_NAME}:/data${VOL_SUFFIX}" \
-v "$PWD/utilities/migrate-synkronus-data.sh:/migrate.sh${SCRIPT_MOUNT_SUFFIX}" \
docker.io/library/alpine:3.21 \
sh /migrate.sh /data

echo "=== STEP 5: verify migrated files ==="
"$RUNTIME" run --rm -v "${VOLUME_NAME}:/data${VOL_SUFFIX}" docker.io/library/alpine:3.21 sh -eu -c '
test -f /data/app-bundle/active/forms/index.txt
test -f /data/app-bundle/versions/0001/bundle.zip
test -d /data/attachments
test -f /data/app-bundles/forms/index.txt
test -f /data/app-bundle-versions/0001/bundle.zip
'

echo "=== STEP 6: idempotence rerun ==="
"$RUNTIME" run --rm \
-v "${VOLUME_NAME}:/data${VOL_SUFFIX}" \
-v "$PWD/utilities/migrate-synkronus-data.sh:/migrate.sh${SCRIPT_MOUNT_SUFFIX}" \
docker.io/library/alpine:3.21 \
sh /migrate.sh /data

echo "=== STEP 7: start stack and verify health ==="
compose_cmd up -d

healthy=0
for _ in $(seq 1 40); do
code=$(curl -sS -o /tmp/upgrade-health-body.txt -w '%{http_code}' --max-time 10 http://localhost:8080/health || true)
if [[ "$code" == "200" ]]; then
echo "health_status=$code"
cat /tmp/upgrade-health-body.txt
healthy=1
break
fi
sleep 2
done

if [[ "$healthy" -ne 1 ]]; then
echo "service did not become healthy after migration" >&2
compose_cmd logs
exit 1
fi
41 changes: 41 additions & 0 deletions .github/workflows/reusable-runtime-flow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Reusable Runtime Flow

on:
workflow_call:
inputs:
runtime:
required: true
type: string
scenario:
required: true
type: string
timeout_minutes:
required: false
type: number
default: 30

permissions:
contents: read
packages: read

jobs:
run-runtime-flow:
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout_minutes }}

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Setup runtime
uses: ./.github/actions/setup-runtime
with:
runtime: ${{ inputs.runtime }}
ghcr-username: ${{ github.actor }}
ghcr-token: ${{ secrets.GITHUB_TOKEN }}

- name: Run runtime scenario flow
run: |
set -euo pipefail
chmod +x ./.github/scripts/runtime-flow.sh
./.github/scripts/runtime-flow.sh "${{ inputs.runtime }}" "${{ inputs.scenario }}"
31 changes: 31 additions & 0 deletions .github/workflows/test-docker-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Test Quickstart with Docker runtime

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

permissions:
contents: read
packages: read

jobs:
docker-manual-install:
uses: ./.github/workflows/reusable-runtime-flow.yml
with:
runtime: docker
scenario: manual
timeout_minutes: 20
secrets: inherit

docker-installer-flow:
uses: ./.github/workflows/reusable-runtime-flow.yml
with:
runtime: docker
scenario: installer
timeout_minutes: 25
secrets: inherit


Loading
Loading