|
| 1 | +[//]: # (.github/CI-CD.md) |
| 2 | + |
| 3 | +## CI/CD & Automation |
| 4 | + |
| 5 | +This repo uses GitHub Actions to build, audit, and deploy a static Next.js export to AWS S3 + CloudFront. The deploy workflow assumes the infrastructure stack already exists and reads its deploy targets from CloudFormation outputs. |
| 6 | + |
| 7 | +### Workflow Layout |
| 8 | + |
| 9 | +| Workflow | File | Trigger | Purpose | |
| 10 | +|----------|------|---------|---------| |
| 11 | +| CI | [`./workflows/ci.yml`](./workflows/ci.yml) | Push to `master`, pull request to `master`, manual dispatch | Orchestrates Actionlint, build, Lighthouse, and gated deploy | |
| 12 | +| Lint GitHub Actions | [`./workflows/lint-github-actions.yml`](./workflows/lint-github-actions.yml) | `workflow_call` | Installs pinned `actionlint` and validates workflow files | |
| 13 | +| Build Static Site | [`./workflows/build-static-site.yml`](./workflows/build-static-site.yml) | `workflow_call` | Builds the static site and uploads the deployable `dist/` artifact | |
| 14 | +| Lighthouse Static Site | [`./workflows/lighthouse-static-site.yml`](./workflows/lighthouse-static-site.yml) | `workflow_call` | Downloads the build artifact and runs Lighthouse CI against it | |
| 15 | +| Deploy Static Site | [`./workflows/deploy-static-site.yml`](./workflows/deploy-static-site.yml) | `workflow_call` | Downloads the artifact, deploys it to AWS, invalidates CloudFront, and creates the next deployment tag | |
| 16 | + |
| 17 | +### Live CI Flow |
| 18 | + |
| 19 | +The top-level CI workflow in [`./workflows/ci.yml`](./workflows/ci.yml) wires the reusable workflows together with the current repo defaults: |
| 20 | + |
| 21 | +| Setting | Current value | |
| 22 | +|---------|---------------| |
| 23 | +| `AWS_REGION` | GitHub repo variable `vars.AWS_REGION` | |
| 24 | +| `CDK_STACK_NAME` | GitHub repo variable `vars.CDK_STACK_NAME` | |
| 25 | +| `APP_URL` | GitHub repo variable `vars.APP_URL` | |
| 26 | +| `ARTIFACT_NAME` | `dist` | |
| 27 | +| `TAG_PREFIX` | `v` | |
| 28 | + |
| 29 | +Jobs run in this order: |
| 30 | + |
| 31 | +| Job | What it does | |
| 32 | +|-----|--------------| |
| 33 | +| Actionlint | Checks out the repo, installs `actionlint` `v1.7.11` from the official release, and validates `.github/workflows/*.yml` | |
| 34 | +| Build | Checks out the repo, installs PNPM, sets up Node from [`.nvmrc`](../.nvmrc), runs `pnpm install --frozen-lockfile`, runs `pnpm build`, and uploads `dist/` | |
| 35 | +| Lighthouse | Downloads the same `dist/` artifact and runs `npx @lhci/cli autorun` | |
| 36 | +| Deploy | Runs only after Actionlint, Build, and Lighthouse succeed, and only for `push` or `workflow_dispatch` on `refs/heads/master` | |
| 37 | + |
| 38 | +`actionlint` is intentionally scoped to workflow files only. It does not validate repo-local composite action metadata under `.github/actions/`. |
| 39 | + |
| 40 | +### GitHub Actions Linting |
| 41 | + |
| 42 | +Workflow linting is defined in [`./workflows/lint-github-actions.yml`](./workflows/lint-github-actions.yml). |
| 43 | + |
| 44 | +| Step | What it does | |
| 45 | +|------|--------------| |
| 46 | +| Checkout | Checks out the repo so `actionlint` can inspect all local workflow files and reusable workflow calls | |
| 47 | +| Install actionlint | Downloads the pinned official release archive for the current Linux runner architecture and adds the extracted binary to `PATH` | |
| 48 | +| Run actionlint | Runs `actionlint` from repo root with default project discovery | |
| 49 | + |
| 50 | +### Build Artifact |
| 51 | + |
| 52 | +The deploy artifact is the static export in `dist/`. |
| 53 | + |
| 54 | +- [`next.config.mjs`](../next.config.mjs) sets `output: 'export'` and `distDir: 'dist'`. |
| 55 | +- [`package.json`](../package.json) defines `pnpm build` as `tsx scripts/generate-og-images.tsx && next build --webpack`. |
| 56 | +- [`scripts/generate-og-images.tsx`](../scripts/generate-og-images.tsx) generates OG images into `public/og` before the static export runs. |
| 57 | + |
| 58 | +### Lighthouse Behavior |
| 59 | + |
| 60 | +Lighthouse CI is configured by [`.lighthouserc.js`](../.lighthouserc.js). |
| 61 | + |
| 62 | +| Setting | Current value | |
| 63 | +|---------|---------------| |
| 64 | +| `staticDistDir` | `./dist` | |
| 65 | +| URLs audited | `http://localhost/index.html`, `http://localhost/blog/index.html` | |
| 66 | +| `numberOfRuns` | `3` | |
| 67 | +| Upload target | `temporary-public-storage` | |
| 68 | + |
| 69 | +Current assertions: |
| 70 | + |
| 71 | +| Category | Level | Minimum score | |
| 72 | +|----------|-------|---------------| |
| 73 | +| Performance | `warn` | `0.9` | |
| 74 | +| Accessibility | `error` | `0.9` | |
| 75 | +| Best Practices | `warn` | `0.9` | |
| 76 | +| SEO | `warn` | `0.9` | |
| 77 | + |
| 78 | +The workflow always uploads `.lighthouseci/` as the `lighthouse-results` artifact, even when the job fails. |
| 79 | + |
| 80 | +### Deployment Behavior |
| 81 | + |
| 82 | +Deploy is defined in [`./workflows/deploy-static-site.yml`](./workflows/deploy-static-site.yml). |
| 83 | + |
| 84 | +| Step | What it does | |
| 85 | +|------|--------------| |
| 86 | +| Checkout | Checks out the repo with `fetch-depth: 0` so tags are available | |
| 87 | +| Download artifact | Downloads the `dist/` artifact into `dist/` | |
| 88 | +| Configure AWS | Uses GitHub OIDC with `AWS_DEPLOY_ROLE_ARN` via `aws-actions/configure-aws-credentials@v4` | |
| 89 | +| Read stack outputs | Calls `aws cloudformation describe-stacks` for `cdk_stack_name` and reads `BucketName` and `DistributionId` | |
| 90 | +| Sync to S3 | Runs `aws s3 sync dist/ "s3://$BUCKET" --delete` | |
| 91 | +| Invalidate CloudFront | Runs `aws cloudfront create-invalidation --distribution-id "$DIST_ID" --paths "/*"` | |
| 92 | +| Tag deployment | Fetches tags, finds the latest matching semver tag, bumps the patch version, creates the new tag, and pushes it | |
| 93 | + |
| 94 | +Additional live deploy behavior: |
| 95 | + |
| 96 | +- Deploys are serialized with the `production-deploy` concurrency group. |
| 97 | +- `cancel-in-progress` is `false`. |
| 98 | +- This workflow deploys static assets only. It does not run `cdk deploy` or update infrastructure. |
| 99 | + |
| 100 | +### Versioning |
| 101 | + |
| 102 | +Successful deploys create the next patch tag matching `TAG_PREFIX`. |
| 103 | + |
| 104 | +| Example | Result | |
| 105 | +|---------|--------| |
| 106 | +| No existing tags | `v0.0.1` | |
| 107 | +| Latest tag is `v1.0.0` | Next deploy creates `v1.0.1` | |
| 108 | +| Latest tag is `v1.4.9` | Next deploy creates `v1.4.10` | |
| 109 | + |
| 110 | +If you manually create a higher semver tag, future deploys continue patch bumps from that latest tag. |
| 111 | + |
| 112 | +List deployment tags: |
| 113 | + |
| 114 | +```bash |
| 115 | +git tag -l 'v*' --sort=-v:refname |
| 116 | +``` |
| 117 | + |
| 118 | +### Inputs, Secrets, and Repo Variables |
| 119 | + |
| 120 | +Reusable workflow interface: |
| 121 | + |
| 122 | +| Workflow | Inputs | Required secrets | |
| 123 | +|----------|--------|------------------| |
| 124 | +| Lint GitHub Actions | None | None | |
| 125 | +| Build Static Site | `app_url`, `artifact_name` (default `dist`), `aws_region` | `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME` | |
| 126 | +| Lighthouse Static Site | `artifact_name` (default `dist`) | None | |
| 127 | +| Deploy Static Site | `artifact_name` (default `dist`), `aws_region`, `cdk_stack_name`, `tag_prefix` (default `v`) | `AWS_DEPLOY_ROLE_ARN` | |
| 128 | + |
| 129 | +Repo-level values used by the live pipeline: |
| 130 | + |
| 131 | +| Kind | Name | Required | Used by | Purpose | |
| 132 | +|------|------|----------|---------|---------| |
| 133 | +| Secret | `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME` | Yes | Build | Cloudinary cloud name embedded into the static site | |
| 134 | +| Secret | `AWS_DEPLOY_ROLE_ARN` | Yes | Deploy | IAM role ARN used for GitHub OIDC authentication | |
| 135 | +| Variable | `AWS_REGION` | Yes | Build, Deploy | Shared AWS region used by the reusable workflows | |
| 136 | +| Variable | `NEXT_PUBLIC_CF_ANALYTICS_TOKEN` | No | Build | Optional Cloudflare Web Analytics token | |
| 137 | +| Variable | `NEXT_PUBLIC_CW_RUM_APP_MONITOR_ID` | No | Build | Optional CloudWatch RUM App Monitor ID | |
| 138 | +| Variable | `NEXT_PUBLIC_CW_RUM_IDENTITY_POOL_ID` | No | Build | Optional CloudWatch RUM Cognito Identity Pool ID | |
| 139 | +| Variable | `NEXT_PUBLIC_SENTRY_DSN` | No | Build | Optional Sentry DSN | |
| 140 | + |
| 141 | +Environment validation in the app: |
| 142 | + |
| 143 | +- [`src/clientEnv.ts`](../src/clientEnv.ts) validates the `NEXT_PUBLIC_*` values used by the app. |
| 144 | +- [`src/buildEnv.ts`](../src/buildEnv.ts) currently defines only `AWS_REGION` and requires it explicitly if the module is imported into future build-time code. |
| 145 | + |
| 146 | +### First Infrastructure Deploy |
| 147 | + |
| 148 | +The GitHub Actions pipeline deploys static assets only. The first `cdk deploy` stays manual and is documented here: |
| 149 | + |
| 150 | +- Runbook: [`../infrastructure/INITIAL_DEPLOYMENT.md`](../infrastructure/INITIAL_DEPLOYMENT.md) |
| 151 | +- Helper script: [`../scripts/initial-aws-deploy.sh`](../scripts/initial-aws-deploy.sh) |
| 152 | +- Additional local env required for the first infrastructure deploy: |
| 153 | + - `AWS_REGION` |
| 154 | + - `CLOUDFRONT_CERTIFICATE_REGION` |
| 155 | + |
| 156 | +### Unused Repo-Local Helpers |
| 157 | + |
| 158 | +These files exist in the repo but are not used by the current workflows: |
| 159 | + |
| 160 | +| Helper | File | Notes | |
| 161 | +|--------|------|-------| |
| 162 | +| Setup Node.js and PNPM | [`./actions/setup-node-pnpm/action.yml`](./actions/setup-node-pnpm/action.yml) | Current workflows call `pnpm/action-setup@v4` and `actions/setup-node@v6` directly | |
| 163 | +| Tag deployment | [`./actions/tag-deployment/action.yml`](./actions/tag-deployment/action.yml) | Supports configurable semver bumps, but the live deploy workflow performs an inline patch bump instead | |
| 164 | + |
| 165 | +### Local Tooling |
| 166 | + |
| 167 | +To match the CI lint job locally, install `actionlint` once on your machine and run it from repo root: |
| 168 | + |
| 169 | +```bash |
| 170 | +brew install actionlint |
| 171 | +actionlint |
| 172 | +``` |
| 173 | + |
| 174 | +Pinned Go install alternative: |
| 175 | + |
| 176 | +```bash |
| 177 | +go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 |
| 178 | +actionlint |
| 179 | +``` |
0 commit comments