Retire the paid status monitor and its deployment #1531
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: Modtale CI/CD | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - develop | |
| - '**' | |
| delete: | |
| workflow_dispatch: | |
| inputs: | |
| prepare_preview_source: | |
| type: boolean | |
| default: false | |
| required: false | |
| cleanup_branches: | |
| type: string | |
| default: '[]' | |
| required: false | |
| schedule: | |
| - cron: '17 3 * * *' | |
| env: | |
| PROJECT_ID: gen-lang-client-0244308719 | |
| REGION: us-central1 | |
| jobs: | |
| deploy: | |
| concurrency: | |
| group: modtale-deploy-${{ github.ref }} | |
| cancel-in-progress: false | |
| if: github.repository == 'Modtale/modtale' && github.event_name == 'push' && github.event.deleted != true | |
| runs-on: ubuntu-latest | |
| environment: ${{ github.ref_name == 'main' && 'production' || github.ref_name == 'develop' && 'develop' || 'branch-preview' }} | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_R2_PROVISIONER: ${{ secrets.CLOUDFLARE_R2_PROVISIONER }} | |
| CLOUDFLARE_API_TOKEN_PROVISIONER: ${{ secrets.CLOUDFLARE_API_TOKEN_PROVISIONER }} | |
| CLOUDFLARE_API_TOKEN_OWNER: ${{ vars.CLOUDFLARE_API_TOKEN_OWNER }} | |
| CLOUDFLARE_R2_JURISDICTION: ${{ vars.CLOUDFLARE_R2_JURISDICTION }} | |
| permissions: | |
| contents: 'read' | |
| actions: 'read' | |
| id-token: 'write' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve last successful deployment | |
| id: previous_deploy | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| previous=$(gh run list --workflow ci-cd.yml --branch "$GITHUB_REF_NAME" --event push --status success --limit 1 --json headSha --jq '.[0].headSha // empty') | |
| echo "sha=${previous:-${{ github.event.before }}}" >> "$GITHUB_OUTPUT" | |
| - name: Detect changed components | |
| id: filter | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PUSH_BEFORE_SHA: ${{ steps.previous_deploy.outputs.sha }} | |
| run: bash .github/scripts/detect-component-changes.sh | |
| - name: Configure Environment | |
| id: config | |
| run: | | |
| BRANCH_NAME=${GITHUB_REF#refs/heads/} | |
| echo "Detected branch: $BRANCH_NAME" | |
| BRANCH_SLUG=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9-]/-/g' | tr '[:upper:]' '[:lower:]' | sed 's/--*/-/g' | cut -c 1-20 | sed 's/^-//;s/-$//') | |
| if [ -z "$BRANCH_SLUG" ]; then | |
| BRANCH_SLUG="${GITHUB_SHA:0:7}" | |
| fi | |
| echo "GIT_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV | |
| echo "BRANCH_SLUG=$BRANCH_SLUG" >> $GITHUB_ENV | |
| if [ "$BRANCH_NAME" = "main" ]; then | |
| echo "ENV_TYPE=prod" >> $GITHUB_ENV | |
| echo "BUILD_SERVICE_ACCOUNT=${{ vars.GCP_BUILD_SERVICE_ACCOUNT }}" >> $GITHUB_ENV | |
| echo "RUNTIME_SERVICE_ACCOUNT=${{ vars.GCP_PROD_RUNTIME_SERVICE_ACCOUNT }}" >> $GITHUB_ENV | |
| echo "BACKEND_SERVICE=modtale-backend" >> $GITHUB_ENV | |
| echo "FRONTEND_SERVICE=modtale-frontend" >> $GITHUB_ENV | |
| echo "DB_NAME=modtale" >> $GITHUB_ENV | |
| echo "SEEDING_ENABLED=false" >> $GITHUB_ENV | |
| echo "SEEDING_MODE=mock" >> $GITHUB_ENV | |
| echo "SEEDING_RESET=false" >> $GITHUB_ENV | |
| echo "SEEDING_SOURCE_DB=modtale-mock-template" >> $GITHUB_ENV | |
| echo "SEEDING_SOURCE_R2_BUCKET_NAME=" >> $GITHUB_ENV | |
| echo "MONGODB_SECRET_NAME=MONGODB_URI" >> $GITHUB_ENV | |
| echo "BACKEND_DOMAIN=https://api.modtale.net" >> $GITHUB_ENV | |
| echo "FRONTEND_DOMAIN=https://modtale.net" >> $GITHUB_ENV | |
| echo "TAG=latest" >> $GITHUB_ENV | |
| echo "R2_BUCKET_NAME=modtale-binaries" >> $GITHUB_ENV | |
| echo "R2_ACCESS_KEY_SECRET_NAME=R2_ACCESS_KEY" >> $GITHUB_ENV | |
| echo "R2_SECRET_KEY_SECRET_NAME=R2_SECRET_KEY" >> $GITHUB_ENV | |
| echo "R2_ENDPOINT_SECRET_NAME=R2_ENDPOINT" >> $GITHUB_ENV | |
| echo "R2_PUBLIC_DOMAIN=https://cdn.modtale.net" >> $GITHUB_ENV | |
| echo "WARDEN_ENABLED=true" >> $GITHUB_ENV | |
| echo "OAUTH_ENABLED=true" >> $GITHUB_ENV | |
| echo "WARDEN_SECRET_NAME=WARDEN_API_KEY" >> $GITHUB_ENV | |
| elif [ "$BRANCH_NAME" = "develop" ]; then | |
| echo "ENV_TYPE=dev" >> $GITHUB_ENV | |
| echo "BUILD_SERVICE_ACCOUNT=${{ vars.GCP_BUILD_SERVICE_ACCOUNT }}" >> $GITHUB_ENV | |
| echo "RUNTIME_SERVICE_ACCOUNT=${{ vars.GCP_DEV_RUNTIME_SERVICE_ACCOUNT }}" >> $GITHUB_ENV | |
| echo "BACKEND_SERVICE=modtale-backend-dev" >> $GITHUB_ENV | |
| echo "FRONTEND_SERVICE=modtale-frontend-dev" >> $GITHUB_ENV | |
| echo "DB_NAME=modtale" >> $GITHUB_ENV | |
| echo "SEEDING_ENABLED=false" >> $GITHUB_ENV | |
| echo "SEEDING_MODE=mock" >> $GITHUB_ENV | |
| echo "SEEDING_RESET=false" >> $GITHUB_ENV | |
| echo "SEEDING_SOURCE_DB=modtale-mock-template" >> $GITHUB_ENV | |
| echo "SEEDING_SOURCE_R2_BUCKET_NAME=" >> $GITHUB_ENV | |
| echo "MONGODB_SECRET_NAME=MONGODB_URI" >> $GITHUB_ENV | |
| echo "BACKEND_DOMAIN=https://dev.api.modtale.net" >> $GITHUB_ENV | |
| echo "FRONTEND_DOMAIN=https://dev.modtale.net" >> $GITHUB_ENV | |
| echo "TAG=dev" >> $GITHUB_ENV | |
| echo "R2_BUCKET_NAME=modtale-binaries" >> $GITHUB_ENV | |
| echo "R2_ACCESS_KEY_SECRET_NAME=R2_ACCESS_KEY" >> $GITHUB_ENV | |
| echo "R2_SECRET_KEY_SECRET_NAME=R2_SECRET_KEY" >> $GITHUB_ENV | |
| echo "R2_ENDPOINT_SECRET_NAME=R2_ENDPOINT" >> $GITHUB_ENV | |
| echo "R2_PUBLIC_DOMAIN=https://cdn.modtale.net" >> $GITHUB_ENV | |
| echo "WARDEN_ENABLED=true" >> $GITHUB_ENV | |
| echo "OAUTH_ENABLED=true" >> $GITHUB_ENV | |
| echo "WARDEN_SECRET_NAME=WARDEN_API_KEY" >> $GITHUB_ENV | |
| else | |
| SLUG="$BRANCH_SLUG" | |
| BRANCH_PREVIEW_SOURCE_R2_BUCKET_NAME="${{ vars.GCP_BRANCH_PREVIEW_SOURCE_R2_BUCKET_NAME }}" | |
| if [ -z "$BRANCH_PREVIEW_SOURCE_R2_BUCKET_NAME" ]; then | |
| BRANCH_PREVIEW_SOURCE_R2_BUCKET_NAME="${{ vars.GCP_PREVIEW_SOURCE_R2_BUCKET_NAME }}" | |
| fi | |
| if [ -z "$BRANCH_PREVIEW_SOURCE_R2_BUCKET_NAME" ]; then | |
| BRANCH_PREVIEW_SOURCE_R2_BUCKET_NAME="modtale-preview-template" | |
| fi | |
| BRANCH_PREVIEW_SOURCE_R2_ACCESS_KEY_SECRET_NAME="${{ vars.GCP_BRANCH_PREVIEW_SOURCE_R2_ACCESS_KEY_SECRET_NAME }}" | |
| if [ -z "$BRANCH_PREVIEW_SOURCE_R2_ACCESS_KEY_SECRET_NAME" ]; then | |
| BRANCH_PREVIEW_SOURCE_R2_ACCESS_KEY_SECRET_NAME="${{ vars.GCP_PREVIEW_SOURCE_R2_ACCESS_KEY_SECRET_NAME }}" | |
| fi | |
| if [ -z "$BRANCH_PREVIEW_SOURCE_R2_ACCESS_KEY_SECRET_NAME" ]; then | |
| BRANCH_PREVIEW_SOURCE_R2_ACCESS_KEY_SECRET_NAME="R2_TEMPLATE_READ_ACCESS_KEY" | |
| fi | |
| BRANCH_PREVIEW_SOURCE_R2_SECRET_KEY_SECRET_NAME="${{ vars.GCP_BRANCH_PREVIEW_SOURCE_R2_SECRET_KEY_SECRET_NAME }}" | |
| if [ -z "$BRANCH_PREVIEW_SOURCE_R2_SECRET_KEY_SECRET_NAME" ]; then | |
| BRANCH_PREVIEW_SOURCE_R2_SECRET_KEY_SECRET_NAME="${{ vars.GCP_PREVIEW_SOURCE_R2_SECRET_KEY_SECRET_NAME }}" | |
| fi | |
| if [ -z "$BRANCH_PREVIEW_SOURCE_R2_SECRET_KEY_SECRET_NAME" ]; then | |
| BRANCH_PREVIEW_SOURCE_R2_SECRET_KEY_SECRET_NAME="R2_TEMPLATE_READ_SECRET_KEY" | |
| fi | |
| BRANCH_PREVIEW_SOURCE_R2_ENDPOINT_SECRET_NAME="${{ vars.GCP_BRANCH_PREVIEW_SOURCE_R2_ENDPOINT_SECRET_NAME }}" | |
| if [ -z "$BRANCH_PREVIEW_SOURCE_R2_ENDPOINT_SECRET_NAME" ]; then | |
| BRANCH_PREVIEW_SOURCE_R2_ENDPOINT_SECRET_NAME="${{ vars.GCP_PREVIEW_SOURCE_R2_ENDPOINT_SECRET_NAME }}" | |
| fi | |
| if [ -z "$BRANCH_PREVIEW_SOURCE_R2_ENDPOINT_SECRET_NAME" ]; then | |
| BRANCH_PREVIEW_SOURCE_R2_ENDPOINT_SECRET_NAME="R2_TEMPLATE_READ_ENDPOINT" | |
| fi | |
| echo "ENV_TYPE=preview" >> $GITHUB_ENV | |
| echo "BUILD_SERVICE_ACCOUNT=${{ vars.GCP_BUILD_SERVICE_ACCOUNT }}" >> $GITHUB_ENV | |
| echo "BACKEND_SERVICE=modtale-backend-$SLUG" >> $GITHUB_ENV | |
| echo "FRONTEND_SERVICE=modtale-frontend-$SLUG" >> $GITHUB_ENV | |
| echo "DB_NAME=modtale-$SLUG" >> $GITHUB_ENV | |
| echo "SEEDING_ENABLED=true" >> $GITHUB_ENV | |
| echo "SEEDING_MODE=template" >> $GITHUB_ENV | |
| echo "SEEDING_RESET=false" >> $GITHUB_ENV | |
| echo "SEEDING_SOURCE_DB=modtale-mock-template" >> $GITHUB_ENV | |
| echo "SEEDING_SOURCE_R2_BUCKET_NAME=$BRANCH_PREVIEW_SOURCE_R2_BUCKET_NAME" >> $GITHUB_ENV | |
| echo "MONGODB_SECRET_NAME=BRANCH_PREVIEW_MONGODB_URI" >> $GITHUB_ENV | |
| echo "TAG=$SLUG" >> $GITHUB_ENV | |
| echo "R2_BUCKET_NAME=modtale-branch-$SLUG" >> $GITHUB_ENV | |
| echo "R2_ACCESS_KEY_SECRET_NAME=branch-preview-$SLUG-r2-access-key" >> $GITHUB_ENV | |
| echo "R2_SECRET_KEY_SECRET_NAME=branch-preview-$SLUG-r2-secret-key" >> $GITHUB_ENV | |
| echo "R2_ENDPOINT_SECRET_NAME=branch-preview-$SLUG-r2-endpoint" >> $GITHUB_ENV | |
| echo "R2_TOKEN_ID_SECRET_NAME=branch-preview-$SLUG-r2-token-id" >> $GITHUB_ENV | |
| echo "R2_TOKEN_NAME=modtale-branch-$SLUG-runtime" >> $GITHUB_ENV | |
| echo "RUNTIME_SERVICE_ACCOUNT=${{ vars.GCP_BRANCH_PREVIEW_RUNTIME_SERVICE_ACCOUNT }}" >> $GITHUB_ENV | |
| echo "SEEDING_SOURCE_R2_ACCESS_KEY_SECRET_NAME=$BRANCH_PREVIEW_SOURCE_R2_ACCESS_KEY_SECRET_NAME" >> $GITHUB_ENV | |
| echo "SEEDING_SOURCE_R2_SECRET_KEY_SECRET_NAME=$BRANCH_PREVIEW_SOURCE_R2_SECRET_KEY_SECRET_NAME" >> $GITHUB_ENV | |
| echo "SEEDING_SOURCE_R2_ENDPOINT_SECRET_NAME=$BRANCH_PREVIEW_SOURCE_R2_ENDPOINT_SECRET_NAME" >> $GITHUB_ENV | |
| echo "R2_PUBLIC_DOMAIN=" >> $GITHUB_ENV | |
| echo "WARDEN_ENABLED=false" >> $GITHUB_ENV | |
| echo "OAUTH_ENABLED=false" >> $GITHUB_ENV | |
| fi | |
| - id: 'auth' | |
| uses: 'google-github-actions/auth@v2' | |
| with: | |
| credentials_json: '${{ secrets.GCP_CREDENTIALS }}' | |
| - name: 'Set up Cloud SDK' | |
| uses: 'google-github-actions/setup-gcloud@v2' | |
| with: | |
| install_components: 'beta' | |
| - name: Ensure Branch R2 Bucket | |
| if: env.ENV_TYPE == 'preview' | |
| run: | | |
| set -euo pipefail | |
| : "${R2_BUCKET_NAME:?R2_BUCKET_NAME must be configured for preview branch deployments.}" | |
| : "${CLOUDFLARE_ACCOUNT_ID:?Set secret CLOUDFLARE_ACCOUNT_ID.}" | |
| : "${CLOUDFLARE_R2_PROVISIONER:?Set secret CLOUDFLARE_R2_PROVISIONER.}" | |
| : "${RUNTIME_SERVICE_ACCOUNT:?Set environment variable GCP_BRANCH_PREVIEW_RUNTIME_SERVICE_ACCOUNT in the branch-preview GitHub environment.}" | |
| mask_secret() { | |
| local value="${1:-}" | |
| if [ -n "$value" ]; then | |
| echo "::add-mask::$value" | |
| fi | |
| } | |
| if [ "${MONGODB_SECRET_NAME:-}" = "MONGODB_URI" ]; then | |
| echo "::error::Branch previews must use BRANCH_PREVIEW_MONGODB_URI, not production MONGODB_URI." | |
| exit 1 | |
| fi | |
| if [ "$SEEDING_SOURCE_R2_BUCKET_NAME" = "$R2_BUCKET_NAME" ]; then | |
| echo "::error::Branch previews must seed from a source R2 bucket that differs from their per-branch target bucket." | |
| exit 1 | |
| fi | |
| if [ "$SEEDING_SOURCE_R2_BUCKET_NAME" = "modtale-binaries" ]; then | |
| echo "::warning::Branch preview is seeding from modtale-binaries. Set GCP_BRANCH_PREVIEW_SOURCE_R2_BUCKET_NAME and matching source secret-name vars to use a sanitized preview source bucket." | |
| fi | |
| source_r2_access_key="$(gcloud secrets versions access latest --project "$PROJECT_ID" --secret="$SEEDING_SOURCE_R2_ACCESS_KEY_SECRET_NAME")" | |
| source_r2_secret_key="$(gcloud secrets versions access latest --project "$PROJECT_ID" --secret="$SEEDING_SOURCE_R2_SECRET_KEY_SECRET_NAME")" | |
| source_r2_endpoint="$(gcloud secrets versions access latest --project "$PROJECT_ID" --secret="$SEEDING_SOURCE_R2_ENDPOINT_SECRET_NAME")" | |
| source_r2_endpoint="$(printf '%s' "$source_r2_endpoint" | sed -E 's#(https?://[^/]+).*#\1#')" | |
| mask_secret "$source_r2_access_key" | |
| mask_secret "$source_r2_secret_key" | |
| if ! command -v aws >/dev/null 2>&1; then | |
| sudo apt-get update | |
| sudo apt-get install -y awscli | |
| fi | |
| AWS_ACCESS_KEY_ID="$source_r2_access_key" \ | |
| AWS_SECRET_ACCESS_KEY="$source_r2_secret_key" \ | |
| AWS_DEFAULT_REGION=auto \ | |
| aws s3api head-bucket \ | |
| --endpoint-url "$source_r2_endpoint" \ | |
| --bucket "$SEEDING_SOURCE_R2_BUCKET_NAME" >/dev/null | |
| existing_token_id="$( | |
| gcloud secrets versions access latest \ | |
| --project "$PROJECT_ID" \ | |
| --secret="$R2_TOKEN_ID_SECRET_NAME" 2>/dev/null || true | |
| )" | |
| mask_secret "$existing_token_id" | |
| latest_secret_version() { | |
| local name="$1" | |
| gcloud secrets versions list "$name" \ | |
| --project "$PROJECT_ID" \ | |
| --filter="state=ENABLED" \ | |
| --sort-by="~createTime" \ | |
| --limit=1 \ | |
| --format="value(name)" | |
| } | |
| ensure_secret_accessor() { | |
| local name="$1" | |
| gcloud secrets add-iam-policy-binding "$name" \ | |
| --project "$PROJECT_ID" \ | |
| --member="serviceAccount:$RUNTIME_SERVICE_ACCOUNT" \ | |
| --role="roles/secretmanager.secretAccessor" \ | |
| --quiet >/dev/null | |
| } | |
| write_secret() { | |
| local name="$1" | |
| local value="$2" | |
| if gcloud secrets describe "$name" --project "$PROJECT_ID" >/dev/null 2>&1; then | |
| printf '%s' "$value" | gcloud secrets versions add "$name" \ | |
| --project "$PROJECT_ID" \ | |
| --data-file=- >/dev/null | |
| else | |
| printf '%s' "$value" | gcloud secrets create "$name" \ | |
| --project "$PROJECT_ID" \ | |
| --replication-policy=automatic \ | |
| --data-file=- >/dev/null | |
| fi | |
| ensure_secret_accessor "$name" | |
| latest_secret_version "$name" | |
| } | |
| existing_r2_access_key="$( | |
| gcloud secrets versions access latest \ | |
| --project "$PROJECT_ID" \ | |
| --secret="$R2_ACCESS_KEY_SECRET_NAME" 2>/dev/null || true | |
| )" | |
| existing_r2_secret_key="$( | |
| gcloud secrets versions access latest \ | |
| --project "$PROJECT_ID" \ | |
| --secret="$R2_SECRET_KEY_SECRET_NAME" 2>/dev/null || true | |
| )" | |
| existing_r2_endpoint="$( | |
| gcloud secrets versions access latest \ | |
| --project "$PROJECT_ID" \ | |
| --secret="$R2_ENDPOINT_SECRET_NAME" 2>/dev/null || true | |
| )" | |
| existing_r2_endpoint="$(printf '%s' "$existing_r2_endpoint" | sed -E 's#(https?://[^/]+).*#\1#')" | |
| mask_secret "$existing_r2_access_key" | |
| mask_secret "$existing_r2_secret_key" | |
| runtime_credentials_valid=false | |
| if [ -n "$existing_r2_access_key" ] && \ | |
| [ -n "$existing_r2_secret_key" ] && \ | |
| [ -n "$existing_r2_endpoint" ]; then | |
| check_file="$(mktemp)" | |
| check_key=".modtale-preview/credential-check-${GITHUB_RUN_ID:-manual}-${GITHUB_RUN_ATTEMPT:-0}" | |
| printf 'ok' > "$check_file" | |
| if AWS_ACCESS_KEY_ID="$existing_r2_access_key" \ | |
| AWS_SECRET_ACCESS_KEY="$existing_r2_secret_key" \ | |
| AWS_DEFAULT_REGION=auto \ | |
| aws s3api put-object \ | |
| --endpoint-url "$existing_r2_endpoint" \ | |
| --bucket "$R2_BUCKET_NAME" \ | |
| --key "$check_key" \ | |
| --body "$check_file" >/dev/null && \ | |
| AWS_ACCESS_KEY_ID="$existing_r2_access_key" \ | |
| AWS_SECRET_ACCESS_KEY="$existing_r2_secret_key" \ | |
| AWS_DEFAULT_REGION=auto \ | |
| aws s3api head-object \ | |
| --endpoint-url "$existing_r2_endpoint" \ | |
| --bucket "$R2_BUCKET_NAME" \ | |
| --key "$check_key" >/dev/null; then | |
| runtime_credentials_valid=true | |
| fi | |
| AWS_ACCESS_KEY_ID="$existing_r2_access_key" \ | |
| AWS_SECRET_ACCESS_KEY="$existing_r2_secret_key" \ | |
| AWS_DEFAULT_REGION=auto \ | |
| aws s3api delete-object \ | |
| --endpoint-url "$existing_r2_endpoint" \ | |
| --bucket "$R2_BUCKET_NAME" \ | |
| --key "$check_key" >/dev/null 2>&1 || true | |
| rm -f "$check_file" | |
| fi | |
| if [ "$runtime_credentials_valid" = "true" ]; then | |
| echo "Reusing existing branch preview R2 runtime credentials." | |
| ensure_secret_accessor "$R2_ACCESS_KEY_SECRET_NAME" | |
| ensure_secret_accessor "$R2_SECRET_KEY_SECRET_NAME" | |
| ensure_secret_accessor "$R2_ENDPOINT_SECRET_NAME" | |
| if gcloud secrets describe "$R2_TOKEN_ID_SECRET_NAME" --project "$PROJECT_ID" >/dev/null 2>&1; then | |
| ensure_secret_accessor "$R2_TOKEN_ID_SECRET_NAME" | |
| R2_TOKEN_ID_SECRET_VERSION="$(latest_secret_version "$R2_TOKEN_ID_SECRET_NAME")" | |
| else | |
| R2_TOKEN_ID_SECRET_VERSION="" | |
| fi | |
| R2_ACCESS_KEY_SECRET_VERSION="$(latest_secret_version "$R2_ACCESS_KEY_SECRET_NAME")" | |
| R2_SECRET_KEY_SECRET_VERSION="$(latest_secret_version "$R2_SECRET_KEY_SECRET_NAME")" | |
| R2_ENDPOINT_SECRET_VERSION="$(latest_secret_version "$R2_ENDPOINT_SECRET_NAME")" | |
| R2_RUNTIME_CREDENTIALS_REPLACED=false | |
| else | |
| echo "Existing branch preview R2 runtime credentials are missing or invalid; provisioning a replacement." | |
| : "${CLOUDFLARE_API_TOKEN_PROVISIONER:?Set secret CLOUDFLARE_API_TOKEN_PROVISIONER.}" | |
| export EXISTING_R2_RUNTIME_TOKEN_ID="$existing_token_id" | |
| runtime_env_file="$(mktemp)" | |
| trap 'rm -f "$runtime_env_file"' EXIT | |
| export R2_RUNTIME_ENV_FILE="$runtime_env_file" | |
| node .github/scripts/cloudflare-r2-preview.mjs provision | |
| set -a | |
| . "$runtime_env_file" | |
| set +a | |
| R2_ACCESS_KEY_SECRET_VERSION="$(write_secret "$R2_ACCESS_KEY_SECRET_NAME" "$R2_RUNTIME_ACCESS_KEY")" | |
| R2_SECRET_KEY_SECRET_VERSION="$(write_secret "$R2_SECRET_KEY_SECRET_NAME" "$R2_RUNTIME_SECRET_KEY")" | |
| R2_ENDPOINT_SECRET_VERSION="$(write_secret "$R2_ENDPOINT_SECRET_NAME" "$R2_RUNTIME_ENDPOINT")" | |
| R2_TOKEN_ID_SECRET_VERSION="$(write_secret "$R2_TOKEN_ID_SECRET_NAME" "$R2_RUNTIME_TOKEN_ID")" | |
| R2_RUNTIME_CREDENTIALS_REPLACED=true | |
| fi | |
| echo "R2_ACCESS_KEY_SECRET_VERSION=$R2_ACCESS_KEY_SECRET_VERSION" >> "$GITHUB_ENV" | |
| echo "R2_SECRET_KEY_SECRET_VERSION=$R2_SECRET_KEY_SECRET_VERSION" >> "$GITHUB_ENV" | |
| echo "R2_ENDPOINT_SECRET_VERSION=$R2_ENDPOINT_SECRET_VERSION" >> "$GITHUB_ENV" | |
| echo "R2_TOKEN_ID_SECRET_VERSION=$R2_TOKEN_ID_SECRET_VERSION" >> "$GITHUB_ENV" | |
| echo "R2_RUNTIME_CREDENTIALS_REPLACED=$R2_RUNTIME_CREDENTIALS_REPLACED" >> "$GITHUB_ENV" | |
| - name: Copy missing preview fixtures on the GitHub runner | |
| if: env.ENV_TYPE == 'preview' | |
| run: python3 .github/scripts/sync-preview-r2.py | |
| - name: Check Existing Services | |
| id: check_services | |
| run: | | |
| # Check backend service | |
| if gcloud run services describe ${{ env.BACKEND_SERVICE }} --region ${{ env.REGION }} >/dev/null 2>&1; then | |
| echo "BACKEND_EXISTS=true" >> $GITHUB_ENV | |
| B_URL=$(gcloud run services describe ${{ env.BACKEND_SERVICE }} --region ${{ env.REGION }} --format="value(status.url)") | |
| echo "EXISTING_BACKEND_URL=$B_URL" >> $GITHUB_ENV | |
| echo "Backend service already exists: $B_URL" | |
| else | |
| echo "BACKEND_EXISTS=false" >> $GITHUB_ENV | |
| echo "Backend service does not exist." | |
| fi | |
| # Check frontend service | |
| if gcloud run services describe ${{ env.FRONTEND_SERVICE }} --region ${{ env.REGION }} >/dev/null 2>&1; then | |
| echo "FRONTEND_EXISTS=true" >> $GITHUB_ENV | |
| F_URL=$(gcloud run services describe ${{ env.FRONTEND_SERVICE }} --region ${{ env.REGION }} --format="value(status.url)") | |
| echo "EXISTING_FRONTEND_URL=$F_URL" >> $GITHUB_ENV | |
| echo "Frontend service already exists: $F_URL" | |
| else | |
| echo "FRONTEND_EXISTS=false" >> $GITHUB_ENV | |
| echo "Frontend service does not exist." | |
| fi | |
| - name: Build and Deploy Backend | |
| id: deploy_backend | |
| if: steps.filter.outputs.backend == 'true' || env.BACKEND_EXISTS == 'false' || env.R2_RUNTIME_CREDENTIALS_REPLACED == 'true' | |
| run: | | |
| cd backend | |
| : "${BUILD_SERVICE_ACCOUNT:?Set GitHub environment variable GCP_BUILD_SERVICE_ACCOUNT.}" | |
| : "${RUNTIME_SERVICE_ACCOUNT:?Set the GitHub environment runtime service account variable for this environment.}" | |
| WARDEN_URL_VALUE="" | |
| if [ "${{ env.ENV_TYPE }}" = "prod" ] || [ "${{ env.ENV_TYPE }}" = "dev" ]; then | |
| WARDEN_URL_VALUE="$( | |
| gcloud secrets versions access latest \ | |
| --project "$PROJECT_ID" \ | |
| --secret WARDEN_URL | |
| )" | |
| if [ -z "$WARDEN_URL_VALUE" ]; then | |
| echo "::error::Secret WARDEN_URL is empty or unavailable." | |
| exit 1 | |
| fi | |
| echo "::add-mask::$WARDEN_URL_VALUE" | |
| fi | |
| # Rotating preview credentials needs a new Cloud Run revision, but the | |
| # application image is unchanged. Avoid paying to compile it again. | |
| if [ "${{ steps.filter.outputs.backend }}" = "true" ] || [ "$BACKEND_EXISTS" = "false" ]; then | |
| bash ../.github/scripts/build-container.sh backend "$TAG" | |
| else | |
| echo "Reusing the existing backend image for a configuration-only rollout." | |
| fi | |
| ARGS=( | |
| "--image" "gcr.io/$PROJECT_ID/modtale-backend:${{ env.TAG }}" | |
| "--region" "$REGION" | |
| "--service-account" "$RUNTIME_SERVICE_ACCOUNT" | |
| "--allow-unauthenticated" | |
| "--use-http2" | |
| "--memory" "1Gi" | |
| "--concurrency" "80" | |
| "--update-env-vars" "MONGODB_DATABASE_NAME=${{ env.DB_NAME }}" | |
| "--update-env-vars" "APP_SEEDING_ENABLED=${{ env.SEEDING_ENABLED }}" | |
| "--update-env-vars" "APP_SEEDING_MODE=${{ env.SEEDING_MODE }}" | |
| "--update-env-vars" "APP_SEEDING_RESET=${{ env.SEEDING_RESET }}" | |
| "--update-env-vars" "APP_SEEDING_SOURCE_DB=${{ env.SEEDING_SOURCE_DB }}" | |
| "--update-env-vars" "APP_SEEDING_SOURCE_R2_BUCKET_NAME=${{ env.SEEDING_SOURCE_R2_BUCKET_NAME }}" | |
| "--update-env-vars" "WARDEN_ENABLED=${{ env.WARDEN_ENABLED }}" | |
| "--update-env-vars" "OAUTH_ENABLED=${{ env.OAUTH_ENABLED }}" | |
| "--update-env-vars" "FRONTEND_URL=${FRONTEND_DOMAIN:-placeholder}" | |
| "--update-env-vars" "BACKEND_URL=${BACKEND_DOMAIN:-placeholder}" | |
| "--update-env-vars" "R2_BUCKET_NAME=${{ env.R2_BUCKET_NAME }}" | |
| "--update-env-vars" "R2_PUBLIC_DOMAIN=${{ env.R2_PUBLIC_DOMAIN }}" | |
| ) | |
| R2_ACCESS_KEY_SECRET_VERSION="${R2_ACCESS_KEY_SECRET_VERSION:-latest}" | |
| R2_SECRET_KEY_SECRET_VERSION="${R2_SECRET_KEY_SECRET_VERSION:-latest}" | |
| R2_ENDPOINT_SECRET_VERSION="${R2_ENDPOINT_SECRET_VERSION:-latest}" | |
| ARGS+=("--update-secrets" "MONGODB_URI=${{ env.MONGODB_SECRET_NAME }}:latest") | |
| ARGS+=("--update-secrets" "R2_ACCESS_KEY=${{ env.R2_ACCESS_KEY_SECRET_NAME }}:$R2_ACCESS_KEY_SECRET_VERSION") | |
| ARGS+=("--update-secrets" "R2_SECRET_KEY=${{ env.R2_SECRET_KEY_SECRET_NAME }}:$R2_SECRET_KEY_SECRET_VERSION") | |
| ARGS+=("--update-secrets" "R2_ENDPOINT=${{ env.R2_ENDPOINT_SECRET_NAME }}:$R2_ENDPOINT_SECRET_VERSION") | |
| if [ "${{ env.ENV_TYPE }}" = "prod" ]; then | |
| # Keep one production instance warm to prevent user-facing cold starts. | |
| ARGS+=("--cpu" "1") | |
| ARGS+=("--min-instances" "1") | |
| ARGS+=("--max-instances" "10") | |
| ARGS+=("--cpu-throttling") | |
| ARGS+=("--update-env-vars" "WARDEN_URL=$WARDEN_URL_VALUE") | |
| ARGS+=("--update-secrets" "SMTP_HOST=SMTP_HOST:latest") | |
| ARGS+=("--update-secrets" "SMTP_USERNAME=SMTP_USERNAME:latest") | |
| ARGS+=("--update-secrets" "SMTP_PASSWORD=SMTP_PASSWORD:latest") | |
| ARGS+=("--update-secrets" "PRE_AUTH_SECRET=PRE_AUTH_SECRET:latest") | |
| ARGS+=("--update-secrets" "GITHUB_CLIENT_ID=GITHUB_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "GITHUB_CLIENT_SECRET=GITHUB_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "GOOGLE_CLIENT_ID=GOOGLE_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "GOOGLE_CLIENT_SECRET=GOOGLE_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "GITLAB_CLIENT_ID=GITLAB_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "GITLAB_CLIENT_SECRET=GITLAB_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "DISCORD_CLIENT_ID=DISCORD_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "DISCORD_CLIENT_SECRET=DISCORD_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "TWITTER_CLIENT_ID=TWITTER_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "TWITTER_CLIENT_SECRET=TWITTER_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "BLUESKY_CLIENT_ID=BLUESKY_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "WARDEN_API_KEY=${{ env.WARDEN_SECRET_NAME }}:latest") | |
| ARGS+=("--update-secrets" "WEBHOOK_URL=WEBHOOK_URL:latest") | |
| ARGS+=("--update-secrets" "HYTALEMODDING_KEY=HYTALEMODDING_KEY:latest") | |
| ARGS+=("--update-secrets" "WIKI_API_KEY=WIKI_API_KEY:latest") | |
| ARGS+=("--update-secrets" "DISCORD_WEBHOOK_URL=DISCORD_WEBHOOK_URL:latest") | |
| ARGS+=("--update-secrets" "ADMIN_DISCORD_WEBHOOK_URL=ADMIN_DISCORD_WEBHOOK_URL:latest") | |
| # Status monitoring has been retired. | |
| ARGS+=("--remove-secrets" "STATUS_DISCORD_WEBHOOK_URL") | |
| elif [ "${{ env.ENV_TYPE }}" = "dev" ]; then | |
| ARGS+=("--cpu" "1") | |
| ARGS+=("--min-instances" "0") | |
| ARGS+=("--max-instances" "3") | |
| ARGS+=("--cpu-throttling") | |
| ARGS+=("--update-env-vars" "WARDEN_URL=$WARDEN_URL_VALUE") | |
| ARGS+=("--update-secrets" "SMTP_HOST=SMTP_HOST:latest") | |
| ARGS+=("--update-secrets" "SMTP_USERNAME=SMTP_USERNAME:latest") | |
| ARGS+=("--update-secrets" "SMTP_PASSWORD=SMTP_PASSWORD:latest") | |
| ARGS+=("--update-secrets" "PRE_AUTH_SECRET=PRE_AUTH_SECRET:latest") | |
| ARGS+=("--update-secrets" "GITHUB_CLIENT_ID=GITHUB_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "GITHUB_CLIENT_SECRET=GITHUB_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "GOOGLE_CLIENT_ID=GOOGLE_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "GOOGLE_CLIENT_SECRET=GOOGLE_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "GITLAB_CLIENT_ID=GITLAB_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "GITLAB_CLIENT_SECRET=GITLAB_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "DISCORD_CLIENT_ID=DISCORD_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "DISCORD_CLIENT_SECRET=DISCORD_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "TWITTER_CLIENT_ID=TWITTER_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "TWITTER_CLIENT_SECRET=TWITTER_CLIENT_SECRET:latest") | |
| ARGS+=("--update-secrets" "BLUESKY_CLIENT_ID=BLUESKY_CLIENT_ID:latest") | |
| ARGS+=("--update-secrets" "WARDEN_API_KEY=${{ env.WARDEN_SECRET_NAME }}:latest") | |
| ARGS+=("--update-secrets" "WIKI_API_KEY=WIKI_API_KEY:latest") | |
| ARGS+=("--remove-secrets" "STATUS_DISCORD_WEBHOOK_URL") | |
| else | |
| : "${RUNTIME_SERVICE_ACCOUNT:?Set environment variable GCP_BRANCH_PREVIEW_RUNTIME_SERVICE_ACCOUNT in the branch-preview GitHub environment.}" | |
| # Preview services are usually idle. Request-based CPU billing avoids | |
| # charging for CPU between requests without changing request capacity. | |
| ARGS+=("--cpu" "1") | |
| ARGS+=("--min-instances" "0") | |
| ARGS+=("--max-instances" "2") | |
| ARGS+=("--cpu-throttling") | |
| ARGS+=("--startup-probe" "httpGet.path=/actuator/health/readiness,httpGet.port=8080,periodSeconds=10,timeoutSeconds=10,failureThreshold=60") | |
| ARGS+=("--update-env-vars" "LOGGING_STRUCTURED_FORMAT_CONSOLE=logstash") | |
| ARGS+=("--update-env-vars" "LOGGING_STRUCTURED_JSON_RENAME_LEVEL=severity") | |
| ARGS+=("--update-env-vars" "LOGGING_STRUCTURED_JSON_ADD_SERVICE_NAME=${{ env.BACKEND_SERVICE }}") | |
| ARGS+=("--update-env-vars" "LOGGING_STRUCTURED_JSON_ADD_COMPONENT=backend") | |
| ARGS+=("--update-env-vars" "LOGGING_STRUCTURED_JSON_ADD_ENVIRONMENT=branch-preview") | |
| ARGS+=("--update-env-vars" "LOGGING_STRUCTURED_JSON_ADD_BRANCH=${{ env.BRANCH_SLUG }}") | |
| ARGS+=("--labels" "app=modtale,environment=branch-preview,component=backend,branch=${{ env.BRANCH_SLUG }}") | |
| ARGS+=("--update-secrets" "APP_SEEDING_SOURCE_R2_ACCESS_KEY=${{ env.SEEDING_SOURCE_R2_ACCESS_KEY_SECRET_NAME }}:latest") | |
| ARGS+=("--update-secrets" "APP_SEEDING_SOURCE_R2_SECRET_KEY=${{ env.SEEDING_SOURCE_R2_SECRET_KEY_SECRET_NAME }}:latest") | |
| ARGS+=("--update-secrets" "APP_SEEDING_SOURCE_R2_ENDPOINT=${{ env.SEEDING_SOURCE_R2_ENDPOINT_SECRET_NAME }}:latest") | |
| ARGS+=("--remove-env-vars" "SMTP_HOST,SMTP_PORT,SMTP_USERNAME,SMTP_PASSWORD,PRE_AUTH_SECRET,GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GITLAB_CLIENT_ID,GITLAB_CLIENT_SECRET,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,TWITTER_CLIENT_ID,TWITTER_CLIENT_SECRET,BLUESKY_CLIENT_ID,BLUESKY_CLIENT_SECRET,WARDEN_URL,WARDEN_API_KEY,WIKI_API_KEY,WEBHOOK_URL,HYTALEMODDING_KEY,DISCORD_WEBHOOK_URL,ADMIN_DISCORD_WEBHOOK_URL") | |
| ARGS+=("--remove-secrets" "STATUS_DISCORD_WEBHOOK_URL") | |
| fi | |
| gcloud run deploy ${{ env.BACKEND_SERVICE }} "${ARGS[@]}" --format="value(status.url)" > backend_url.txt | |
| if [ "${{ env.ENV_TYPE }}" = "prod" ]; then | |
| BACKEND_REVISION=$(gcloud run services describe "$BACKEND_SERVICE" --region "$REGION" --format="value(status.latestReadyRevisionName)") | |
| echo "revision=$BACKEND_REVISION" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Determine API URL | |
| run: | | |
| if [ -f backend/backend_url.txt ]; then | |
| BACKEND_SERVICE_URL=$(cat backend/backend_url.txt) | |
| else | |
| BACKEND_SERVICE_URL=$EXISTING_BACKEND_URL | |
| fi | |
| : "${BACKEND_SERVICE_URL:?Backend service URL was not resolved.}" | |
| if [ "$ENV_TYPE" = prod ]; then | |
| echo "SSR_API_URL=$BACKEND_DOMAIN/api/v1" >> "$GITHUB_ENV" | |
| else | |
| echo "SSR_API_URL=$BACKEND_SERVICE_URL/api/v1" >> "$GITHUB_ENV" | |
| fi | |
| if [ -n "${BACKEND_DOMAIN:-}" ]; then | |
| echo "API_URL=$BACKEND_DOMAIN/api/v1" >> $GITHUB_ENV | |
| else | |
| echo "API_URL=$BACKEND_SERVICE_URL/api/v1" >> $GITHUB_ENV | |
| fi | |
| - name: Build and Deploy Frontend | |
| id: deploy_frontend | |
| if: steps.filter.outputs.frontend == 'true' || env.FRONTEND_EXISTS == 'false' | |
| run: | | |
| cd frontend | |
| : "${BUILD_SERVICE_ACCOUNT:?Set GitHub environment variable GCP_BUILD_SERVICE_ACCOUNT.}" | |
| : "${RUNTIME_SERVICE_ACCOUNT:?Set the GitHub environment runtime service account variable for this environment.}" | |
| bash ../.github/scripts/build-container.sh frontend "$TAG" \ | |
| --build-arg "PUBLIC_API_URL=$API_URL" --build-arg "SSR_API_URL=$SSR_API_URL" | |
| FRONTEND_ARGS=( | |
| "--image" "gcr.io/$PROJECT_ID/modtale-frontend:$TAG" | |
| "--region" "$REGION" | |
| "--service-account" "$RUNTIME_SERVICE_ACCOUNT" | |
| "--allow-unauthenticated" | |
| "--cpu" "1" | |
| "--memory" "256Mi" | |
| "--concurrency" "80" | |
| "--cpu-throttling" | |
| "--set-env-vars" "PUBLIC_API_URL=$API_URL" | |
| ) | |
| if [ "${{ env.ENV_TYPE }}" = "preview" ]; then | |
| FRONTEND_ARGS+=("--min-instances" "0") | |
| FRONTEND_ARGS+=("--max-instances" "2") | |
| FRONTEND_ARGS+=("--set-env-vars" "LOG_SERVICE_NAME=${{ env.FRONTEND_SERVICE }}") | |
| FRONTEND_ARGS+=("--set-env-vars" "LOG_COMPONENT=frontend") | |
| FRONTEND_ARGS+=("--set-env-vars" "LOG_ENVIRONMENT=branch-preview") | |
| FRONTEND_ARGS+=("--set-env-vars" "LOG_BRANCH=${{ env.BRANCH_SLUG }}") | |
| FRONTEND_ARGS+=("--labels" "app=modtale,environment=branch-preview,component=frontend,branch=${{ env.BRANCH_SLUG }}") | |
| elif [ "${{ env.ENV_TYPE }}" = "dev" ]; then | |
| FRONTEND_ARGS+=("--min-instances" "0") | |
| FRONTEND_ARGS+=("--max-instances" "3") | |
| else | |
| FRONTEND_ARGS+=("--min-instances" "1") | |
| FRONTEND_ARGS+=("--max-instances" "3") | |
| fi | |
| gcloud run deploy "$FRONTEND_SERVICE" "${FRONTEND_ARGS[@]}" --format="value(status.url)" > frontend_url.txt | |
| if [ "${{ env.ENV_TYPE }}" = "prod" ]; then | |
| FRONTEND_REVISION=$(gcloud run services describe "$FRONTEND_SERVICE" --region "$REGION" --format="value(status.latestReadyRevisionName)") | |
| echo "revision=$FRONTEND_REVISION" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Notify Admin Bot of Frontend Production Deploy | |
| if: env.ENV_TYPE == 'prod' && steps.deploy_frontend.outputs.revision != '' | |
| env: | |
| ADMIN_DISCORD_WEBHOOK_URL: ${{ secrets.ADMIN_DISCORD_WEBHOOK_URL }} | |
| COMPONENT_NAME: Frontend | |
| SERVICE_NAME: ${{ env.FRONTEND_SERVICE }} | |
| SERVICE_URL: ${{ env.FRONTEND_DOMAIN }} | |
| DEPLOYED_REVISION: ${{ steps.deploy_frontend.outputs.revision }} | |
| run: bash .github/scripts/notify-admin-deploy.sh | |
| - name: Determine Frontend URL | |
| run: | | |
| if [ "${{ env.ENV_TYPE }}" = "preview" ]; then | |
| if [ -f frontend/frontend_url.txt ]; then | |
| echo "FINAL_FRONTEND_URL=$(cat frontend/frontend_url.txt)" >> $GITHUB_ENV | |
| else | |
| echo "FINAL_FRONTEND_URL=${{ env.EXISTING_FRONTEND_URL }}" >> $GITHUB_ENV | |
| fi | |
| else | |
| echo "FINAL_FRONTEND_URL=${{ env.FRONTEND_DOMAIN }}" >> $GITHUB_ENV | |
| fi | |
| - name: Update Backend CORS and Self-Awareness | |
| id: update_backend_self_awareness | |
| # Only preview URLs are unknown during the initial backend deploy. Production | |
| # and develop use their configured domains and avoid a second paid rollout. | |
| if: env.ENV_TYPE == 'preview' && (steps.filter.outputs.backend == 'true' || env.BACKEND_EXISTS == 'false' || steps.filter.outputs.frontend == 'true' || env.FRONTEND_EXISTS == 'false') | |
| run: | | |
| BEFORE_REVISION=$(gcloud run services describe "${{ env.BACKEND_SERVICE }}" --region "$REGION" --format="value(status.latestReadyRevisionName)" 2>/dev/null || true) | |
| if [ -f backend/backend_url.txt ]; then | |
| B_URL=$(cat backend/backend_url.txt) | |
| else | |
| B_URL=${EXISTING_BACKEND_URL:-} | |
| fi | |
| if [ -n "${BACKEND_DOMAIN:-}" ]; then | |
| PUBLIC_BACKEND_URL=$BACKEND_DOMAIN | |
| else | |
| PUBLIC_BACKEND_URL=$B_URL | |
| fi | |
| gcloud run services update ${{ env.BACKEND_SERVICE }} \ | |
| --region $REGION \ | |
| --update-env-vars "FRONTEND_URL=$FINAL_FRONTEND_URL,BACKEND_URL=$PUBLIC_BACKEND_URL" | |
| if [ "${{ env.ENV_TYPE }}" = "prod" ]; then | |
| BACKEND_REVISION=$(gcloud run services describe "$BACKEND_SERVICE" --region "$REGION" --format="value(status.latestReadyRevisionName)") | |
| if [ "$BACKEND_REVISION" != "$BEFORE_REVISION" ]; then | |
| echo "revision=$BACKEND_REVISION" >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| - name: Notify Admin Bot of Backend Production Deploy | |
| if: env.ENV_TYPE == 'prod' && (steps.update_backend_self_awareness.outputs.revision != '' || steps.deploy_backend.outputs.revision != '') | |
| env: | |
| ADMIN_DISCORD_WEBHOOK_URL: ${{ secrets.ADMIN_DISCORD_WEBHOOK_URL }} | |
| COMPONENT_NAME: Backend | |
| SERVICE_NAME: ${{ env.BACKEND_SERVICE }} | |
| SERVICE_URL: ${{ env.BACKEND_DOMAIN }} | |
| DEPLOYED_REVISION: ${{ steps.update_backend_self_awareness.outputs.revision || steps.deploy_backend.outputs.revision }} | |
| run: bash .github/scripts/notify-admin-deploy.sh | |
| - name: Retain two preview R2 credential versions | |
| if: env.ENV_TYPE == 'preview' && env.R2_RUNTIME_CREDENTIALS_REPLACED == 'true' | |
| run: | | |
| set -euo pipefail | |
| for secret_name in \ | |
| "$R2_ACCESS_KEY_SECRET_NAME" \ | |
| "$R2_SECRET_KEY_SECRET_NAME" \ | |
| "$R2_ENDPOINT_SECRET_NAME" \ | |
| "$R2_TOKEN_ID_SECRET_NAME"; do | |
| # Keep the current version plus one rollback version. Delayed | |
| # destruction preserves a seven-day recovery window without letting | |
| # rotated preview credentials accumulate indefinitely. | |
| gcloud secrets update "$secret_name" \ | |
| --project "$PROJECT_ID" \ | |
| --version-destroy-ttl="604800s" \ | |
| --quiet | |
| mapfile -t old_versions < <( | |
| gcloud secrets versions list "$secret_name" \ | |
| --project "$PROJECT_ID" \ | |
| --filter='state=ENABLED' \ | |
| --format='value(name.basename())' \ | |
| | sort -rn \ | |
| | tail -n +3 | |
| ) | |
| for version in "${old_versions[@]}"; do | |
| gcloud secrets versions destroy "$version" \ | |
| --secret "$secret_name" \ | |
| --project "$PROJECT_ID" \ | |
| --quiet | |
| done | |
| done | |
| - name: Generate Deployment Summary | |
| if: always() | |
| run: | | |
| PUBLIC_API_URL="${API_URL:-}" | |
| # Define status emojis and labels | |
| if [ "${{ steps.filter.outputs.backend }}" = "true" ] || [ "${{ env.BACKEND_EXISTS }}" = "false" ] || [ "${{ env.R2_RUNTIME_CREDENTIALS_REPLACED }}" = "true" ]; then | |
| if [ "${{ env.R2_RUNTIME_CREDENTIALS_REPLACED }}" = "true" ] && [ "${{ steps.filter.outputs.backend }}" != "true" ] && [ "${{ env.BACKEND_EXISTS }}" != "false" ]; then | |
| BACKEND_STATUS="Deployed (R2 Credentials Replaced)" | |
| elif [ "${{ steps.filter.outcome }}" = "success" ]; then | |
| BACKEND_STATUS="Deployed (Fresh Build)" | |
| else | |
| BACKEND_STATUS="Deployment Failed" | |
| fi | |
| if [ -f backend/backend_url.txt ]; then | |
| B_URL=$(cat backend/backend_url.txt) | |
| else | |
| B_URL="${{ env.EXISTING_BACKEND_URL }}" | |
| fi | |
| else | |
| BACKEND_STATUS="✨ Cached (No Changes / Reused)" | |
| B_URL="${{ env.EXISTING_BACKEND_URL }}" | |
| fi | |
| if [ "${{ steps.filter.outputs.frontend }}" = "true" ] || [ "${{ env.FRONTEND_EXISTS }}" = "false" ]; then | |
| if [ "${{ steps.filter.outcome }}" = "success" ]; then | |
| FRONTEND_STATUS="Deployed (Fresh Build)" | |
| else | |
| FRONTEND_STATUS="Deployment Failed" | |
| fi | |
| if [ -f frontend/frontend_url.txt ]; then | |
| F_URL=$(cat frontend/frontend_url.txt) | |
| else | |
| F_URL="${{ env.EXISTING_FRONTEND_URL }}" | |
| fi | |
| else | |
| FRONTEND_STATUS="✨ Cached (No Changes / Reused)" | |
| F_URL="${{ env.EXISTING_FRONTEND_URL }}" | |
| fi | |
| # Output to GitHub Step Summary | |
| echo "## Modtale Deployment Dashboard" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Environment Context" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Triggered By:** \`${{ github.actor }}\` via \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Environment:** \`${{ env.ENV_TYPE }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Component | Status | Target Service | Service URL |" >> $GITHUB_STEP_SUMMARY | |
| echo "|---|---|---|---|" >> $GITHUB_STEP_SUMMARY | |
| if [ -n "$PUBLIC_API_URL" ]; then | |
| echo "| **Backend API** | $BACKEND_STATUS | \`${{ env.BACKEND_SERVICE }}\` | [API Endpoint]($PUBLIC_API_URL) |" >> $GITHUB_STEP_SUMMARY | |
| elif [ -n "$B_URL" ]; then | |
| echo "| **Backend API** | $BACKEND_STATUS | \`${{ env.BACKEND_SERVICE }}\` | [API Endpoint]($B_URL/api/v1) |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| **Backend API** | $BACKEND_STATUS | \`${{ env.BACKEND_SERVICE }}\` | *N/A* |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ -n "$F_URL" ]; then | |
| echo "| **Frontend App** | $FRONTEND_STATUS | \`${{ env.FRONTEND_SERVICE }}\` | [App URL]($F_URL) |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| **Frontend App** | $FRONTEND_STATUS | \`${{ env.FRONTEND_SERVICE }}\` | *N/A* |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| echo "*View deployment details in [Google Cloud Console](https://console.cloud.google.com/run?project=${{ env.PROJECT_ID }}).* " >> $GITHUB_STEP_SUMMARY | |
| prepare-preview-source: | |
| concurrency: | |
| group: modtale-template-reader | |
| cancel-in-progress: false | |
| if: github.repository == 'Modtale/modtale' && github.event_name == 'workflow_dispatch' && inputs.prepare_preview_source | |
| runs-on: ubuntu-latest | |
| environment: branch-preview | |
| permissions: | |
| contents: read | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_R2_PROVISIONER: ${{ secrets.CLOUDFLARE_R2_PROVISIONER }} | |
| CLOUDFLARE_API_TOKEN_PROVISIONER: ${{ secrets.CLOUDFLARE_API_TOKEN_PROVISIONER }} | |
| CLOUDFLARE_API_TOKEN_OWNER: ${{ vars.CLOUDFLARE_API_TOKEN_OWNER }} | |
| R2_BUCKET_NAME: modtale-preview-template | |
| R2_TOKEN_NAME: modtale-preview-template-reader | |
| RUNTIME_SERVICE_ACCOUNT: ${{ vars.GCP_BRANCH_PREVIEW_RUNTIME_SERVICE_ACCOUNT }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: '${{ secrets.GCP_CREDENTIALS }}' | |
| - uses: google-github-actions/setup-gcloud@v2 | |
| - name: Provision isolated template reader | |
| run: | | |
| set -euo pipefail | |
| : "${RUNTIME_SERVICE_ACCOUNT:?Preview runtime service account required}" | |
| if gcloud secrets describe R2_TEMPLATE_READ_ACCESS_KEY --project "$PROJECT_ID" >/dev/null 2>&1; then | |
| echo "Template reader is already provisioned." | |
| exit 0 | |
| fi | |
| runtime_env_file=$(mktemp) | |
| trap 'rm -f "$runtime_env_file"' EXIT | |
| export R2_RUNTIME_ENV_FILE="$runtime_env_file" | |
| node .github/scripts/cloudflare-r2-preview.mjs provision-read-only | |
| set -a | |
| . "$runtime_env_file" | |
| set +a | |
| for entry in ACCESS_KEY SECRET_KEY ENDPOINT; do | |
| variable="R2_RUNTIME_$entry" | |
| printf '%s' "${!variable}" | gcloud secrets create "R2_TEMPLATE_READ_$entry" \ | |
| --project "$PROJECT_ID" --replication-policy=automatic --data-file=- --quiet | |
| gcloud secrets add-iam-policy-binding "R2_TEMPLATE_READ_$entry" \ | |
| --project "$PROJECT_ID" --member="serviceAccount:$RUNTIME_SERVICE_ACCOUNT" \ | |
| --role=roles/secretmanager.secretAccessor --quiet >/dev/null | |
| done | |
| find-orphan-previews: | |
| if: github.repository == 'Modtale/modtale' && (github.event_name == 'delete' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') | |
| runs-on: ubuntu-latest | |
| environment: branch-preview | |
| permissions: | |
| contents: read | |
| outputs: | |
| branches: ${{ steps.inventory.outputs.branches }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: '${{ secrets.GCP_CREDENTIALS }}' | |
| - uses: google-github-actions/setup-gcloud@v2 | |
| - id: inventory | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EXPLICIT_CLEANUP_BRANCHES: ${{ inputs.cleanup_branches || '[]' }} | |
| run: | | |
| set -euo pipefail | |
| branches=$(python3 .github/scripts/find-orphan-previews.py) | |
| echo "branches=$branches" >> "$GITHUB_OUTPUT" | |
| cleanup-branch-preview: | |
| concurrency: | |
| group: modtale-preview-cleanup-${{ matrix.branch }} | |
| cancel-in-progress: false | |
| name: Clean Up Deleted Branch Preview (${{ matrix.branch }}) | |
| needs: find-orphan-previews | |
| if: needs.find-orphan-previews.outputs.branches != '[]' | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 3 | |
| matrix: | |
| branch: ${{ fromJSON(needs.find-orphan-previews.outputs.branches) }} | |
| runs-on: ubuntu-latest | |
| environment: branch-preview | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_R2_PROVISIONER: ${{ secrets.CLOUDFLARE_R2_PROVISIONER }} | |
| CLOUDFLARE_API_TOKEN_PROVISIONER: ${{ secrets.CLOUDFLARE_API_TOKEN_PROVISIONER }} | |
| CLOUDFLARE_API_TOKEN_OWNER: ${{ vars.CLOUDFLARE_API_TOKEN_OWNER }} | |
| CLOUDFLARE_R2_JURISDICTION: ${{ vars.CLOUDFLARE_R2_JURISDICTION }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Prepare cleanup names | |
| env: | |
| DELETED_BRANCH: ${{ matrix.branch }} | |
| run: | | |
| set -euo pipefail | |
| branch_name="$DELETED_BRANCH" | |
| slug="$(printf '%s' "$branch_name" | sed 's/[^a-zA-Z0-9-]/-/g' | tr '[:upper:]' '[:lower:]' | sed 's/--*/-/g' | cut -c 1-20 | sed 's/^-//;s/-$//')" | |
| if [ -z "$slug" ]; then | |
| deleted_sha="${{ github.event.before }}" | |
| slug="${deleted_sha:0:7}" | |
| fi | |
| if [ -z "$slug" ] || [ "$slug" = "main" ] || [ "$slug" = "develop" ]; then | |
| echo "::error::Refusing to clean up protected or empty branch slug '$slug'." | |
| exit 1 | |
| fi | |
| echo "BRANCH_SLUG=$slug" >> "$GITHUB_ENV" | |
| echo "BACKEND_SERVICE=modtale-backend-$slug" >> "$GITHUB_ENV" | |
| echo "FRONTEND_SERVICE=modtale-frontend-$slug" >> "$GITHUB_ENV" | |
| echo "DB_NAME=modtale-$slug" >> "$GITHUB_ENV" | |
| echo "MONGODB_SECRET_NAME=BRANCH_PREVIEW_MONGODB_URI" >> "$GITHUB_ENV" | |
| echo "R2_BUCKET_NAME=modtale-branch-$slug" >> "$GITHUB_ENV" | |
| echo "R2_ACCESS_KEY_SECRET_NAME=branch-preview-$slug-r2-access-key" >> "$GITHUB_ENV" | |
| echo "R2_SECRET_KEY_SECRET_NAME=branch-preview-$slug-r2-secret-key" >> "$GITHUB_ENV" | |
| echo "R2_ENDPOINT_SECRET_NAME=branch-preview-$slug-r2-endpoint" >> "$GITHUB_ENV" | |
| echo "R2_TOKEN_ID_SECRET_NAME=branch-preview-$slug-r2-token-id" >> "$GITHUB_ENV" | |
| echo "IMAGE_TAG=$slug" >> "$GITHUB_ENV" | |
| - name: Check out cleanup scripts | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - id: auth | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: '${{ secrets.GCP_CREDENTIALS }}' | |
| - name: Set up Cloud SDK | |
| uses: google-github-actions/setup-gcloud@v2 | |
| - name: Verify no live branch owns this preview | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: python3 .github/scripts/assert-preview-orphan.py "$BRANCH_SLUG" | |
| - name: Delete branch Cloud Run services | |
| run: | | |
| gcloud run services delete "$BACKEND_SERVICE" \ | |
| --project "$PROJECT_ID" \ | |
| --region "$REGION" \ | |
| --quiet || true | |
| gcloud run services delete "$FRONTEND_SERVICE" \ | |
| --project "$PROJECT_ID" \ | |
| --region "$REGION" \ | |
| --quiet || true | |
| - name: Remove branch image tags | |
| run: | | |
| gcloud container images untag "gcr.io/$PROJECT_ID/modtale-backend:$IMAGE_TAG" --quiet || true | |
| gcloud container images untag "gcr.io/$PROJECT_ID/modtale-frontend:$IMAGE_TAG" --quiet || true | |
| - name: Drop branch Mongo database | |
| run: | | |
| set -euo pipefail | |
| case "$DB_NAME" in | |
| modtale-*) | |
| ;; | |
| *) | |
| echo "::error::Refusing to drop non-preview Mongo database '$DB_NAME'." | |
| exit 1 | |
| ;; | |
| esac | |
| mongodb_uri="$(gcloud secrets versions access latest --project "$PROJECT_ID" --secret="$MONGODB_SECRET_NAME")" | |
| echo "::add-mask::$mongodb_uri" | |
| npm install --silent --prefix "$RUNNER_TEMP/mongo-cleanup" mongodb@6 | |
| MONGODB_URI="$mongodb_uri" \ | |
| NODE_PATH="$RUNNER_TEMP/mongo-cleanup/node_modules" \ | |
| node .github/scripts/drop-preview-db.cjs | |
| - name: Delete branch R2 bucket | |
| run: | | |
| set -euo pipefail | |
| mask_secret() { | |
| local value="${1:-}" | |
| if [ -n "$value" ]; then | |
| echo "::add-mask::$value" | |
| fi | |
| } | |
| case "$R2_BUCKET_NAME" in | |
| modtale-branch-*) | |
| ;; | |
| *) | |
| echo "::error::Refusing to delete non-branch R2 bucket '$R2_BUCKET_NAME'." | |
| exit 1 | |
| ;; | |
| esac | |
| r2_access_key="$(gcloud secrets versions access latest --project "$PROJECT_ID" --secret="$R2_ACCESS_KEY_SECRET_NAME" 2>/dev/null || true)" | |
| r2_secret_key="$(gcloud secrets versions access latest --project "$PROJECT_ID" --secret="$R2_SECRET_KEY_SECRET_NAME" 2>/dev/null || true)" | |
| r2_endpoint="$(gcloud secrets versions access latest --project "$PROJECT_ID" --secret="$R2_ENDPOINT_SECRET_NAME" 2>/dev/null || true)" | |
| r2_endpoint="$(printf '%s' "$r2_endpoint" | sed -E 's#(https?://[^/]+).*#\1#')" | |
| mask_secret "$r2_access_key" | |
| mask_secret "$r2_secret_key" | |
| if [ -z "$r2_access_key" ] || [ -z "$r2_secret_key" ] || [ -z "$r2_endpoint" ]; then | |
| echo "Per-branch R2 runtime secrets are missing; skipping object empty step." | |
| elif ! command -v aws >/dev/null 2>&1; then | |
| sudo apt-get update | |
| sudo apt-get install -y awscli | |
| fi | |
| if [ -n "$r2_access_key" ] && \ | |
| [ -n "$r2_secret_key" ] && \ | |
| [ -n "$r2_endpoint" ] && \ | |
| AWS_ACCESS_KEY_ID="$r2_access_key" \ | |
| AWS_SECRET_ACCESS_KEY="$r2_secret_key" \ | |
| AWS_DEFAULT_REGION=auto \ | |
| aws s3api head-bucket \ | |
| --endpoint-url "$r2_endpoint" \ | |
| --bucket "$R2_BUCKET_NAME" >/dev/null 2>&1; then | |
| AWS_ACCESS_KEY_ID="$r2_access_key" \ | |
| AWS_SECRET_ACCESS_KEY="$r2_secret_key" \ | |
| AWS_DEFAULT_REGION=auto \ | |
| aws s3 rm \ | |
| --endpoint-url "$r2_endpoint" \ | |
| "s3://$R2_BUCKET_NAME" \ | |
| --recursive | |
| else | |
| echo "R2 bucket '$R2_BUCKET_NAME' does not exist; nothing to delete." | |
| fi | |
| runtime_token_id="$( | |
| gcloud secrets versions access latest \ | |
| --project "$PROJECT_ID" \ | |
| --secret="$R2_TOKEN_ID_SECRET_NAME" 2>/dev/null || true | |
| )" | |
| if [ -z "$runtime_token_id" ]; then | |
| runtime_token_id="$( | |
| gcloud secrets versions access latest \ | |
| --project "$PROJECT_ID" \ | |
| --secret="$R2_ACCESS_KEY_SECRET_NAME" 2>/dev/null || true | |
| )" | |
| fi | |
| mask_secret "$runtime_token_id" | |
| export R2_RUNTIME_TOKEN_ID="$runtime_token_id" | |
| : "${CLOUDFLARE_ACCOUNT_ID:?Set secret CLOUDFLARE_ACCOUNT_ID.}" | |
| : "${CLOUDFLARE_R2_PROVISIONER:?Set secret CLOUDFLARE_R2_PROVISIONER.}" | |
| : "${CLOUDFLARE_API_TOKEN_PROVISIONER:?Set secret CLOUDFLARE_API_TOKEN_PROVISIONER.}" | |
| node .github/scripts/cloudflare-r2-preview.mjs cleanup | |
| for secret_name in \ | |
| "$R2_ACCESS_KEY_SECRET_NAME" \ | |
| "$R2_SECRET_KEY_SECRET_NAME" \ | |
| "$R2_ENDPOINT_SECRET_NAME" \ | |
| "$R2_TOKEN_ID_SECRET_NAME"; do | |
| gcloud secrets delete "$secret_name" \ | |
| --project "$PROJECT_ID" \ | |
| --quiet || true | |
| done |