fix(public,frontend): tighten search filter and install URL #171
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 | |
| on: | |
| push: | |
| branches: | |
| - staging | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| description: "Environment/stack to deploy" | |
| required: true | |
| default: "staging" | |
| type: choice | |
| options: | |
| - staging | |
| - main | |
| concurrency: | |
| group: deploy-${{ github.event_name == 'push' && github.ref_name || format('dispatch-{0}', github.run_id) }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| resolve-target: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| deploy_environment: ${{ steps.target.outputs.deploy_environment }} | |
| steps: | |
| - name: Resolve deployment environment | |
| id: target | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| DEPLOY_ENV="${{ github.ref_name }}" | |
| else | |
| DEPLOY_ENV="${{ inputs.environment }}" | |
| fi | |
| case "$DEPLOY_ENV" in | |
| staging|main) ;; | |
| *) | |
| echo "Unsupported deploy environment: $DEPLOY_ENV" | |
| exit 1 | |
| ;; | |
| esac | |
| echo "deploy_environment=$DEPLOY_ENV" >> "$GITHUB_OUTPUT" | |
| decide-deploy: | |
| needs: resolve-target | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| actions: read | |
| outputs: | |
| should_deploy: ${{ steps.decide.outputs.should_deploy || steps.finalize.outputs.should_deploy }} | |
| steps: | |
| - name: Decide whether to deploy | |
| id: decide | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| # Workflow dispatches always deploy. | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "Deploy requested via workflow dispatch." | |
| echo "should_deploy=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # For push events, detect whether any deployable files changed | |
| # since the last successful deploy. | |
| echo "Detecting deployable changes since last successful deploy..." | |
| - name: Checkout | |
| if: steps.decide.outputs.should_deploy != 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Find last successfully deployed commit | |
| id: last_deploy | |
| if: steps.decide.outputs.should_deploy != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Paginate through successful workflow runs and find the most recent | |
| # one where the "deploy" job actually ran (conclusion != skipped). | |
| LAST_SHA="" | |
| PAGE=1 | |
| while [ -z "$LAST_SHA" ]; do | |
| RUNS=$(gh api \ | |
| "/repos/${{ github.repository }}/actions/workflows/deploy.yml/runs?branch=${{ github.ref_name }}&status=success&per_page=20&page=${PAGE}" \ | |
| --jq '.workflow_runs[].id') | |
| if [ -z "$RUNS" ]; then | |
| break | |
| fi | |
| for RUN_ID in $RUNS; do | |
| DEPLOY_CONCLUSION=$(gh api \ | |
| "/repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs" \ | |
| --jq '.jobs[] | select(.name == "deploy") | .conclusion') | |
| if [ "$DEPLOY_CONCLUSION" = "success" ]; then | |
| LAST_SHA=$(gh api \ | |
| "/repos/${{ github.repository }}/actions/runs/${RUN_ID}" \ | |
| --jq '.head_sha') | |
| break | |
| fi | |
| done | |
| PAGE=$((PAGE + 1)) | |
| # Safety valve: don't paginate forever | |
| if [ "$PAGE" -gt 10 ]; then | |
| break | |
| fi | |
| done | |
| if [ -z "$LAST_SHA" ]; then | |
| # No previous successful deploy: treat everything as changed | |
| # by comparing against the root commit. | |
| LAST_SHA=$(git rev-list --max-parents=0 HEAD) | |
| echo "No previous successful deploy found; comparing against repo root: $LAST_SHA" | |
| else | |
| echo "Last successful deploy was at: $LAST_SHA" | |
| fi | |
| echo "sha=$LAST_SHA" >> "$GITHUB_OUTPUT" | |
| - name: Detect deployable path changes since last deploy | |
| id: filter | |
| if: steps.decide.outputs.should_deploy != 'true' | |
| uses: dorny/paths-filter@v3 | |
| with: | |
| base: ${{ steps.last_deploy.outputs.sha }} | |
| filters: | | |
| deployable: | |
| - 'src/helm/openerrata/**' | |
| - 'src/typescript/api/**' | |
| - 'src/typescript/shared/**' | |
| - 'src/typescript/frontend/**' | |
| - 'src/typescript/pulumi/**' | |
| - 'src/typescript/package.json' | |
| - 'src/typescript/pnpm-lock.yaml' | |
| - 'src/typescript/pnpm-workspace.yaml' | |
| - 'src/typescript/api/Dockerfile' | |
| - 'src/typescript/frontend/Dockerfile' | |
| - '.github/workflows/deploy.yml' | |
| - '.github/actions/**' | |
| - name: Finalize deploy decision | |
| if: steps.decide.outputs.should_deploy != 'true' | |
| id: finalize | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ steps.filter.outputs.deployable }}" = "true" ]; then | |
| echo "Deployable files changed since last successful deploy." | |
| echo "should_deploy=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "No deployable files changed since last successful deploy; skipping." | |
| echo "should_deploy=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Quality gate: six parallel jobs. | |
| helm-lint: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Helm | |
| uses: azure/setup-helm@v4 | |
| - name: Lint chart | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| helm lint src/helm/openerrata -f src/helm/openerrata/ci-values.yaml | |
| helm template openerrata src/helm/openerrata -f src/helm/openerrata/ci-values.yaml > /dev/null | |
| typecheck: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup TypeScript workspace | |
| uses: ./.github/actions/setup-typescript-workspace | |
| - name: Typecheck | |
| run: pnpm typecheck | |
| lint: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup TypeScript workspace | |
| uses: ./.github/actions/setup-typescript-workspace | |
| - name: Lint and static checks | |
| run: pnpm lint:ci | |
| test: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| services: | |
| postgres: | |
| image: postgres:17 | |
| env: | |
| POSTGRES_PASSWORD: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup TypeScript workspace | |
| uses: ./.github/actions/setup-typescript-workspace | |
| - name: Run TypeScript quality test suite | |
| run: pnpm run test:ci:quality | |
| build: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup TypeScript workspace | |
| uses: ./.github/actions/setup-typescript-workspace | |
| - name: Build workspace | |
| run: pnpm build | |
| - name: Archive extension dist artifact | |
| shell: bash | |
| run: tar -C extension -czf "$RUNNER_TEMP/extension-dist.tgz" dist | |
| - name: Upload extension dist artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: extension-dist-${{ github.sha }} | |
| path: ${{ runner.temp }}/extension-dist.tgz | |
| retention-days: 1 | |
| - name: Archive frontend build artifact | |
| shell: bash | |
| run: tar -C frontend -czf "$RUNNER_TEMP/frontend-build.tgz" build | |
| - name: Upload frontend build artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: frontend-build-${{ github.sha }} | |
| path: ${{ runner.temp }}/frontend-build.tgz | |
| retention-days: 1 | |
| e2e: | |
| needs: [resolve-target, decide-deploy, build] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup TypeScript workspace | |
| uses: ./.github/actions/setup-typescript-workspace | |
| with: | |
| generate-prisma: "false" | |
| install-playwright-chromium: "true" | |
| - name: Download extension dist artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: extension-dist-${{ github.sha }} | |
| path: ${{ runner.temp }} | |
| - name: Restore extension dist artifact | |
| shell: bash | |
| run: tar -C extension -xzf "$RUNNER_TEMP/extension-dist.tgz" | |
| - name: Run extension e2e tests | |
| run: pnpm run test:e2e:extension:ci | |
| frontend-e2e: | |
| needs: [resolve-target, decide-deploy, build] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup TypeScript workspace | |
| uses: ./.github/actions/setup-typescript-workspace | |
| with: | |
| generate-prisma: "false" | |
| install-playwright-chromium: "true" | |
| - name: Download frontend build artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: frontend-build-${{ github.sha }} | |
| path: ${{ runner.temp }} | |
| - name: Restore frontend build artifact | |
| shell: bash | |
| run: tar -C frontend -xzf "$RUNNER_TEMP/frontend-build.tgz" | |
| - name: Run frontend e2e smoke tests | |
| run: pnpm run test:e2e:frontend:ci | |
| # Docker image build runs in parallel with quality gate jobs. | |
| # The deploy job gates on both build-image AND all quality gate jobs, | |
| # so we never deploy unchecked code while saving wall-clock time. | |
| build-image: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| packages: write | |
| outputs: | |
| image_repository: ${{ steps.meta.outputs.image_repository }} | |
| image_tag: ${{ steps.meta.outputs.image_tag }} | |
| latest_tag: ${{ steps.meta.outputs.latest_tag }} | |
| image_digest: ${{ steps.build.outputs.digest }} | |
| frontend_image_repository: ${{ steps.meta.outputs.frontend_image_repository }} | |
| frontend_image_tag: ${{ steps.meta.outputs.image_tag }} | |
| frontend_latest_tag: ${{ steps.meta.outputs.latest_tag }} | |
| frontend_image_digest: ${{ steps.build-frontend.outputs.digest }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Compute image metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| OWNER="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" | |
| DEPLOY_ENV="${{ needs.resolve-target.outputs.deploy_environment }}" | |
| SHORT_SHA="$(echo '${{ github.sha }}' | cut -c1-12)" | |
| IMAGE_TAG="${DEPLOY_ENV}-${SHORT_SHA}" | |
| LATEST_TAG="${DEPLOY_ENV}-latest" | |
| echo "image_repository=ghcr.io/${OWNER}/openerrata-api" >> "$GITHUB_OUTPUT" | |
| echo "frontend_image_repository=ghcr.io/${OWNER}/openerrata-frontend" >> "$GITHUB_OUTPUT" | |
| echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT" | |
| echo "latest_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT" | |
| - name: Build and push API image | |
| id: build | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: src/typescript | |
| file: src/typescript/api/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ steps.meta.outputs.image_repository }}:${{ steps.meta.outputs.image_tag }} | |
| cache-from: type=registry,ref=${{ steps.meta.outputs.image_repository }}:${{ steps.meta.outputs.latest_tag }} | |
| cache-to: type=inline | |
| provenance: true | |
| sbom: true | |
| - name: Build and push frontend image | |
| id: build-frontend | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: src/typescript | |
| file: src/typescript/frontend/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ steps.meta.outputs.frontend_image_repository }}:${{ steps.meta.outputs.image_tag }} | |
| cache-from: type=registry,ref=${{ steps.meta.outputs.frontend_image_repository }}:${{ steps.meta.outputs.latest_tag }} | |
| cache-to: type=inline | |
| provenance: true | |
| sbom: true | |
| deploy: | |
| needs: [resolve-target, decide-deploy, helm-lint, typecheck, lint, test, build, e2e, frontend-e2e, build-image] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| environment: ${{ needs.resolve-target.outputs.deploy_environment }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup TypeScript workspace | |
| uses: ./.github/actions/setup-typescript-workspace | |
| - name: Install Pulumi CLI | |
| uses: pulumi/setup-pulumi@v2 | |
| - name: Configure kubeconfig | |
| shell: bash | |
| env: | |
| KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${KUBE_CONFIG_DATA:-}" ]; then | |
| echo "KUBE_CONFIG_DATA secret is required" | |
| exit 1 | |
| fi | |
| mkdir -p "$HOME/.kube" | |
| TMP_CONFIG="$(mktemp)" | |
| if printf '%s' "$KUBE_CONFIG_DATA" | base64 --decode > "$TMP_CONFIG" 2>/dev/null \ | |
| && grep -q '^apiVersion:' "$TMP_CONFIG"; then | |
| mv "$TMP_CONFIG" "$HOME/.kube/config" | |
| else | |
| rm -f "$TMP_CONFIG" | |
| printf '%s\n' "$KUBE_CONFIG_DATA" > "$HOME/.kube/config" | |
| fi | |
| chmod 600 "$HOME/.kube/config" | |
| - name: Configure Pulumi stack values | |
| working-directory: src/typescript/pulumi | |
| shell: bash | |
| env: | |
| PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} | |
| STACK: ${{ needs.resolve-target.outputs.deploy_environment }} | |
| PULUMI_BLOB_STORAGE_BUCKET: ${{ secrets.PULUMI_BLOB_STORAGE_BUCKET }} | |
| PULUMI_BLOB_STORAGE_PUBLIC_URL_PREFIX: ${{ secrets.PULUMI_BLOB_STORAGE_PUBLIC_URL_PREFIX }} | |
| PULUMI_BLOB_STORAGE_ENDPOINT: ${{ secrets.PULUMI_BLOB_STORAGE_ENDPOINT }} | |
| PULUMI_DATABASE_URL: ${{ secrets.PULUMI_DATABASE_URL }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| PULUMI_BLOB_STORAGE_ACCESS_KEY_ID: ${{ secrets.PULUMI_BLOB_STORAGE_ACCESS_KEY_ID }} | |
| PULUMI_BLOB_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.PULUMI_BLOB_STORAGE_SECRET_ACCESS_KEY }} | |
| PULUMI_API_HOSTNAME: ${{ vars.PULUMI_API_HOSTNAME }} | |
| PULUMI_INGRESS_CLASS_NAME: ${{ vars.PULUMI_INGRESS_CLASS_NAME }} | |
| PULUMI_DNS_PROVIDER: ${{ vars.PULUMI_DNS_PROVIDER }} | |
| PULUMI_CLOUDFLARE_ZONE_ID: ${{ secrets.PULUMI_CLOUDFLARE_ZONE_ID }} | |
| PULUMI_CLOUDFLARE_PROXIED: ${{ vars.PULUMI_CLOUDFLARE_PROXIED }} | |
| PULUMI_CLOUDFLARE_RECORD_TARGET: ${{ vars.PULUMI_CLOUDFLARE_RECORD_TARGET }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }} | |
| AWS_REGION: ${{ secrets.AWS_REGION }} | |
| AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} | |
| CI_IMAGE_REPOSITORY: ${{ needs.build-image.outputs.image_repository }} | |
| CI_IMAGE_TAG: ${{ needs.build-image.outputs.image_tag }} | |
| CI_IMAGE_DIGEST: ${{ needs.build-image.outputs.image_digest }} | |
| CI_FRONTEND_IMAGE_REPOSITORY: ${{ needs.build-image.outputs.frontend_image_repository }} | |
| CI_FRONTEND_IMAGE_TAG: ${{ needs.build-image.outputs.frontend_image_tag }} | |
| CI_FRONTEND_IMAGE_DIGEST: ${{ needs.build-image.outputs.frontend_image_digest }} | |
| PULUMI_FRONTEND_HOSTNAME: ${{ vars.PULUMI_FRONTEND_HOSTNAME }} | |
| run: | | |
| set -euo pipefail | |
| pulumi stack select "$STACK" --create | |
| if [ -z "${CI_IMAGE_REPOSITORY:-}" ] || [ -z "${CI_IMAGE_TAG:-}" ] || [ -z "${CI_IMAGE_DIGEST:-}" ]; then | |
| echo "Build image metadata is required (CI_IMAGE_REPOSITORY, CI_IMAGE_TAG, CI_IMAGE_DIGEST)." | |
| exit 1 | |
| fi | |
| pulumi config set imageRepository "$CI_IMAGE_REPOSITORY" --stack "$STACK" | |
| pulumi config set imageTag "$CI_IMAGE_TAG" --stack "$STACK" | |
| pulumi config set imageDigest "$CI_IMAGE_DIGEST" --stack "$STACK" | |
| pulumi config set releaseName "openerrata-${STACK}" --stack "$STACK" | |
| pulumi config set namespace "openerrata-${STACK}" --stack "$STACK" | |
| api_hostname="${PULUMI_API_HOSTNAME:-}" | |
| if [ -z "$api_hostname" ] && [ "$STACK" = "staging" ]; then | |
| api_hostname="api.staging.branch.openerrata.com" | |
| fi | |
| if [ -n "$api_hostname" ]; then | |
| pulumi config set apiHostname "$api_hostname" --stack "$STACK" | |
| pulumi config set ingressEnabled "true" --stack "$STACK" | |
| else | |
| pulumi config rm apiHostname --stack "$STACK" >/dev/null 2>&1 || true | |
| pulumi config set ingressEnabled "false" --stack "$STACK" | |
| fi | |
| frontend_hostname="${PULUMI_FRONTEND_HOSTNAME:-}" | |
| if [ -z "$frontend_hostname" ] && [ "$STACK" = "staging" ]; then | |
| frontend_hostname="staging.branch.openerrata.com" | |
| fi | |
| if [ -n "$frontend_hostname" ]; then | |
| pulumi config set frontendEnabled "true" --stack "$STACK" | |
| pulumi config set frontendHostname "$frontend_hostname" --stack "$STACK" | |
| pulumi config set frontendIngressEnabled "true" --stack "$STACK" | |
| else | |
| pulumi config set frontendEnabled "false" --stack "$STACK" | |
| pulumi config rm frontendHostname --stack "$STACK" >/dev/null 2>&1 || true | |
| pulumi config rm frontendIngressEnabled --stack "$STACK" >/dev/null 2>&1 || true | |
| fi | |
| if [ -n "${CI_FRONTEND_IMAGE_REPOSITORY:-}" ]; then | |
| pulumi config set frontendImageRepository "$CI_FRONTEND_IMAGE_REPOSITORY" --stack "$STACK" | |
| fi | |
| if [ -n "${CI_FRONTEND_IMAGE_TAG:-}" ]; then | |
| pulumi config set frontendImageTag "$CI_FRONTEND_IMAGE_TAG" --stack "$STACK" | |
| fi | |
| if [ -n "${CI_FRONTEND_IMAGE_DIGEST:-}" ]; then | |
| pulumi config set frontendImageDigest "$CI_FRONTEND_IMAGE_DIGEST" --stack "$STACK" | |
| fi | |
| if [ -n "${PULUMI_INGRESS_CLASS_NAME:-}" ]; then | |
| pulumi config set ingressClassName "$PULUMI_INGRESS_CLASS_NAME" --stack "$STACK" | |
| else | |
| pulumi config rm ingressClassName --stack "$STACK" >/dev/null 2>&1 || true | |
| fi | |
| dns_provider="${PULUMI_DNS_PROVIDER:-none}" | |
| case "$dns_provider" in | |
| none|"") | |
| pulumi config set dnsProvider "none" --stack "$STACK" | |
| pulumi config rm cloudflareZoneId --stack "$STACK" >/dev/null 2>&1 || true | |
| pulumi config rm cloudflareProxied --stack "$STACK" >/dev/null 2>&1 || true | |
| pulumi config rm cloudflareRecordTarget --stack "$STACK" >/dev/null 2>&1 || true | |
| ;; | |
| cloudflare) | |
| if [ -z "${PULUMI_CLOUDFLARE_ZONE_ID:-}" ] || [ -z "${CLOUDFLARE_API_TOKEN:-}" ]; then | |
| echo "PULUMI_CLOUDFLARE_ZONE_ID and CLOUDFLARE_API_TOKEN are required when PULUMI_DNS_PROVIDER=cloudflare." | |
| exit 1 | |
| fi | |
| if [ -z "$api_hostname" ]; then | |
| echo "PULUMI_API_HOSTNAME is required when PULUMI_DNS_PROVIDER=cloudflare." | |
| exit 1 | |
| fi | |
| pulumi config set dnsProvider "cloudflare" --stack "$STACK" | |
| pulumi config set cloudflareZoneId "$PULUMI_CLOUDFLARE_ZONE_ID" --stack "$STACK" | |
| pulumi config set cloudflareProxied "${PULUMI_CLOUDFLARE_PROXIED:-true}" --stack "$STACK" | |
| if [ -n "${PULUMI_CLOUDFLARE_RECORD_TARGET:-}" ]; then | |
| pulumi config set cloudflareRecordTarget "$PULUMI_CLOUDFLARE_RECORD_TARGET" --stack "$STACK" | |
| else | |
| pulumi config rm cloudflareRecordTarget --stack "$STACK" >/dev/null 2>&1 || true | |
| fi | |
| ;; | |
| *) | |
| echo "Unsupported PULUMI_DNS_PROVIDER value: $dns_provider" | |
| echo "Supported values: none, cloudflare" | |
| exit 1 | |
| ;; | |
| esac | |
| needs_aws_resources=false | |
| manual_blob_vars=( | |
| PULUMI_BLOB_STORAGE_BUCKET | |
| PULUMI_BLOB_STORAGE_PUBLIC_URL_PREFIX | |
| PULUMI_BLOB_STORAGE_SECRET_ACCESS_KEY | |
| ) | |
| configured_manual_blob_values=0 | |
| for manual_blob_var in "${manual_blob_vars[@]}"; do | |
| if [ -n "${!manual_blob_var:-}" ]; then | |
| configured_manual_blob_values=$((configured_manual_blob_values + 1)) | |
| fi | |
| done | |
| if [ "$configured_manual_blob_values" -ne 0 ] && [ "$configured_manual_blob_values" -ne 3 ]; then | |
| echo "Manual blob storage config is partial." | |
| echo "Set all or none of: PULUMI_BLOB_STORAGE_BUCKET, PULUMI_BLOB_STORAGE_PUBLIC_URL_PREFIX, PULUMI_BLOB_STORAGE_SECRET_ACCESS_KEY" | |
| exit 1 | |
| fi | |
| if [ "$configured_manual_blob_values" -eq 3 ]; then | |
| echo "Using manually provided blob storage configuration." | |
| pulumi config set blobStorageBucket "$PULUMI_BLOB_STORAGE_BUCKET" --stack "$STACK" | |
| pulumi config set blobStoragePublicUrlPrefix "$PULUMI_BLOB_STORAGE_PUBLIC_URL_PREFIX" --stack "$STACK" | |
| pulumi config set blobStorageAccessKeyId "${PULUMI_BLOB_STORAGE_ACCESS_KEY_ID:-openerrata}" --stack "$STACK" | |
| pulumi config set --secret blobStorageSecretAccessKey "$PULUMI_BLOB_STORAGE_SECRET_ACCESS_KEY" --stack "$STACK" | |
| if [ -n "${PULUMI_BLOB_STORAGE_ENDPOINT:-}" ]; then | |
| pulumi config set blobStorageEndpoint "$PULUMI_BLOB_STORAGE_ENDPOINT" --stack "$STACK" | |
| else | |
| pulumi config rm blobStorageEndpoint --stack "$STACK" >/dev/null 2>&1 || true | |
| fi | |
| else | |
| echo "No manual blob storage credentials supplied; using Pulumi-managed AWS S3 blob storage." | |
| needs_aws_resources=true | |
| pulumi config rm blobStorageBucket --stack "$STACK" >/dev/null 2>&1 || true | |
| pulumi config rm blobStoragePublicUrlPrefix --stack "$STACK" >/dev/null 2>&1 || true | |
| pulumi config rm blobStorageEndpoint --stack "$STACK" >/dev/null 2>&1 || true | |
| pulumi config rm blobStorageAccessKeyId --stack "$STACK" >/dev/null 2>&1 || true | |
| pulumi config rm blobStorageSecretAccessKey --stack "$STACK" >/dev/null 2>&1 || true | |
| fi | |
| if [ -n "${PULUMI_DATABASE_URL:-}" ]; then | |
| echo "Using manually provided database URL." | |
| pulumi config set --secret databaseUrl "$PULUMI_DATABASE_URL" --stack "$STACK" | |
| else | |
| echo "No database URL supplied; using Pulumi-managed AWS RDS database." | |
| needs_aws_resources=true | |
| pulumi config rm databaseUrl --stack "$STACK" >/dev/null 2>&1 || true | |
| fi | |
| if [ "$needs_aws_resources" = "true" ]; then | |
| if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then | |
| echo "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when Pulumi-managed AWS resources are enabled." | |
| exit 1 | |
| fi | |
| pulumi config set aws:region "${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}" --stack "$STACK" | |
| fi | |
| if [ -n "${OPENAI_API_KEY:-}" ]; then | |
| pulumi config set --secret openaiApiKey "$OPENAI_API_KEY" --stack "$STACK" | |
| fi | |
| - name: Cancel stale Pulumi lock | |
| working-directory: src/typescript/pulumi | |
| shell: bash | |
| env: | |
| PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} | |
| STACK: ${{ needs.resolve-target.outputs.deploy_environment }} | |
| run: | | |
| set +e | |
| output="$(pulumi cancel --stack "$STACK" --yes 2>&1)" | |
| rc=$? | |
| set -e | |
| echo "$output" | |
| if [ $rc -eq 0 ]; then | |
| exit 0 | |
| fi | |
| if echo "$output" | grep -qi "no update in progress"; then | |
| echo "No active lock; continuing." | |
| exit 0 | |
| fi | |
| if echo "$output" | grep -qi "has never been updated"; then | |
| echo "Stack has never been updated; no lock to cancel." | |
| exit 0 | |
| fi | |
| echo "pulumi cancel failed unexpectedly" | |
| exit $rc | |
| - name: Delete stale selector/migrate jobs | |
| shell: bash | |
| env: | |
| TARGET_NAMESPACE: openerrata-${{ needs.resolve-target.outputs.deploy_environment }} | |
| run: | | |
| set -euo pipefail | |
| # Selector and migrate jobs can be left behind with stale field managers | |
| # (e.g. after interrupted updates), causing apply conflicts or await | |
| # hangs. Clear them before `pulumi up` so the chart can recreate them | |
| # with the current image and manager. | |
| kubectl -n "$TARGET_NAMESPACE" delete job \ | |
| -l 'app.kubernetes.io/component in (selector,migrate)' \ | |
| --ignore-not-found=true | |
| - name: Pulumi deploy | |
| working-directory: src/typescript/pulumi | |
| shell: bash | |
| env: | |
| PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} | |
| STACK: ${{ needs.resolve-target.outputs.deploy_environment }} | |
| PULUMI_K8S_DELETE_UNREACHABLE: "true" | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }} | |
| AWS_REGION: ${{ secrets.AWS_REGION }} | |
| AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| pulumi stack select "$STACK" --create | |
| TARGET_NAMESPACE="openerrata-${STACK}" | |
| API_SERVICE_NAME="${TARGET_NAMESPACE}-api" | |
| PULUMI_TIMEOUT_SECONDS=1200 | |
| set +e | |
| timeout --signal=TERM --kill-after=30s "${PULUMI_TIMEOUT_SECONDS}s" \ | |
| pulumi up \ | |
| --stack "$STACK" \ | |
| --yes \ | |
| --non-interactive \ | |
| --skip-preview \ | |
| --refresh=true | |
| pulumi_rc=$? | |
| set -e | |
| if [ "$pulumi_rc" -eq 0 ]; then | |
| exit 0 | |
| fi | |
| if [ "$pulumi_rc" -eq 124 ] || [ "$pulumi_rc" -eq 137 ]; then | |
| echo "::error::Pulumi deploy timed out after ${PULUMI_TIMEOUT_SECONDS}s." | |
| else | |
| echo "::error::Pulumi deploy failed with exit code ${pulumi_rc}." | |
| fi | |
| echo "::group::Pulumi stack history" | |
| pulumi stack history --stack "$STACK" --json | jq '.[0]' || true | |
| echo "::endgroup::" | |
| echo "::group::Kubernetes diagnostics (${TARGET_NAMESPACE})" | |
| kubectl -n "$TARGET_NAMESPACE" get deploy,pods,svc,ep,endpointslice,job || true | |
| kubectl -n "$TARGET_NAMESPACE" get service "$API_SERVICE_NAME" -o yaml || true | |
| kubectl -n "$TARGET_NAMESPACE" get endpoints "$API_SERVICE_NAME" -o yaml || true | |
| kubectl -n "$TARGET_NAMESPACE" get endpointslice -l "kubernetes.io/service-name=$API_SERVICE_NAME" -o yaml || true | |
| kubectl -n "$TARGET_NAMESPACE" get events --sort-by=.lastTimestamp | tail -n 200 || true | |
| echo "::endgroup::" | |
| echo "::group::Cancel in-flight Pulumi update" | |
| set +e | |
| cancel_output="$(pulumi cancel --stack "$STACK" --yes 2>&1)" | |
| cancel_rc=$? | |
| set -e | |
| echo "$cancel_output" | |
| if [ "$cancel_rc" -ne 0 ] && ! echo "$cancel_output" | grep -qi "no update in progress"; then | |
| echo "::warning::pulumi cancel returned exit code ${cancel_rc}" | |
| fi | |
| echo "::endgroup::" | |
| exit "$pulumi_rc" | |
| promote-image-latest: | |
| needs: [resolve-target, decide-deploy, build-image, deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Promote deployed API image to latest tag | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| SOURCE_IMAGE="${{ needs.build-image.outputs.image_repository }}@${{ needs.build-image.outputs.image_digest }}" | |
| TARGET_IMAGE="${{ needs.build-image.outputs.image_repository }}:${{ needs.build-image.outputs.latest_tag }}" | |
| docker buildx imagetools create --tag "$TARGET_IMAGE" "$SOURCE_IMAGE" | |
| - name: Promote deployed frontend image to latest tag | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| SOURCE_IMAGE="${{ needs.build-image.outputs.frontend_image_repository }}@${{ needs.build-image.outputs.frontend_image_digest }}" | |
| TARGET_IMAGE="${{ needs.build-image.outputs.frontend_image_repository }}:${{ needs.build-image.outputs.frontend_latest_tag }}" | |
| docker buildx imagetools create --tag "$TARGET_IMAGE" "$SOURCE_IMAGE" | |
| post-deploy-smoke: | |
| needs: [resolve-target, decide-deploy, deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup TypeScript workspace | |
| uses: ./.github/actions/setup-typescript-workspace | |
| with: | |
| generate-prisma: "false" | |
| install-playwright-chromium: "true" | |
| - name: Resolve frontend URL | |
| id: frontend-url | |
| shell: bash | |
| env: | |
| STACK: ${{ needs.resolve-target.outputs.deploy_environment }} | |
| PULUMI_FRONTEND_HOSTNAME: ${{ vars.PULUMI_FRONTEND_HOSTNAME }} | |
| run: | | |
| set -euo pipefail | |
| hostname="${PULUMI_FRONTEND_HOSTNAME:-}" | |
| if [ -z "$hostname" ] && [ "$STACK" = "staging" ]; then | |
| hostname="staging.branch.openerrata.com" | |
| fi | |
| if [ -z "$hostname" ]; then | |
| echo "No frontend hostname configured; skipping post-deploy smoke tests." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "frontend_url=https://${hostname}" >> "$GITHUB_OUTPUT" | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Run post-deploy smoke tests | |
| if: steps.frontend-url.outputs.skip != 'true' | |
| env: | |
| FRONTEND_BASE_URL: ${{ steps.frontend-url.outputs.frontend_url }} | |
| run: pnpm run test:e2e:frontend:post-deploy |