#!/bin/sh set -eu WORKSHOP_TEST_PORT=${WORKSHOP_TEST_PORT:-18090} WORKSHOP_TEST_PROJECT="homarr-workshop-test-$$" WORKSHOP_ROLLBACK_PORT=${WORKSHOP_ROLLBACK_PORT:-$((WORKSHOP_TEST_PORT + 1))} WORKSHOP_ROLLBACK_PROJECT="homarr-workshop-rollback-$$" WORKSHOP_ROLLBACK_SNAPSHOT="/tmp/$WORKSHOP_ROLLBACK_PROJECT.json" WORKSHOP_MIGRATION_COUNT=$( node -e "const fs = require('node:fs'); console.log(fs.readdirSync('apps/workshop/pb_migrations').filter((name) => name.endsWith('.js')).length)" ) ROLLBACK_CONTAINER="" node apps/workshop/tests/workshop-contracts.mjs node apps/workshop/tests/workshop-migration.mjs node apps/workshop/tests/entrypoint-url.mjs node apps/workshop/tests/workshop-identity.mjs node apps/workshop/tests/workshop-oauth-hook.mjs node scripts/build-workshop-validator.mjs cleanup() { if [ -n "$ROLLBACK_CONTAINER" ]; then docker stop "$ROLLBACK_CONTAINER" >/dev/null 2>&1 || true fi docker compose -p "$WORKSHOP_ROLLBACK_PROJECT" \ -f apps/workshop/docker-compose.yml down --volumes --remove-orphans >/dev/null 2>&1 || true PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \ -f apps/workshop/docker-compose.yml down --volumes --remove-orphans >/dev/null 2>&1 || true rm -f "$WORKSHOP_ROLLBACK_SNAPSHOT" } trap cleanup EXIT wait_for_workshop() { port=$1 for attempt in $(seq 1 60); do if curl --fail --silent "http://127.0.0.1:$port/api/health" >/dev/null; then return fi if [ "$attempt" -eq 60 ]; then return 1 fi sleep 1 done } start_rollback_server() { ROLLBACK_CONTAINER=$( PB_EXPOSE_PORT="$WORKSHOP_ROLLBACK_PORT" docker compose -p "$WORKSHOP_ROLLBACK_PROJECT" \ -f apps/workshop/docker-compose.yml run --rm -d --service-ports --no-deps \ --entrypoint pocketbase workshop \ serve --http="0.0.0.0:$WORKSHOP_ROLLBACK_PORT" --origins="*" --dir=/pb_data \ --hooksDir=/tmp --migrationsDir=/tmp --publicDir=/pb_public --automigrate=false ) wait_for_workshop "$WORKSHOP_ROLLBACK_PORT" } stop_rollback_server() { docker stop "$ROLLBACK_CONTAINER" >/dev/null ROLLBACK_CONTAINER="" } run_rollback_assertion() { WORKSHOP_TEST_URL="http://127.0.0.1:$WORKSHOP_ROLLBACK_PORT" \ WORKSHOP_ROLLBACK_SNAPSHOT="$WORKSHOP_ROLLBACK_SNAPSHOT" \ node apps/workshop/tests/workshop-rollback.integration.mjs "$1" } PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \ -f apps/workshop/docker-compose.yml up --build -d for attempt in $(seq 1 60); do if curl --fail --silent "http://127.0.0.1:$WORKSHOP_TEST_PORT/api/health" >/dev/null; then break fi if [ "$attempt" -eq 60 ]; then PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \ -f apps/workshop/docker-compose.yml logs --no-color workshop exit 1 fi sleep 1 done docker compose -p "$WORKSHOP_TEST_PROJECT" -f apps/workshop/docker-compose.yml exec -T workshop \ pocketbase superuser create \ workshop-test@example.invalid 'WorkshopLocalTest123!' --dir=/pb_data WORKSHOP_TEST_URL="http://127.0.0.1:$WORKSHOP_TEST_PORT" node apps/workshop/tests/workshop.integration.mjs # PocketBase rate limits use the client IP as the counter key. Run the # differential corpus in restart-separated shards so the adversarial coverage # can grow without weakening the production write limits. for corpus_shard in 0 1; do PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \ -f apps/workshop/docker-compose.yml restart workshop for attempt in $(seq 1 60); do if curl --fail --silent "http://127.0.0.1:$WORKSHOP_TEST_PORT/api/health" >/dev/null; then break fi if [ "$attempt" -eq 60 ]; then exit 1 fi sleep 1 done WORKSHOP_TEST_URL="http://127.0.0.1:$WORKSHOP_TEST_PORT" \ WORKSHOP_CORPUS_SHARD="$corpus_shard" WORKSHOP_CORPUS_SHARD_COUNT=2 pnpm exec tsx \ apps/workshop/tests/workshop-validator-corpus.mts done for client_id in workshop-test-client workshop-rotated-client; do GITHUB_CLIENT_ID="$client_id" GITHUB_CLIENT_SECRET="workshop-test-secret" PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" \ docker compose -p "$WORKSHOP_TEST_PROJECT" -f apps/workshop/docker-compose.yml up -d --force-recreate workshop for attempt in $(seq 1 60); do if curl --fail --silent "http://127.0.0.1:$WORKSHOP_TEST_PORT/api/health" >/dev/null; then break fi if [ "$attempt" -eq 60 ]; then exit 1 fi sleep 1 done WORKSHOP_TEST_URL="http://127.0.0.1:$WORKSHOP_TEST_PORT" EXPECTED_GITHUB_CLIENT_ID="$client_id" \ node apps/workshop/tests/oauth-sync.integration.mjs done GITHUB_CLIENT_ID="workshop-partial-client" GITHUB_CLIENT_SECRET="" WORKSHOP_REQUIRE_OAUTH=false \ PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \ -f apps/workshop/docker-compose.yml up -d --force-recreate workshop partial_rejected=false for attempt in $(seq 1 30); do if PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \ -f apps/workshop/docker-compose.yml logs --no-color workshop | grep -q "partial_credentials"; then partial_rejected=true break fi sleep 1 done if [ "$partial_rejected" != true ]; then echo "Workshop accepted a partial GitHub OAuth configuration" exit 1 fi GITHUB_CLIENT_ID="" GITHUB_CLIENT_SECRET="" WORKSHOP_REQUIRE_OAUTH=true PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" \ docker compose -p "$WORKSHOP_TEST_PROJECT" -f apps/workshop/docker-compose.yml \ up -d --force-recreate workshop required_rejected=false for attempt in $(seq 1 30); do if PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \ -f apps/workshop/docker-compose.yml logs --no-color workshop | grep -q "required_but_disabled"; then required_rejected=true break fi sleep 1 done if [ "$required_rejected" != true ]; then echo "Workshop started without OAuth while WORKSHOP_REQUIRE_OAUTH=true" exit 1 fi PB_EXPOSE_PORT="$WORKSHOP_TEST_PORT" docker compose -p "$WORKSHOP_TEST_PROJECT" \ -f apps/workshop/docker-compose.yml stop workshop WORKSHOP_IMAGE_ID=$( docker compose -p "$WORKSHOP_TEST_PROJECT" -f apps/workshop/docker-compose.yml images -q workshop ) docker image tag "$WORKSHOP_IMAGE_ID" "$WORKSHOP_ROLLBACK_PROJECT-workshop:latest" PB_EXPOSE_PORT="$WORKSHOP_ROLLBACK_PORT" docker compose -p "$WORKSHOP_ROLLBACK_PROJECT" \ -f apps/workshop/docker-compose.yml run --rm --no-deps --workdir /tmp --entrypoint pocketbase workshop \ superuser create workshop-rollback@example.invalid 'WorkshopRollback123!' --dir=/pb_data \ --hooksDir=/tmp --migrationsDir=/tmp --automigrate=false start_rollback_server run_rollback_assertion fresh-prepare stop_rollback_server PB_EXPOSE_PORT="$WORKSHOP_ROLLBACK_PORT" docker compose -p "$WORKSHOP_ROLLBACK_PROJECT" \ -f apps/workshop/docker-compose.yml run --rm --no-deps --entrypoint pocketbase workshop \ migrate up --dir=/pb_data --migrationsDir=/pb_migrations --hooksDir=/tmp --automigrate=false start_rollback_server run_rollback_assertion fresh-migrated stop_rollback_server printf 'y\n' | PB_EXPOSE_PORT="$WORKSHOP_ROLLBACK_PORT" docker compose -p "$WORKSHOP_ROLLBACK_PROJECT" \ -f apps/workshop/docker-compose.yml run --rm --no-deps --entrypoint pocketbase workshop \ migrate down "$WORKSHOP_MIGRATION_COUNT" --dir=/pb_data --migrationsDir=/pb_migrations \ --hooksDir=/tmp --automigrate=false start_rollback_server run_rollback_assertion fresh-restored run_rollback_assertion prepare stop_rollback_server PB_EXPOSE_PORT="$WORKSHOP_ROLLBACK_PORT" docker compose -p "$WORKSHOP_ROLLBACK_PROJECT" \ -f apps/workshop/docker-compose.yml run --rm --no-deps --entrypoint pocketbase workshop \ migrate up --dir=/pb_data --migrationsDir=/pb_migrations --hooksDir=/tmp --automigrate=false start_rollback_server run_rollback_assertion migrated stop_rollback_server printf 'y\n' | PB_EXPOSE_PORT="$WORKSHOP_ROLLBACK_PORT" docker compose -p "$WORKSHOP_ROLLBACK_PROJECT" \ -f apps/workshop/docker-compose.yml run --rm --no-deps --entrypoint pocketbase workshop \ migrate down "$WORKSHOP_MIGRATION_COUNT" --dir=/pb_data --migrationsDir=/pb_migrations \ --hooksDir=/tmp --automigrate=false start_rollback_server run_rollback_assertion restored stop_rollback_server PB_EXPOSE_PORT="$WORKSHOP_ROLLBACK_PORT" docker compose -p "$WORKSHOP_ROLLBACK_PROJECT" \ -f apps/workshop/docker-compose.yml run --rm --no-deps --entrypoint pocketbase workshop \ migrate up --dir=/pb_data --migrationsDir=/pb_migrations --hooksDir=/tmp --automigrate=false start_rollback_server run_rollback_assertion migrated stop_rollback_server