Promote Pulumi stacks with GitHub Actions

Preview pull requests, promote dev and staging changes, and keep production behind approval using GitHub Actions with OIDC and Pulumi ESC-oriented configuration.

Switch variant

Choose a different platform.

Download blueprint

Get this GitHub Actions blueprint project as a zip. Switch Pulumi language here to keep the download aligned with the install commands and blueprint program on the page.

Download the TypeScript blueprint with the matching Pulumi program, dependency files, and README.

Download TypeScript blueprint

Download the Python blueprint with the matching Pulumi program, dependency files, and README.

Download Python blueprint

Download the Go blueprint with the matching Pulumi program, dependency files, and README.

Download Go blueprint

This guide gives you a promotion pipeline for Pulumi stacks. It keeps one application infrastructure project in infrastructure/, creates dev, staging, and production stacks, and shows how to move changes from review to production without long-lived cloud keys.

Use it when your team wants a small, auditable path from pull request preview to production approval before layering on policy packs, drift detection, or service-specific release automation.

Architecture

The blueprint separates three concerns:

  • infrastructure/ holds the Pulumi program your team promotes through environments.
  • dev, staging, and production stacks carry environment-specific config and state.
  • GitHub Actions runs previews and updates with credentials supplied by Pulumi ESC or OIDC instead of static cloud keys.

This starter stays cloud-neutral. Replace the sample stack with your real resources after the promotion mechanics work.

GitHub Actions variant

This variant ships two workflows:

  • .github/workflows/pulumi-preview.yml runs pulumi preview on pull requests.
  • .github/workflows/pulumi-deploy.yml deploys dev, then staging, then waits for the GitHub protected environment named production before deploying production.

The workflows grant id-token: write, use pulumi/auth-actions@v1 to exchange GitHub OIDC for a short-lived Pulumi Cloud session, then use pulumi/esc-action@v1 to load your ESC environment for cloud credentials and config. Set PULUMI_ORG and PULUMI_ESC_ENVIRONMENT as GitHub environment variables, and keep real cloud role identifiers in Pulumi ESC or cloud trust policy, not in source control.

Prerequisites

You need:

  • a Pulumi Cloud organization and the Pulumi CLI
    • Python 3.11 or newer and a virtual environment tool
    • Go 1.23 or newer
  • a GitHub repository that contains this starter
  • Pulumi ESC environments or cloud OIDC roles for each stack
  • three Pulumi stacks named dev, staging, and production

The starter uses example organization and repository setting names. Set your own values as Pulumi config or CI environment values before your first run.

Download the starter

Download the starter for the language you want to use:

  • infrastructure/index.ts as the sample Pulumi stack

  • language-specific project files for the Pulumi program

  • platform-specific CI/CD files for the selected setup

  • infrastructure/__main__.py as the sample Pulumi stack

  • language-specific project files for the Pulumi program

  • platform-specific CI/CD files for the selected setup

  • infrastructure/main.go as the sample Pulumi stack

  • language-specific project files for the Pulumi program

  • platform-specific CI/CD files for the selected setup

The archive includes two workflow files that run preview and promotion from your repository plus the sample Pulumi program under infrastructure/.

Quickstart

Unzip the starter, install dependencies, and create the three stacks:

cd pulumi-cicd-promotion-github-actions
cd infrastructure
npm install
pulumi stack init dev
pulumi stack init staging
pulumi stack init production
cd pulumi-cicd-promotion-github-actions
python3 -m venv .venv
source .venv/bin/activate
cd infrastructure
pip install -r requirements.txt
pulumi stack init dev
pulumi stack init staging
pulumi stack init production
cd pulumi-cicd-promotion-github-actions
cd infrastructure
go mod tidy
pulumi stack init dev
pulumi stack init staging
pulumi stack init production

Attach each stack to an ESC environment or OIDC-backed cloud configuration before running the pipeline. Keep stack-specific settings in Pulumi config, GitHub environments, or Pulumi ESC.

Promotion flow

  1. Open a pull request and review the pulumi preview result before merge.
  2. Merge to main to update dev.
  3. Let the pipeline promote the same commit to staging.
  4. Release to production only after GitHub protected environment named production is satisfied.

This shape keeps review fast while making the production step explicit.

Credential model

Use OIDC and Pulumi ESC for cloud access:

  • grant the runner id-token: write when using GitHub Actions
  • configure cloud trust policies outside the starter, scoped to the repository, branch, and environment
  • import cloud credentials through Pulumi ESC or deployment OIDC settings
  • avoid static access keys, checked-in tokens, and repository-wide secrets with broad permissions

The starter references environment variables and Pulumi config names only. It does not include real tokens or cloud key material.

Sample Pulumi stack

This small stack stands in for your real application infrastructure. The CI/CD wiring targets the same infrastructure/ project for dev, staging, and production.

import * as pulumi from "@pulumi/pulumi";
import * as random from "@pulumi/random";

const stack = pulumi.getStack();
const config = new pulumi.Config();
const serviceName = config.get("serviceName") ?? "promotion-demo";

const suffix = new random.RandomPet("release-suffix", {
    prefix: `${serviceName}-${stack}`,
    length: 2,
});

export const environment = stack;
export const releaseName = suffix.id;
import pulumi
import pulumi_random as random

stack = pulumi.get_stack()
config = pulumi.Config()
service_name = config.get("serviceName") or "promotion-demo"

suffix = random.RandomPet("release-suffix",
    prefix=f"{service_name}-{stack}",
    length=2)

pulumi.export("environment", stack)
pulumi.export("releaseName", suffix.id)
package main

import (
    "fmt"

    "github.com/pulumi/pulumi-random/sdk/v4/go/random"
    "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)

func Program(ctx *pulumi.Context) error {
        cfg := config.New(ctx, "")
        serviceName := cfg.Get("serviceName")
        if serviceName == "" {
            serviceName = "promotion-demo"
        }

        suffix, err := random.NewRandomPet(ctx, "release-suffix", &random.RandomPetArgs{
            Prefix: pulumi.String(fmt.Sprintf("%s-%s", serviceName, ctx.Stack())),
            Length: pulumi.Int(2),
        })
        if err != nil {
            return err
        }

        ctx.Export("environment", pulumi.String(ctx.Stack()))
        ctx.Export("releaseName", suffix.ID())
        return nil
}

func main() {
    pulumi.Run(Program)
}

Next steps

Replace the sample stack with your real infrastructure, then add policy checks, drift detection, and stack tags that match your governance model. If your production process requires a change record or release train, make that system trigger the final production deployment rather than auto-deploying every merge.

Frequently asked questions

Which stacks does this blueprint assume?
It assumes one Pulumi project with dev, staging, and production stacks. You can rename the stacks, but keep the promotion order and approval gate explicit.
Where do cloud credentials live?
The workflows and deployment settings are written for short-lived credentials from OIDC and Pulumi ESC. Do not store static cloud keys in the repository or workflow files.
Why is production different from staging?
Production changes need a deliberate human or policy-controlled release step. GitHub Actions uses a protected environment named production, while Pulumi Deployments leaves production as click-to-deploy or API-triggered because approval policy is organization-specific.
Can I adapt this to a monorepo?
Yes. Keep the Pulumi project under a stable directory such as infrastructure/, then use path filters so unrelated application changes do not trigger infrastructure deployments.
Does this create cloud resources?
The starter uses a tiny Pulumi program so the CI/CD shape is easy to test. Replace it with your real stack once the promotion flow works in your organization.