Add Danish (da_DK) language support (#279) #284
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |