Skip to content

feat(infra): add centralized IaC deployment capability module - #38

Open
mimukit wants to merge 6 commits into
mainfrom
issue-29-centralized-service-deployment-via-iac
Open

feat(infra): add centralized IaC deployment capability module#38
mimukit wants to merge 6 commits into
mainfrom
issue-29-centralized-service-deployment-via-iac

Conversation

@mimukit

@mimukit mimukit commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Adds infra, a new capability module that deploys Saasaloy Workers to Cloudflare via a centralized Pulumi IaC stack: it discovers each app/package's wrangler.jsonc, translates the declared bindings (D1 databases, vars) into Pulumi Cloudflare resources, pushes worker secrets (with a denylist for the infra deploy credentials themselves), and hands off each service's build output to Pulumi via the bundler's generated manifest.

Closes #29

Changes

  • modules/infra/registry-item.json — new saasaloy:capability module descriptor: root dependsOn: [], one scaffolds[] entry for the infra workspace, and reminder-only envVars for CLOUDFLARE_API_TOKEN, CLOUDFLARE_DEFAULT_ACCOUNT_ID, PULUMI_CONFIG_PASSPHRASE.
  • modules/infra/files/ — the scaffolded infra package: Pulumi.yaml, index.ts (stack entry), src/discover.ts (globs wrangler.jsonc across apps/* and packages/*), src/translate.ts (binding → Pulumi resource translation + bundle handoff), src/secrets.ts (worker secrets push with credential denylist), package.json, tsconfig.json.
  • modules/infra/skills/saasaloy-infra/SKILL.md — module skill doc.
  • packages/cli/templates/base/pnpm-workspace.yaml, packages/cli/templates/base/AGENTS.md — wire the new root-level infra/ workspace into the base template so pnpm picks it up, plus protobufjs added to allowBuilds for @pulumi/pulumi's gRPC postinstall.
  • docs/qa/qa-infra-iac-2026-07-25.md — manual QA plan (see Definition of Done below).

Assumptions

  • registry-item.json: type saasaloy:capability, dependsOn: [] (root module), empty dependencies/patches. envVars (CLOUDFLARE_API_TOKEN, CLOUDFLARE_DEFAULT_ACCOUNT_ID, PULUMI_CONFIG_PASSPHRASE) are printed reminders only — no .env-writing mechanism was invented.
  • One scaffolds[] entry, workspace infra, root-level workspace. Edited the base-template pnpm-workspace.yaml + AGENTS.md so pnpm sees the new root-level infra/ workspace; also added protobufjs to allowBuilds for @pulumi/pulumi's gRPC postinstall step.
  • Added a tsconfig.json to the scaffold (not in the original file list) so typecheck is meaningful, matching the api/database idiom. module/moduleResolution are overridden to nodenext — the ESM path Pulumi actually runs under — since infra runs un-bundled.
  • discover.ts globs apps/*/wrangler.jsonc and packages/*/wrangler.jsonc using the existing jsonc-parser toolbox already in the repo — no new dependency.
  • translate.ts handles d1_databases and vars bindings; any unknown binding kind throws loudly ("infra doesn't support '<type>' yet") rather than silently dropping it.
  • Bundle handoff is resolved by running the service's own build and then reading @cloudflare/vite-plugin's generated dist/<name>/wrangler.json manifest for the entry file, rather than guessing a filename.
  • @pulumi/pulumi + @pulumi/cloudflare are exact-pinned; wrangler is a devDep. These are pnpm-invisible template deps (per AGENTS.md conventions, tracked outside pnpm-workspace.yaml's normal resolution).

Known follow-ups / unresolved nits (not blocking)

  • secrets.ts's denylist is intentionally over-broad by prefix: any app secret a Worker legitimately needs that happens to start with CLOUDFLARE_ or PULUMI_ is skipped and never pushed. This is a documented, accepted tradeoff — flagged so a future consumer isn't surprised by a "missing" secret.
  • No unit tests ship with the module, consistent with api/database (which also ship none). The secrets denylist and discover error-path guarantees rest on code inspection plus the manual QA plan, not automated tests.
  • translate.ts's readBundle assumes single-entry-file worker output; a code-split build would upload only the entry chunk. This v1 limitation is documented in code.
  • Heads-up for reviewers pulling this branch: a plain pnpm install currently fails on the repo's 3-day dependency install cooldown for the freshly-published @pulumi packages (ERR_PNPM_NO_MATURE_MATCHING_VERSION). This is a timing artifact, not a resolution bug — it installs cleanly with --config.minimumReleaseAge=0, and the cooldown lapses on its own within 3 days.

Definition of Done status

The issue's DoD — pulumi up producing a real workers.dev URL with a provisioned D1 database, GET /health green on the edge, and pulumi destroy cleaning up — has not been exercised in this build. It requires a live Cloudflare account, and no credentials were available in the build environment.

It is written up as a runnable hand-QA procedure (TC-1, marked REQUIRES-LIVE-CLOUDFLARE / NOT YET VERIFIED) in the committed QA plan: docs/qa/qa-infra-iac-2026-07-25.md.

The checks that are verifiable without a live cloud account — schema validity, clean scaffold, install resolution, typecheck, translate.ts's loud-fail on unknown bindings, discover.ts globbing, the secrets denylist, and that api/database are untouched — were all run and passed. They're recorded in that same QA doc.

A human needs to run TC-1 against a real Cloudflare account before this merges.

Test plan

  • Automated: schema validity, clean scaffold, install resolution, typecheck, translate.ts loud-fail path, discover.ts globbing, secrets denylist, api/database untouched — see docs/qa/qa-infra-iac-2026-07-25.md
  • Manual, requires live Cloudflare credentials: TC-1 in the same QA doc — pulumi up → real workers.dev URL + provisioned D1, GET /health green, pulumi destroy cleans up

mimukit added 6 commits July 25, 2026 18:39
Introduce the infra capability module (issue #29 / ADR 0021), which scaffolds a
root-level Pulumi workspace for centralized Cloudflare deployment, and wire it
into the base template so pnpm recognizes the new workspace.

- add modules/infra/registry-item.json declaring the capability, its env vars,
  and its scaffolded workspace files
- scaffold files/{package.json,tsconfig.json,Pulumi.yaml,index.ts} and
  src/{discover,translate,secrets}.ts for the Pulumi program
- add the saasaloy-infra agent skill for working with the module
- register infra as a root-level pnpm workspace entry and allow the
  protobufjs postinstall build needed by @pulumi/pulumi's gRPC transport
- document the infra workspace in the base template AGENTS.md so agents
  don't try to move it under apps/* or packages/*
infra/.env commonly holds both infra's own deploy credentials and Worker
secrets side by side, and secrets.ts pushed everything not already a plain
vars binding — including CLOUDFLARE_API_TOKEN, CLOUDFLARE_DEFAULT_ACCOUNT_ID,
and PULUMI_CONFIG_PASSPHRASE — straight to every deployed Worker's secret
store.

- add an isInfraCredential denylist (exact keys plus PULUMI_/CLOUDFLARE_
  prefixes) and filter it out of pushSecrets alongside the existing vars skip
- document the guarantee in the saasaloy-infra skill so it isn't rediscovered
  as a surprise later
jsonc-parser's parse() is lenient by default — on malformed input it returns
a best-effort partial object rather than undefined, so the prior 'config is
falsy' check alone could silently ship a truncated config.

- pass an errors array to parse() and throw with the parse error code and
  offset when it's non-empty, before falling back to the existing shape check
Pulumi runs this program directly via ts-node/tsx, with no bundler in the
loop, and imports use explicit .js extensions per Node ESM — but the base
config's inherited moduleResolution is 'Bundler', which assumes a bundler
resolves those extensions.

- override module/moduleResolution to nodenext, Pulumi's documented ESM
  path, so resolution matches how this code actually runs
Two comment-only corrections found in review.

- Pulumi.yaml: the prior comment implied the file:// backend path resolves
  relative to Pulumi.yaml itself; it actually resolves against the process
  cwd, and only lands at infra/state/ because every documented way of
  running infra cwds into infra/ first — spell that out so it isn't
  misread later
- translate.ts: document the v1 limitation that readBundle only follows
  manifest.main, so a code-split build would silently drop chunks beyond
  the entry file
Issue #29 shipped the centralized infra deployment module without a
human-executable test plan; this records one for reviewers.

- covers registry-item scaffold, wrangler.jsonc discovery/translation,
  secrets denylist, and typecheck as automated-verification-by-ai-agent
- flags TC-1 (live Cloudflare pulumi up/destroy) as not exercised in
  this sandbox and documents the repro steps and cooldown override
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(infra): centralized service deployment via IaC

1 participant