Skip to content

Add Danish (da_DK) language support (#279) #284

Add Danish (da_DK) language support (#279)

Add Danish (da_DK) language support (#279) #284

name: Upgrade Smoke Test
# Simulates a real-world upgrade: installs the latest stable release, then
# applies the current branch's migrations on top. Catches migration regressions
# that would break existing installations.
on:
push:
branches: [main]
paths:
- 'installer/database/**'
- 'installer/classes/Installer.php'
- 'app/Models/AuthorRepository.php'
- 'app/Support/ContributorBackfill.php'
- 'app/Support/ContributorSync.php'
- 'app/Support/SearchIndexBuilder.php'
- 'app/Support/Updater.php'
- 'scripts/list-source-expectations.php'
- 'composer.json'
- 'composer.lock'
- 'version.json'
- '.github/workflows/ci-upgrade-smoke.yml'
pull_request:
branches: [main]
paths:
- 'installer/database/**'
- 'installer/classes/Installer.php'
- 'app/Models/AuthorRepository.php'
- 'app/Support/ContributorBackfill.php'
- 'app/Support/ContributorSync.php'
- 'app/Support/SearchIndexBuilder.php'
- 'app/Support/Updater.php'
- 'scripts/list-source-expectations.php'
- 'composer.json'
- 'composer.lock'
- 'version.json'
- '.github/workflows/ci-upgrade-smoke.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
upgrade-smoke:
name: Upgrade from latest release → verify schema
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: pinakes_upgrade
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mysqli, curl, zip, mbstring
coverage: none
github-token: ''
- name: Install PHP dependencies
run: composer install --no-interaction --prefer-dist --no-progress --no-scripts
- name: Wait for MySQL
run: until mysqladmin ping -h"127.0.0.1" --silent; do sleep 1; done
# ── Step 1: Resolve latest stable release ─────────────────────────────
- name: Resolve latest release
id: release
env:
GH_REPO: ${{ github.repository }}
run: |
HTTP_STATUS=$(curl -s -o /tmp/release.json -w "%{http_code}" \
"https://api.github.com/repos/${GH_REPO}/releases/latest")
if [ "$HTTP_STATUS" = "404" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No releases found (HTTP 404) — skipping upgrade path (fresh install test only)"
exit 0
elif [ "$HTTP_STATUS" != "200" ]; then
echo "GitHub API returned HTTP $HTTP_STATUS — aborting"
exit 1
fi
TAG=$(python3 -c "import json; d=json.load(open('/tmp/release.json')); print(d.get('tag_name',''))")
ASSET=$(python3 -c "import json; d=json.load(open('/tmp/release.json')); assets=[a['browser_download_url'] for a in d.get('assets',[]) if a['name'].endswith('.zip')]; print(assets[0] if assets else '')")
if [ -z "$TAG" ] || [ -z "$ASSET" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No ZIP asset found in latest release — skipping upgrade path"
exit 0
fi
{
echo "tag=$TAG"
echo "asset=$ASSET"
echo "skip=false"
} >> "$GITHUB_OUTPUT"
echo "Latest release: $TAG ($ASSET)"
# ── Step 2: Install released schema ───────────────────────────────────
- name: Install released schema (baseline)
if: steps.release.outputs.skip == 'false'
env:
ASSET_URL: ${{ steps.release.outputs.asset }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
curl -sfL -o /tmp/release.zip "$ASSET_URL"
mkdir -p /tmp/pinakes-release
unzip -q /tmp/release.zip -d /tmp/pinakes-release
SCHEMA=$(find /tmp/pinakes-release -name "schema.sql" -path "*/database/*" | head -1)
if [ -z "$SCHEMA" ]; then
echo "schema.sql not found in release ZIP — aborting"
exit 1
fi
echo "Using schema: $SCHEMA (from $RELEASE_TAG)"
mysql -h 127.0.0.1 -u root -proot pinakes_upgrade < "$SCHEMA"
echo "✓ Base schema installed from $RELEASE_TAG"
# ── Step 3: Apply released migrations (idempotent baseline) ───────────
# Released migrations run on top of the released schema.sql which is
# already fully migrated up to that release. Every error here is by
# definition idempotent: duplicate columns/keys, FKs that already exist,
# RENAME/CHANGE on columns that were already renamed in schema.sql, etc.
# The extended idempotent filter covers cases that older migration files
# (pre-guard era) produce when re-run on an already-migrated schema.
- name: Apply released migrations
if: steps.release.outputs.skip == 'false'
run: |
MIGRATIONS=$(find /tmp/pinakes-release -type d -name "migrations" | head -1)
if [ -z "$MIGRATIONS" ]; then
echo "No migrations dir in release ZIP — skipping"
exit 0
fi
FAILED=0
mapfile -t RELEASE_MIGRATIONS < <(find "$MIGRATIONS" -maxdepth 1 -type f -name 'migrate_*.sql' | sort -V)
for sql in "${RELEASE_MIGRATIONS[@]}"; do
echo "→ $(basename "$sql")"
OUTPUT=$(mysql --force -h 127.0.0.1 -u root -proot pinakes_upgrade < "$sql" 2>&1) || true
while IFS= read -r line; do
if echo "$line" | grep -qiE '^ERROR'; then
if echo "$line" | grep -qiE \
'duplicate (column name|key|index)|table .* already exists|check constraint .* already exists|can.t drop|duplicate foreign key constraint name|unknown column'; then
echo " ↳ idempotent: $line"
else
echo " ✗ $line"
FAILED=1
fi
fi
done <<< "$OUTPUT"
done
[ "$FAILED" -eq 0 ] || exit 1
echo "✓ Released migrations applied"
# ── Step 4: Fresh-install fallback (no prior release) ─────────────────
- name: Install base schema (fresh-install fallback)
if: steps.release.outputs.skip == 'true'
run: |
mysql -h 127.0.0.1 -u root -proot pinakes_upgrade < installer/database/schema.sql
echo "✓ Base schema installed from current branch (no prior release)"
# ── Step 5: Apply current branch migrations (upgrade simulation) ───────
- name: Apply current branch migrations
run: |
FAILED=0
mapfile -t CURRENT_MIGRATIONS < <(find installer/database/migrations -maxdepth 1 -type f -name 'migrate_*.sql' | sort -V)
for sql in "${CURRENT_MIGRATIONS[@]}"; do
echo "→ $(basename "$sql")"
OUTPUT=$(mysql --force -h 127.0.0.1 -u root -proot pinakes_upgrade < "$sql" 2>&1) || true
while IFS= read -r line; do
if echo "$line" | grep -qiE '^ERROR'; then
if echo "$line" | grep -qiE 'duplicate (column name|key|index)|table .* already exists|check constraint .* already exists|can.t drop|duplicate foreign key constraint name'; then
echo " ↳ idempotent: $line"
else
echo " ✗ $line"
FAILED=1
fi
fi
done <<< "$OUTPUT"
done
[ "$FAILED" -eq 0 ] || exit 1
echo "✓ Current branch migrations applied"
# ── Step 6: Exercise runtime-only migration work ─────────────────
- name: Run contributor backfill through the application updater
run: |
mysql -h 127.0.0.1 -u root -proot pinakes_upgrade <<'SQL'
DELETE FROM system_settings
WHERE category = 'migrations' AND setting_key = 'contributors_backfilled';
DELETE FROM libri WHERE titolo = 'ZZ upgrade smoke contributor backfill';
INSERT INTO libri (titolo, illustratore, curatore)
VALUES (
'ZZ upgrade smoke contributor backfill',
'ZZ Upgrade Illustrator One; ZZ Upgrade Illustrator Two',
'García Márquez, Gabriel José'
);
SQL
TARGET_VERSION=$(python3 -c "import json; print(json.load(open('version.json')).get('version', ''))")
[ -n "$TARGET_VERSION" ] || { echo "Target version missing from version.json"; exit 1; }
TARGET_VERSION="$TARGET_VERSION" php <<'PHP'
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
mysqli_report(MYSQLI_REPORT_OFF);
$db = new mysqli('127.0.0.1', 'root', 'root', 'pinakes_upgrade', 3306);
if ($db->connect_errno !== 0) {
fwrite(STDERR, "Database connection failed: {$db->connect_error}\n");
exit(1);
}
$db->set_charset('utf8mb4');
$target = (string) getenv('TARGET_VERSION');
$result = (new App\Support\Updater($db))->runMigrations($target, $target);
if (!$result['success']) {
fwrite(STDERR, 'Updater failed: ' . ($result['error'] ?? 'unknown error') . "\n");
exit(1);
}
PHP
BOOK_ID=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT id FROM libri WHERE titolo='ZZ upgrade smoke contributor backfill' LIMIT 1")
[ -n "$BOOK_ID" ] || { echo "Backfill fixture book missing"; exit 1; }
MARKER=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT setting_value FROM system_settings WHERE category='migrations' AND setting_key='contributors_backfilled'")
ILLUSTRATORS=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT COUNT(*) FROM libri_autori WHERE libro_id=$BOOK_ID AND ruolo='illustratore'")
CURATORS=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT COUNT(*) FROM libri_autori WHERE libro_id=$BOOK_ID AND ruolo='curatore'")
CANONICAL_CURATOR=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT COUNT(*) FROM libri_autori la JOIN autori a ON a.id=la.autore_id WHERE la.libro_id=$BOOK_ID AND la.ruolo='curatore' AND a.nome='Gabriel José García Márquez'")
PROVENANCE=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT COUNT(*) FROM libri_autori_import_sources WHERE libro_id=$BOOK_ID AND source='legacy-backfill'")
[ "$MARKER" = "1" ] || { echo "Contributor backfill marker not set"; exit 1; }
[ "$ILLUSTRATORS" -eq 2 ] || { echo "Expected 2 illustrator links, got $ILLUSTRATORS"; exit 1; }
[ "$CURATORS" -eq 1 ] || { echo "Expected 1 curator link, got $CURATORS"; exit 1; }
[ "$CANONICAL_CURATOR" -eq 1 ] || { echo "SBN comma name was not preserved and normalized correctly"; exit 1; }
[ "$PROVENANCE" -eq 3 ] || { echo "Expected 3 provenance rows, got $PROVENANCE"; exit 1; }
echo "✓ Runtime contributor backfill completed through Updater"
# ── Step 7: Verify schema after upgrade ──────────────────────────────
- name: Verify core tables present
run: |
TABLE_OUTPUT=$(php scripts/list-source-expectations.php tables)
mapfile -t CORE_TABLES <<< "$TABLE_OUTPUT"
[ "${#CORE_TABLES[@]}" -gt 0 ] || { echo "No tables derived from schema.sql"; exit 1; }
FAILED=0
MISSING=""
for t in "${CORE_TABLES[@]}"; do
EXISTS=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='pinakes_upgrade' AND table_name='$t'")
if [ "$EXISTS" -eq 0 ]; then
MISSING="$MISSING $t"
FAILED=1
fi
done
if [ "$FAILED" -eq 0 ]; then
echo "✓ All ${#CORE_TABLES[@]} schema.sql tables present"
else
echo "✗ Missing tables:$MISSING"
exit 1
fi
- name: Verify ENUM values
run: |
FAILED=0
V=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS \
WHERE TABLE_SCHEMA='pinakes_upgrade' \
AND TABLE_NAME='oai_deleted_records' \
AND COLUMN_NAME='entity_type'" 2>/dev/null || echo "")
if [ -n "$V" ]; then
echo "oai_deleted_records.entity_type: $V"
if echo "$V" | grep -q "archival_unit"; then
echo " ✓ 'archival_unit' present"
else
echo " ✗ 'archival_unit' missing"
FAILED=1
fi
if echo "$V" | grep -q "'archive_unit'"; then
echo " ✗ old typo 'archive_unit' still present"
FAILED=1
fi
fi
V=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS \
WHERE TABLE_SCHEMA='pinakes_upgrade' \
AND TABLE_NAME='prestiti' \
AND COLUMN_NAME='origine'" 2>/dev/null || echo "")
if [ -n "$V" ]; then
echo "prestiti.origine: $V"
if echo "$V" | grep -q "'ncip'"; then
echo " ✓ 'ncip' present"
else
echo " ✗ 'ncip' missing from prestiti.origine"
FAILED=1
fi
fi
[ "$FAILED" -eq 0 ] || exit 1
- name: Verify key columns from recent migrations
run: |
FAILED=0
check_col() {
local TABLE="$1" COL="$2"
EXISTS=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='pinakes_upgrade' AND table_name='$TABLE' AND column_name='$COL'")
if [ "$EXISTS" -eq 0 ]; then
echo " ✗ Missing: $TABLE.$COL"
FAILED=1
else
echo " ✓ $TABLE.$COL"
fi
}
# v0.7.0 — VIAF/ISNI
check_col autori viaf_id
check_col autori isni_id
# v0.7.3 — NCIP
check_col prestiti origine
# v0.7.4 — NCIP partner attrs
check_col ncip_partners isil
check_col ncip_partners notes
# v0.7.4 — digital_assets
check_col digital_assets libro_id
check_col digital_assets url
check_col digital_assets md5_hash
check_col digital_assets filesize
check_col digital_assets filetype
[ "$FAILED" -eq 0 ] || exit 1
- name: Verify plugin registrations
run: |
EXPECTED="api-book-scraper archives bibframe-linked-data deezer dewey-editor digital-library discogs goodlib musicbrainz ncip-server oai-pmh-server open-library openurl-resolver resource-sync viaf-authority z39-server"
FAILED=0
for plugin in $EXPECTED; do
EXISTS=$(mysql -h 127.0.0.1 -u root -proot pinakes_upgrade -sN -e \
"SELECT COUNT(*) FROM plugins WHERE name='$plugin'" 2>/dev/null || echo "0")
if [ "$EXISTS" -eq 0 ]; then
echo " ✗ Plugin not registered: $plugin"
FAILED=1
else
echo " ✓ $plugin"
fi
done
[ "$FAILED" -eq 0 ] || exit 1