From 3f1b474fddd9c6a9f9d204f692b9f6e4b047b776 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 17 Apr 2026 11:17:59 +0200 Subject: [PATCH] feat(deploy): add vocab course change detection and sync step - Implemented a new workflow step to detect changes in vocab course files and conditionally sync content. - Added a script to check for specific changes in the repository and trigger the sync process if necessary. - Introduced a new npm script for syncing vocab course content in the backend package.json. --- .gitea/workflows/deploy.yml | 37 ++++++- backend/package.json | 1 + backend/scripts/sync-vocab-course-content.js | 105 +++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 backend/scripts/sync-vocab-course-content.js diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index c659f9f..3058b97 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -10,6 +10,33 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Detect vocab course changes + id: vocab_course_changes + shell: bash + run: | + set -euo pipefail + + BASE="${{ gitea.event.before }}" + HEAD="${{ gitea.sha }}" + + if [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]] || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then + BASE="HEAD~1" + fi + + git diff --name-only "$BASE" "$HEAD" > changed-files.txt + cat changed-files.txt + + if grep -E '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/migrations/.*vocab|docs/.*(COURSE|VOCAB|BISAYA|GERMAN_FOR_BISAYA))' changed-files.txt; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + - name: Prepare SSH run: | mkdir -p ~/.ssh @@ -31,4 +58,12 @@ jobs: ssh -i ~/.ssh/id_ed25519 \ -p "${{ secrets.PROD_PORT }}" \ "${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \ - "/home/tsschulz/deploy-yourpart-bluegreen.sh" \ No newline at end of file + "/home/tsschulz/deploy-yourpart-bluegreen.sh" + + - name: Sync vocab course content + if: steps.vocab_course_changes.outputs.changed == 'true' + run: | + ssh -i ~/.ssh/id_ed25519 \ + -p "${{ secrets.PROD_PORT }}" \ + "${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \ + "cd /opt/yourpart && npm --prefix backend run sync:vocab-courses" diff --git a/backend/package.json b/backend/package.json index 505b9c6..820cb8d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "cleanup-connections": "node cleanup-connections.js", "diag:town-worth": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-town-product-worth-stats.mjs", "diag:moneyflow": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-moneyflow-report.mjs", + "sync:vocab-courses": "node scripts/sync-vocab-course-content.js", "lockfile:sync": "npm install --package-lock-only", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/backend/scripts/sync-vocab-course-content.js b/backend/scripts/sync-vocab-course-content.js new file mode 100644 index 0000000..13657ca --- /dev/null +++ b/backend/scripts/sync-vocab-course-content.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node +/** + * Synchronisiert Kursstruktur und generierte Übungen nach Kursänderungen. + * + * Dieses Script ist für CI/CD gedacht und darf keinen Lernfortschritt löschen. + * Es führt bewusst nicht apply-bisaya-course-refresh.js aus, weil dieses Script + * Lektions- und Übungsfortschritte zurücksetzt. + * + * Verwendung: + * node backend/scripts/sync-vocab-course-content.js + * node backend/scripts/sync-vocab-course-content.js --scope=bisaya + * node backend/scripts/sync-vocab-course-content.js --scope=german-for-bisaya + * VOCAB_COURSE_SYNC_DRY_RUN=1 node backend/scripts/sync-vocab-course-content.js + */ + +import { spawn } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '../..'); + +const SAFE_SYNC_STEPS = { + bisaya: [ + 'backend/scripts/migrate-bisaya-zahlen-split.js', + 'backend/scripts/update-bisaya-didactics.js', + 'backend/scripts/extend-bisaya-course-phase3.js', + 'backend/scripts/extend-bisaya-course-phase4.js', + 'backend/scripts/extend-bisaya-course-phase5.js', + 'backend/scripts/create-bisaya-course-content.js' + ], + 'german-for-bisaya': [ + 'backend/scripts/extend-german-for-bisaya-course-phase3.js', + 'backend/scripts/extend-german-for-bisaya-course-phase4.js', + 'backend/scripts/extend-german-for-bisaya-course-phase5.js', + 'backend/scripts/create-german-for-bisaya-course-content.js' + ] +}; + +function parseScopes() { + const scopeArg = process.argv.find((arg) => arg.startsWith('--scope=')); + const rawScope = scopeArg?.slice('--scope='.length) || process.env.VOCAB_COURSE_SYNC_SCOPE || 'all'; + const requested = rawScope + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean); + + if (requested.length === 0 || requested.includes('all')) { + return Object.keys(SAFE_SYNC_STEPS); + } + + const unknownScopes = requested.filter((scope) => !SAFE_SYNC_STEPS[scope]); + if (unknownScopes.length > 0) { + throw new Error(`Unbekannter Kurs-Sync-Scope: ${unknownScopes.join(', ')}`); + } + + return requested; +} + +function runNodeScript(scriptPath) { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [scriptPath], { + cwd: repoRoot, + env: process.env, + stdio: 'inherit' + }); + + child.on('error', reject); + child.on('exit', (code, signal) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`${scriptPath} beendet mit ${signal || `Exit-Code ${code}`}`)); + }); + }); +} + +async function main() { + const scopes = parseScopes(); + const dryRun = process.env.VOCAB_COURSE_SYNC_DRY_RUN === '1' || process.argv.includes('--dry-run'); + const steps = scopes.flatMap((scope) => SAFE_SYNC_STEPS[scope]); + + console.log('Vokabelkurs-Sync'); + console.log(`Scopes: ${scopes.join(', ')}`); + console.log(`Schritte: ${steps.length}`); + + for (const step of steps) { + console.log(`\n→ ${step}`); + if (dryRun) continue; + await runNodeScript(step); + } + + if (dryRun) { + console.log('\nDry-Run abgeschlossen. Es wurden keine Scripts ausgeführt.'); + } else { + console.log('\nVokabelkurs-Sync abgeschlossen.'); + } +} + +main().catch((error) => { + console.error('Vokabelkurs-Sync fehlgeschlagen:', error); + process.exit(1); +});