|
| 1 | +# apiref.phpstan.org infrastructure (CDK) |
| 2 | + |
| 3 | +AWS CDK app (TypeScript) that defines the production infra for |
| 4 | +[apiref.phpstan.org](https://apiref.phpstan.org) — the auto-generated ApiGen |
| 5 | +reference for the PHPStan codebase. S3 origin, CloudFront distribution, edge |
| 6 | +function for per-version landing-page redirects, security headers policy, ACM |
| 7 | +cert, and the IAM roles that GitHub Actions assumes via OIDC. |
| 8 | + |
| 9 | +See `README.md` for the bootstrap, cutover, and cleanup runbook. |
| 10 | + |
| 11 | +This stack mirrors the main-site infra at |
| 12 | +[`phpstan-dist`/website/infra](https://github.com/phpstan/phpstan/tree/2.2.x/website/infra) |
| 13 | +— same patterns, same conventions; reach for that repo first when looking for |
| 14 | +prior art. |
| 15 | + |
| 16 | +## Stacks |
| 17 | + |
| 18 | +Both stacks deploy to `us-east-1` (required for CloudFront + ACM). |
| 19 | + |
| 20 | +| Stack | Defined in | Resources | |
| 21 | +| --- | --- | --- | |
| 22 | +| `PhpstanApirefOidcRoles` | `lib/oidc-roles-stack.ts` | `phpstan-apiref-infra-deploy` IAM role used by `apiref-infra.yml`. **Reuses** the account-wide OIDC provider — does NOT create a new one (IAM rejects duplicates of the same provider URL). | |
| 23 | +| `PhpstanApirefWebsite` | `lib/apiref-stack.ts` | Private S3 bucket (OAC, versioned), CloudFront distribution, CF Function 2.0, Response Headers Policy, DNS-validated ACM cert for `apiref.phpstan.org`, and `phpstan-apiref-deploy` IAM role used by `apiref.yml`. | |
| 24 | + |
| 25 | +`bin/infra.ts` is the CDK app entrypoint. It hard-codes the account/region/repo/zone constants and reads one CDK context flag, `productionAlias`, that toggles whether `apiref.phpstan.org` is attached to the distribution. |
| 26 | + |
| 27 | +## The `productionAlias` flag |
| 28 | + |
| 29 | +Defined in `cdk.json` under `context`, default `false`. |
| 30 | + |
| 31 | +- `false` (pre-cutover): distribution has no aliases and no ACM cert attached. CloudFormation can create the distribution without conflict even while the legacy `E37G1C2KWNAPBD` still owns the alias. The distribution serves on its `*.cloudfront.net` domain for pre-cutover testing. |
| 32 | +- `true` (post-cutover): distribution carries `apiref.phpstan.org` and uses the ACM cert. |
| 33 | + |
| 34 | +The CDK code generates `Aliases: null` and `ViewerCertificate: null` when `productionAlias: false`. CloudFormation treats both as absent. |
| 35 | + |
| 36 | +## Out-of-band resources |
| 37 | + |
| 38 | +The Route 53 record for `apiref.phpstan.org` is **not** managed by CDK. It was |
| 39 | +created/updated out-of-band during the cutover (raw `change-resource-record-sets`), |
| 40 | +and CloudFormation cannot UPSERT a record that already exists outside its own |
| 41 | +state. Same pattern as apex/www on the main site. |
| 42 | + |
| 43 | +## Edge function |
| 44 | + |
| 45 | +`functions/apiref-version-redirects.js` is the CloudFront Function 2.0 source. |
| 46 | +It's a lookup-table version of the legacy `apiref-phpstan-org-viewer-request` |
| 47 | +JS 1.0 function — same job: 301-redirect bare version URIs (e.g. `/2.2.x` or |
| 48 | +`/2.2.x/`) to that version's landing page (`<version>/namespace-PHPStan.html`), |
| 49 | +and `/` to the current "latest" (2.2.x in this migration). |
| 50 | + |
| 51 | +302 → 301 was an intentional change to match the main site's redirects. |
| 52 | + |
| 53 | +The lookup table `VERSION_REDIRECTS` is hand-curated. When a new release branch |
| 54 | +is added (say 2.3.x), append three entries: `'/2.3.x'`, `'/2.3.x/'`, both |
| 55 | +mapping to `/2.3.x/namespace-PHPStan.html`. If 2.3.x should become the new |
| 56 | +latest, also update the `'/'` entry. Then `npm test` ensures the lookup table |
| 57 | +size and `/` mapping stay in sync. |
| 58 | + |
| 59 | +The file ends with `if (typeof module !== 'undefined') module.exports = {...}` |
| 60 | +so it can be imported by Node-based unit tests. In the CloudFront runtime |
| 61 | +`module` is undefined, so the export is silently skipped. |
| 62 | + |
| 63 | +## Project layout |
| 64 | + |
| 65 | +``` |
| 66 | +apigen/infra/ |
| 67 | +├── bin/infra.ts # CDK app entrypoint — wires both stacks |
| 68 | +├── lib/ |
| 69 | +│ ├── oidc-roles-stack.ts # IAM role (reuses existing OIDC provider) |
| 70 | +│ └── apiref-stack.ts # everything that serves traffic |
| 71 | +├── functions/ |
| 72 | +│ └── apiref-version-redirects.js # CloudFront Function 2.0 source |
| 73 | +├── test/ |
| 74 | +│ ├── apiref-version-redirects.test.ts # Vitest: 25 redirect cases |
| 75 | +│ └── apiref-stack.test.ts # Vitest: 11 CDK assertions |
| 76 | +├── cdk.json # CDK config + context (incl. productionAlias) |
| 77 | +├── package.json |
| 78 | +├── tsconfig.json |
| 79 | +├── vitest.config.ts |
| 80 | +├── README.md # bootstrap + cutover runbook (human-facing) |
| 81 | +└── CLAUDE.md # this file |
| 82 | +``` |
| 83 | + |
| 84 | +## Conventions |
| 85 | + |
| 86 | +Same as the main-site infra: |
| 87 | + |
| 88 | +- **Tabs for indentation** in TS, JSON, and JS files. |
| 89 | +- **2-space indent** for YAML workflows. |
| 90 | +- **Pin GitHub Actions to commit SHAs** with the version in a trailing comment — matches the repo style and what `step-security/harden-runner` audits. |
| 91 | +- **No `module.exports` / ESM imports in `functions/*.js`** — they run in the CloudFront Function runtime, not Node. The only allowed exception is the `typeof module` guard for unit-test interop. |
| 92 | +- Resource IDs in CDK use **PascalCase**. Resource *names* (`bucketName`, `roleName`, `functionName`, `responseHeadersPolicyName`) use **kebab-case** with the `phpstan-apiref-` prefix so they're easy to spot in the console. |
| 93 | +- Output exports use the `PhpstanApiref…` prefix. |
| 94 | + |
| 95 | +## Commands |
| 96 | + |
| 97 | +```sh |
| 98 | +npm ci # install (run after pulling) |
| 99 | +npm run check # tsc --noEmit |
| 100 | +npm test # vitest run — 36 tests (redirect fn + stack assertions) |
| 101 | +npm run synth # cdk synth --all (no AWS creds needed) |
| 102 | +npm run diff # cdk diff --all (needs AWS creds for the target account) |
| 103 | +npm run deploy # cdk deploy --all |
| 104 | +``` |
| 105 | + |
| 106 | +`npm test` is the gate before any deploy — the CI workflow runs `check` + `test` + `synth` in a `test` job and blocks `diff` and `deploy` on it via `needs: test`. |
| 107 | + |
| 108 | +## CI |
| 109 | + |
| 110 | +`.github/workflows/apiref-infra.yml` triggers on PRs and pushes that touch |
| 111 | +`apigen/infra/**` or the workflow file itself. Three jobs (same as the main |
| 112 | +site's `website-infra.yml`): |
| 113 | + |
| 114 | +1. `test` — `npm ci && npm run check && npm test && npx cdk synth --all` (no AWS creds). |
| 115 | +2. `diff` (needs: `test`) — assumes `APIREF_INFRA_DEPLOY_ROLE_ARN` via OIDC, runs `cdk diff --all`, posts a sticky PR comment. |
| 116 | +3. `deploy` (needs: `[test, diff]`, only on push to `2.2.x`) — assumes the same role, runs `cdk deploy --all --require-approval never`. |
| 117 | + |
| 118 | +The `apiref.yml` workflow (the actual content deploy) uses `paths-ignore` via the inline `!apigen/infra/**` form so infra-only edits don't kick off a (slow) ApiGen rebuild. |
| 119 | + |
| 120 | +## When to edit what |
| 121 | + |
| 122 | +- **New release branch** (need a `/X.Y.x` → `/X.Y.x/namespace-PHPStan.html` redirect) → add three entries to `VERSION_REDIRECTS` in `functions/apiref-version-redirects.js` plus three test cases in `test/apiref-version-redirects.test.ts`. If it's the new latest, update `'/'` too. |
| 123 | +- **Changing security headers** → `lib/apiref-stack.ts` (`responseHeadersPolicy` block), not the function. |
| 124 | +- **Adding cache behaviors or new functions** → `lib/apiref-stack.ts`. Extend `test/apiref-stack.test.ts`. |
| 125 | +- **Changing the trust policy** (e.g. allowing another branch to deploy) → `lib/oidc-roles-stack.ts` for infra deploys, or `lib/apiref-stack.ts` for the content deploy role. |
| 126 | +- **Cutover flag** → `cdk.json` `context.productionAlias`. Only flip after the cutover script has done its work. |
| 127 | + |
| 128 | +## What lives elsewhere |
| 129 | + |
| 130 | +- The ApiGen tool, theme, and PHP filters — `../` (`apigen/apigen.neon`, `apigen/src/`, `apigen/theme/`). |
| 131 | +- The PHP source code that ApiGen reads — `../../src/`. |
| 132 | +- The build + publish pipeline — `.github/workflows/apiref.yml`. |
| 133 | +- The main-site (`phpstan.org`) infra — separate repo `phpstan/phpstan` (the "dist" repo), under `website/infra/`. Identical patterns; consult it first when wondering "how did we solve X for the main site?". |
0 commit comments