pulumi/pulumi
Patterns and conventions
A grab-bag of conventions you'll need to internalize before writing maintainable code in this repo.
Copyright headers
Every new source file (Go, TS, Python, .proto) starts with the standard Apache-2.0 header. Bump the year for new files to the current year:
// Copyright 2026, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");Existing files keep their original year unless they're substantially rewritten.
Error handling (Go)
Errors are wrapped with fmt.Errorf("...%w", err) to keep errors.Is/errors.As working. Sentinel errors are exported when they're meant to be matched (see pkg/engine/errors.go, pkg/backend/backenderr/).
Two domain-specific patterns:
*result.Result— used in older engine code as a tri-state (success / bail / err). New code prefers plainerror.promise.Promise— Go's flavor of "future";sdk/go/common/promise/. Used heavily inpkg/resource/deploy/.
Wrap engine-internal errors with enough context to be useful in the user-facing diagnostic stream — the engine's event sink (pkg/engine/eventsink.go) is what surfaces them.
Diagnostics
User-facing messages route through sdk/go/common/diag/. Don't fmt.Println from engine code. Severity levels: Debug, Info, Infoerr, Warning, Error. The CLI display layer (pkg/backend/display/, pkg/cmd/pulumi/display/) renders them.
Goroutine ownership
Every spawned goroutine has an owner that knows when it should exit. Patterns:
errgroup.Groupfor "wait-for-all-or-first-error".context.Contextfor cancellation.cancel := context.WithCancel(...); defer cancel()so leakage is a compile/lint error.
pkg/resource/deploy/goroutine_panic_recovery.go is the engine's safety net — it catches panics in step-executor goroutines and surfaces them as engine errors instead of process aborts.
Resources and URNs
- Always pass
URNs to internal APIs, never raw strings. - Use
tokens.Typefor type tokens, not strings. Seesdk/go/common/tokens/. - Resource state structs are immutable — to mutate, build a new one via
state_builder.go.
SDK conventions
Go SDK (sdk/go/pulumi/)
- Resources implement
Resourceand embed eitherCustomResourceStateorComponentResourceState. - Inputs use
<Type>Inputinterfaces; concrete values use<Type>Output(lazy, dependency-aware).types_builtins.gois generated fromsdk/go/pulumi/generate/. - Pass
pulumi.NewResourceOptions(...)rather than positional options where possible.
Node SDK (sdk/nodejs/)
- TypeScript-first; types are exported. Files use
.ts. Output<T>is the central abstraction.output(value)lifts;output.apply(fn)projects.- Async work belongs inside
Output.applycallbacks, not in the constructor.
Python SDK (sdk/python/)
- Uses dataclasses +
pulumi.Output[T]. Awaitablesemantics:Outputis awaitable in the sense that.future()returns an asyncio future.- Type hints are required on public APIs.
File-naming conventions
| Pattern | Meaning |
|---|---|
*_test.go |
Standard Go tests. |
example_test.go |
Documentation examples (godoc). |
*_legacy_test.go |
Tests for behavior preserved for backwards compatibility. |
*_benchmark_test.go |
Benchmarks. |
*_test_fixtures/ |
Golden files (PULUMI_ACCEPT=1 updates). |
Linters that bite
golangci-lint 2.9.0with the config in.golangci.yml. Common gotchas:requiredfield(custom plugin in.golangci/) flags missing fields in struct literals;gofumptis stricter thangofmt.biomefor TS/JS/JSON. Both lint and format. Config:sdk/nodejs/biome.json.eslintfor the Node SDK only (sdk/nodejs/.eslintrc.js).rufffor Python.buf/protoc linters forproto/.
make lint runs all of them; make lint_fix auto-fixes what can be fixed.
Generated code
- Don't hand-edit. Edit the source and regenerate.
- Sources of truth:
proto/*.proto,pkg/codegen/schema/pulumi.json,sdk/go/pulumi/generate/,tools/automation/specification.json. - Generated paths committed to the repo:
sdk/proto/go/,sdk/nodejs/proto/,sdk/python/lib/pulumi/runtime/proto/,sdk/go/pulumi/types_builtins*.go,sdk/{nodejs,python}/automation/interface/. - CI enforces freshness (
make check_proto).
Cross-module changes
go.mod files in pkg/, sdk/, tests/, and the language-host modules are independent. A change in sdk/ that breaks pkg/ is not caught until you build pkg/. Always:
mise exec -- make work # creates go.work
mise exec -- make tidy # checks all modules
mise exec -- make build # builds the worldSchema metaschema
pkg/codegen/schema/pulumi.json is the schema used to validate provider schemas. It must:
- Be valid JSON Schema.
- Pass biome formatting (
make lint_pulumi_json). - Be backwards-compatible — adding required fields is a breaking change for existing providers.
Hooks and transforms
When adding new engine-side hooks (e.g. resource lifecycle hooks), they live in pkg/resource/deploy/resource_hooks.go and the corresponding monitor RPC is in proto/pulumi/resource.proto (RegisterResourceHook). Transforms — both per-resource and stack-wide (RegisterStackTransform) — modify resource inputs before the engine sees them. Both are first-class features and must be tested in lifecycle tests.
Anti-patterns to avoid
- ❌ String-typed URNs anywhere in engine code.
- ❌ Calling
fmt.Printlnfrom anything outsidepkg/cmd/pulumi/. - ❌ Touching the on-disk snapshot directly — go through the
Backendinterface. - ❌ Synchronously calling out to a provider from the step generator (it must stay non-blocking).
- ❌ Adding
replacedirectives in committedgo.modfiles (usego.worklocally instead). - ❌ Editing generated proto code by hand.
See also
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.