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
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
dist
.git
.env
infra
*.log
.tap
.DS_Store
38 changes: 27 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# MyArtverse Backend Config
MA_PORT=8081
MA_INTERFACE=0.0.0.0

# Frontend URL (production — Vercel)
# FRONTEND_URL=https://dev.myartverse.app
# FRONTEND_ORIGINS=https://dev.myartverse.app,https://dev.myartverse.dev

# Frontend parts (local dev fallback)
MA_FRONTEND_DOMAIN=localhost
MA_FRONTEND_PORT=3000
MA_FRONTEND_HTTP=http://

# Cookie domain — scoped to API host (not the Vercel frontend domain)
# COOKIE_DOMAIN=.myartverse.app

MA_JWT_SECRET=JWT_SECRET
MA_COOKIE_SECRET=cookienomnom
MA_SESSION_SECRET=sessionnomnomsessionnomnomsessionnomnomsessionnomnom
Expand All @@ -16,24 +26,27 @@ DB_PASS=postgres
DB_HOST=localhost
DB_PORT=5432

# Email Config
# Email transport: smtp (local MailSlurper) or resend
EMAIL_TRANSPORT=resend

# Resend (production)
RESEND_API_KEY=
RESEND_FROM_EMAIL=MyArtverse <onboarding@resend.dev>

# SMTP (local dev — MailSlurper UI at http://localhost:8085)
SMTP_EMAIL_HOST=127.0.0.1
SMTP_EMAIL_PORT=2500
SMTP_EMAIL_SSL=false
SMTP_EMAIL_FROM=myartverse@myartverse.com
# Leave blank if using Mailslurp
SMTP_EMAIL_USER=
SMTP_EMAIL_PASS=

# AWS S3 Config
# Get these keys through MinIO from the MinIO Console (http://localhost:9000) or your cloud provider.
# AWS S3 — local dev uses MinIO; production uses IAM role (no keys, no endpoint)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# These values are for local development with MinIO, change these for production deployment
AWS_DEFAULT_REGION="us-east-1"
# These values are for local development with MinIO, change these for production deployment
S3_ENDPOINT="http://localhost:9000"
AWS_DEFAULT_REGION=us-east-1
S3_ENDPOINT=http://localhost:9000
S3_BUCKET=
# Production CDN base URL (CloudFront)
# S3_PUBLIC_URL=https://cdn.myartverse.app

GOOGLE_CLIENT_ID=VALUE_GO_HERE
GOOGLE_CLIENT_SECRET=VALUE_GO_HERE
Expand All @@ -53,4 +66,7 @@ TIKTOK_REDIRECT_URI=http://INTERFACE:PORT/v1/auth/tiktok/callback

API_BASE_URL=http://INTERFACE:PORT


# Sentry (production error monitoring — optional locally)
# SENTRY_DSN=https://xxx@xxx.ingest.us.sentry.io/xxx
# SENTRY_ENVIRONMENT=production
# SENTRY_TRACES_SAMPLE_RATE=0.1
182 changes: 182 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# GitHub Actions

Automated CI and deploy for the MyArtverse API.

## Workflows

| Workflow | Trigger | Purpose |
|----------|---------|---------|
| **CI** | PR + push to `prod` / `main` | API build + lint; CDK synth |
| **Deploy API** | Push to `prod` (app paths) or manual | Docker → ECR → EC2 |
| **Deploy Infrastructure** | Manual only | CDK stack update |

## Setup (one time)

### 1. GitHub secrets

**Settings → Secrets and variables → Actions → New repository secret**

| Secret | Description |
|--------|-------------|
| `AWS_ACCESS_KEY_ID` | IAM user access key |
| `AWS_SECRET_ACCESS_KEY` | IAM user secret key |

### 2. IAM user

Create user `github-actions-myartverse` in IAM. Attach **one** of the policies below.

#### Policy A — Deploy API only (recommended)

