Cloudflare-DDNS: store API token in a 600 env file and build the bina… #828
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: Set state to is_deleted in pocketbase | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - "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="" | |
| ct_slugs="" | |
| # Deleted JSON files: get slug from previous commit | |
| deleted_json=$(git diff --name-only --diff-filter=D "$BEFORE" "$AFTER" -- 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_ct=$(git diff --name-only --diff-filter=D "$BEFORE" "$AFTER" -- ct/ | grep '\.sh$' || true) | |
| for f in $deleted_ct; do | |
| [[ -z "$f" ]] && continue | |
| base="${f##*/}" | |
| base="${base%.sh}" | |
| [[ -n "$base" ]] && ct_slugs="$ct_slugs $base" | |
| done | |
| 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' ' ') | |
| ct_slugs=$(echo $ct_slugs | xargs -n1 2>/dev/null | sort -u | tr '\n' ' ') | |
| if [[ -z "$slugs" ]]; then | |
| echo "No deleted JSON or script files to mark as deleted in PocketBase." | |
| echo "count=0" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "$slugs" > slugs_to_delete.txt | |
| echo "$ct_slugs" > ct_slugs_to_stub.txt | |
| echo "count=$(echo $slugs | wc -w)" >> "$GITHUB_OUTPUT" | |
| echo "Slugs to mark as deleted: $slugs" | |
| [[ -n "$ct_slugs" ]] && echo "CT stubs to generate: $ct_slugs" | |
| - name: Mark as deleted in 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, redirectCount) { | |
| redirectCount = redirectCount || 0; | |
| 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) { | |
| if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { | |
| if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl)); | |
| const redirectUrl = url.resolve(fullUrl, res.headers.location); | |
| res.resume(); | |
| resolve(request(redirectUrl, opts, redirectCount + 1)); | |
| return; | |
| } | |
| 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'; | |
| const patchBody = JSON.stringify({ is_deleted: true }); | |
| 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 patchRes = await request(recordsUrl + '/' + existingId, { | |
| method: 'PATCH', | |
| headers: { 'Authorization': token, 'Content-Type': 'application/json' }, | |
| body: patchBody | |
| }); | |
| if (patchRes.ok) { | |
| console.log('Set is_deleted=true for slug "' + slug + '" (id=' + existingId + ').'); | |
| } else { | |
| console.warn('PATCH failed for slug "' + slug + '": ' + patchRes.statusCode + ' ' + patchRes.body); | |
| } | |
| } | |
| console.log('Done.'); | |
| })().catch(e => { console.error(e); process.exit(1); }); | |
| ENDSCRIPT | |
| shell: bash | |
| - name: Generate CT stubs for deleted scripts | |
| 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: | | |
| if [[ ! -s ct_slugs_to_stub.txt ]]; then | |
| echo "No deleted ct/*.sh files; skipping stub generation." | |
| exit 0 | |
| fi | |
| node << 'ENDSCRIPT' | |
| (async function() { | |
| const fs = require('fs'); | |
| const https = require('https'); | |
| const http = require('http'); | |
| const path = require('path'); | |
| const url = require('url'); | |
| function request(fullUrl, opts, redirectCount) { | |
| redirectCount = redirectCount || 0; | |
| 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) { | |
| if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { | |
| if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl)); | |
| const redirectUrl = url.resolve(fullUrl, res.headers.location); | |
| res.resume(); | |
| resolve(request(redirectUrl, opts, redirectCount + 1)); | |
| return; | |
| } | |
| 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 ctSlugs = fs.readFileSync('ct_slugs_to_stub.txt', 'utf8').trim().split(/\s+/).filter(Boolean); | |
| if (!ctSlugs.length) { | |
| console.log('No ct slugs to process.'); | |
| return; | |
| } | |
| const authRes = await request(apiBase + '/collections/users/auth-with-password', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| identity: process.env.POCKETBASE_ADMIN_EMAIL, | |
| password: process.env.POCKETBASE_ADMIN_PASSWORD | |
| }) | |
| }); | |
| if (!authRes.ok) throw new Error('Auth failed: ' + authRes.body); | |
| const token = JSON.parse(authRes.body).token; | |
| const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records'; | |
| for (const slug of ctSlugs) { | |
| const filter = "(slug='" + slug + "')"; | |
| const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1&fields=slug,name,deleted_message', { | |
| headers: { 'Authorization': token } | |
| }); | |
| if (!listRes.ok) { | |
| console.warn('Failed to fetch record for slug "' + slug + '"'); | |
| continue; | |
| } | |
| const list = JSON.parse(listRes.body); | |
| const rec = list.items && list.items[0]; | |
| const appName = (rec && rec.name) ? rec.name : slug; | |
| const deletedMessage = (rec && rec.deleted_message && rec.deleted_message.trim()) | |
| ? rec.deleted_message.trim() | |
| : 'This script was removed and cannot be installed or updated.'; | |
| const stubPath = path.join('ct', slug + '.sh'); | |
| const content = [ | |
| "#!/usr/bin/env bash", | |
| "source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)", | |
| "# Copyright (c) 2021-2026 community-scripts ORG", | |
| "# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE", | |
| "", | |
| `APP="${appName.replace(/"/g, '\\"')}"`, | |
| "", | |
| 'header_info "$APP"', | |
| 'variables', | |
| 'color', | |
| "", | |
| 'msg_error "This script is no longer available in community-scripts."', | |
| `msg_error "${deletedMessage.replace(/"/g, '\\"')}"`, | |
| `msg_warn "More info: https://community-scripts.org/scripts/${slug}"`, | |
| "exit 1" | |
| ].join('\n') + '\n'; | |
| fs.writeFileSync(stubPath, content); | |
| console.log('Generated stub: ' + stubPath); | |
| } | |
| })().catch(e => { console.error(e); process.exit(1); }); | |
| ENDSCRIPT | |
| shell: bash | |
| - name: Commit generated stubs | |
| if: steps.slugs.outputs.count != '0' | |
| run: | | |
| if git diff --quiet -- ct; then | |
| echo "No generated ct stubs to commit." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add ct/*.sh | |
| git commit -m "chore: add deleted script stubs" | |
| git push | |
| shell: bash |