# Daily full logical export: all hub user ids + per-user notes/proposals (canister stays up). # Requires: deployed hub WASM with operator export + secret set via admin_set_operator_export_secret. # Secrets: KNOWTATION_OPERATOR_EXPORT_KEY (matches canister); URL optional (defaults from canister_ids.json). name: Operator full hub export on: schedule: - cron: '30 7 * * *' workflow_dispatch: permissions: contents: read jobs: export: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: npm ci - name: Verify operator export secret env: KNOWTATION_OPERATOR_EXPORT_KEY: ${{ secrets.KNOWTATION_OPERATOR_EXPORT_KEY || vars.KNOWTATION_OPERATOR_EXPORT_KEY }} run: | python3 << 'PY' import os k = (os.environ.get("KNOWTATION_OPERATOR_EXPORT_KEY") or "").strip() if not k: print("::error::KNOWTATION_OPERATOR_EXPORT_KEY is missing or whitespace-only. Add Actions secret or variable.") raise SystemExit(1) PY echo "KNOWTATION_OPERATOR_EXPORT_KEY is set (value not shown)." - name: Probe operator export (HTTP status only) env: KNOWTATION_OPERATOR_EXPORT_URL: ${{ secrets.KNOWTATION_OPERATOR_EXPORT_URL || vars.KNOWTATION_OPERATOR_EXPORT_URL }} KNOWTATION_OPERATOR_EXPORT_KEY: ${{ secrets.KNOWTATION_OPERATOR_EXPORT_KEY || vars.KNOWTATION_OPERATOR_EXPORT_KEY }} run: | set -e ID="$(python3 -c "import json; print(json.load(open('hub/icp/canister_ids.json'))['hub']['ic'])")" BASE="$(python3 -c "import os; u=(os.environ.get('KNOWTATION_OPERATOR_EXPORT_URL') or '').strip().rstrip('/'); print(u)")" if [ -z "$BASE" ]; then BASE="https://${ID}.raw.icp0.io" echo "KNOWTATION_OPERATOR_EXPORT_URL unset; using ${BASE} from canister_ids.json" fi echo "Probe base URL: ${BASE}" code="$(curl -sS -o /tmp/op-export-body -w '%{http_code}' \ -H "X-Operator-Export-Key: ${KNOWTATION_OPERATOR_EXPORT_KEY}" \ "${BASE}/api/v1/operator/export?limit=1")" echo "GET /api/v1/operator/export?limit=1 → HTTP ${code}" head -c 300 /tmp/op-export-body || true echo "" if [ "$code" != "200" ]; then echo "::error::Operator export returned HTTP ${code}. 401 = GitHub KNOWTATION_OPERATOR_EXPORT_KEY does not match admin_set_operator_export_secret. 503 = secret not set on canister." exit 1 fi - name: Full operator export env: KNOWTATION_OPERATOR_EXPORT_URL: ${{ secrets.KNOWTATION_OPERATOR_EXPORT_URL || vars.KNOWTATION_OPERATOR_EXPORT_URL }} KNOWTATION_OPERATOR_EXPORT_KEY: ${{ secrets.KNOWTATION_OPERATOR_EXPORT_KEY || vars.KNOWTATION_OPERATOR_EXPORT_KEY }} KNOWTATION_CANISTER_BACKUP_ENCRYPT_KEY_HEX: ${{ secrets.KNOWTATION_CANISTER_BACKUP_ENCRYPT_KEY_HEX || vars.KNOWTATION_CANISTER_BACKUP_ENCRYPT_KEY_HEX }} KNOWTATION_CANISTER_BACKUP_S3_BUCKET: ${{ secrets.KNOWTATION_CANISTER_BACKUP_S3_BUCKET || vars.KNOWTATION_CANISTER_BACKUP_S3_BUCKET }} KNOWTATION_CANISTER_BACKUP_S3_PREFIX: ${{ secrets.KNOWTATION_CANISTER_BACKUP_S3_PREFIX || vars.KNOWTATION_CANISTER_BACKUP_S3_PREFIX }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID || vars.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY || vars.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ secrets.AWS_REGION || vars.AWS_REGION }} run: node scripts/canister-operator-full-export.mjs - name: Debug on failure (backup files and hub URL resolution) if: failure() env: KNOWTATION_OPERATOR_EXPORT_URL: ${{ secrets.KNOWTATION_OPERATOR_EXPORT_URL || vars.KNOWTATION_OPERATOR_EXPORT_URL }} KNOWTATION_CANISTER_URL: ${{ secrets.KNOWTATION_CANISTER_URL || vars.KNOWTATION_CANISTER_URL }} KNOWTATION_CANISTER_BACKUP_URL: ${{ secrets.KNOWTATION_CANISTER_BACKUP_URL || vars.KNOWTATION_CANISTER_BACKUP_URL }} run: | echo "=== backups/ (if any) ===" ls -la backups 2>/dev/null || echo "(no backups directory)" find . -maxdepth 4 -name 'operator-full-export*' -print 2>/dev/null || true echo "=== URL env (values may be empty) ===" echo "KNOWTATION_OPERATOR_EXPORT_URL length: ${#KNOWTATION_OPERATOR_EXPORT_URL}" echo "KNOWTATION_CANISTER_URL length: ${#KNOWTATION_CANISTER_URL}" echo "KNOWTATION_CANISTER_BACKUP_URL length: ${#KNOWTATION_CANISTER_BACKUP_URL}" echo "(Script resolves base URL as: OPERATOR_EXPORT_URL, then CANISTER_URL, then CANISTER_BACKUP_URL, then canister_ids.)" - name: Upload export artifact if: success() uses: actions/upload-artifact@v4 with: name: operator-full-export-${{ github.run_id }} path: backups/operator-full-export-* retention-days: 90 if-no-files-found: error