Use this if you only run **Deploy API** from GitHub.

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ECRLogin",
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
},
{
"Sid": "ECRPush",
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Resource": "arn:aws:ecr:us-east-2:414061810268:repository/myartverse-api"
},
{
"Sid": "StackOutputs",
"Effect": "Allow",
"Action": "cloudformation:DescribeStacks",
"Resource": "arn:aws:cloudformation:us-east-2:414061810268:stack/MyArtverseStack/*"
},
{
"Sid": "DeployOnEc2",
"Effect": "Allow",
"Action": [
"ssm:SendCommand",
"ssm:GetCommandInvocation",
"ssm:ListCommandInvocations"
],
"Resource": "*"
},
{
"Sid": "CloudFrontCdnUrl",
"Effect": "Allow",
"Action": "cloudfront:GetDistribution",
"Resource": "arn:aws:cloudfront::414061810268:distribution/*"
}
]
}
```

Replace `414061810268` with your AWS account ID if different.

#### Policy B — Deploy Infrastructure (CDK)

Add this **in addition to Policy A** only if you run **Deploy Infrastructure** from Actions. Prefer running CDK from your laptop with admin/SSO instead.

```json
{
"Sid": "CDKDeploy",
"Effect": "Allow",
"Action": [
"cloudformation:*",
"iam:*",
"ec2:*",
"rds:*",
"s3:*",
"elasticloadbalancing:*",
"acm:*",
"secretsmanager:*",
"ecr:*",
"logs:*",
"cloudfront:*",
"ssm:*",
"route53:*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-2"
}
}
}
```

### 3. App secrets (not GitHub)

Runtime config (JWT, Sentry, Resend, OAuth) lives in **AWS Secrets Manager** (`myartverse/app/config`). Deploy API refreshes EC2 `.env` from there — no GitHub secrets needed for those.

## Usage

### Automatic API deploy

Merge or push to **`prod`** when these paths change:

- `src/**`, `Dockerfile`, `package.json`, `yarn.lock`, `infra/scripts/**`, etc.

```bash
git push origin prod
```

### Manual API deploy

**Actions → Deploy API → Run workflow**

Optional inputs:

- `frontend_url` — default `https://dev.myartverse.app`
- `frontend_domain` — default `dev.myartverse.app`

### Manual infra deploy

**Actions → Deploy Infrastructure → Run workflow**

Optional inputs:

- `certificate_arn` — ACM cert for HTTPS on ALB
- `api_hostname` — `api.myartverse.app`
- `cookie_domain` — `.myartverse.app`
- `frontend_domain` — `dev.myartverse.app`

## What each workflow does

### CI

- `yarn build` + `yarn lint` on the API
- `cdk synth` on infra (validates CloudFormation without deploying)

### Deploy API

1. Configure AWS credentials from GitHub secrets
2. Run `infra/scripts/deploy-api.sh`:
- Build Docker image on the runner
- Push to ECR
- SSM command on EC2: refresh `.env`, pull image, restart container
3. Curl `/health` until OK

### Deploy Infrastructure

1. `npm ci` in `infra/`
2. `cdk deploy MyArtverseStack` with context from workflow inputs

## Troubleshooting

| Failure | Likely cause |
|---------|----------------|
| `Credentials could not be loaded` | Missing or wrong GitHub secrets |
| `AccessDenied` on `ecr:*` | IAM Policy A incomplete |
| `AccessDenied` on `ssm:*` | Missing SSM permissions |
| SSM command failed | Check EC2 instance id in stack; SSM agent running |
| Health check failed | Container crash — `docker logs myartverse-api` on EC2 |
| CI CDK synth fails | Run `npm ci` in `infra/` locally; fix TypeScript errors |
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI

on:
pull_request:
branches: [prod, main]
push:
branches: [prod, main]

jobs:
api:
name: API build & lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: yarn

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Build
run: yarn build

- name: Lint
run: yarn lint

infra:
name: CDK synth
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: infra/package-lock.json

- name: Install CDK dependencies
run: npm ci

- name: Synth CloudFormation template
run: npx cdk synth --quiet
77 changes: 77 additions & 0 deletions .github/workflows/deploy-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Deploy API

on:
push:
branches: [prod]
paths:
- "src/**"
- "Dockerfile"
- ".dockerignore"
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "infra/scripts/**"
- ".github/workflows/deploy-api.yml"
workflow_dispatch:
inputs:
frontend_url:
description: "Frontend origin for CORS and email links"
required: false
default: https://dev.myartverse.app
frontend_domain:
description: "Frontend hostname (no protocol)"
required: false
default: dev.myartverse.app

concurrency:
group: deploy-api-${{ github.ref }}
cancel-in-progress: true

env:
AWS_REGION: us-east-2
FRONTEND_URL: https://dev.myartverse.app
FRONTEND_DOMAIN: dev.myartverse.app

permissions:
contents: read

jobs:
deploy:
name: Build, push, and deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}

- name: Deploy to EC2 via ECR
env:
FRONTEND_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.frontend_url || 'https://dev.myartverse.app' }}
FRONTEND_DOMAIN: ${{ github.event_name == 'workflow_dispatch' && inputs.frontend_domain || 'dev.myartverse.app' }}
run: |
chmod +x infra/scripts/deploy-api.sh infra/scripts/refresh-env.sh
./infra/scripts/deploy-api.sh

- name: Health check
run: |
API_URL=$(aws cloudformation describe-stacks \
--stack-name MyArtverseStack \
--region "$AWS_REGION" \
--query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" \
--output text)
echo "Checking $API_URL/health"
for i in 1 2 3 4 5; do
if curl -fsS "$API_URL/health" | grep -q '"status":"ok"'; then
echo "Health check passed"
exit 0
fi
echo "Attempt $i failed, retrying..."
sleep 10
done
echo "Health check failed"
exit 1
Loading
Loading