From 9bdbd0afb38f32e3439ce2146e45bdf251da50d7 Mon Sep 17 00:00:00 2001 From: Keval Kapdee Date: Mon, 21 Sep 2026 11:10:13 +0100 Subject: [PATCH] Run tests on PRs, auto-deploy staging, then gate prod PRs only pytest. Push to main applies the stg stack unattended, then the existing prd environment still blocks production. --- .github/workflows/deploy.yml | 97 ++++++++++++++++++++++++++++++++---- .github/workflows/test.yml | 23 +++++++++ README.md | 12 +++-- scripts/deploy-stg.sh | 4 +- 4 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 24a7b57..c183a43 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,30 +4,53 @@ on: push: branches: [main] +concurrency: + group: deploy-main + cancel-in-progress: false + env: AWS_REGION: eu-west-2 jobs: + test: + uses: ./.github/workflows/test.yml + build-frontend: + needs: [test] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - name: Build frontend + - name: Install pnpm working-directory: frontend - env: - VITE_POSTHOG_API_KEY: ${{ vars.POSTHOG_API_KEY }} run: | npm install -g pnpm pnpm install --frozen-lockfile - pnpm run build + + - name: Build staging frontend + working-directory: frontend + env: + VITE_POSTHOG_API_KEY: "" + run: pnpm run build + + - uses: actions/upload-artifact@v7 + with: + name: frontend-dist-stg + path: frontend/dist/ + + - name: Build production frontend + working-directory: frontend + env: + VITE_POSTHOG_API_KEY: ${{ vars.POSTHOG_API_KEY }} + run: pnpm run build - uses: actions/upload-artifact@v7 with: - name: frontend-dist + name: frontend-dist-prd path: frontend/dist/ build-layer: + needs: [test] runs-on: ubuntu-latest permissions: id-token: write @@ -68,15 +91,69 @@ jobs: --query LayerVersionArn --output text) echo "arn=$LAYER_ARN" >> "$GITHUB_OUTPUT" + deploy-stg: + needs: [test, build-layer, build-frontend] + runs-on: ubuntu-latest + # No required reviewers. The OIDC subject is environment:stg, so the deploy + # role trust must allow that (in addition to environment:prd). + environment: stg + permissions: + id-token: write + contents: read + env: + TF_VAR_environment: stg + TF_VAR_lambda_layer_arn: ${{ needs.build-layer.outputs.layer_arn }} + TF_VAR_posthog_api_key: "" + steps: + - uses: actions/checkout@v6 + + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - uses: actions/download-artifact@v7 + with: + name: frontend-dist-stg + path: frontend/dist/ + + - uses: opentofu/setup-opentofu@v2 + with: + tofu_version: "1.12.0" + + - name: OpenTofu apply (staging) + working-directory: infra + run: | + tofu init -input=false -reconfigure \ + -backend-config="key=syncify-stg/terraform.tfstate" + tofu apply -input=false -auto-approve + + - name: Sync frontend to S3 + working-directory: infra + run: | + BUCKET=$(tofu output -raw s3_bucket_name) + aws s3 sync ../frontend/dist/ "s3://$BUCKET/" --delete + + - name: Invalidate CloudFront cache + working-directory: infra + run: | + DIST_ID=$(tofu output -raw cloudfront_distribution_id) + aws cloudfront create-invalidation \ + --distribution-id "$DIST_ID" \ + --paths "/*" + echo "stg live at https://$(tofu output -raw cloudfront_domain)" >> "$GITHUB_STEP_SUMMARY" + # Produce a plan and save it. This runs automatically so the diff is visible # before anyone approves the apply. plan: - needs: [build-layer] + needs: [test, build-layer] runs-on: ubuntu-latest permissions: id-token: write contents: read env: + TF_VAR_environment: prd TF_VAR_posthog_api_key: ${{ vars.POSTHOG_API_KEY }} TF_VAR_lambda_layer_arn: ${{ needs.build-layer.outputs.layer_arn }} steps: @@ -102,7 +179,7 @@ jobs: working-directory: infra run: | { - echo '## Terraform plan' + echo '## Terraform plan (prd)' echo '```' tofu show -no-color tfplan echo '```' @@ -122,9 +199,9 @@ jobs: # Manual gate: the `prd` environment has required reviewers, so this job waits # for approval and shows as "deploying to prd". It applies the exact plan - # reviewed above (no re-plan). + # reviewed above (no re-plan). Staging must already be live. apply: - needs: [plan, build-frontend] + needs: [plan, build-frontend, deploy-stg] runs-on: ubuntu-latest environment: prd permissions: @@ -146,7 +223,7 @@ jobs: - uses: actions/download-artifact@v7 with: - name: frontend-dist + name: frontend-dist-prd path: frontend/dist/ - uses: opentofu/setup-opentofu@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1fb8ebd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + workflow_call: + +jobs: + pytest: + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + env: + AWS_DEFAULT_REGION: eu-west-2 + AWS_ACCESS_KEY_ID: testing + AWS_SECRET_ACCESS_KEY: testing + steps: + - uses: actions/checkout@v6 + + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b + + - name: Run tests + run: uv run --group dev pytest diff --git a/README.md b/README.md index df6dc89..d5b3f9d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Fully serverless on AWS, running at roughly $1-5/month. - **Scheduling** — one EventBridge Schedule per user (rate 24h) feeds the SQS queue automatically; created on signup and deleted on account deletion or revoked Spotify access - **Database** — DynamoDB; sync request history expires after 1 year via TTL - **IaC** — Terraform in `infra/` -- **CI/CD** — GitHub Actions deploys on push to `main` using OIDC (no stored AWS keys); frontend and Lambda layer builds run in parallel +- **CI/CD** — PRs run `pytest`. Push to `main` auto-deploys staging, then production waits on the `prd` GitHub Environment approval. OIDC, no stored AWS keys. ## Deploying your own instance @@ -41,7 +41,7 @@ Create an S3 bucket in your target region for Terraform state, then update the ` ### 2. Create a GitHub Actions deploy role -Create an IAM role trusted by GitHub Actions OIDC (`token.actions.githubusercontent.com`) and scoped to your repository and the `prd` environment. Attach the following AWS managed policies: +Create an IAM role trusted by GitHub Actions OIDC (`token.actions.githubusercontent.com`) and scoped to your repository. The production apply job uses the `prd` environment; staging auto-apply uses `stg`. The deploy role's trust policy must allow both `repo:OWNER/REPO:environment:prd` and `repo:OWNER/REPO:environment:stg`. Attach the following AWS managed policies: - `AWSLambda_FullAccess` - `AmazonAPIGatewayAdministrator` @@ -58,7 +58,7 @@ For IAM (needed to manage Lambda execution roles), attach a custom policy scoped ### 3. Configure GitHub environment secrets -Create a `prd` environment in your GitHub repo settings and add the following secrets: +Create a `prd` environment (required reviewers) and a `stg` environment (no reviewers) in your GitHub repo settings. Add the following to repository variables / the `prd` environment as you already do: | Secret | Description | |---|---| @@ -70,7 +70,11 @@ Create a `prd` environment in your GitHub repo settings and add the following se ### 4. Deploy -Push to `main`. The workflow builds the frontend (with PostHog key baked in as a `VITE_` variable) and the Lambda dependency layer in parallel, then runs `terraform apply`, syncs the frontend to S3, and invalidates the CloudFront cache. +Push to `main`. GitHub Actions runs the test suite, publishes a Lambda layer, auto-applies the **staging** stack (`syncify-stg-*`, backend key `syncify-stg/terraform.tfstate`), and syncs a PostHog-free frontend to the staging bucket. Production uses the same commit: it plans against the prod state, then waits for `prd` environment approval before apply and a PostHog-keyed frontend sync. + +PRs run tests only; they do not deploy. + +`scripts/deploy-stg.sh` is still available for a local staging push from a dirty tree. ### 5. First-time setup after deploy diff --git a/scripts/deploy-stg.sh b/scripts/deploy-stg.sh index d2d948b..3796292 100755 --- a/scripts/deploy-stg.sh +++ b/scripts/deploy-stg.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash -# Deploy the current working tree to the syncify-stg-* stack: +# Deploy the current working tree to the syncify-stg-* stack. +# Staging also auto-deploys from GitHub Actions on push to main; use this script +# for a dirty local tree. # 1. tofu apply against the staging state key (in-place updates to the two Lambdas) # 2. build the frontend (no PostHog so stg events don't pollute prod analytics) # 3. sync the dist/ bundle to the stg SPA bucket and invalidate CloudFront