name: Push JSON changes to PocketBase on: push: branches: - main paths: - "frontend/public/json/**" jobs: push-json: runs-on: self-hosted steps: - name: Checkout Repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get changed JSON files with slug id: changed run: | changed=$(git diff --name-only "${{ github.event.before }}" "${{ github.event.after }}" -- frontend/public/json/ | grep '\.json$' || true) with_slug="" for f in $changed; do [[ -f "$f" ]] || continue jq -e '.slug' "$f" >/dev/null 2>&1 && with_slug="$with_slug $f" done with_slug=$(echo $with_slug | xargs -n1) if [[ -z "$with_slug" ]]; then echo "No app JSON files changed (or no files with slug)." echo "count=0" >> "$GITHUB_OUTPUT" exit 0 fi echo "$with_slug" > changed_app_jsons.txt echo "count=$(echo "$with_slug" | wc -w)" >> "$GITHUB_OUTPUT" - name: Push to PocketBase if: steps.changed.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 files = fs.readFileSync('changed_app_jsons.txt', 'utf8').trim().split(/\s+/).filter(Boolean); const authBody = JSON.stringify({ identity: process.env.POCKETBASE_ADMIN_EMAIL, password: process.env.POCKETBASE_ADMIN_PASSWORD }); let authRes = await request(apiBase + '/admins/auth-with-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: authBody }); if (!authRes.ok && authRes.statusCode === 404) { authRes = await request(raw + '/admins/auth-with-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: authBody }); } if (!authRes.ok) throw new Error('Auth failed (check POCKETBASE_URL): ' + authRes.body); const token = JSON.parse(authRes.body).token; const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records'; for (const file of files) { if (!fs.existsSync(file)) continue; const data = JSON.parse(fs.readFileSync(file, 'utf8')); if (!data.slug) { console.log('Skipping', file, '(no slug)'); continue; } var payload = { name: data.name, slug: data.slug, script_created: data.date_created || data.script_created, script_updated: data.date_created || data.script_updated, updateable: data.updateable, privileged: data.privileged, port: data.interface_port != null ? data.interface_port : data.port, documentation: data.documentation, website: data.website, logo: data.logo, description: data.description, config_path: data.config_path, default_user: (data.default_credentials && data.default_credentials.username) || data.default_user, default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd, categories: data.categories, install_methods: data.install_methods, notes: data.notes, type: data.type, is_dev: false }; if (data.version !== undefined) payload.version = data.version; if (data.changelog !== undefined) payload.changelog = data.changelog; if (data.screenshots !== undefined) payload.screenshots = data.screenshots; const filter = "(slug='" + data.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('Updating', file, '(slug=' + data.slug + ')'); const r = await request(recordsUrl + '/' + existingId, { method: 'PATCH', headers: { 'Authorization': token, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!r.ok) throw new Error('PATCH failed: ' + r.body); } else { console.log('Creating', file, '(slug=' + data.slug + ')'); const r = await request(recordsUrl, { method: 'POST', headers: { 'Authorization': token, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!r.ok) throw new Error('POST failed: ' + r.body); } } console.log('Done.'); })().catch(e => { console.error(e); process.exit(1); }); ENDSCRIPT shell: bash