Open-Source Wikis

/

Pulumi

/

How to contribute

/

Patterns and conventions

pulumi/pulumi

Patterns and conventions

A grab-bag of conventions you'll need to internalize before writing maintainable code in this repo.

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 plain error.
  • promise.Promise — Go's flavor of "future"; sdk/go/common/promise/. Used heavily in pkg/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.Group for "wait-for-all-or-first-error".
  • context.Context for 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.Type for type tokens, not strings. See sdk/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 Resource and embed either CustomResourceState or ComponentResourceState.
  • Inputs use <Type>Input interfaces; concrete values use <Type>Output (lazy, dependency-aware). types_builtins.go is generated from sdk/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.apply callbacks, not in the constructor.

Python SDK (sdk/python/)

  • Uses dataclasses + pulumi.Output[T].
  • Awaitable semantics: Output is 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.0 with the config in .golangci.yml. Common gotchas: requiredfield (custom plugin in .golangci/) flags missing fields in struct literals; gofumpt is stricter than gofmt.
  • biome for TS/JS/JSON. Both lint and format. Config: sdk/nodejs/biome.json.
  • eslint for the Node SDK only (sdk/nodejs/.eslintrc.js).
  • ruff for Python.
  • buf/protoc linters for proto/.

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 world

Schema 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.Println from anything outside pkg/cmd/pulumi/.
  • ❌ Touching the on-disk snapshot directly — go through the Backend interface.
  • ❌ Synchronously calling out to a provider from the step generator (it must stay non-blocking).
  • ❌ Adding replace directives in committed go.mod files (use go.work locally 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.

Patterns and conventions – Pulumi wiki | Factory