Skip to content

Building Dynamic PR Environment for 83443/merge by @johnswanson #3932

Building Dynamic PR Environment for 83443/merge by @johnswanson

Building Dynamic PR Environment for 83443/merge by @johnswanson #3932

Workflow file for this run

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')) }}