Imported from nextjs/adapter-k8s (
AGENTS.md). Install upstream withnpx skills add nextjs/adapter-k8s. Copyright stays with the author.
AGENTS.md
What this is
@next-community/adapter-k8s — a Next.js adapter (Next >=16.3.0 <16.4.0 adapterPath API) that deploys
Next.js apps to Kubernetes (GKE preset plus portable/generic targets). At build time it analyzes
the route structure and generates pool servers, an ext_proc routing service, a Helm chart, and
Dockerfiles. A CLI (adapter-k8s) provisions GCP infrastructure where the target calls for it
and runs zero-downtime blue/green deploys.
Status: experimental, correctness-first ("make it work, make it correct, make it fast" — fast is deliberately last). Favor correct, bounded behavior over throughput.
Commands
npm run build— clean + declaration types + esbuild bundles (adapter, pool-server, cache-handler, routing-service, cli) + buf proto codegennpm test— unit tests (vitest; hermetic — Docker-gated integration tests skip without Docker)npm run test:e2e— local e2e harness (scripts/e2e-local.sh). A full run should take ~16-17 min. If it doesn't, the invocation is wrong: the script auto-builds+packs the adapter ONCE and exportsADAPTER_K8S_PREBUILT_TARBALL(never let per-deploy rebuilds happen), and concurrency comes fromNEXT_TEST_CONCURRENCY(default 4 — do NOT raise it; c=16 was measured to produce ~205 browser/image contention failures on this machine). Pin the Next ref explicitly (2nd arg) — see the run-mechanics section of the project_e2e_run_baseline memory for known-bad refs and flake buckets before triaging failures. Never edit src/ or scripts/ while a run is in flight (deploys re-pack from the working tree).npm run test:e2e:live(and per-fixture variants) — live e2e against real GKE; needs env vars and deployed fixtures. Don't run casually.npm run lint/npm run fmt— oxlint / oxfmtnpx tsc --noEmit— typecheck
Run npm test and npx tsc --noEmit before considering any change done.
Repo map (the interesting parts)
src/adapter.ts— adapter entry.modifyConfig(K8s-safe buildId, immutable assets, Valkey cache-handler registration) andonBuildComplete(classify → manifests → Helm chart → Docker contexts). The staging machinery (stageFile, symlink dereferencing, monorepo asset rebasing) lives here.src/classify.ts/src/manifest.ts/src/cel.ts/src/extension-chain.ts— build-time route→pool classification, routing manifest, CEL match condition for the traffic extension, and the extension-chain JSON.src/emit/— Helm chart templates (templates/*.ts, plain TS string builders, not Go templating), Dockerfiles, static-asset manifest,.dockerignore. Input sanitizers (assertSafe*) live intemplates/utils.ts.src/cli/— init / deploy / rollback / destroy / doctor / emulate / describe / tail. All shell-outs go throughexec.ts(execFile-style,shell: false— keep it that way). Deploy state (current/previous build) is instate.ts(local file + cluster ConfigMap).src/pool-server/— the per-pod Node server: loads Next build outputs viaimport(), serves static/public files, PPR shell resume, ISR,/_next/image, cross-pool proxying.dispatch.tsis the heart;resolve.tsruns@next/routinglocally as the fail-safe path.src/routing-service/— ext_proc server (HTTP/2 + TLS via connectrpc) that runs middleware/rewrites/redirects at the load balancer.protos/is generated — regenerate withnpm run build:protos, never hand-edit.src/pool-server/valkey-cache/— deliberately zero-dep RESP2 client +use cachehandler + incremental cache handler + shared tag manifest (cross-replicarevalidateTag).fixtures/— Next.js test apps (main, pages, edge, i18n-rewrite, interception). Experimental WebSocket route-handler support is unit/transport tested; stable Next.js does not generate its unpublishedupgradeHandlercontract, so there is no framework fixture yet.tests/— vitest suites.test/(singular) is different: deploy-test manifests for the e2e shell scripts.plans/adapter-gke-design-doc.md— the design doc.plans/is tracked.docs/— tracked user-facing docs:targets.md(target model + journeys),configuration.md(config reference),lifecycle.md(deploy/rollback/destroy/doctor/state),verification.md,ci-cd.md.reports/is gitignored.skills/— Agent Skills SHIPPED IN THE PUBLISHED PACKAGE (package.jsonfiles), for agents working in apps that depend on this adapter — not tooling for this repo. Keep them consumer- facing: no references to this repo's internals (src/ paths, fixture-only headers). Every command, flag, and config key they mention must exist; re-audit against src/ when the CLI or config surface changes.
Conventions
- ESM source with
.jssuffixes on relative imports; bundles built with esbuild. Package is"type": "module". - Comments explain why, often referencing past incidents or upstream Next.js behavior (tags
like
L13,M4a, "H2"). Keep them; add to them when fixing edge cases — they are the institutional memory of this repo. - Minimal runtime dependencies by design (the RESP client is zero-dep on purpose).
- Validate at the point of consumption. Any value that is operator/build-controlled (build
id, release name, hostnames, registry, namespace, pool names, project/region) must pass the
assertSafe*validators insrc/emit/templates/utils.tsbefore reaching YAML, CEL,helm --set, or shell argv — even if it was validated upstream. - Secrets: never on argv, never in logs; secret-bearing files are written mode
0600;.k8s-adapter/must stay gitignored (init scaffolds this). - Dataplane error handling: fail open to cache-miss / local re-resolution; fail closed on auth (middleware throw → 500, never bypass middleware).
Invariants that must hold
- Fail-safe layering. A request reaching a pool without trusted dispatch headers gets full
local resolution (middleware still runs). Client-supplied
x-*dispatch headers are never trusted without the internal secret (timing-safe compare, deleted before handlers see them). - Middleware is never bypassed. CDN must not cache middleware-covered routes (pool sends
Cache-Control: no-cache); extproc failure mode is _closed when the app has middleware. - Blue/green ordering. Active Service selectors are patched only after every new pod is
verified serving on
/healthz; the previous build is kept at 0 replicas; deploy state is committed only after cutover. The selector value comes from the same sanitizer that stamps the pod label (a mismatch drains the Service to zero endpoints). - Parity with
next start.routing-common.tshelpers are pinned to empirically verified upstream behavior — the tests document this. Don't regress it; verify against the real@next/routing(seetests/pool-server/resolve-real-routing.test.ts). - Clean chart regeneration. The generated chart dir is wiped each build so removed
pools/builds can't leave stale templates that
helm upgradewould re-apply. - Kubectl context pinning. CLI commands that touch the cluster run
gcloud container clusters get-credentialsfirst, before reading cluster state (destroy historically missed this — don't reintroduce that class of bug).
Testing
- Unit tests are hermetic: localhost-only sockets,
mkdtemptmp dirs, env save/restore around mutations. Keep them that way. *.integration.test.tsneed Docker (ephemeral Valkey) and skip cleanly otherwise.- CLI tests mock
exec.js— seetests/cli/rollback.test.tsfor the pattern. - Live e2e (
vitest.e2e-live.config.ts) runs against real GCP deployments gated on per-fixture env vars.