Skip to content

fix(data-warehouse): show a catalog placeholder while new source loads #284639

fix(data-warehouse): show a catalog placeholder while new source loads

fix(data-warehouse): show a catalog placeholder while new source loads #284639

Workflow file for this run

# 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 }}