feat(infra): add centralized IaC deployment capability module - #38
Open
mimukit wants to merge 6 commits into
Open
feat(infra): add centralized IaC deployment capability module#38mimukit wants to merge 6 commits into
mimukit wants to merge 6 commits into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
infra, a new capability module that deploys Saasaloy Workers to Cloudflare via a centralized Pulumi IaC stack: it discovers each app/package'swrangler.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— newsaasaloy:capabilitymodule descriptor: rootdependsOn: [], onescaffolds[]entry for theinfraworkspace, and reminder-onlyenvVarsforCLOUDFLARE_API_TOKEN,CLOUDFLARE_DEFAULT_ACCOUNT_ID,PULUMI_CONFIG_PASSPHRASE.modules/infra/files/— the scaffoldedinfrapackage:Pulumi.yaml,index.ts(stack entry),src/discover.ts(globswrangler.jsoncacrossapps/*andpackages/*),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-levelinfra/workspace into the base template so pnpm picks it up, plusprotobufjsadded toallowBuildsfor@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: typesaasaloy:capability,dependsOn: [](root module), emptydependencies/patches.envVars(CLOUDFLARE_API_TOKEN,CLOUDFLARE_DEFAULT_ACCOUNT_ID,PULUMI_CONFIG_PASSPHRASE) are printed reminders only — no.env-writing mechanism was invented.scaffolds[]entry, workspaceinfra, root-level workspace. Edited the base-templatepnpm-workspace.yaml+AGENTS.mdso pnpm sees the new root-levelinfra/workspace; also addedprotobufjstoallowBuildsfor@pulumi/pulumi's gRPC postinstall step.tsconfig.jsonto the scaffold (not in the original file list) so typecheck is meaningful, matching theapi/databaseidiom.module/moduleResolutionare overridden tonodenext— the ESM path Pulumi actually runs under — sinceinfraruns un-bundled.discover.tsglobsapps/*/wrangler.jsoncandpackages/*/wrangler.jsoncusing the existingjsonc-parsertoolbox already in the repo — no new dependency.translate.tshandlesd1_databasesandvarsbindings; any unknown binding kind throws loudly ("infra doesn't support '<type>' yet") rather than silently dropping it.@cloudflare/vite-plugin's generateddist/<name>/wrangler.jsonmanifest for the entry file, rather than guessing a filename.@pulumi/pulumi+@pulumi/cloudflareare exact-pinned;wrangleris a devDep. These are pnpm-invisible template deps (per AGENTS.md conventions, tracked outsidepnpm-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 withCLOUDFLARE_orPULUMI_is skipped and never pushed. This is a documented, accepted tradeoff — flagged so a future consumer isn't surprised by a "missing" secret.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'sreadBundleassumes single-entry-file worker output; a code-split build would upload only the entry chunk. This v1 limitation is documented in code.pnpm installcurrently fails on the repo's 3-day dependency install cooldown for the freshly-published@pulumipackages (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 upproducing a realworkers.devURL with a provisioned D1 database,GET /healthgreen on the edge, andpulumi destroycleaning 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.tsglobbing, the secrets denylist, and thatapi/databaseare 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
translate.tsloud-fail path,discover.tsglobbing, secrets denylist,api/databaseuntouched — seedocs/qa/qa-infra-iac-2026-07-25.mdpulumi up→ realworkers.devURL + provisioned D1,GET /healthgreen,pulumi destroycleans up