- Bun workspaces (packages/* + bootstrap); Pulumi nodejs runtime under
packagemanager: bun (no npm fallback needed).
- bootstrap/config.ts: typed FoundationConfig per CONTRACT_001; loadConfig()
fails closed, aggregating all missing+malformed keys in one error. Reads flat
dotted keys; image digests excluded (they live in VERSIONS, D5).
- bootstrap/Pulumi.foundation.yaml: non-secret placeholders only (RFC-5737 vm.host,
.invalid offsite); no encryptionsalt/secrets committed (D2). pulumi preview = 0
resources under the passphrase provider via gitignored file:// state backend.
- Stage-1 vendoring: packages/pulumi-{docker,vault} as @olsitec/* (source-only,
logic unchanged). vault's 5 type-only imports from modules/olsitec re-homed
verbatim into pulumi-vault/olsitec-types.ts to keep the egg self-contained.
Realizes PLAN-002 §10 T02; ADR-005 / 000_TOPOLOGY.md §5 Stage-1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.8 KiB
8.8 KiB
T02 — Pulumi project skeleton — Handoff
Task: PLAN-002 §10 T02 · Mode: BUILD · Date: 2026-06-30 Author role: implementation agent · Status: complete, validated by execution (see below).
The lead reviews and commits. Nothing was
git add/git commited by this task.
1. Files created / touched
Workspace root
package.json— Bun workspace root (@olsitec/foundation, private,workspaces: ["packages/*","bootstrap"]).- Removed placeholders
bootstrap/.gitkeep,packages/.gitkeep(replaced by real content). bun.lock— generated bybun install(links the workspace; no secrets — verified).
Vendored module packages/pulumi-docker/ (@olsitec/pulumi-docker)
index.ts,tsconfig.json,.editorconfig,.gitignore— copied verbatim from/Users/andiolsi/work/olsicloud4/pulumi/modules/docker/.package.json— renameddocker→@olsitec/pulumi-docker; addedversion0.0.0,main/types(index.ts), and an explicit@pulumi/pulumidependency (see §4).VENDORED.md— source path, copy date, Stage-1 note, Stage-2 deferral.
Vendored module packages/pulumi-vault/ (@olsitec/pulumi-vault)
index.ts,policy.ts,tsconfig.json,.editorconfig,.gitignore— copied verbatim from/Users/andiolsi/work/olsicloud4/pulumi/modules/vault/.olsitec-types.ts— NEW: the 5 type-only declarations the upstreamindex.tsimported from../../modules/olsitec, copied verbatim (see §3). The single import line inindex.tswas re-pointed../../modules/olsitec→./olsitec-types. No runtime logic changed.tsconfig.json—filesextended withpolicy.ts+olsitec-types.ts(standalone typecheck).package.json— renamedvault→@olsitec/pulumi-vault;version/main/types; added explicit@pulumi/pulumidependency (§4).VENDORED.md— source path, copy date, Stage-1 note, the type-only re-home, and a flag that the inherited Layer-1 credential surface (VaultProject/minio/garage/…) is to be trimmed in a later Layer-0 refactor (NOT in Stage-1, which preserves source).
bootstrap/ (the egg, single Pulumi project)
Pulumi.yaml—name: foundation,runtime: nodejs+options.packagemanager: bun.package.json— depends on@olsitec/pulumi-docker/@olsitec/pulumi-vault(workspace:*) +@pulumi/pulumi.tsconfig.json— Olsitec-standard compiler options;files: [config.ts, index.ts].config.ts—FoundationConfiginterface (CONTRACT_001 §1.1) +loadConfig()(fails closed) +sshPrivateKeyPath()(ENV).index.ts— no-op scaffold: callsloadConfig(), creates NO resources, exports non-secret outputs.Pulumi.foundation.yaml— NON-secret placeholders only; no secrets, no encryptionsalt.
Documentation
documentation/agents/task_002_pulumi_skeleton/000_subtask_outline.mddocumentation/agents/task_002_pulumi_skeleton/003_handoff.md(this file)
2. Validated-by-execution vs. authored-only
Validated by running it (env: bun 1.3.9, pulumi v3.243.0, node v24.10.0, macOS):
bun installlinks the workspace;bun pm lsshows all three workspace members;require.resolve("@olsitec/pulumi-docker"|"@olsitec/pulumi-vault")frombootstrapresolves topackages/*/index.ts. ✅ (acceptance: workspace resolves the two packages)tsc --noEmitexit 0 for all three projects (bootstrap + both vendored packages). ✅pulumi previewunder the passphrase secrets provider (localfile://bootstrap/statebackend- throwaway
PULUMI_CONFIG_PASSPHRASEin ENV): exit 0, loads config, 0 real resources (only the no-op Stack), prints the scaffold outputs. ✅ (acceptance: preview runs on empty/stub stack, passphrase provider)
- throwaway
- Fail-closed demonstrated: removing
foundation:vm.host+foundation:features.backupmadepulumi previewexit 1 with one error listing both missing keys; restoring made it pass again. ✅ (acceptance: config rejects missing required keys) - No secrets committed:
bootstrap/state/is gitignored (git check-ignoreconfirms); the committedPulumi.foundation.yamlhas no realencryptionsalt:/secure: v1:value line (only explanatory comment prose mentions the words);bun.lockhas no secret leakage. ✅
Authored but NOT executed (out of scope — no VM, no pulumi up ever):
- Actual Docker-over-SSH provisioning, Vault init/unseal, any container — all LATER tasks (T03+).
- The vendored modules' runtime behaviour was not exercised (they are libraries; only typechecked + consumed by the typechecked bootstrap). Their logic is byte-identical to the upstream source apart from the documented type-only re-home in pulumi-vault.
3. CONTRACT_001 ambiguities / decisions
- Config key shape (resolved by execution). CONTRACT_001 §1.1 shows a nested interface but §1.2
gives flat dotted example keys (
foundation:hosts.forge,foundation:features.forgejo). Pulumi stores and exposes these flat —getObject("hosts")does NOT reassemblehosts.forge+hosts.vaultinto an object.loadConfig()therefore reads each leaf by its full dotted key with the typed accessor for its kind (get/getNumber/getBoolean, andgetObjectfor the two arrays), then assembles the nestedFoundationConfig. This is the idiomatic match for the §1.2 YAML and is robust to Pulumi quoting scalars ("2222","true"). No contract change needed. - No further ambiguity blocked typing. Every §1.1 field maps 1:1 to a placeholder key.
- Secrets (§1.3) intentionally not required by
loadConfig()— they are seeded by later tasks (T05 Vault capture etc.); requiring them now would block the T02 acceptance (empty/stub stack).
4. Bun-vs-npm decision + rationale
Decision: Bun (Olsitec footgun 16.3 prefers Bun; 000_TOPOLOGY.md §3 specifies Bun workspaces).
- Pulumi's nodejs runtime supports
options.packagemanager: bun;pulumi previewran cleanly under it (the program executes via Pulumi's bundled ts-node against bun-installed deps). No fallback to npm was needed. - One vendoring fix forced by Bun's isolated store: the upstream modules never declared
@pulumi/pulumidirectly (they relied on it being hoisted into their ownnode_modules). Bun's.bun/store layout does not hoist it where standalonetscon a package can find it, so both vendoredpackage.jsons now list@pulumi/pulumi(^3.138.0, matching olsitec-core) as an explicit dependency. This is a packaging-metadata correction (the import was always real); no module logic changed. It also makes the packages correct for Stage-2 publishing. (This is the escalation-worthy "vendoring reveals a dependency" item — judged minor and fixed in place, not a design change.)
5. Recommended follow-ups
- Strip
encryptionsaltbefore each commit ofPulumi.foundation.yaml. Everypulumi preview/upunder the passphrase provider re-appends anencryptionsalt:line and re-quotes scalars. The committed file has been left clean; a header comment documents the strip step. Once the first real secret is added (T05), the encryptionsalt becomes load-bearing and SHOULD then be committed alongside thesecure: v1:values — revisit this guidance at that point. packages/pulumi-vaultLayer-0 trim (later task).VaultBootstrap/VaultProjectstill carry the Layer-1 minio/garage/cockroach/mongo credential surface. The egg only needsVaultInitialization(init/unseal capture) + a minimalVaultBootstrap. Trim per 000_TOPOLOGY.md §5.1 "refactor for Layer 0" — out of scope for Stage-1 vendoring.- Run
pin-digeststo replacePIN_DIGESTinVERSIONSbefore any realpulumi up(T01/preflight concern). - Stage-2 publish (
@olsitec/pulumi-*to the foundation Forgejo npm registry) once it exists — semantic-release-monorepo + Conventional Commits (000_TOPOLOGY.md §5, memory: olsitec-charts-conventional-commits). - Lint config not vendored — the upstream modules had no per-package eslint file (only an
.editorconfig, which was copied); olsitec-core'seslint.config.mjsis project-level. Add a workspace-level lint setup in a later tooling task if desired.
6. How to reproduce the validation
cd ~/work/olsitec-foundation/foundation
bun install
bunx tsc --noEmit -p bootstrap/tsconfig.json
bunx tsc --noEmit -p packages/pulumi-docker/tsconfig.json
bunx tsc --noEmit -p packages/pulumi-vault/tsconfig.json
cd bootstrap
export PULUMI_HOME="$(pwd)/state/.pulumi-home"
export PULUMI_CONFIG_PASSPHRASE='<throwaway-dev-only>' # never commit
pulumi login "file://$(pwd)/state"
pulumi stack init foundation --secrets-provider=passphrase # first time only
pulumi preview --stack foundation
# NOTE: preview rewrites Pulumi.foundation.yaml — `git checkout` it or strip the
# appended `encryptionsalt:` line before committing. state/ is gitignored.