

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# phpstan.org website infrastructure (CDK)23AWS CDK app (TypeScript) that defines the production infra for [phpstan.org](https://phpstan.org):4S3 origin, CloudFront distribution, edge function, security headers policy,5ACM cert, Route 53 records, and the IAM roles that GitHub Actions assumes via OIDC.67See `README.md` for the bootstrap and cutover runbook. See `../CLAUDE.md` for8the parent website project conventions.910## Stacks1112Both stacks deploy to `us-east-1` (required for CloudFront + ACM).1314| Stack | Defined in | Resources |15| --- | --- | --- |16| `PhpstanOrgGithubOidc` | `lib/github-oidc-stack.ts` | GitHub OIDC provider + `phpstan-org-infra-deploy` role (used by `website-infra.yml` to deploy this CDK app) |17| `PhpstanOrgWebsite` | `lib/website-stack.ts` | S3 bucket (OAC, private, versioned), CloudFront distribution carrying all three aliases (apex + www + `new.phpstan.org`), CF Function 2.0, Response Headers Policy, ACM cert (DNS-validated, covers all 3 hostnames), the `new.phpstan.org` Route 53 record, and `phpstan-org-website-deploy` role (used by `website.yml` to sync content + invalidate) |1819`bin/infra.ts` is the CDK app entrypoint. It hard-codes the account/region/repo/zone constants. No runtime flags.2021## Out-of-band resources2223The apex (`phpstan.org`) and www (`www.phpstan.org`) Route 53 records are **not** managed by CDK. They were created during the initial cutover from the legacy distributions via raw `change-resource-record-sets` calls, and CloudFormation cannot UPSERT a record that already exists outside its own state. They are managed manually via the AWS Console or CLI. The `new.phpstan.org` record is the only Route 53 record CDK touches.2425If you change the new distribution's CloudFront domain (e.g., a recreate), you must also update the apex/www Route 53 records to point at the new domain — CDK will not do it for you. The README has the rollback runbook with explicit `change-resource-record-sets` payloads.2627## Edge function2829`functions/phpstan-org-edge.js` is the CloudFront Function 2.0 source, written30as plain ES5-ish JS so it runs in the CF runtime unchanged. It replaces three31pieces of legacy infrastructure:32331. The viewer-response CF Function `secure-headers-response` — its security headers moved to the Response Headers Policy in `website-stack.ts`. Not handled by the function.342. The viewer-request CF Function `phpstan-org-viewer-request` — `.html` strip → 301 (was 302).353. The origin-request Lambda@Edge `web-phpstan-prg-rewrite-url` — `/r/*` → `/try.html`, `/error-identifiers/*` → `.html` append, clean-URL → `.html` append.3637It also takes over the www→apex 301, which was previously a second CloudFront38distribution (`E3VJ14QANBNGO9`).3940The file uses a `typeof module !== 'undefined'` guard at the bottom to export41`handler` for Node-based unit tests; in the CF runtime there is no `module`42global so the export is silently skipped.4344### Behavior to preserve when editing4546The function runs on **viewer-request**, so its rewrites form the cache key. The47order matters:48491. `host === 'www.phpstan.org'` → 301 to `https://phpstan.org${uri}${qs}` (host check first so .html strip never fires for www).502. `uri.endsWith('.html')` → 301 to `uri` without `.html` (clean-URL canonical form).513. `uri.startsWith('/r/')` → rewrite to `/try.html` (playground short links — all `/r/*` resolve to the same page).524. `uri.startsWith('/error-identifiers/')` and not `.js`/`.css` → append `.html`.535. Last path segment has no `.` and is non-empty → append `.html`.546. Trailing slash on a non-root URI (`uri.length > 1`) → 301 to the slash-less canonical form (`/blog/foo/` → `/blog/foo`, which then gets `.html` appended on the follow-up request). The root `/` is left alone — CloudFront's `defaultRootObject: 'index.html'` handles it *after* the function runs, and redirecting `/` would loop.5556Querystring is preserved on the 301 redirects via the `formatQuerystring` helper, which handles both single-value and multi-value params.5758## Project layout5960```61website/infra/62├── bin/infra.ts # CDK app entrypoint — wires both stacks63├── lib/64│ ├── github-oidc-stack.ts # OIDC provider + infra-deploy role65│ └── website-stack.ts # everything that serves traffic66├── functions/67│ └── phpstan-org-edge.js # CloudFront Function 2.0 source68├── test/69│ ├── phpstan-org-edge.test.ts # Vitest: 22 cases on the edge function70│ └── website-stack.test.ts # Vitest: CDK assertions on the synth template71├── cdk.json # CDK config + context (incl. productionAliases)72├── package.json73├── tsconfig.json74├── vitest.config.ts75├── README.md # bootstrap + cutover runbook (human-facing)76└── CLAUDE.md # this file77```7879## Conventions8081- **Tabs for indentation** in TS, JSON, and JS files (matches the parent website repo).82- **2-space indent** for YAML workflows (matches the existing `.github/workflows/` style).83- **Pin GitHub Actions to commit SHAs** with the version in a comment — matches the existing repo style and what `step-security/harden-runner` audits.84- **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.85- Resource IDs in CDK use **PascalCase** (CDK convention). Resource *names* (`bucketName`, `roleName`, `functionName`, `responseHeadersPolicyName`) use **kebab-case** with the `phpstan-org-` prefix so they're easy to spot in the console alongside other workloads in this account.86- Output exports use the `PhpstanOrg…` prefix (`PhpstanOrgWebsiteBucketName`, etc.) so other stacks (or scripts) can reference them by name.8788## Commands8990```sh91npm ci # install (run after pulling)92npm run check # tsc --noEmit93npm test # vitest run — 32 tests (edge function + stack assertions)94npm run synth # cdk synth --all (no AWS creds needed)95npm run diff # cdk diff --all (needs AWS creds for the target account)96npm run deploy # cdk deploy --all97```9899`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`.100101## CI102103`.github/workflows/website-infra.yml` triggers on PRs and pushes that touch104`website/infra/**` or the workflow file itself. It has three jobs:1051061. `test` — `npm ci && npm run check && npm test && npx cdk synth --all` (no AWS creds).1072. `diff` (needs: `test`) — assumes `INFRA_DEPLOY_ROLE_ARN` via OIDC, runs `cdk diff --all`, posts a sticky PR comment with the diff.1083. `deploy` (needs: `[test, diff]`, only on push to `2.2.x`) — assumes the same role, runs `cdk deploy --all --require-approval never`.109110The deploy is gated on **both** test and diff succeeding, so a broken edge111function unit test or a CDK synth error blocks the deploy.112113## When to edit what114115- **Changing edge logic** (URL rewrites, redirects, headers in JS) → `functions/phpstan-org-edge.js` + add a test case to `test/phpstan-org-edge.test.ts`.116- **Changing security headers** → `lib/website-stack.ts` (the `responseHeadersPolicy` block), not the function.117- **Adding cache behaviors, origins, or new functions** → `lib/website-stack.ts`. If the change is non-obvious, extend `test/website-stack.test.ts` with an assertion.118- **Changing the trust policy** (e.g. allowing another branch to deploy) → `lib/github-oidc-stack.ts`.119- **Toggling test/production mode** → `cdk.json` `context.productionAliases`. This is the cutover knob; see `README.md` for the full sequence.120121## What lives elsewhere122123- The website source and build pipeline (Eleventy + Vite + Tailwind) — `../` (see `../CLAUDE.md`).124- The S3 sync + CloudFront invalidation that publishes the website itself — `.github/workflows/website.yml` (uses the `phpstan-org-website-deploy` role from this stack).125- The playground API and runner — `../../playground-api/`, `../../playground-runner/` (separate Serverless Framework stacks, not in scope here).126- The `apiref.phpstan.org` distribution and its CF Function — not modeled here; same modernization pattern would apply but is a separate piece of work.127
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| phpstan/phpstanCLAUDE.md · 14k | CLAUDE.md | buildarchgitdeployment | 82/100 | 14 days ago | |
| phpstan/phpstanwebsite/CLAUDE.md · 14k | CLAUDE.md | buildtestlint-formatarch+2 | 89/100 | 14 days ago | |
| phpstan/phpstanwebsite/errors/CLAUDE.md · 14k | CLAUDE.md | lint-formatstyledo-notdocs | 77/100 | 14 days ago | |
| phpstan/phpstanwebsite/src/_posts/CLAUDE.md · 14k | CLAUDE.md | lint-formatstylearchtypes+1 | 74/100 | 14 days ago |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 7 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 46 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 14 days ago | |
| tyrchen/geektime-bootcamp-aiw7/genslides/backend/CLAUDE.md · 230 | CLAUDE.md | testlint-formatstylearch+6 | 100/100 | 9 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.5k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 14 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 14 days ago | |
| microsoft/playwrightCLAUDE.md · 95k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 7 days ago | |
| tphakala/birdnet-goCLAUDE.md · 1.6k | CLAUDE.md | buildtestlint-formatstyle+8 | 100/100 | today | |
| livewire/livewireCLAUDE.md · 24k | CLAUDE.md | setupbuildteststyle+4 | 100/100 | 14 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/phpstan-phpstan-website-infra-claude)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.