Telemetry backup #75
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: Telemetry backup | |
| # Daily snapshot of api.getbindery.dev's installs DB. Pulls a SQLite | |
| # VACUUM INTO copy via the token-gated /api/backup endpoint and stores | |
| # it as a workflow artifact (90-day retention) so the data survives | |
| # cluster loss, accidental PVC deletion, or schema mistakes. | |
| # | |
| # Restoring: `gh run download <run-id> -n telemetry-backup` then | |
| # `kubectl cp <file> bindery-ping/<pod>:/data/telemetry.db` after a | |
| # `kubectl scale deployment/bindery-ping --replicas=0` to release the | |
| # WAL lock. | |
| # | |
| # Required repo secret: TELEMETRY_STATS_TOKEN (same value as the | |
| # `stats-token` key in the bindery-ping-secret k8s Secret). | |
| on: | |
| schedule: | |
| - cron: "17 3 * * *" # 03:17 UTC daily — off-hours, off-minute to dodge runner contention | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| backup: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Fetch backup | |
| env: | |
| STATS_TOKEN: ${{ secrets.TELEMETRY_STATS_TOKEN }} | |
| run: | | |
| if [ -z "$STATS_TOKEN" ]; then | |
| echo "::error::TELEMETRY_STATS_TOKEN secret is not set" | |
| exit 1 | |
| fi | |
| DATE=$(date -u +%Y-%m-%d) | |
| OUT="bindery-telemetry-${DATE}.db" | |
| HTTP=$(curl -sS -o "$OUT" -w "%{http_code}" \ | |
| -H "Authorization: Bearer $STATS_TOKEN" \ | |
| "https://api.getbindery.dev/api/backup") | |
| if [ "$HTTP" != "200" ]; then | |
| echo "::error::backup endpoint returned HTTP $HTTP" | |
| head -c 500 "$OUT" || true | |
| exit 1 | |
| fi | |
| # Sanity: a valid SQLite file starts with the literal string | |
| # "SQLite format 3\0" and is at least a few KB. Catches the case | |
| # where a 200 with a non-DB body slips through. | |
| SIZE=$(stat -c%s "$OUT") | |
| if [ "$SIZE" -lt 1024 ]; then | |
| echo "::error::backup is suspiciously small: ${SIZE} bytes" | |
| exit 1 | |
| fi | |
| if ! head -c 16 "$OUT" | grep -q "SQLite format 3"; then | |
| echo "::error::response body is not a SQLite database" | |
| exit 1 | |
| fi | |
| echo "Fetched ${OUT} (${SIZE} bytes)" | |
| # Quick integrity check using sqlite3 if available on the runner. | |
| if command -v sqlite3 >/dev/null 2>&1; then | |
| ROWS=$(sqlite3 "$OUT" "SELECT COUNT(*) FROM installs") | |
| echo "Verified: ${ROWS} rows in installs table" | |
| fi | |
| echo "ARTIFACT_NAME=$OUT" >> "$GITHUB_ENV" | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.ARTIFACT_NAME }} | |
| path: ${{ env.ARTIFACT_NAME }} | |
| retention-days: 90 | |
| if-no-files-found: error |