feat: add first-party Tinybird analytics #88
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: Analytics staging | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| paths: | |
| - ".github/workflows/analytics.yml" | |
| - "package.json" | |
| - "pnpm-lock.yaml" | |
| - "packages/local-docker/**" | |
| - "packages/analytics/**" | |
| - "packages/database/**" | |
| - "packages/env/server.ts" | |
| - "packages/web-backend/src/ProductAnalytics/**" | |
| - "packages/web-backend/src/Tinybird/**" | |
| - "packages/web-backend/src/Organisations/**" | |
| - "packages/web-domain/**" | |
| - "apps/cli/**" | |
| - "apps/desktop/**" | |
| - "apps/web/**" | |
| - "apps/cli/src/analytics.rs" | |
| - "apps/mobile/**" | |
| - "apps/web/actions/analytics/**" | |
| - "apps/web/actions/organization/**" | |
| - "apps/web/__tests__/unit/*analytics*.test.ts" | |
| - "apps/web/__tests__/unit/developer-credits-webhook.test.ts" | |
| - "apps/web/app/Layout/*Analytics*.tsx" | |
| - "apps/web/app/admin/analytics/**" | |
| - "apps/web/app/api/analytics/**" | |
| - "apps/web/app/api/cron/drain-product-analytics-outbox/**" | |
| - "apps/web/app/api/cron/reconcile-product-analytics/**" | |
| - "apps/web/app/api/cron/recover-organization-invite-delivery/**" | |
| - "apps/web/app/api/cron/recover-product-analytics-erasure/**" | |
| - "apps/web/app/api/cron/refresh-product-analytics/**" | |
| - "apps/web/app/api/events/**" | |
| - "apps/web/app/api/invite/accept/route.ts" | |
| - "apps/web/app/api/settings/billing/**" | |
| - "apps/web/app/api/webhooks/stripe/route.ts" | |
| - "apps/web/app/mobile/checkout/**" | |
| - "apps/web/app/api/mobile/**" | |
| - "apps/web/app/utils/analytics.ts" | |
| - "apps/web/app/utils/product-analytics.ts" | |
| - "apps/web/lib/analytics/**" | |
| - "apps/web/lib/organization-invite-delivery.ts" | |
| - "apps/web/proxy.ts" | |
| - "apps/web/vercel.json" | |
| - "apps/web/lib/rate-limit.ts" | |
| - "apps/web/workflows/*product-analytics*" | |
| - "apps/web/workflows/recover-organization-invite-delivery.ts" | |
| - "apps/desktop/src-tauri/src/lib.rs" | |
| - "apps/desktop/src-tauri/src/product_analytics.rs" | |
| - "apps/desktop/src/utils/analytics.ts" | |
| - "apps/desktop/src/utils/product-analytics.ts" | |
| - "apps/desktop/src/utils/*analytics*.test.ts" | |
| - "scripts/analytics/**" | |
| workflow_dispatch: | |
| permissions: | |
| actions: read | |
| contents: read | |
| deployments: read | |
| pull-requests: read | |
| statuses: read | |
| concurrency: | |
| group: >- | |
| ${{ ((github.event_name == 'pull_request' && github.event.pull_request.number == 2003 && github.event.pull_request.head.ref == 'codex/first-party-analytics') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/codex/first-party-analytics')) && 'analytics-staging-37b8fef9-817f-4c3c-b21f-218c36a6077d' || format('analytics-staging-out-of-scope-{0}', github.run_id) }} | |
| cancel-in-progress: false | |
| env: | |
| TINYBIRD_WORKSPACE_ID: 37b8fef9-817f-4c3c-b21f-218c36a6077d | |
| jobs: | |
| validate: | |
| name: Validate exact analytics SHA | |
| if: >- | |
| (github.event_name == 'pull_request' && | |
| github.event.pull_request.number == 2003 && | |
| github.event.pull_request.head.ref == 'codex/first-party-analytics') || | |
| (github.event_name == 'workflow_dispatch' && | |
| github.ref == 'refs/heads/codex/first-party-analytics') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| services: | |
| mysql: | |
| image: mysql:8.4 | |
| env: | |
| MYSQL_ALLOW_EMPTY_PASSWORD: "yes" | |
| ports: | |
| - 3306:3306 | |
| options: >- | |
| --health-cmd="mysqladmin ping --silent" | |
| --health-interval=5s | |
| --health-timeout=5s | |
| --health-retries=20 | |
| env: | |
| EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| EVENT_NUMBER: ${{ github.event.pull_request.number || '' }} | |
| HEAD_REF: ${{ github.event.pull_request.head.ref || '' }} | |
| steps: | |
| - name: Check out exact requested SHA | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 1 | |
| - uses: ./.github/actions/setup-js | |
| - name: Refuse an out-of-scope event, branch, PR, or checkout | |
| run: node scripts/analytics/staging-ci.js verify-scope --actual-sha "$(git rev-parse HEAD)" | |
| - name: Run analytics contract and regression suites | |
| run: pnpm analytics:test | |
| - name: Prove analytics leases, receipts, and shared-cookie erasure in MySQL | |
| env: | |
| CAP_PRODUCT_ANALYTICS_MYSQL_E2E: "1" | |
| run: >- | |
| pnpm --filter @cap/web exec vitest run | |
| __tests__/e2e/product-analytics-mysql-e2e.test.ts | |
| - name: Validate Docker Compose | |
| run: node scripts/analytics/analytics-cli.js compose-check | |
| - name: Build and test Tinybird datafiles with fixtures | |
| run: node scripts/analytics/analytics-cli.js local | |
| - name: Stop Tinybird Local | |
| if: always() | |
| run: node scripts/analytics/analytics-cli.js local-stop | |
| validate-desktop: | |
| name: Validate desktop analytics outbox | |
| if: >- | |
| (github.event_name == 'pull_request' && | |
| github.event.pull_request.number == 2003 && | |
| github.event.pull_request.head.ref == 'codex/first-party-analytics') || | |
| (github.event_name == 'workflow_dispatch' && | |
| github.ref == 'refs/heads/codex/first-party-analytics') | |
| runs-on: macos-14 | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Check out exact requested SHA | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 1 | |
| - name: Install Rust 1.88 | |
| uses: dtolnay/rust-toolchain@1.88.0 | |
| - uses: ./.github/actions/setup-rust-cache | |
| with: | |
| target: aarch64-apple-darwin | |
| - name: Prepare pinned native dependencies | |
| run: node scripts/setup.js | |
| - name: Create compile-only Tauri sidecars | |
| run: | | |
| target="$(rustc -vV | sed -n 's|host: ||p')" | |
| binaries="apps/desktop/src-tauri/binaries" | |
| install -d "$binaries" | |
| for binary in cap-muxer cap-exporter cap-cli; do | |
| install -m 755 /dev/null "$binaries/$binary-$target" | |
| done | |
| - name: Compile the desktop analytics path | |
| run: cargo check -p cap-desktop --locked | |
| - name: Prove durable desktop analytics behavior | |
| run: cargo test -p cap-desktop product_analytics::tests --lib --locked | |
| deploy-staging: | |
| name: Deploy and prove analytics staging | |
| needs: [validate, validate-desktop] | |
| if: needs.validate.result == 'success' && needs.validate-desktop.result == 'success' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 180 | |
| environment: staging | |
| env: | |
| EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| EVENT_NUMBER: ${{ github.event.pull_request.number || '' }} | |
| HEAD_REF: ${{ github.event.pull_request.head.ref || '' }} | |
| ANALYTICS_TEST_RUN_ID: run_${{ github.run_id }}_${{ github.run_attempt }}_${{ github.event.pull_request.head.sha || github.sha }} | |
| ANALYTICS_PREVIEW_ACCESS_URL: https://cap-web-git-codex-first-party-analytics-mc-ilroy.vercel.app | |
| steps: | |
| - name: Check out exact requested SHA | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 1 | |
| - uses: ./.github/actions/setup-js | |
| - name: Refuse an out-of-scope event, branch, PR, or checkout | |
| run: node scripts/analytics/staging-ci.js verify-scope --actual-sha "$(git rev-parse HEAD)" | |
| - name: Refuse missing, non-staging, or wrong-workspace credentials | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_COPY_TOKEN: ${{ secrets.TINYBIRD_STAGING_COPY_TOKEN }} | |
| TINYBIRD_STAGING_ERASURE_LOOKUP_TOKEN: ${{ secrets.TINYBIRD_STAGING_ERASURE_LOOKUP_TOKEN }} | |
| TINYBIRD_STAGING_SCHEDULER_TOKEN: ${{ secrets.TINYBIRD_STAGING_SCHEDULER_TOKEN }} | |
| TINYBIRD_STAGING_INGEST_TOKEN: ${{ secrets.TINYBIRD_STAGING_INGEST_TOKEN }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| TINYBIRD_STAGING_CLEANUP_TOKEN: ${{ secrets.TINYBIRD_STAGING_CLEANUP_TOKEN }} | |
| run: node scripts/analytics/staging-ci.js verify-credentials | |
| - name: Wait for successful exact-SHA Vercel preview | |
| id: vercel | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: node scripts/analytics/staging-ci.js wait-vercel | |
| - name: Refuse a preview bound outside Tinybird staging | |
| env: | |
| ANALYTICS_PREVIEW_URL: ${{ env.ANALYTICS_PREVIEW_ACCESS_URL }} | |
| CAP_ANALYTICS_STAGING_TEST_SECRET: ${{ secrets.CAP_ANALYTICS_STAGING_TEST_SECRET }} | |
| TINYBIRD_STAGING_CLEANUP_TOKEN: ${{ secrets.TINYBIRD_STAGING_CLEANUP_TOKEN }} | |
| TINYBIRD_STAGING_COPY_TOKEN: ${{ secrets.TINYBIRD_STAGING_COPY_TOKEN }} | |
| TINYBIRD_STAGING_ERASURE_LOOKUP_TOKEN: ${{ secrets.TINYBIRD_STAGING_ERASURE_LOOKUP_TOKEN }} | |
| TINYBIRD_STAGING_INGEST_TOKEN: ${{ secrets.TINYBIRD_STAGING_INGEST_TOKEN }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| TINYBIRD_STAGING_SCHEDULER_TOKEN: ${{ secrets.TINYBIRD_STAGING_SCHEDULER_TOKEN }} | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} | |
| VERCEL_PREVIEW_SHARE_SECRET: ${{ secrets.VERCEL_PREVIEW_SHARE_SECRET }} | |
| run: node scripts/analytics/staging-ci.js attest-preview | |
| - name: Retire only a superseded Tinybird staging predecessor | |
| id: retired-deployment | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js discard-retired-deployment | |
| --artifact "$RUNNER_TEMP/analytics-retired-deployment.json" | |
| - name: Upload retired Tinybird deployment evidence | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: analytics-retired-deployment-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/analytics-retired-deployment.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| - name: Persist the pre-create Tinybird recovery boundary | |
| env: | |
| VERCEL_DEPLOYMENT_ID: ${{ steps.vercel.outputs.deployment_id }} | |
| VERCEL_PREVIEW_ACCESS_URL: ${{ env.ANALYTICS_PREVIEW_ACCESS_URL }} | |
| VERCEL_PREVIEW_URL: ${{ steps.vercel.outputs.url }} | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js prepare-deployment-boundary | |
| --output "$RUNNER_TEMP/analytics-deployment-boundary.json" | |
| - name: Upload the immutable pre-create recovery boundary | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: analytics-recovery-${{ github.run_id }}-${{ github.run_attempt }}-00-precreate | |
| path: ${{ runner.temp }}/analytics-deployment-boundary.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| - name: Record Tinybird deployment boundary | |
| run: echo "TINYBIRD_DEPLOYMENT_STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" >> "$GITHUB_ENV" | |
| - name: Check Tinybird cloud deployment plan | |
| id: deployment-check | |
| env: | |
| TINYBIRD_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TB_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TB_HOST: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| run: >- | |
| docker compose --file packages/local-docker/docker-compose.yml --profile analytics | |
| run --rm tinybird-cloud-cli --cloud deployment create --allow-destructive-operations --check | |
| - name: Create isolated Tinybird staging deployment | |
| id: create-deployment | |
| env: | |
| TINYBIRD_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TB_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TB_HOST: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| run: | | |
| set -o pipefail | |
| set +e | |
| docker compose --file packages/local-docker/docker-compose.yml --profile analytics run --rm tinybird-cloud-cli --cloud deployment create --allow-destructive-operations --wait | tee "$RUNNER_TEMP/tinybird-create-output.txt" | |
| create_exit="${PIPESTATUS[0]}" | |
| set -e | |
| node -e 'const fs = require("node:fs"); fs.writeFileSync(process.argv[2], JSON.stringify({ exitCode: Number(process.argv[3]), output: fs.readFileSync(process.argv[1], "utf8") }));' "$RUNNER_TEMP/tinybird-create-output.txt" "$RUNNER_TEMP/tinybird-create-output.json" "$create_exit" | |
| - name: Resolve only the deployment created by this run | |
| id: tinybird | |
| env: | |
| TINYBIRD_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TB_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TB_HOST: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| run: | | |
| docker compose --file packages/local-docker/docker-compose.yml --profile analytics run --rm tinybird-cloud-cli --cloud --output json deployment ls > "$RUNNER_TEMP/tinybird-deployments.json" | |
| node scripts/analytics/staging-ci.js select-deployment --input "$RUNNER_TEMP/tinybird-deployments.json" --create-output "$RUNNER_TEMP/tinybird-create-output.json" --boundary "$RUNNER_TEMP/analytics-deployment-boundary.json" --minimum-created-at "$TINYBIRD_DEPLOYMENT_STARTED_AT" | |
| - name: Prepare immutable synthetic cleanup state before ingestion | |
| env: | |
| VERCEL_DEPLOYMENT_ID: ${{ steps.vercel.outputs.deployment_id }} | |
| VERCEL_PREVIEW_ACCESS_URL: ${{ env.ANALYTICS_PREVIEW_ACCESS_URL }} | |
| VERCEL_PREVIEW_URL: ${{ steps.vercel.outputs.url }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js prepare-seed | |
| --run-id "$ANALYTICS_TEST_RUN_ID" | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --needs-promotion "${{ steps.tinybird.outputs.needs_promotion }}" | |
| --boundary "$RUNNER_TEMP/analytics-deployment-boundary.json" | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Upload the immutable pre-ingestion recovery checkpoint | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: analytics-recovery-${{ github.run_id }}-${{ github.run_attempt }}-20-preseed | |
| path: | | |
| ${{ runner.temp }}/analytics-staging-state.json | |
| ${{ runner.temp }}/analytics-staging-report.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| - name: Refuse source mutations after exact-SHA checkout | |
| run: | | |
| git diff --exit-code | |
| test -z "$(git status --porcelain --untracked-files=no)" | |
| - name: Re-run fixture assertions after staging creation | |
| env: | |
| TINYBIRD_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TB_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TB_HOST: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| run: node scripts/analytics/analytics-cli.js test | |
| - name: Prove exact candidate endpoints before promotion | |
| id: verify | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js verify-preseed | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --target "${{ steps.tinybird.outputs.needs_promotion == 'true' && 'staging' || 'live' }}" | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Persist the exact Tinybird promotion plan | |
| id: promotion-plan | |
| if: steps.tinybird.outputs.needs_promotion == 'true' | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js prepare-promotion | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| - name: Upload the immutable pre-promotion recovery checkpoint | |
| if: steps.promotion-plan.outcome == 'success' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: analytics-recovery-${{ github.run_id }}-${{ github.run_attempt }}-30-prepromote | |
| path: | | |
| ${{ runner.temp }}/analytics-staging-state.json | |
| ${{ runner.temp }}/analytics-staging-report.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| - name: Promote the verified staging deployment | |
| id: promote | |
| if: steps.tinybird.outputs.needs_promotion == 'true' | |
| continue-on-error: true | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| run: | | |
| test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" | |
| git diff --exit-code | |
| test -z "$(git status --porcelain --untracked-files=no)" | |
| node scripts/analytics/staging-ci.js verify-pr-head | |
| node scripts/analytics/staging-ci.js promote-deployment --deployment-id "${{ steps.tinybird.outputs.id }}" --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| - name: Resolve authoritative Tinybird state after the promotion attempt | |
| id: deployment-state | |
| if: always() && steps.tinybird.outcome == 'success' | |
| env: | |
| TINYBIRD_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TB_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TB_HOST: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| run: | | |
| for resolution_attempt in $(seq 1 30); do | |
| if ! docker compose --file packages/local-docker/docker-compose.yml --profile analytics run --rm tinybird-cloud-cli --cloud --output json deployment ls > "$RUNNER_TEMP/tinybird-final-deployments.json"; then | |
| if [[ "$resolution_attempt" -eq 30 ]]; then | |
| exit 1 | |
| fi | |
| sleep 5 | |
| continue | |
| fi | |
| recover_pending=false | |
| if [[ "$resolution_attempt" -eq 30 ]]; then | |
| recover_pending=true | |
| fi | |
| if node scripts/analytics/staging-ci.js resolve-deployment-state --input "$RUNNER_TEMP/tinybird-final-deployments.json" --deployment-id "${{ steps.tinybird.outputs.id }}" --recover-pending "$recover_pending"; then | |
| exit 0 | |
| else | |
| resolution_exit=$? | |
| fi | |
| if [[ "$resolution_exit" -ne 75 ]]; then | |
| exit "$resolution_exit" | |
| fi | |
| sleep 5 | |
| done | |
| exit 1 | |
| - name: Refuse to proceed without an authoritative live deployment | |
| if: steps.deployment-state.outputs.promoted != 'true' || steps.promote.outcome == 'failure' | |
| run: exit 1 | |
| - name: Seed bounded duplicate and conflict probes into exact live staging | |
| id: seed | |
| env: | |
| VERCEL_DEPLOYMENT_ID: ${{ steps.vercel.outputs.deployment_id }} | |
| VERCEL_PREVIEW_URL: ${{ steps.vercel.outputs.url }} | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_INGEST_TOKEN: ${{ secrets.TINYBIRD_STAGING_INGEST_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js seed | |
| --run-id "$ANALYTICS_TEST_RUN_ID" | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Upload the immutable post-ingestion recovery checkpoint | |
| if: steps.seed.outcome == 'success' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: analytics-recovery-${{ github.run_id }}-${{ github.run_attempt }}-35-postseed | |
| path: | | |
| ${{ runner.temp }}/analytics-staging-state.json | |
| ${{ runner.temp }}/analytics-staging-report.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| - name: Verify representative ingestion performance | |
| id: ingestion-budget | |
| run: >- | |
| node scripts/analytics/staging-ci.js verify-ingestion-budget | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Prove least-privilege staging token scopes | |
| id: token-scopes | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_COPY_TOKEN: ${{ secrets.TINYBIRD_STAGING_COPY_TOKEN }} | |
| TINYBIRD_STAGING_ERASURE_LOOKUP_TOKEN: ${{ secrets.TINYBIRD_STAGING_ERASURE_LOOKUP_TOKEN }} | |
| TINYBIRD_STAGING_SCHEDULER_TOKEN: ${{ secrets.TINYBIRD_STAGING_SCHEDULER_TOKEN }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| TINYBIRD_STAGING_INGEST_TOKEN: ${{ secrets.TINYBIRD_STAGING_INGEST_TOKEN }} | |
| TINYBIRD_STAGING_CLEANUP_TOKEN: ${{ secrets.TINYBIRD_STAGING_CLEANUP_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js verify-token-scopes | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Prove the exact-SHA deployed browser tracker | |
| id: browser-e2e | |
| env: | |
| ANALYTICS_PREVIEW_URL: ${{ env.ANALYTICS_PREVIEW_ACCESS_URL }} | |
| ANALYTICS_BROWSER_RUN_ID: ${{ env.ANALYTICS_TEST_RUN_ID }}_preview | |
| ANALYTICS_STATE_PATH: ${{ runner.temp }}/analytics-staging-state.json | |
| ANALYTICS_ARTIFACT_PATH: ${{ runner.temp }}/analytics-staging-report.json | |
| CAP_ANALYTICS_STAGING_TEST_SECRET: ${{ secrets.CAP_ANALYTICS_STAGING_TEST_SECRET }} | |
| VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} | |
| VERCEL_PREVIEW_SHARE_SECRET: ${{ secrets.VERCEL_PREVIEW_SHARE_SECRET }} | |
| run: | | |
| pnpm --dir apps/chrome-extension exec playwright install --with-deps chromium | |
| pnpm --dir apps/chrome-extension exec playwright test e2e/analytics-staging.spec.ts | |
| - name: Probe the exact-SHA Vercel browser collector and staging rate limit | |
| id: preview-api | |
| env: | |
| CAP_ANALYTICS_STAGING_TEST_SECRET: ${{ secrets.CAP_ANALYTICS_STAGING_TEST_SECRET }} | |
| VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} | |
| VERCEL_PREVIEW_SHARE_SECRET: ${{ secrets.VERCEL_PREVIEW_SHARE_SECRET }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js probe-preview | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Prove exact-SHA durable server delivery | |
| id: server-delivery | |
| env: | |
| CAP_ANALYTICS_STAGING_TEST_SECRET: ${{ secrets.CAP_ANALYTICS_STAGING_TEST_SECRET }} | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} | |
| VERCEL_PREVIEW_SHARE_SECRET: ${{ secrets.VERCEL_PREVIEW_SHARE_SECRET }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js probe-server | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Rebuild promoted decision and health copies | |
| id: promoted-copies | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_COPY_TOKEN: ${{ secrets.TINYBIRD_STAGING_COPY_TOKEN }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_INGEST_TOKEN: ${{ secrets.TINYBIRD_STAGING_INGEST_TOKEN }} | |
| TINYBIRD_STAGING_SCHEDULER_TOKEN: ${{ secrets.TINYBIRD_STAGING_SCHEDULER_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js run-copies | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --phase promoted | |
| --target live | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Measure populated decision endpoint performance | |
| id: populated-performance | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js verify | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --baseline-deployment-id "${{ steps.promote.outputs.previous_live_id || steps.tinybird.outputs.id }}" | |
| --target live | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Prove promoted delivery, business values, and decision deduplication | |
| id: verify-promoted | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js verify-promoted | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Prove exact staging rollback and restoration | |
| id: rollback-drill | |
| if: steps.verify-promoted.outcome == 'success' && steps.promote.outputs.promoted == 'true' | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js drill-rollback | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --previous-live-id "${{ steps.promote.outputs.previous_live_id }}" | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Delete the scoped identity through the exact-SHA application path | |
| id: erase-identity | |
| env: | |
| CAP_ANALYTICS_STAGING_TEST_SECRET: ${{ secrets.CAP_ANALYTICS_STAGING_TEST_SECRET }} | |
| VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} | |
| VERCEL_PREVIEW_SHARE_SECRET: ${{ secrets.VERCEL_PREVIEW_SHARE_SECRET }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js erase-synthetic-identity | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Prove identity erasure and out-of-scope control preservation | |
| id: verify-erasure | |
| if: steps.erase-identity.outcome == 'success' | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js verify-synthetic-identity-erasure | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Quiesce scheduled and active Copy jobs before final cleanup | |
| id: pause-copies | |
| if: always() && steps.seed.outcome != 'skipped' && steps.deployment-state.outputs.promoted == 'true' | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_COPY_TOKEN: ${{ secrets.TINYBIRD_STAGING_COPY_TOKEN }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_SCHEDULER_TOKEN: ${{ secrets.TINYBIRD_STAGING_SCHEDULER_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js set-copy-schedules | |
| --action pause | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Delete strictly scoped synthetic raw rows | |
| id: cleanup | |
| if: always() && steps.seed.outcome != 'skipped' && (steps.deployment-state.outputs.target == 'staging' || steps.pause-copies.outcome == 'success') | |
| env: | |
| CAP_ANALYTICS_STAGING_TEST_SECRET: ${{ secrets.CAP_ANALYTICS_STAGING_TEST_SECRET }} | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_CLEANUP_TOKEN: ${{ secrets.TINYBIRD_STAGING_CLEANUP_TOKEN }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} | |
| VERCEL_PREVIEW_SHARE_SECRET: ${{ secrets.VERCEL_PREVIEW_SHARE_SECRET }} | |
| run: | | |
| if [[ ! -f "$RUNNER_TEMP/analytics-staging-state.json" ]]; then | |
| echo "required=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "required=true" >> "$GITHUB_OUTPUT" | |
| node scripts/analytics/staging-ci.js cleanup \ | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" \ | |
| --target "${{ steps.deployment-state.outputs.target || (steps.tinybird.outputs.needs_promotion == 'true' && 'staging' || 'live') }}" \ | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" \ | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Retract synthetic rows from every derived copy | |
| id: cleanup-copies | |
| if: always() && steps.cleanup.outputs.required == 'true' && steps.cleanup.outcome == 'success' && steps.cleanup.outputs.requires_copies == 'true' | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_COPY_TOKEN: ${{ secrets.TINYBIRD_STAGING_COPY_TOKEN }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_SCHEDULER_TOKEN: ${{ secrets.TINYBIRD_STAGING_SCHEDULER_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js run-copies | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --phase cleanup | |
| --target "${{ steps.cleanup.outputs.target }}" | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Prove synthetic cleanup no longer affects queries | |
| id: verify-cleanup | |
| if: always() && steps.cleanup-copies.outcome == 'success' | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js verify-cleanup | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --target "${{ steps.cleanup-copies.outputs.target }}" | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Resume reviewed Copy schedules | |
| id: resume-copies | |
| if: always() && steps.pause-copies.outcome == 'success' | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_COPY_TOKEN: ${{ secrets.TINYBIRD_STAGING_COPY_TOKEN }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_SCHEDULER_TOKEN: ${{ secrets.TINYBIRD_STAGING_SCHEDULER_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js set-copy-schedules | |
| --action resume | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Persist finalization eligibility | |
| id: recovery-ready | |
| if: steps.pause-copies.outcome == 'success' && steps.verify-cleanup.outcome == 'success' && steps.rollback-drill.outcome == 'success' && steps.resume-copies.outcome == 'success' | |
| run: >- | |
| node scripts/analytics/staging-ci.js mark-recovery-ready | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Upload the immutable pre-finalization recovery checkpoint | |
| if: steps.recovery-ready.outcome == 'success' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: analytics-recovery-${{ github.run_id }}-${{ github.run_attempt }}-40-ready-to-finalize | |
| path: | | |
| ${{ runner.temp }}/analytics-staging-state.json | |
| ${{ runner.temp }}/analytics-staging-report.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| - name: Finalize the fully verified staging promotion | |
| id: finalize | |
| if: steps.recovery-ready.outcome == 'success' | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js finalize-promotion | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --previous-live-id "${{ steps.promote.outputs.previous_live_id }}" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Restore the previous staging deployment on failure | |
| id: rollback | |
| if: always() && steps.promote.outputs.previous_live_id != '' && steps.finalize.outcome != 'success' && steps.rollback-drill.outputs.rollback_target_usable != 'false' && (steps.seed.outcome == 'skipped' || steps.verify-cleanup.outcome == 'success') | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js rollback-promotion | |
| --deployment-id "${{ steps.tinybird.outputs.id }}" | |
| --previous-live-id "${{ steps.promote.outputs.previous_live_id }}" | |
| --state "$RUNNER_TEMP/analytics-staging-state.json" | |
| --artifact "$RUNNER_TEMP/analytics-staging-report.json" | |
| - name: Discard an unpromoted staging deployment after cleanup | |
| id: discard | |
| if: always() && steps.rollback.outcome != 'success' && (steps.deployment-state.outputs.discard == 'true' || steps.cleanup.outputs.requires_discard == 'true') | |
| env: | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| run: | | |
| discard_args=(--deployment-id "${{ steps.tinybird.outputs.id }}") | |
| if [[ -f "$RUNNER_TEMP/analytics-staging-report.json" ]]; then | |
| discard_args+=(--artifact "$RUNNER_TEMP/analytics-staging-report.json") | |
| fi | |
| node scripts/analytics/staging-ci.js discard-deployment "${discard_args[@]}" | |
| - name: Upload redacted staging evidence | |
| if: always() && steps.cleanup.outputs.required == 'true' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: analytics-staging-${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/analytics-staging-report.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| recover-staging: | |
| name: Recover interrupted analytics staging | |
| needs: deploy-staging | |
| if: always() && needs.deploy-staging.result != 'success' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| environment: staging | |
| permissions: | |
| actions: read | |
| contents: read | |
| deployments: read | |
| pull-requests: read | |
| env: | |
| EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| EVENT_NUMBER: ${{ github.event.pull_request.number || '' }} | |
| HEAD_REF: ${{ github.event.pull_request.head.ref || '' }} | |
| ANALYTICS_TEST_RUN_ID: run_${{ github.run_id }}_${{ github.run_attempt }}_${{ github.event.pull_request.head.sha || github.sha }} | |
| steps: | |
| - name: Check out the interrupted exact SHA | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| fetch-depth: 1 | |
| - name: Locate immutable recovery checkpoints | |
| id: checkpoints | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CHECKPOINT_PREFIX: analytics-recovery-${{ github.run_id }}-${{ github.run_attempt }}- | |
| run: | | |
| count="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100" --jq '[.artifacts[] | select(.expired == false and (.name | startswith(env.CHECKPOINT_PREFIX)))] | length')" | |
| if [[ "$count" -eq 0 ]]; then | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| - name: Download immutable recovery checkpoints | |
| if: steps.checkpoints.outputs.found == 'true' | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 | |
| with: | |
| pattern: analytics-recovery-${{ github.run_id }}-${{ github.run_attempt }}-* | |
| path: ${{ runner.temp }}/analytics-recovery-checkpoints | |
| - name: Reconcile and recover only the owned staging run | |
| id: recover | |
| if: steps.checkpoints.outputs.found == 'true' | |
| env: | |
| CAP_ANALYTICS_STAGING_TEST_SECRET: ${{ secrets.CAP_ANALYTICS_STAGING_TEST_SECRET }} | |
| TINYBIRD_STAGING_URL: ${{ secrets.TINYBIRD_STAGING_URL }} | |
| TINYBIRD_STAGING_CLEANUP_TOKEN: ${{ secrets.TINYBIRD_STAGING_CLEANUP_TOKEN }} | |
| TINYBIRD_STAGING_COPY_TOKEN: ${{ secrets.TINYBIRD_STAGING_COPY_TOKEN }} | |
| TINYBIRD_STAGING_DEPLOY_TOKEN: ${{ secrets.TINYBIRD_STAGING_DEPLOY_TOKEN }} | |
| TINYBIRD_STAGING_READ_TOKEN: ${{ secrets.TINYBIRD_STAGING_READ_TOKEN }} | |
| TINYBIRD_STAGING_SCHEDULER_TOKEN: ${{ secrets.TINYBIRD_STAGING_SCHEDULER_TOKEN }} | |
| VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} | |
| VERCEL_PREVIEW_SHARE_SECRET: ${{ secrets.VERCEL_PREVIEW_SHARE_SECRET }} | |
| run: >- | |
| node scripts/analytics/staging-ci.js recover | |
| --checkpoint-directory "$RUNNER_TEMP/analytics-recovery-checkpoints" | |
| --artifact "$RUNNER_TEMP/analytics-recovery-report.json" | |
| - name: Upload redacted recovery evidence | |
| if: always() && steps.checkpoints.outputs.found == 'true' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 | |
| with: | |
| name: analytics-recovery-result-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: | | |
| ${{ runner.temp }}/analytics-recovery-report.json | |
| ${{ runner.temp }}/analytics-recovery-checkpoints/**/analytics-staging-report.json | |
| if-no-files-found: error | |
| retention-days: 30 |