feat: add v2 stack (backend, runner, ui-v2) with release workflow
All checks were successful
Release / Scraper / Test (push) Successful in 10s
Release / UI / Build (push) Successful in 26s
Release / v2 / Build ui-v2 (push) Successful in 17s
Release / Scraper / Docker (push) Successful in 47s
Release / UI / Docker (push) Successful in 56s
CI / Scraper / Lint (pull_request) Successful in 7s
CI / Scraper / Test (pull_request) Successful in 8s
CI / UI / Build (pull_request) Successful in 16s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
Release / v2 / Docker / ui-v2 (push) Successful in 56s
Release / v2 / Test backend (push) Successful in 4m35s
iOS CI / Build (pull_request) Successful in 4m28s
Release / v2 / Docker / backend (push) Successful in 1m29s
Release / v2 / Docker / runner (push) Successful in 1m39s
iOS CI / Test (pull_request) Successful in 9m51s
All checks were successful
Release / Scraper / Test (push) Successful in 10s
Release / UI / Build (push) Successful in 26s
Release / v2 / Build ui-v2 (push) Successful in 17s
Release / Scraper / Docker (push) Successful in 47s
Release / UI / Docker (push) Successful in 56s
CI / Scraper / Lint (pull_request) Successful in 7s
CI / Scraper / Test (pull_request) Successful in 8s
CI / UI / Build (pull_request) Successful in 16s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
Release / v2 / Docker / ui-v2 (push) Successful in 56s
Release / v2 / Test backend (push) Successful in 4m35s
iOS CI / Build (pull_request) Successful in 4m28s
Release / v2 / Docker / backend (push) Successful in 1m29s
Release / v2 / Docker / runner (push) Successful in 1m39s
iOS CI / Test (pull_request) Successful in 9m51s
- backend/: Go API server and runner binaries with PocketBase + MinIO storage - ui-v2/: SvelteKit frontend rewrite - docker-compose-new.yml: compose file for the v2 stack - .gitea/workflows/release-v2.yaml: CI/CD for backend, runner, and ui-v2 Docker Hub images - scripts/pb-init.sh: migrate from wget to curl, add superuser bootstrap for fresh installs - .env.example: document DOCKER_BUILDKIT=1 for Colima users
This commit is contained in:
@@ -17,19 +17,49 @@ PB_PASSWORD="${POCKETBASE_ADMIN_PASSWORD:-changeme123}"
|
||||
|
||||
log() { echo "[pb-init] $*"; }
|
||||
|
||||
# ─── 0. Ensure curl is available ─────────────────────────────────────────────
|
||||
if ! command -v curl > /dev/null 2>&1; then
|
||||
apk add --no-cache curl > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# ─── 1. Wait for PocketBase to be ready ──────────────────────────────────────
|
||||
log "waiting for PocketBase at $PB_URL ..."
|
||||
until wget -qO- "$PB_URL/api/health" > /dev/null 2>&1; do
|
||||
until curl -sf "$PB_URL/api/health" > /dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
log "PocketBase is up"
|
||||
|
||||
# ─── 2. Authenticate and obtain a superuser token ────────────────────────────
|
||||
# ─── 2. Ensure the superuser exists, then authenticate ───────────────────────
|
||||
#
|
||||
# The muchobien/pocketbase image does NOT auto-create a superuser from env vars.
|
||||
# On a fresh install PocketBase exposes a one-time install JWT in its log output
|
||||
# at /pb_data/logs/ — but we can't read that from here.
|
||||
#
|
||||
# Strategy:
|
||||
# a) Try to auth normally (works on subsequent runs once the account exists).
|
||||
# b) If that returns 400/401, PocketBase is fresh. Use the install token
|
||||
# obtained from the /_/ redirect Location header (PocketBase v0.23+).
|
||||
|
||||
log "ensuring superuser $PB_EMAIL exists ..."
|
||||
|
||||
# Try to get the install token from the /_/ redirect Location header.
|
||||
LOCATION=$(curl -sf -o /dev/null -w "%{redirect_url}" "$PB_URL/_/" 2>/dev/null || true)
|
||||
if echo "$LOCATION" | grep -q "pbinstal/"; then
|
||||
INSTALL_TOKEN=$(echo "$LOCATION" | sed 's|.*pbinstal/||' | tr -d ' \r\n')
|
||||
log "install token found — creating superuser via install endpoint"
|
||||
curl -sf -X POST "$PB_URL/api/collections/_superusers/records" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $INSTALL_TOKEN" \
|
||||
-d "{\"email\":\"$PB_EMAIL\",\"password\":\"$PB_PASSWORD\",\"passwordConfirm\":\"$PB_PASSWORD\"}" \
|
||||
> /dev/null 2>&1 || true
|
||||
log "superuser create attempted (may already exist)"
|
||||
fi
|
||||
|
||||
# ─── 3. Authenticate and obtain a superuser token ────────────────────────────
|
||||
log "authenticating as $PB_EMAIL ..."
|
||||
AUTH_RESPONSE=$(wget -qO- \
|
||||
--header="Content-Type: application/json" \
|
||||
--post-data="{\"identity\":\"$PB_EMAIL\",\"password\":\"$PB_PASSWORD\"}" \
|
||||
"$PB_URL/api/collections/_superusers/auth-with-password")
|
||||
AUTH_RESPONSE=$(curl -sf -X POST "$PB_URL/api/collections/_superusers/auth-with-password" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"identity\":\"$PB_EMAIL\",\"password\":\"$PB_PASSWORD\"}")
|
||||
|
||||
TOKEN=$(echo "$AUTH_RESPONSE" | sed 's/.*"token":"\([^"]*\)".*/\1/')
|
||||
if [ -z "$TOKEN" ] || [ "$TOKEN" = "$AUTH_RESPONSE" ]; then
|
||||
@@ -38,16 +68,16 @@ if [ -z "$TOKEN" ] || [ "$TOKEN" = "$AUTH_RESPONSE" ]; then
|
||||
fi
|
||||
log "auth token obtained"
|
||||
|
||||
# ─── 3. Helpers ───────────────────────────────────────────────────────────────
|
||||
# ─── 4. Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
create_collection() {
|
||||
NAME="$1"
|
||||
BODY="$2"
|
||||
STATUS=$(wget -qSO- \
|
||||
--header="Content-Type: application/json" \
|
||||
--header="Authorization: Bearer $TOKEN" \
|
||||
--post-data="$BODY" \
|
||||
"$PB_URL/api/collections" 2>&1 | grep "^ HTTP/" | awk '{print $2}')
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-X POST "$PB_URL/api/collections" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d "$BODY")
|
||||
case "$STATUS" in
|
||||
200|201) log "created collection: $NAME" ;;
|
||||
400|422) log "collection already exists (skipped): $NAME" ;;
|
||||
@@ -59,14 +89,13 @@ create_collection() {
|
||||
#
|
||||
# Checks whether FIELD_NAME exists in COLLECTION's schema. If it is missing,
|
||||
# sends a PATCH with the full current fields list plus the new field appended.
|
||||
# Uses only busybox sh + wget + sed/awk — no python/jq required.
|
||||
ensure_field() {
|
||||
COLL="$1"
|
||||
FIELD_NAME="$2"
|
||||
FIELD_TYPE="$3"
|
||||
|
||||
SCHEMA=$(wget -qO- \
|
||||
--header="Authorization: Bearer $TOKEN" \
|
||||
SCHEMA=$(curl -sf \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"$PB_URL/api/collections/$COLL" 2>/dev/null)
|
||||
|
||||
# Check if the field already exists (look for "name":"<FIELD_NAME>" in the fields array)
|
||||
@@ -81,27 +110,24 @@ ensure_field() {
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract current fields array (everything between the outermost [ ] of "fields":[...])
|
||||
# and append the new field object before the closing bracket.
|
||||
# Extract current fields array and append the new field before the closing bracket.
|
||||
CURRENT_FIELDS=$(echo "$SCHEMA" | sed 's/.*"fields":\(\[.*\]\).*/\1/')
|
||||
# Strip the trailing ] and append the new field
|
||||
TRIMMED=$(echo "$CURRENT_FIELDS" | sed 's/]$//')
|
||||
NEW_FIELDS="${TRIMMED},{\"name\":\"${FIELD_NAME}\",\"type\":\"${FIELD_TYPE}\"}]"
|
||||
PATCH_BODY="{\"fields\":${NEW_FIELDS}}"
|
||||
|
||||
STATUS=$(wget -qSO- \
|
||||
--header="Content-Type: application/json" \
|
||||
--header="Authorization: Bearer $TOKEN" \
|
||||
--body-data="$PATCH_BODY" \
|
||||
--method=PATCH \
|
||||
"$PB_URL/api/collections/$COLLECTION_ID" 2>&1 | grep "^ HTTP/" | awk '{print $2}')
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-X PATCH "$PB_URL/api/collections/$COLLECTION_ID" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d "$PATCH_BODY")
|
||||
case "$STATUS" in
|
||||
200|201) log "patched $COLL — added field: $FIELD_NAME ($FIELD_TYPE)" ;;
|
||||
*) log "WARNING: patch returned $STATUS when adding $FIELD_NAME to $COLL" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ─── 4. Create collections (idempotent — skips if already exist) ─────────────
|
||||
# ─── 5. Create collections (idempotent — skips if already exist) ─────────────
|
||||
|
||||
create_collection "books" '{
|
||||
"name": "books",
|
||||
@@ -195,7 +221,7 @@ create_collection "user_settings" '{
|
||||
]
|
||||
}'
|
||||
|
||||
# ─── 5. Schema migrations (idempotent field additions) ───────────────────────
|
||||
# ─── 6. Schema migrations (idempotent field additions) ───────────────────────
|
||||
# Ensures fields added after initial deploy are present in existing instances.
|
||||
|
||||
ensure_field "progress" "user_id" "text"
|
||||
@@ -229,4 +255,79 @@ create_collection "comment_votes" '{
|
||||
]
|
||||
}'
|
||||
|
||||
create_collection "scraping_tasks" '{
|
||||
"name": "scraping_tasks",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{"name": "kind", "type": "text"},
|
||||
{"name": "target_url", "type": "text"},
|
||||
{"name": "from_chapter", "type": "number"},
|
||||
{"name": "to_chapter", "type": "number"},
|
||||
{"name": "worker_id", "type": "text"},
|
||||
{"name": "status", "type": "text", "required": true},
|
||||
{"name": "books_found", "type": "number"},
|
||||
{"name": "chapters_scraped", "type": "number"},
|
||||
{"name": "chapters_skipped", "type": "number"},
|
||||
{"name": "errors", "type": "number"},
|
||||
{"name": "error_message", "type": "text"},
|
||||
{"name": "started", "type": "date"},
|
||||
{"name": "finished", "type": "date"}
|
||||
]
|
||||
}'
|
||||
|
||||
create_collection "audio_jobs" '{
|
||||
"name": "audio_jobs",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{"name": "cache_key", "type": "text", "required": true},
|
||||
{"name": "slug", "type": "text", "required": true},
|
||||
{"name": "chapter", "type": "number", "required": true},
|
||||
{"name": "voice", "type": "text"},
|
||||
{"name": "worker_id", "type": "text"},
|
||||
{"name": "status", "type": "text", "required": true},
|
||||
{"name": "error_message", "type": "text"},
|
||||
{"name": "started", "type": "date"},
|
||||
{"name": "finished", "type": "date"}
|
||||
]
|
||||
}'
|
||||
|
||||
create_collection "user_library" '{
|
||||
"name": "user_library",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{"name": "session_id", "type": "text", "required": true},
|
||||
{"name": "user_id", "type": "text"},
|
||||
{"name": "slug", "type": "text", "required": true},
|
||||
{"name": "saved_at", "type": "date"}
|
||||
]
|
||||
}'
|
||||
|
||||
create_collection "user_sessions" '{
|
||||
"name": "user_sessions",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{"name": "user_id", "type": "text", "required": true},
|
||||
{"name": "session_id", "type": "text", "required": true},
|
||||
{"name": "user_agent", "type": "text"},
|
||||
{"name": "ip", "type": "text"},
|
||||
{"name": "created_at", "type": "date"},
|
||||
{"name": "last_seen", "type": "date"}
|
||||
]
|
||||
}'
|
||||
|
||||
create_collection "user_subscriptions" '{
|
||||
"name": "user_subscriptions",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{"name": "follower_id", "type": "text", "required": true},
|
||||
{"name": "followee_id", "type": "text", "required": true},
|
||||
{"name": "created", "type": "date"}
|
||||
]
|
||||
}'
|
||||
|
||||
# ─── 7. Post-initial-deploy field additions ───────────────────────────────────
|
||||
# heartbeat_at is used by the backend runner to detect stale tasks.
|
||||
ensure_field "scraping_tasks" "heartbeat_at" "date"
|
||||
ensure_field "audio_jobs" "heartbeat_at" "date"
|
||||
|
||||
log "all collections ready"
|
||||
|
||||
Reference in New Issue
Block a user