diff --git a/.github/workflows/delete-pocketbase-entry-on-removal.yml b/.github/workflows/delete-pocketbase-entry-on-removal.yml new file mode 100644 index 000000000..473568efc --- /dev/null +++ b/.github/workflows/delete-pocketbase-entry-on-removal.yml @@ -0,0 +1,150 @@ +name: Delete PocketBase entry on script/JSON removal + +on: + push: + branches: + - main + paths: + - "frontend/public/json/**" + - "vm/**" + - "tools/**" + - "turnkey/**" + - "ct/**" + - "install/**" + +jobs: + delete-pocketbase-entry: + runs-on: self-hosted + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get slugs from deleted JSON and script files + id: slugs + run: | + BEFORE="${{ github.event.before }}" + AFTER="${{ github.event.after }}" + slugs="" + + # Deleted JSON files: get slug from previous commit + deleted_json=$(git diff --name-only --diff-filter=D "$BEFORE" "$AFTER" -- frontend/public/json/ | grep '\.json$' || true) + for f in $deleted_json; do + [[ -z "$f" ]] && continue + s=$(git show "$BEFORE:$f" 2>/dev/null | jq -r '.slug // empty' 2>/dev/null || true) + [[ -n "$s" ]] && slugs="$slugs $s" + done + + # Deleted script files: derive slug from path + deleted_sh=$(git diff --name-only --diff-filter=D "$BEFORE" "$AFTER" -- ct/ install/ tools/ turnkey/ vm/ | grep '\.sh$' || true) + for f in $deleted_sh; do + [[ -z "$f" ]] && continue + base="${f##*/}" + base="${base%.sh}" + if [[ "$f" == install/* && "$base" == *-install ]]; then + s="${base%-install}" + else + s="$base" + fi + [[ -n "$s" ]] && slugs="$slugs $s" + done + + slugs=$(echo $slugs | xargs -n1 | sort -u | tr '\n' ' ') + if [[ -z "$slugs" ]]; then + echo "No deleted JSON or script files to remove from PocketBase." + echo "count=0" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "$slugs" > slugs_to_delete.txt + echo "count=$(echo $slugs | wc -w)" >> "$GITHUB_OUTPUT" + echo "Slugs to delete: $slugs" + + - name: Delete from PocketBase + if: steps.slugs.outputs.count != '0' + env: + POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }} + POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }} + POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }} + POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }} + run: | + node << 'ENDSCRIPT' + (async function() { + const fs = require('fs'); + const https = require('https'); + const http = require('http'); + const url = require('url'); + + function request(fullUrl, opts) { + return new Promise(function(resolve, reject) { + const u = url.parse(fullUrl); + const isHttps = u.protocol === 'https:'; + const body = opts.body; + const options = { + hostname: u.hostname, + port: u.port || (isHttps ? 443 : 80), + path: u.path, + method: opts.method || 'GET', + headers: opts.headers || {} + }; + if (body) options.headers['Content-Length'] = Buffer.byteLength(body); + const lib = isHttps ? https : http; + const req = lib.request(options, function(res) { + let data = ''; + res.on('data', function(chunk) { data += chunk; }); + res.on('end', function() { + resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data }); + }); + }); + req.on('error', reject); + if (body) req.write(body); + req.end(); + }); + } + + const raw = process.env.POCKETBASE_URL.replace(/\/$/, ''); + const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api'; + const coll = process.env.POCKETBASE_COLLECTION; + const slugs = fs.readFileSync('slugs_to_delete.txt', 'utf8').trim().split(/\s+/).filter(Boolean); + + const authUrl = apiBase + '/collections/users/auth-with-password'; + const authBody = JSON.stringify({ + identity: process.env.POCKETBASE_ADMIN_EMAIL, + password: process.env.POCKETBASE_ADMIN_PASSWORD + }); + const authRes = await request(authUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: authBody + }); + if (!authRes.ok) { + throw new Error('Auth failed. Response: ' + authRes.body); + } + const token = JSON.parse(authRes.body).token; + const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records'; + + for (const slug of slugs) { + const filter = "(slug='" + slug + "')"; + const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', { + headers: { 'Authorization': token } + }); + const list = JSON.parse(listRes.body); + const existingId = list.items && list.items[0] && list.items[0].id; + if (!existingId) { + console.log('No PocketBase record for slug "' + slug + '", skipping.'); + continue; + } + const delRes = await request(recordsUrl + '/' + existingId, { + method: 'DELETE', + headers: { 'Authorization': token } + }); + if (delRes.ok) { + console.log('Deleted PocketBase record for slug "' + slug + '" (id=' + existingId + ').'); + } else { + console.warn('DELETE failed for slug "' + slug + '": ' + delRes.statusCode + ' ' + delRes.body); + } + } + console.log('Done.'); + })().catch(e => { console.error(e); process.exit(1); }); + ENDSCRIPT + shell: bash