Skip to content

Commit cda2363

Browse files
cdk infra updates
1 parent 0eaf26f commit cda2363

24 files changed

Lines changed: 1829 additions & 260 deletions

.github/CI-CD.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
[//]: # (.github/CI-CD.md)
2+
3+
## CI/CD & Automation
4+
5+
This project uses GitHub Actions to build, validate, and deploy a static Next.js site to AWS (S3 + CloudFront).
6+
7+
### Workflow Layout
8+
9+
| Workflow | File | Trigger | Purpose |
10+
|----------|------|---------|---------|
11+
| CI | [`ci.yml`](workflows/ci.yml) | Push, pull request, manual dispatch | Orchestrates build, Lighthouse, and gated deploy |
12+
| Build Static Site | [`build-static-site.yml`](workflows/build-static-site.yml) | `workflow_call` | Builds the site and uploads the deployable artifact |
13+
| Lighthouse Static Site | [`lighthouse-static-site.yml`](workflows/lighthouse-static-site.yml) | `workflow_call` | Runs Lighthouse against the uploaded build artifact |
14+
| Deploy Static Site | [`deploy-static-site.yml`](workflows/deploy-static-site.yml) | `workflow_call` | Downloads the artifact, deploys to AWS, invalidates CloudFront, and tags the release |
15+
16+
### CI Flow
17+
18+
The top-level CI workflow builds once, validates that exact artifact with Lighthouse, and only then deploys it on `master`.
19+
20+
| Job | Description |
21+
|-----|-------------|
22+
| Build | Installs dependencies, runs `pnpm build`, and uploads `dist/` as an artifact |
23+
| Lighthouse | Downloads the `dist/` artifact and runs `npx @lhci/cli autorun` |
24+
| Deploy | Downloads the same `dist/` artifact, syncs it to S3, invalidates CloudFront, and creates a semver deployment tag |
25+
26+
### Reuse Across Repos
27+
28+
The reusable workflows in `.github/workflows/` are designed to be called by another repo with the same stack. The caller should:
29+
30+
1. Standardize on the same GitHub secret and variable names.
31+
2. Pass repo-specific values such as `app_url` and `cdk_stack_name` as workflow inputs.
32+
3. Pin external reusable workflow references to a commit SHA when another repo starts calling them.
33+
34+
### Deployment Behavior
35+
36+
Deploy runs only after both Build and Lighthouse succeed.
37+
38+
| Step | Description |
39+
|------|-------------|
40+
| Configure AWS | OIDC authentication via `AWS_DEPLOY_ROLE_ARN` |
41+
| Read CDK stack outputs | Fetches the S3 bucket name and CloudFront distribution ID from CloudFormation |
42+
| Sync to S3 | `aws s3 sync dist/ s3://<bucket> --delete` |
43+
| Invalidate CloudFront | Creates a `/*` invalidation |
44+
| Tag deployment | Auto-increments the semver patch version on successful deploy |
45+
46+
### Versioning
47+
48+
Deployments are tagged with semver versions that auto-increment on each successful deploy.
49+
50+
| Action | Example |
51+
|--------|---------|
52+
| Automatic (every deploy) | `v1.0.0` -> `v1.0.1` -> `v1.0.2` |
53+
| Manual minor bump | `git tag v1.1.0 && git push origin v1.1.0` |
54+
| Manual major bump | `git tag v2.0.0 && git push origin v2.0.0` |
55+
56+
List all deployment versions:
57+
58+
```bash
59+
git tag -l 'v*' --sort=-v:refname
60+
```
61+
62+
### Required Secrets & Variables
63+
64+
#### Secrets
65+
66+
| Secret | Required | Purpose |
67+
|--------|----------|---------|
68+
| `AWS_DEPLOY_ROLE_ARN` | Yes | IAM role ARN for GitHub OIDC authentication |
69+
| `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME` | Yes | Cloudinary cloud name embedded in the static build |
70+
71+
#### Variables / Environment
72+
73+
| Variable | Required | Default | Purpose |
74+
|----------|----------|---------|---------|
75+
| `AWS_REGION` | No | `us-east-2` | AWS region for deployment and public client config |
76+
| `NEXT_PUBLIC_APP_URL` | No | `http://localhost:3000` | Site URL embedded in static output |
77+
| `NEXT_PUBLIC_AWS_REGION` | No | `us-east-2` | AWS region exposed to the client |
78+
| `NEXT_PUBLIC_CF_ANALYTICS_TOKEN` | No || Cloudflare Web Analytics token |
79+
| `NEXT_PUBLIC_CW_RUM_APP_MONITOR_ID` | No || CloudWatch RUM App Monitor ID |
80+
| `NEXT_PUBLIC_CW_RUM_IDENTITY_POOL_ID` | No || CloudWatch RUM Cognito Identity Pool ID |
81+
| `NEXT_PUBLIC_SENTRY_DSN` | No || Sentry DSN |
82+
83+
Environment variable schemas are validated at build time by [`src/buildEnv.ts`](../src/buildEnv.ts) and [`src/clientEnv.ts`](../src/clientEnv.ts).
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
name: 'Setup Node.js and PNPM'
2-
description: 'Sets up Node.js and PNPM based on project configuration files'
1+
# .github/actions/setup-node-pnpm/action.yml
2+
3+
name: Setup Node.js and PNPM
4+
description: Sets up Node.js and PNPM based on project configuration
35

46
runs:
5-
using: "composite"
7+
using: composite
68
steps:
7-
- uses: pnpm/action-setup@v4
9+
- name: Setup PNPM
10+
uses: pnpm/action-setup@v4
811

9-
- uses: actions/setup-node@v4
12+
- name: Setup Node.js
13+
uses: actions/setup-node@v6
1014
with:
11-
node-version-file: '.nvmrc'
12-
cache: 'pnpm'
15+
node-version-file: .nvmrc
16+
cache: pnpm
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Tag deployment
2+
description: Find the latest semantic deployment tag, bump it, and push the new tag
3+
4+
inputs:
5+
tag-prefix:
6+
description: Prefix for deployment tags
7+
required: false
8+
default: "v"
9+
10+
initial-version:
11+
description: Version to start from when no matching tags exist
12+
required: false
13+
default: "0.0.0"
14+
15+
bump:
16+
description: Which semver component to bump (major, minor, patch)
17+
required: false
18+
default: "patch"
19+
20+
remote:
21+
description: Git remote to push the tag to
22+
required: false
23+
default: "origin"
24+
25+
outputs:
26+
new-tag:
27+
description: The newly created tag
28+
value: ${{ steps.tag.outputs.new-tag }}
29+
30+
runs:
31+
using: composite
32+
steps:
33+
- name: Run tag deployment script
34+
id: tag
35+
shell: bash
36+
env:
37+
INPUT_TAG_PREFIX: ${{ inputs.tag-prefix }}
38+
INPUT_INITIAL_VERSION: ${{ inputs.initial-version }}
39+
INPUT_BUMP: ${{ inputs.bump }}
40+
INPUT_REMOTE: ${{ inputs.remote }}
41+
run: |
42+
bash "${{ github.action_path }}/tag-deployment.sh"
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
TAG_PREFIX="${INPUT_TAG_PREFIX:-v}"
5+
INITIAL_VERSION="${INPUT_INITIAL_VERSION:-0.0.0}"
6+
BUMP="${INPUT_BUMP:-patch}"
7+
REMOTE="${INPUT_REMOTE:-origin}"
8+
9+
log_info() {
10+
echo ""
11+
echo "==> $1"
12+
}
13+
14+
log_success() {
15+
echo "$1"
16+
}
17+
18+
log_warn() {
19+
echo "⚠️ $1"
20+
}
21+
22+
log_error() {
23+
echo "ERROR: $1" >&2
24+
}
25+
26+
fetch_tags() {
27+
log_info "Fetching tags from remote '$REMOTE'"
28+
git fetch --force --tags "$REMOTE"
29+
log_success "Tags fetched successfully"
30+
}
31+
32+
get_latest_tag() {
33+
git tag -l "${TAG_PREFIX}*" --sort=-v:refname | head -n 1
34+
}
35+
36+
resolve_latest_tag() {
37+
local latest_tag
38+
latest_tag="$(get_latest_tag)"
39+
40+
if [ -z "$latest_tag" ]; then
41+
log_warn "No existing tags found with prefix '${TAG_PREFIX}'"
42+
latest_tag="${TAG_PREFIX}${INITIAL_VERSION}"
43+
echo "$latest_tag"
44+
return
45+
fi
46+
47+
echo "$latest_tag"
48+
}
49+
50+
extract_version() {
51+
local tag="$1"
52+
echo "${tag#${TAG_PREFIX}}"
53+
}
54+
55+
validate_version() {
56+
local version="$1"
57+
58+
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
59+
log_error "Latest matching tag version '$version' is not valid semver"
60+
exit 1
61+
fi
62+
}
63+
64+
bump_version() {
65+
local version="$1"
66+
local major minor patch
67+
68+
IFS='.' read -r major minor patch <<< "$version"
69+
70+
case "$BUMP" in
71+
major)
72+
major=$((major + 1))
73+
minor=0
74+
patch=0
75+
;;
76+
minor)
77+
minor=$((minor + 1))
78+
patch=0
79+
;;
80+
patch)
81+
patch=$((patch + 1))
82+
;;
83+
*)
84+
log_error "Unsupported bump type '$BUMP'. Use: major, minor, or patch."
85+
exit 1
86+
;;
87+
esac
88+
89+
echo "${major}.${minor}.${patch}"
90+
}
91+
92+
ensure_tag_does_not_exist() {
93+
local new_tag="$1"
94+
95+
if git rev-parse "$new_tag" >/dev/null 2>&1; then
96+
log_error "Tag '$new_tag' already exists"
97+
exit 1
98+
fi
99+
}
100+
101+
create_and_push_tag() {
102+
local new_tag="$1"
103+
104+
log_info "Creating tag '$new_tag'"
105+
git tag "$new_tag"
106+
107+
log_info "Pushing tag '$new_tag' to remote '$REMOTE'"
108+
git push "$REMOTE" "$new_tag"
109+
110+
log_success "Tagged deployment as $new_tag"
111+
}
112+
113+
write_outputs() {
114+
local new_tag="$1"
115+
116+
if [ -n "${GITHUB_OUTPUT:-}" ]; then
117+
echo "new-tag=$new_tag" >> "$GITHUB_OUTPUT"
118+
log_success "Exported output new-tag=$new_tag"
119+
else
120+
log_warn "GITHUB_OUTPUT is not set; skipping GitHub Actions output export"
121+
fi
122+
}
123+
124+
print_summary() {
125+
local latest_tag="$1"
126+
local new_tag="$2"
127+
128+
echo ""
129+
echo "Tag deployment summary"
130+
echo "----------------------"
131+
echo "Remote: $REMOTE"
132+
echo "Prefix: $TAG_PREFIX"
133+
echo "Bump type: $BUMP"
134+
echo "Previous tag: $latest_tag"
135+
echo "New tag: $new_tag"
136+
}
137+
138+
main() {
139+
local latest_tag version bumped_version new_tag
140+
141+
log_info "Starting deployment tagging"
142+
echo "Remote: $REMOTE"
143+
echo "Tag prefix: $TAG_PREFIX"
144+
echo "Initial version: $INITIAL_VERSION"
145+
echo "Bump type: $BUMP"
146+
147+
fetch_tags
148+
149+
log_info "Resolving latest tag"
150+
latest_tag="$(resolve_latest_tag)"
151+
echo "Latest tag: $latest_tag"
152+
153+
version="$(extract_version "$latest_tag")"
154+
validate_version "$version"
155+
156+
log_info "Calculating next version from '$version'"
157+
bumped_version="$(bump_version "$version")"
158+
new_tag="${TAG_PREFIX}${bumped_version}"
159+
echo "Next tag: $new_tag"
160+
161+
ensure_tag_does_not_exist "$new_tag"
162+
create_and_push_tag "$new_tag"
163+
write_outputs "$new_tag"
164+
print_summary "$latest_tag" "$new_tag"
165+
}
166+
167+
main "$@"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Build Static Site
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
app_url:
7+
description: Public site URL embedded in the build output.
8+
required: true
9+
type: string
10+
artifact_name:
11+
description: Name for the uploaded build artifact.
12+
required: false
13+
type: string
14+
default: dist
15+
aws_region:
16+
description: AWS region exposed to the client build.
17+
required: false
18+
type: string
19+
default: us-east-2
20+
secrets:
21+
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME:
22+
description: Cloudinary cloud name used in the static build.
23+
required: true
24+
25+
permissions:
26+
contents: read
27+
28+
jobs:
29+
build:
30+
name: Build
31+
runs-on: ubuntu-latest
32+
env:
33+
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: ${{ secrets.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME }}
34+
NEXT_PUBLIC_APP_URL: ${{ inputs.app_url }}
35+
NEXT_PUBLIC_AWS_REGION: ${{ inputs.aws_region }}
36+
NEXT_PUBLIC_CF_ANALYTICS_TOKEN: ${{ vars.NEXT_PUBLIC_CF_ANALYTICS_TOKEN }}
37+
NEXT_PUBLIC_CW_RUM_APP_MONITOR_ID: ${{ vars.NEXT_PUBLIC_CW_RUM_APP_MONITOR_ID }}
38+
NEXT_PUBLIC_CW_RUM_IDENTITY_POOL_ID: ${{ vars.NEXT_PUBLIC_CW_RUM_IDENTITY_POOL_ID }}
39+
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
- uses: pnpm/action-setup@v4
44+
45+
- uses: actions/setup-node@v6
46+
with:
47+
node-version-file: .nvmrc
48+
cache: pnpm
49+
50+
- run: pnpm install --frozen-lockfile
51+
52+
- name: Build
53+
run: pnpm build
54+
55+
- uses: actions/upload-artifact@v4
56+
with:
57+
name: ${{ inputs.artifact_name }}
58+
path: dist/
59+
retention-days: 1

0 commit comments

Comments
 (0)