fix(data-warehouse): show a catalog placeholder while new source loads #284639
Workflow file for this run
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
| # Hobby deployment smoke test and preview workflow | |
| # | |
| # Two modes: | |
| # - Smoke test (default): Creates ephemeral droplet, runs health check, destroys | |
| # - Preview (hobby-preview label): Creates/updates persistent droplet for manual testing | |
| # | |
| # Runs automatically when deployment-related files change, or on-demand via hobby-preview label. | |
| # Debug: SSH to droplet and run `tail -f /var/log/cloud-init-output.log` | |
| name: E2E Hobby CI | |
| on: | |
| push: | |
| branches: | |
| - 'release-*.*' | |
| schedule: | |
| # Nightly smoke test against master. The paths filter only runs this job on PRs that | |
| # touch deploy-path files, so a hobby install broken by ordinary product code (a bad | |
| # migration, a service that can't boot) would otherwise go uncaught until a release push. | |
| - cron: '0 6 * * *' | |
| pull_request: | |
| types: [opened, synchronize, labeled, unlabeled] | |
| concurrency: | |
| # Keep unrelated labels out of the group that owns active smoke tests and previews. | |
| group: >- | |
| ${{ github.workflow }}-${{ | |
| contains(fromJSON('["labeled", "unlabeled"]'), github.event.action) | |
| && !( | |
| (github.event.action == 'labeled' | |
| && contains(fromJSON('["hobby-preview", "no-depot-docker-cache"]'), github.event.label.name)) | |
| || (github.event.action == 'unlabeled' && github.event.label.name == 'hobby-preview') | |
| ) | |
| && format('ignored-label-{0}', github.event.label.name) | |
| || github.head_ref || github.ref | |
| }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| deployments: write | |
| jobs: | |
| # Determine if we should run based on changed files or preview label | |
| # See .github/actions/paths-filter/README.md for filter semantics | |
| changes: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| # trunk-merge/** heads are skipped: merge-queue PRs are ephemeral and every | |
| # constituent PR already ran this in full. | |
| if: >- | |
| github.event_name == 'pull_request' | |
| && !startsWith(github.head_ref, 'trunk-merge/') | |
| && (!contains(fromJSON('["labeled", "unlabeled"]'), github.event.action) | |
| || (github.event.action == 'labeled' | |
| && contains(fromJSON('["hobby-preview", "no-depot-docker-cache"]'), github.event.label.name)) | |
| || (github.event.action == 'unlabeled' && github.event.label.name == 'hobby-preview')) | |
| name: Determine need to run hobby checks | |
| outputs: | |
| should_run: ${{ steps.filter.outputs.hobby == 'true' || steps.check-label.outputs.has_label == 'true' }} | |
| installer_changed: ${{ steps.filter.outputs.installer }} | |
| label_removed: ${{ steps.check-label.outputs.label_removed }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: .github/actions/paths-filter | |
| sparse-checkout-cone-mode: false | |
| - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| id: app-token | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository | |
| with: | |
| client-id: ${{ vars.GH_APP_POSTHOG_PATHS_FILTER_APP_ID }} | |
| private-key: ${{ secrets.GH_APP_POSTHOG_PATHS_FILTER_PRIVATE_KEY }} | |
| - uses: ./.github/actions/paths-filter | |
| id: filter | |
| with: | |
| token: ${{ steps.app-token.outputs.token || github.token }} | |
| filters: | | |
| hobby: | |
| - Dockerfile | |
| - docker-compose.base.yml | |
| - docker-compose.hobby.yml | |
| # Hobby-specific scripts | |
| - 'bin/deploy-hobby' | |
| - 'bin/hobby-ci-setup-user.py' | |
| - 'bin/hobby-ci.py' | |
| - 'bin/upgrade-hobby' | |
| - 'bin/helpers/render-hobby-compose.sh' | |
| - 'bin/test/test_render_hobby_compose.py' | |
| - 'bin/migrate-*-hobby' | |
| # Docker runtime (Dockerfile CMD chain) | |
| - 'bin/docker' | |
| - 'bin/docker-worker' | |
| - 'bin/docker-worker-beat' | |
| - 'bin/docker-worker-celery' | |
| - 'bin/docker-server' | |
| - 'bin/celery-queues.env' | |
| - 'bin/migrate' | |
| - 'bin/migrate-check' | |
| - 'bin/posthog-node' | |
| - 'bin/hobby-installer/**' | |
| - 'docker/**' | |
| - uv.lock | |
| - .github/workflows/ci-hobby.yml | |
| - .github/actions/setup-uv/** | |
| installer: | |
| - 'bin/hobby-installer/**' | |
| - name: Check for preview label | |
| id: check-label | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| env: | |
| HOBBY_CHANGED: ${{ steps.filter.outputs.hobby }} | |
| with: | |
| script: | | |
| const labels = context.payload.pull_request.labels || []; | |
| const hasLabel = labels.some(l => l.name.toLowerCase() === 'hobby-preview'); | |
| // Check if hobby-preview label was just removed | |
| const labelRemoved = context.payload.action === 'unlabeled' && | |
| context.payload.label?.name?.toLowerCase() === 'hobby-preview'; | |
| console.log(`hobby-preview label: ${hasLabel}, label removed: ${labelRemoved}, hobby files changed: ${process.env.HOBBY_CHANGED}`); | |
| core.setOutput('has_label', hasLabel.toString()); | |
| core.setOutput('label_removed', labelRemoved.toString()); | |
| upgrade-render-test: | |
| name: Test hobby upgrade rendering | |
| needs: changes | |
| if: ${{ !cancelled() && (needs.changes.outputs.should_run == 'true' || github.event_name == 'push' || github.event_name == 'schedule') }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| sparse-checkout: | | |
| bin/helpers/render-hobby-compose.sh | |
| bin/test/test_render_hobby_compose.py | |
| sparse-checkout-cone-mode: false | |
| - name: Install envsubst | |
| run: sudo apt-get update && sudo apt-get install -y gettext-base | |
| - name: Check upgrade rendering | |
| # Branches opened before the renderer landed have no test file yet; skip instead of failing discovery. | |
| run: | | |
| if [ ! -f bin/test/test_render_hobby_compose.py ]; then | |
| echo "Renderer test is not on this branch yet, skipping" | |
| exit 0 | |
| fi | |
| python3 -m unittest discover -s bin/test -p test_render_hobby_compose.py | |
| wait-for-docker-image: | |
| name: Wait for Docker image build | |
| needs: changes | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 90 | |
| if: github.event_name == 'pull_request' && needs.changes.outputs.should_run == 'true' | |
| outputs: | |
| preview-mode: ${{ steps.check-preview.outputs.preview }} | |
| deployment-id: ${{ steps.create-deployment.outputs.deployment-id }} | |
| conclusion: ${{ steps.wait-for-image.outputs.conclusion }} | |
| steps: | |
| - name: 'Check for preview mode' | |
| id: check-preview | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| console.log(`Fetching labels for PR #${context.issue.number}`); | |
| try { | |
| const { data: pullRequest } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| }); | |
| const labels = pullRequest.labels.map(label => label.name); | |
| console.log(`Found labels: ${labels.join(', ')}`); | |
| const hasPreviewLabel = labels.some(label => | |
| label.toLowerCase() === 'hobby-preview' | |
| ); | |
| if (hasPreviewLabel) { | |
| console.log('✅ Preview mode enabled - found "hobby-preview" label'); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'preview=true\n'); | |
| } else { | |
| console.log('🧪 Smoke test mode - "hobby-preview" label not found'); | |
| console.log(`Available labels: ${labels.join(', ')}`); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'preview=false\n'); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching PR labels:', error.message); | |
| console.log('Defaulting to smoke test mode'); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'preview=false\n'); | |
| } | |
| - name: Check out CI report scripts | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: | | |
| .github | |
| frontend/bin/ci-report | |
| - name: Post initial hobby preview status | |
| if: steps.check-preview.outputs.preview == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| PREVIEW_MODE: ${{ steps.check-preview.outputs.preview }} | |
| run: node .github/scripts/post-hobby-section.mjs initial | |
| - name: Create GitHub deployment | |
| id: create-deployment | |
| if: steps.check-preview.outputs.preview == 'true' | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const prNumber = context.issue.number; | |
| const environment = `preview-pr-${prNumber}`; | |
| // Use PR branch name - GitHub resolves to SHA and links to PR | |
| const ref = context.payload.pull_request?.head?.ref || context.ref; | |
| const { data: deployment } = await github.rest.repos.createDeployment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: ref, | |
| environment: environment, | |
| auto_merge: false, | |
| required_contexts: [], | |
| description: `Hobby preview for PR #${prNumber}` | |
| }); | |
| console.log(`Created deployment ${deployment.id} for ${environment}`); | |
| // Set initial status to pending (waiting for Docker build) | |
| await github.rest.repos.createDeploymentStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| deployment_id: deployment.id, | |
| state: 'pending', | |
| log_url: `${context.payload.repository.html_url}/actions/runs/${context.runId}`, | |
| description: 'Waiting for Docker image build...' | |
| }); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `deployment-id=${deployment.id}\n`); | |
| - name: Wait for container image build to complete | |
| uses: ./.github/actions/wait-for-check | |
| id: wait-for-image | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| checkName: >- | |
| ${{ github.event.action == 'labeled' | |
| && contains(fromJSON('["hobby-preview", "no-depot-docker-cache"]'), github.event.label.name) | |
| && 'Build labeled Docker image' | |
| || 'Build Docker image' }} | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| timeoutSeconds: '3600' | |
| intervalSeconds: '30' | |
| - name: Post Docker build failure to CI report | |
| if: always() && steps.check-preview.outputs.preview == 'true' && steps.wait-for-image.outputs.conclusion != 'success' | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| PREVIEW_MODE: ${{ steps.check-preview.outputs.preview }} | |
| BUILD_CONCLUSION: ${{ steps.wait-for-image.outputs.conclusion }} | |
| run: node .github/scripts/post-hobby-section.mjs build-failed | |
| - name: Check Docker image build result | |
| env: | |
| BUILD_CONCLUSION: ${{ steps.wait-for-image.outputs.conclusion }} | |
| run: | | |
| conclusion="$BUILD_CONCLUSION" | |
| if [ "$conclusion" = "skipped" ]; then | |
| echo "Docker image build skipped" | |
| exit 0 | |
| elif [ "$conclusion" != "success" ]; then | |
| echo "Docker image build failed with conclusion: $conclusion" | |
| exit 1 | |
| fi | |
| - name: Post Docker image ready status to CI report | |
| if: steps.check-preview.outputs.preview == 'true' && steps.wait-for-image.outputs.conclusion == 'success' | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| PREVIEW_MODE: ${{ steps.check-preview.outputs.preview }} | |
| run: node .github/scripts/post-hobby-section.mjs image-ready | |
| hobby-test: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 120 | |
| name: Setup DO Hobby Instance and test | |
| needs: [wait-for-docker-image, changes] | |
| # A status function is required to read wait-for-docker-image's conclusion after it | |
| # failed or was skipped. !cancelled() rather than always() so a superseded PR run stops | |
| # this job promptly; the explicit conclusion/push/schedule checks preserve when it runs. | |
| if: ${{ !cancelled() && ((needs.changes.outputs.should_run == 'true' && needs.wait-for-docker-image.outputs.conclusion == 'success') || github.event_name == 'push' || github.event_name == 'schedule') }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| clean: false | |
| - name: Clean up data directories with container permissions | |
| run: | | |
| # Use docker to clean up files created by containers | |
| [ -d "data" ] && docker run --rm -v "$(pwd)/data:/data" alpine sh -c "rm -rf /data/seaweedfs /data/minio" || true | |
| continue-on-error: true | |
| - name: Install uv | |
| uses: ./.github/actions/setup-uv | |
| with: | |
| enable-cache: false | |
| - name: Set environment variables from previous job | |
| run: | | |
| echo "PREVIEW_MODE=${{ github.event_name == 'pull_request' && needs.wait-for-docker-image.outputs.preview-mode || 'false' }}" >> $GITHUB_ENV | |
| echo "HOBBY_DEPLOYMENT_ID=${{ needs.wait-for-docker-image.outputs.deployment-id }}" >> $GITHUB_ENV | |
| - name: Setup DO Hobby Instance | |
| run: uv run bin/hobby-ci.py create "$BRANCH_NAME" "${{ github.run_id }}-${{ github.run_attempt }}" "${{ github.sha }}" "${{ github.event.pull_request.number || 'unknown' }}" 2>&1 | tee /tmp/hobby-ci-output.txt | |
| env: | |
| BRANCH_NAME: ${{ github.head_ref || github.ref_name }} | |
| DIGITALOCEAN_TOKEN: ${{ secrets.DIGITAL_OCEAN_HOBBY_TOKEN }} | |
| DIGITALOCEAN_SSH_PRIVATE_KEY: ${{ secrets.DO_DEPLOY_SSH_PRIVATE_KEY }} | |
| INSTALLER_CHANGED: ${{ needs.changes.outputs.installer_changed || 'false' }} | |
| - name: Post instance created status to CI report | |
| if: env.PREVIEW_MODE == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: node .github/scripts/post-hobby-section.mjs instance-created | |
| - name: Update GitHub deployment - instance created | |
| if: env.PREVIEW_MODE == 'true' && env.HOBBY_DEPLOYMENT_ID | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const deploymentId = process.env.HOBBY_DEPLOYMENT_ID; | |
| // Parse instance URL from droplet info | |
| let instanceUrl = ''; | |
| if (fs.existsSync('/tmp/droplet_info.txt')) { | |
| const dropletInfo = fs.readFileSync('/tmp/droplet_info.txt', 'utf8'); | |
| const urlMatch = dropletInfo.match(/URL: (.*)/); | |
| if (urlMatch) instanceUrl = urlMatch[1]; | |
| } | |
| // Update status to in_progress with URL | |
| await github.rest.repos.createDeploymentStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| deployment_id: parseInt(deploymentId), | |
| state: 'in_progress', | |
| environment_url: instanceUrl || undefined, | |
| log_url: `${context.payload.repository.html_url}/actions/runs/${context.runId}`, | |
| description: 'Running smoke tests...' | |
| }); | |
| console.log(`Updated deployment ${deploymentId} to in_progress`); | |
| - name: Wait for cloud-init | |
| id: cloud-init | |
| timeout-minutes: 40 | |
| run: | | |
| set +e | |
| uv run bin/hobby-ci.py wait-for-cloud-init | |
| echo "TEST_EXIT_CODE=$?" >> $GITHUB_ENV | |
| exit 0 | |
| env: | |
| DIGITALOCEAN_TOKEN: ${{ secrets.DIGITAL_OCEAN_HOBBY_TOKEN }} | |
| DIGITALOCEAN_SSH_PRIVATE_KEY: ${{ secrets.DO_DEPLOY_SSH_PRIVATE_KEY }} | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| RUN_ID: ${{ github.run_id }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| - name: Wait for health check | |
| if: env.TEST_EXIT_CODE == '0' | |
| id: health-check | |
| # Must stay above wait_for_health_check's internal budget in bin/hobby-ci.py so the | |
| # script times out first and reports container diagnostics instead of the runner | |
| # killing the step. Raise both together: the budget covers the full Django migration | |
| # run against an empty Postgres before /_health answers, which grows over time. | |
| timeout-minutes: 65 | |
| run: | | |
| set +e | |
| uv run bin/hobby-ci.py wait-for-health | |
| echo "TEST_EXIT_CODE=$?" >> $GITHUB_ENV | |
| exit 0 | |
| env: | |
| DIGITALOCEAN_TOKEN: ${{ secrets.DIGITAL_OCEAN_HOBBY_TOKEN }} | |
| DIGITALOCEAN_SSH_PRIVATE_KEY: ${{ secrets.DO_DEPLOY_SSH_PRIVATE_KEY }} | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| RUN_ID: ${{ github.run_id }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| - name: Smoke test event ingestion | |
| if: env.TEST_EXIT_CODE == '0' && env.PREVIEW_MODE == 'false' | |
| timeout-minutes: 10 | |
| run: | | |
| set +e | |
| uv run bin/hobby-ci.py smoke-test-ingestion | |
| echo "TEST_EXIT_CODE=$?" >> $GITHUB_ENV | |
| exit 0 | |
| env: | |
| DIGITALOCEAN_TOKEN: ${{ secrets.DIGITAL_OCEAN_HOBBY_TOKEN }} | |
| DIGITALOCEAN_SSH_PRIVATE_KEY: ${{ secrets.DO_DEPLOY_SSH_PRIVATE_KEY }} | |
| - name: Generate demo data | |
| if: env.TEST_EXIT_CODE == '0' && env.PREVIEW_MODE == 'true' && env.HOBBY_DROPLET_NEW == 'true' | |
| timeout-minutes: 15 | |
| run: uv run bin/hobby-ci.py generate-demo-data | |
| env: | |
| DIGITALOCEAN_TOKEN: ${{ secrets.DIGITAL_OCEAN_HOBBY_TOKEN }} | |
| DIGITALOCEAN_SSH_PRIVATE_KEY: ${{ secrets.DO_DEPLOY_SSH_PRIVATE_KEY }} | |
| - name: Post final hobby preview status to CI report | |
| if: always() && env.PREVIEW_MODE == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: node .github/scripts/post-hobby-section.mjs final | |
| - name: Update GitHub deployment status | |
| if: always() && env.PREVIEW_MODE == 'true' && env.HOBBY_DEPLOYMENT_ID | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const deploymentId = process.env.HOBBY_DEPLOYMENT_ID; | |
| const testExitCode = process.env.TEST_EXIT_CODE; | |
| const testPassed = testExitCode === '0'; | |
| // Parse instance URL from droplet info | |
| let instanceUrl = ''; | |
| if (fs.existsSync('/tmp/droplet_info.txt')) { | |
| const dropletInfo = fs.readFileSync('/tmp/droplet_info.txt', 'utf8'); | |
| const urlMatch = dropletInfo.match(/URL: (.*)/); | |
| if (urlMatch) instanceUrl = urlMatch[1]; | |
| } | |
| const state = testPassed ? 'success' : 'failure'; | |
| const description = testPassed | |
| ? 'Preview deployment ready' | |
| : 'Smoke tests failed'; | |
| await github.rest.repos.createDeploymentStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| deployment_id: parseInt(deploymentId), | |
| state: state, | |
| environment_url: instanceUrl || undefined, | |
| log_url: `${context.payload.repository.html_url}/actions/runs/${context.runId}`, | |
| description: description | |
| }); | |
| console.log(`Updated deployment ${deploymentId} to ${state}`); | |
| - name: Fetch logs after test | |
| if: env.TEST_EXIT_CODE != '0' | |
| timeout-minutes: 5 | |
| run: uv run bin/hobby-ci.py fetch-logs || echo "Could not fetch logs" | |
| env: | |
| DIGITALOCEAN_TOKEN: ${{ secrets.DIGITAL_OCEAN_HOBBY_TOKEN }} | |
| DIGITALOCEAN_SSH_PRIVATE_KEY: ${{ secrets.DO_DEPLOY_SSH_PRIVATE_KEY }} | |
| - name: Show droplet info | |
| if: always() | |
| run: cat /tmp/droplet_info.txt 2>/dev/null || echo "No droplet info available" | |
| - name: Upload logs as artifact | |
| if: always() | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: hobby-ci-logs | |
| path: | | |
| /tmp/cloud-init-output.log | |
| /tmp/droplet_info.txt | |
| /tmp/docker-compose-logs.txt | |
| if-no-files-found: warn | |
| - name: Post-cleanup step | |
| if: always() && env.PREVIEW_MODE == 'false' | |
| run: uv run bin/hobby-ci.py destroy | |
| env: | |
| DIGITALOCEAN_TOKEN: ${{ secrets.DIGITAL_OCEAN_HOBBY_TOKEN }} | |
| - name: Check test result | |
| if: always() | |
| run: | | |
| if [ "$TEST_EXIT_CODE" != "0" ]; then | |
| echo "❌ Test failed with exit code $TEST_EXIT_CODE" | |
| exit 1 | |
| fi | |
| echo "✅ Test passed" | |
| # Cleanup preview when hobby-preview label is removed | |
| cleanup-on-label-removal: | |
| name: Cleanup preview on label removal | |
| needs: changes | |
| if: needs.changes.outputs.label_removed == 'true' | |
| uses: ./.github/workflows/pr-cleanup.yml | |
| # these permissions are required by pr-cleanup.yml | |
| permissions: | |
| deployments: write | |
| with: | |
| pr_number: ${{ github.event.pull_request.number }} | |
| secrets: | |
| DIGITAL_OCEAN_HOBBY_TOKEN: ${{ secrets.DIGITAL_OCEAN_HOBBY_TOKEN }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} |