demo-hover #53
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 | |
| 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" | |
| detect-changes: | |
| needs: resolve-target | |
| if: github.event_name == 'push' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| deployable: ${{ steps.filter.outputs.deployable }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Find last successfully deployed commit | |
| id: last_deploy | |
| 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 | |
| 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/pulumi/**' | |
| - 'src/typescript/package.json' | |
| - 'src/typescript/pnpm-lock.yaml' | |
| - 'src/typescript/pnpm-workspace.yaml' | |
| - 'src/typescript/api/Dockerfile' | |
| - '.github/workflows/deploy.yml' | |
| decide-deploy: | |
| needs: [resolve-target, detect-changes] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_deploy: ${{ steps.decide.outputs.should_deploy }} | |
| steps: | |
| - name: Decide whether to deploy | |
| id: decide | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| should_deploy=false | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| should_deploy=true | |
| elif [ "${{ needs.detect-changes.outputs.deployable }}" = "true" ]; then | |
| should_deploy=true | |
| fi | |
| if [ "$should_deploy" = "true" ]; then | |
| echo "Deploy requested; deploying." | |
| else | |
| echo "No deployable files changed since last successful deploy; skipping." | |
| fi | |
| echo "should_deploy=$should_deploy" >> "$GITHUB_OUTPUT" | |
| # Quality gate: four parallel jobs replacing the former sequential quality-gate. | |
| # Only the test job needs a real Postgres; the others use a dummy DATABASE_URL | |
| # just so `prisma generate` can parse the schema without connecting. | |
| typecheck: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.24.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22.13.1" | |
| cache: pnpm | |
| cache-dependency-path: src/typescript/pnpm-lock.yaml | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Generate Prisma client | |
| run: pnpm db:generate | |
| - name: Typecheck | |
| run: pnpm typecheck | |
| lint: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.24.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22.13.1" | |
| cache: pnpm | |
| cache-dependency-path: src/typescript/pnpm-lock.yaml | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Generate Prisma client | |
| run: pnpm db:generate | |
| - name: Lint and dead-code checks | |
| run: pnpm lint && pnpm knip | |
| test: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| 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: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.24.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22.13.1" | |
| cache: pnpm | |
| cache-dependency-path: src/typescript/pnpm-lock.yaml | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Generate Prisma client | |
| run: pnpm db:generate | |
| - name: Run tests | |
| run: pnpm test | |
| build: | |
| needs: [resolve-target, decide-deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.24.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22.13.1" | |
| cache: pnpm | |
| cache-dependency-path: src/typescript/pnpm-lock.yaml | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Generate Prisma client | |
| run: pnpm db:generate | |
| - name: Build workspace | |
| run: pnpm build | |
| # 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 | |
| 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 }} | |
| 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:]')" | |
| REPOSITORY="ghcr.io/${OWNER}/openerrata-api" | |
| 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=$REPOSITORY" >> "$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: false | |
| deploy: | |
| needs: [resolve-target, decide-deploy, typecheck, lint, test, build, build-image] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| environment: ${{ needs.resolve-target.outputs.deploy_environment }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| defaults: | |
| run: | |
| working-directory: src/typescript | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.24.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22.13.1" | |
| cache: pnpm | |
| cache-dependency-path: src/typescript/pnpm-lock.yaml | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - 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 }} | |
| 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" | |
| if [ -n "${PULUMI_API_HOSTNAME:-}" ]; then | |
| pulumi config set apiHostname "$PULUMI_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 | |
| 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 "${PULUMI_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 | |
| pulumi up \ | |
| --stack "$STACK" \ | |
| --yes \ | |
| --non-interactive \ | |
| --skip-preview \ | |
| --refresh=true | |
| promote-image-latest: | |
| needs: [resolve-target, decide-deploy, build-image, deploy] | |
| if: needs.decide-deploy.outputs.should_deploy == 'true' | |
| runs-on: ubuntu-latest | |
| 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 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" |