Repository navigation
Building Dynamic PR Environment for 83443/merge by @johnswanson #3932
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
| name: Deploy PR Environment | |
| run-name: Building Dynamic PR Environment for ${{ github.ref_name }} by @${{ github.actor }} | |
| on: | |
| pull_request: | |
| types: [ opened, labeled, synchronize, reopened ] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| id-token: write | |
| contents: read | |
| actions: read | |
| pull-requests: write | |
| jobs: | |
| build: | |
| if: >- | |
| ${{ | |
| github.event.pull_request.head.repo.full_name == 'metabase/metabase' && | |
| ( | |
| github.event.action != 'labeled' || | |
| contains(fromJSON('["PR-Env", "PR-Env-self-hosting", "PR-Env-analytics-dev", "PR-Env-Stats-Dump"]'), github.event.label.name) | |
| ) && | |
| ( | |
| contains(github.event.pull_request.labels.*.name, 'PR-Env') || | |
| contains(github.event.pull_request.labels.*.name, 'PR-Env-self-hosting') || | |
| contains(github.event.pull_request.labels.*.name, 'PR-Env-analytics-dev') || | |
| contains(github.event.pull_request.labels.*.name, 'PR-Env-Stats-Dump') | |
| ) | |
| }} | |
| uses: ./.github/workflows/uberjar.yml | |
| secrets: inherit | |
| containerize: | |
| needs: [build] | |
| uses: ./.github/workflows/containerize-jar.yml | |
| secrets: inherit | |
| with: | |
| edition: ee | |
| registry: ecr | |
| repo: metabase-enterprise | |
| tag: ${{ github.event.pull_request.head.sha }} | |
| commit: ${{ github.event.pull_request.head.sha }} | |
| platforms: linux/amd64 | |
| deploy: | |
| needs: [containerize] | |
| runs-on: ${{ vars.DEFAULT_RUNNER_KEY }} | |
| name: PR Review ENV | |
| timeout-minutes: 60 | |
| outputs: | |
| self_hosting: ${{ steps.label.outputs.self_hosting }} | |
| analytics_dev_mode: ${{ steps.label.outputs.analytics_dev_mode }} | |
| stats_dump: ${{ steps.label.outputs.stats_dump }} | |
| steps: | |
| - name: Checkout source code | |
| uses: actions/checkout@v7 | |
| - name: Resolve Labels | |
| id: label | |
| env: | |
| LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} | |
| run: | # bash | |
| self_hosting=false | |
| analytics_dev_mode=false | |
| stats_dump=false | |
| while IFS= read -r label; do | |
| case "$label" in | |
| PR-Env-self-hosting) self_hosting=true ;; | |
| PR-Env-analytics-dev) analytics_dev_mode=true ;; | |
| PR-Env-Stats-Dump) stats_dump=true ;; | |
| esac | |
| done < <(jq -r '.[]' <<< "$LABELS") | |
| echo "self_hosting=${self_hosting} analytics_dev_mode=${analytics_dev_mode} stats_dump=${stats_dump}" | |
| { | |
| echo "self_hosting=${self_hosting}" | |
| echo "analytics_dev_mode=${analytics_dev_mode}" | |
| echo "stats_dump=${stats_dump}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Tailscale | |
| uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4.1.2 | |
| with: | |
| oauth-client-id: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_CLIENT_ID }} | |
| oauth-secret: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_SECRET }} | |
| tags: tag:ci | |
| version: 1.50.1 | |
| sha256sum: d9fe6b480fb5078f0aa57dace686898dda7e2a768884271159faa74846bfb576 | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 | |
| with: | |
| role-to-assume: ${{ secrets.PR_ENV_IAM_ROLE }} | |
| role-session-name: GitHub_to_AWS_via_FederatedOIDC | |
| aws-region: us-east-1 | |
| - name: Setup psql client | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y postgresql-client | |
| - name: Create app database if not exists | |
| id: create_app_db | |
| run: | | |
| export PGPASSWORD="${PR_ENV_DB_PASSWORD}" | |
| export PGUSER="${PR_ENV_DB_USER}" | |
| export PGHOST="${PR_ENV_DB_HOST}" | |
| export PGDATABASE="${PR_ENV_DB_NAME}" | |
| if psql -tc "SELECT 1 FROM pg_database WHERE datname = 'hosting_pr${{ github.event.number }}'" | grep -q 1; then | |
| echo "db_created=false" >> "$GITHUB_OUTPUT" | |
| else | |
| psql -c "CREATE DATABASE hosting_pr${{ github.event.number }}" | |
| echo "db_created=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| env: | |
| PR_ENV_DB_PASSWORD: ${{ secrets.PR_ENV_DB_PASSWORD }} | |
| PR_ENV_DB_USER: ${{ secrets.PR_ENV_DB_USER }} | |
| PR_ENV_DB_HOST: ${{ secrets.PR_ENV_DB_HOST }} | |
| PR_ENV_DB_NAME: ${{ secrets.PR_ENV_DB_NAME }} | |
| - name: Verify stats dump load status | |
| id: verify_stats_dump | |
| if: ${{ steps.label.outputs.stats_dump == 'true' }} | |
| run: | | |
| export PGPASSWORD="${PR_ENV_DB_PASSWORD}" | |
| ADMIN="psql -h ${PR_ENV_DB_HOST} -U ${PR_ENV_DB_USER} -d ${PR_ENV_DB_NAME}" | |
| MARKED=$($ADMIN -tAc "SELECT 1 FROM pg_database d WHERE d.datname='hosting_pr${{ github.event.number }}' AND shobj_description(d.oid, 'pg_database') = 'metabase-pr-env stats-dump loaded';") | |
| if [ "$MARKED" = "1" ]; then | |
| echo "Stats dump already loaded; skipping reload." | |
| echo "should_load=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Stats dump not loaded (or previous load incomplete); recreating database." | |
| $ADMIN -c "ALTER DATABASE \"hosting_pr${{ github.event.number }}\" CONNECTION LIMIT 0;" || true | |
| $ADMIN -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='hosting_pr${{ github.event.number }}' AND pid <> pg_backend_pid();" || true | |
| $ADMIN -c "DROP DATABASE IF EXISTS \"hosting_pr${{ github.event.number }}\" WITH (FORCE);" | |
| $ADMIN -c "CREATE DATABASE \"hosting_pr${{ github.event.number }}\";" | |
| echo "should_load=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| env: | |
| PR_ENV_DB_PASSWORD: ${{ secrets.PR_ENV_DB_PASSWORD }} | |
| PR_ENV_DB_USER: ${{ secrets.PR_ENV_DB_USER }} | |
| PR_ENV_DB_HOST: ${{ secrets.PR_ENV_DB_HOST }} | |
| PR_ENV_DB_NAME: ${{ secrets.PR_ENV_DB_NAME }} | |
| - name: Locate latest stats dump in S3 | |
| id: locate_stats_dump | |
| if: ${{ steps.label.outputs.stats_dump == 'true' && steps.verify_stats_dump.outputs.should_load == 'true' }} | |
| env: | |
| STATS_DUMP_BUCKET: metabase-cloud-stats-backups-coredev | |
| run: | | |
| LATEST_KEY=$(aws s3api list-objects-v2 \ | |
| --bucket "${STATS_DUMP_BUCKET}" \ | |
| --query "reverse(sort_by(Contents[?ends_with(Key, '.dump')], &LastModified))[0].Key" \ | |
| --output text) | |
| if [ -z "$LATEST_KEY" ] || [ "$LATEST_KEY" = "None" ]; then | |
| echo "No *.dump found in s3://${STATS_DUMP_BUCKET}" >&2 | |
| exit 1 | |
| fi | |
| echo "Latest dump: $LATEST_KEY" | |
| echo "key=$LATEST_KEY" >> "$GITHUB_OUTPUT" | |
| - name: Download stats dump | |
| if: ${{ steps.label.outputs.stats_dump == 'true' && steps.verify_stats_dump.outputs.should_load == 'true' }} | |
| env: | |
| STATS_DUMP_BUCKET: metabase-cloud-stats-backups-coredev | |
| run: | | |
| aws s3 cp --no-progress \ | |
| "s3://${STATS_DUMP_BUCKET}/${{ steps.locate_stats_dump.outputs.key }}" \ | |
| /tmp/stats-dump.dump | |
| echo "Downloaded $(stat -c%s /tmp/stats-dump.dump) bytes" | |
| file /tmp/stats-dump.dump | |
| - name: Prepare app database schema | |
| if: ${{ steps.label.outputs.stats_dump == 'true' && steps.verify_stats_dump.outputs.should_load == 'true' }} | |
| run: | | |
| PGPASSWORD="${PR_ENV_DB_PASSWORD}" psql \ | |
| -h "${PR_ENV_DB_HOST}" \ | |
| -U "${PR_ENV_DB_USER}" \ | |
| -d "hosting_pr${{ github.event.number }}" \ | |
| --no-align --tuples-only --expanded \ | |
| --command='DROP SCHEMA public CASCADE; CREATE SCHEMA public; CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public;' | |
| env: | |
| PR_ENV_DB_PASSWORD: ${{ secrets.PR_ENV_DB_PASSWORD }} | |
| PR_ENV_DB_USER: ${{ secrets.PR_ENV_DB_USER }} | |
| PR_ENV_DB_HOST: ${{ secrets.PR_ENV_DB_HOST }} | |
| - name: Restore stats dump into app database | |
| if: ${{ steps.label.outputs.stats_dump == 'true' && steps.verify_stats_dump.outputs.should_load == 'true' }} | |
| run: | | |
| set +e | |
| echo "::group::pg_restore / pg client versions" | |
| pg_restore --version | |
| psql --version | |
| echo "::endgroup::" | |
| echo "::group::Server version" | |
| PGPASSWORD="${PR_ENV_DB_PASSWORD}" psql \ | |
| -h "${PR_ENV_DB_HOST}" -U "${PR_ENV_DB_USER}" -d "${PR_ENV_DB_NAME}" \ | |
| -tAc 'SHOW server_version;' | |
| echo "::group::pg_restore" | |
| PGPASSWORD="${PR_ENV_DB_PASSWORD}" pg_restore \ | |
| -h "${PR_ENV_DB_HOST}" \ | |
| -U "${PR_ENV_DB_USER}" \ | |
| -d "hosting_pr${{ github.event.number }}" \ | |
| --single-transaction \ | |
| --no-acl \ | |
| --no-owner \ | |
| --schema=public \ | |
| /tmp/stats-dump.dump | |
| RESTORE_EXIT=$? | |
| echo "::endgroup::" | |
| echo "pg_restore exit code: ${RESTORE_EXIT}" | |
| if [ "${RESTORE_EXIT}" -ne 0 ]; then | |
| echo "Restore failed; the database will be rebuilt on the next run because it has not been marked as loaded." | |
| exit "${RESTORE_EXIT}" | |
| fi | |
| env: | |
| PR_ENV_DB_PASSWORD: ${{ secrets.PR_ENV_DB_PASSWORD }} | |
| PR_ENV_DB_USER: ${{ secrets.PR_ENV_DB_USER }} | |
| PR_ENV_DB_HOST: ${{ secrets.PR_ENV_DB_HOST }} | |
| PR_ENV_DB_NAME: ${{ secrets.PR_ENV_DB_NAME }} | |
| - name: Mark database as stats-dump loaded | |
| if: ${{ steps.label.outputs.stats_dump == 'true' && steps.verify_stats_dump.outputs.should_load == 'true' }} | |
| run: | | |
| PGPASSWORD="${PR_ENV_DB_PASSWORD}" psql \ | |
| -h "${PR_ENV_DB_HOST}" \ | |
| -U "${PR_ENV_DB_USER}" \ | |
| -d "hosting_pr${{ github.event.number }}" \ | |
| -c "COMMENT ON DATABASE \"hosting_pr${{ github.event.number }}\" IS 'metabase-pr-env stats-dump loaded';" | |
| env: | |
| PR_ENV_DB_PASSWORD: ${{ secrets.PR_ENV_DB_PASSWORD }} | |
| PR_ENV_DB_USER: ${{ secrets.PR_ENV_DB_USER }} | |
| PR_ENV_DB_HOST: ${{ secrets.PR_ENV_DB_HOST }} | |
| - name: Download Deployment YAML template | |
| if: ${{ steps.label.outputs.self_hosting != 'true' }} | |
| run: aws s3 cp s3://metabase-pr-env/metabase.yml.tmpl ./metabase.yml.tmpl | |
| - name: Render Deployment YAML | |
| if: ${{ steps.label.outputs.self_hosting != 'true' }} | |
| uses: nowactions/envsubst@c4b8e3977354d294659a7ec164279ce8fba5c2e1 # v1.0.1 | |
| with: | |
| input: ./metabase.yml.tmpl | |
| output: ./metabase.yml | |
| env: | |
| IMAGE_TAG: ${{ github.event.pull_request.head.sha }} | |
| PR_NUMBER: ${{ github.event.number }} | |
| MB_PREMIUM_EMBEDDING_TOKEN: ${{ secrets.PR_ENV_MB_PREMIUM_EMBEDDING_TOKEN }} | |
| SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| MB_ANALYTICS_DEV_MODE: ${{ steps.label.outputs.analytics_dev_mode }} | |
| - name: Inject stats-dump env vars into Deployment YAML | |
| if: ${{ steps.label.outputs.self_hosting != 'true' && steps.label.outputs.stats_dump == 'true' }} | |
| env: | |
| MB_ENCRYPTION_SECRET_KEY: ${{ secrets.PR_ENV_STATS_DUMP_ENCRYPTION_KEY }} | |
| run: | | |
| yq -i 'select(.kind == "Metabase").spec.configuration.envVars += [ | |
| {"name": "MB_ENCRYPTION_SECRET_KEY", "value": strenv(MB_ENCRYPTION_SECRET_KEY)}, | |
| {"name": "MB_DISABLE_SCHEDULER", "value": "true"} | |
| ]' ./metabase.yml | |
| - name: Create OIDC Token | |
| id: create-oidc-token | |
| shell: bash | |
| run: | | |
| export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${PR_ENV_K8S_AUDIENCE}" | |
| IDTOKEN=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" -H "Accept: application/json; api-version=2.0" "$OIDC_URL_WITH_AUDIENCE" | jq -r .value) | |
| echo "::add-mask::${IDTOKEN}" | |
| echo "idToken=${IDTOKEN}" >>"$GITHUB_OUTPUT" | |
| env: | |
| PR_ENV_K8S_AUDIENCE: ${{ secrets.PR_ENV_K8S_AUDIENCE }} | |
| - name: Setup Kube Context | |
| uses: azure/k8s-set-context@89b837d75b40a7bd2ddafde837473c212db8b313 # v5.0.0 | |
| with: | |
| method: kubeconfig | |
| kubeconfig: | | |
| kind: Config | |
| apiVersion: v1 | |
| current-context: default | |
| clusters: | |
| - name: default | |
| cluster: | |
| certificate-authority-data: ${{ secrets.PR_ENV_K8S_CERTIFICATE_AUTHORITY_DATA }} | |
| server: ${{ secrets.PR_ENV_K8S_SERVER }} | |
| users: | |
| - name: oidc-token | |
| user: | |
| token: ${{ steps.create-oidc-token.outputs.IDTOKEN }} | |
| contexts: | |
| - name: default | |
| context: | |
| cluster: default | |
| namespace: default | |
| user: oidc-token | |
| - name: Deploy PR Review ENV | |
| if: ${{ steps.label.outputs.self_hosting != 'true' }} | |
| run: | | |
| kubectl apply -f ./metabase.yml | |
| - name: Download Helm values template (self-hosting) | |
| if: ${{ steps.label.outputs.self_hosting == 'true' }} | |
| run: aws s3 cp s3://metabase-pr-env/values.yaml.tmpl ./values.yaml.tmpl | |
| - name: Render Helm values (self-hosting) | |
| if: ${{ steps.label.outputs.self_hosting == 'true' }} | |
| uses: nowactions/envsubst@c4b8e3977354d294659a7ec164279ce8fba5c2e1 # v1.0.1 | |
| with: | |
| input: ./values.yaml.tmpl | |
| output: ./values.yaml | |
| env: | |
| IMAGE_TAG: ${{ github.event.pull_request.head.sha }} | |
| PR_NUMBER: ${{ github.event.number }} | |
| MB_PREMIUM_EMBEDDING_TOKEN: ${{ secrets.PR_ENV_MB_PREMIUM_EMBEDDING_TOKEN }} | |
| SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }} | |
| QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }} | |
| OPEN_AI_API_KEY: ${{ secrets.OPEN_AI_API_KEY }} | |
| DB_HOST: ${{ secrets.PR_ENV_DB_HOST }} | |
| DB_USERNAME: ${{ secrets.PR_ENV_DB_USER }} | |
| DB_PASSWORD: ${{ secrets.PR_ENV_DB_PASSWORD }} | |
| CONFIG: ${{ secrets.PR_ENV_CONFIG }} | |
| MB_ANALYTICS_DEV_MODE: ${{ steps.label.outputs.analytics_dev_mode }} | |
| - name: Setup Helm | |
| if: ${{ steps.label.outputs.self_hosting == 'true' }} | |
| uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0 | |
| with: | |
| version: "latest" | |
| - name: Login to Quay.io | |
| if: ${{ steps.label.outputs.self_hosting == 'true' }} | |
| run: | | |
| helm registry login quay.io \ | |
| --username "${QUAY_USERNAME}" \ | |
| --password "${QUAY_PASSWORD}" | |
| env: | |
| QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }} | |
| QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }} | |
| - name: Deploy PR Review ENV (self-hosting) | |
| if: ${{ steps.label.outputs.self_hosting == 'true' }} | |
| run: | | |
| helm upgrade --install hosting-pr${{ github.event.number }} \ | |
| oci://quay.io/enterprisemetabase/chart-metabase-stack \ | |
| --namespace hosting-pr${{ github.event.number }} \ | |
| --create-namespace \ | |
| --values ./values.yaml \ | |
| --atomic \ | |
| --timeout 10m | |
| preview_links: | |
| runs-on: ${{ vars.SLIM_RUNNER_KEY }} | |
| timeout-minutes: 5 | |
| needs: [deploy] | |
| steps: | |
| - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 | |
| with: | |
| recreate: true | |
| message: | | |
| 👋 Deploying a preview environment for commit ${{ github.event.pull_request.head.sha || github.sha }}. | |
| ✅ Preview: | |
| https://pr${{ github.event.number }}.coredev.metabase.com | |
| 🗒️ [login instructions](https://www.notion.so/metabase/Ephemeral-PR-Environments-c74a3710c87a460dbf4fc0007a001458) | |
| 📦 Deployment type: ${{ needs.deploy.outputs.self_hosting == 'true' && 'PR-Env-self-hosting' || (needs.deploy.outputs.analytics_dev_mode == 'true' && 'PR-Env-analytics-dev' || (needs.deploy.outputs.stats_dump == 'true' && 'PR-Env-Stats-Dump' || 'PR-Env')) }} |