Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 87 additions & 10 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -102,7 +179,7 @@ jobs:
working-directory: infra
run: |
{
echo '## Terraform plan'
echo '## Terraform plan (prd)'
echo '```'
tofu show -no-color tfplan
echo '```'
Expand All @@ -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:
Expand All @@ -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
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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`
Expand All @@ -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 |
|---|---|
Expand All @@ -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

Expand Down
4 changes: 3 additions & 1 deletion scripts/deploy-stg.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading