Publishing Components from GitHub Actions
Automating the publication of Pulumi components from GitHub Actions to your Pulumi Cloud private registry enables robust CI/CD workflows for infrastructure building blocks. This guide walks through setting up automated testing and publishing workflows that ensure quality while maintaining fast iteration cycles.
Prerequisites
- A Pulumi component authored and working locally (see Build a Component)
- A GitHub repository containing your component code
- Access to a Pulumi Cloud private registry
- Configure Pulumi and GitHub integrations: OIDC, GitHub App, etc.
- Component documentation written and committed to your repository
Development Workflow Overview
The recommended workflow for developing and publishing components follows these stages:
- Local Development: Author your component using a local development workflow
- Repository Setup: Create a GitHub repository with proper documentation
- Testing Infrastructure: Write comprehensive unit and integration tests
- Automated Testing: Set up GitHub Actions for continuous testing
- Versioned Releases: Create semantic versioned releases on GitHub
- Automated Publishing: Automate the release and publishing process
Repository Structure
Organize your component repository with the following structure:
my-component/
├── README.md # Component documentation
├── PulumiPlugin.yaml # Runtime specification
├── Makefile # Build and test commands
├── tests/ # Test directory
│ ├── unit/ # Unit tests (language-specific)
│ └── integration/ # Integration tests
├── examples/ # Usage examples
└── .github/
└── workflows/
├── test.yml # Test workflow
└── release.yml # Release workflow
We recommend one component per repository, rather than a single repository for all your components. That allows you to version and release each component separately. In some cases, it may make sense to package a few highly correlated components together in the same repository, but this should be done with caution using integration tests between the components.
Example Component Repository
For the purposes of this documentation, please refer to the example component repository. This GitHub repo contains the component code, tests, and GitHub Actions described here. If you’d like to follow along, pull this repo locally as a reference.
Create a Makefile
We recommend that you create a Makefile
to standardize your build and test commands. This example Makefile
shows how to set up some basic commands like make build
and make test
, which can run the various tasks for you. Since the component in our example is written using Go, we use go build
and go test
to build and run unit tests. For integration tests, we use a local workbench that runs pulumi preview
to validate the component end-to-end.
# ./Makefile
.PHONY: test unit-test integration-test build clean
# Run all tests
test: unit-test integration-test
# Run unit tests
unit-test:
@echo "Running unit tests..."
go test
# Run integration tests with pulumi preview
integration-test:
@echo "Running integration tests..."
PULUMI_CONFIG_PASSPHRASE="foo" pulumi login --local;
PULUMI_CONFIG_PASSPHRASE="foo" pulumi install;
-PULUMI_CONFIG_PASSPHRASE="foo" pulumi cancel --stack organization/static-page-integration-test/dev --yes;
-PULUMI_CONFIG_PASSPHRASE="foo" pulumi destroy --stack organization/static-page-integration-test/dev --yes --refresh --remove;
-PULUMI_CONFIG_PASSPHRASE="foo" pulumi -C tests/integration stack init organization/static-page-integration-test/dev --non-interactive;
PULUMI_CONFIG_PASSPHRASE="foo" pulumi -C tests/integration stack select organization/static-page-integration-test/dev;
-PULUMI_CONFIG_PASSPHRASE="foo" pulumi -C tests/integration config set aws:region us-west-2;
PULUMI_CONFIG_PASSPHRASE="foo" pulumi -C tests/integration package add ../..;
PULUMI_CONFIG_PASSPHRASE="foo" pulumi -C tests/integration preview;
PULUMI_CONFIG_PASSPHRASE="foo" pulumi logout --local;
rm tests/integration/Pulumi.dev.yaml
# Build the component
build:
@echo "Building component..."
go build
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
rm static-page-component
Testing Your Component
Unit Tests
Write unit tests that validate your component’s logic without creating cloud resources by using the integration
library from the Pulumi Provider SDK. Here we can set up a mock provider server to catch calls for resource creation and return mock resources back.
// ./main_test.go
package main
import (
"testing"
"github.com/blang/semver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
p "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi-go-provider/infer"
"github.com/pulumi/pulumi-go-provider/integration"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/property"
)
func TestConstruct(t * testing.T) {
// Configure Mocks: The provider is roughly the same as in our main.go
myProvider, err: = infer.NewProviderBuilder().
WithNamespace("example").
WithComponents(
infer.ComponentF(NewStaticPage),
).
WithModuleMap(map[tokens.ModuleName] tokens.ModuleName {
"static-page-component": "index",
}).
Build()
require.NoError(t, err)
// Configure Mocks: The Server catches calls to create resources, and returns mock resources instead.
server, err: = integration.NewServer(
t.Context(),
"example",
semver.MustParse("0.1.0"),
integration.WithProvider(myProvider),
integration.WithMocks( & integration.MockResourceMonitor {
NewResourceF: func(args integration.MockResourceArgs)(string, property.Map, error) {
// NewResourceF is called as the each resource is registered
switch {
case args.TypeToken == "aws:s3/bucketWebsiteConfigurationV2:BucketWebsiteConfigurationV2":
assert.Equal(t, args.Name, "test-static-page-website")
return args.Name, property.NewMap(map[string] property.Value {
"websiteEndpoint": property.New("http://pulumi.com"),
}), nil
}
return "", property.Map {}, nil
},
}),
)
require.NoError(t, err)
// test the "static-page-component:index:StaticPage" component
// We try to construct a StaticPage component named "test-static-page"
// The mock will set the endpoint value
resp, err: = server.Construct(p.ConstructRequest {
Urn: "urn:pulumi:stack::project::static-page-component:index:StaticPage::test-static-page",
Inputs: property.NewMap(map[string] property.Value {
"indexContent": property.New("test content"),
}),
})
require.NoError(t, err)
// check that we got the correct output. If something was broken then we'd never get the call
// to create the BucketWebsiteConfigurationV2 object, and thus, never get this mock value back
require.Equal(t, property.NewMap(map[string] property.Value {
"endpoint": property.New("http://pulumi.com"),
}), resp.State)
}
Run the unit tests using go test
:
$ go test
or if you have the Makefile
set up already:
$ make unit-test
Integration Tests
For integration tests, set up a small local test workbench using a YAML Pulumi program. Then you can use pulumi preview
to validate resource creation:
# ./tests/integration/Pulumi.yaml
name: static-page-integration-test
description: A minimal Pulumi YAML program
runtime: yaml
packages:
static-page-component: ../..
resources:
my-static-page:
type: static-page-component:StaticPage
properties:
indexContent: "test content"
outputs:
endpoint: ${my-static-page.endpoint}
Run integration tests with:
$ cd tests/integration
$ pulumi preview
or if you have the Makefile
set up already:
$ make integration-test
GitHub Actions Setup
Test Workflow
Before we publish, we need to be able to validate our code in an automated fashion. For that, we will set up a test workflow in GitHub Actions.
Create .github/workflows/test.yml
for continuous testing:
# ./.github/workflows/test.yml
name: Test Workflow
on:
pull_request: # Trigger on a PR
workflow_dispatch: # Allows manual triggering of the workflow
jobs:
integration-test:
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
### Set variables for the action.
env:
PULUMI_ORG: 'pulumi' # Your Pulumi organization
steps:
- uses: actions/checkout@v4
- name: Authenticate to Pulumi
uses: pulumi/auth-actions@v1
with:
organization: ${{ env.PULUMI_ORG }}
requested-token-type: urn:pulumi:token-type:access_token:organization
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-region: us-west-2
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Run Preview
uses: pulumi/actions@v6
with:
command: preview
work-dir: tests/integration
stack-name: ${{ env.PULUMI_ORG }}/static-page-integration-test/dev
upsert: true
unit-test:
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go ✨
uses: actions/setup-go@v3
with:
go-version: '1.24'
- name: Downloading dependencies 📦
run: go mod download
- name: Run Tests
run: go test
In this workflow, we use some Pulumi-specific GitHub Actions, as well as some off-the-shelf standard actions.
For the integration tests:
actions/checkout
- check out the code into the Github runnerpulumi/auth-actions
- authenticate with Pulumi Cloud (make sure to setup GitHub OIDC)aws-actions/configure-aws-credentials
- set up AWS credentialspulumi/actions
- Run a Pulumi command, in this case,pulumi preview
In the pulumi/actions
step, we use one of Pulumi’s GitHub Actions and configure it to run the preview
command, change the root directory with work-dir
, set the correct stack name by passing in the Pulumi organization name via the env.PULUMI_ORG
variable we set earlier in the file, and configure upsert
to make sure that the stack gets created if it doesn’t exist yet, or updated if it does.
For the unit tests we also use actions/setup-go
to install the Golang tooling. That sets us up to run go mod download
to install our dependencies and go test
to run the unit tests.
Release and Publishing Workflow
Finally, we need some automation to cut a release and have it publish directly to your Pulumi private repository. Create .github/workflows/release.yml
for automated publishing:
# ./.github/workflows/release.yml
name: Release Workflow
on:
push:
tags:
- '*' # Trigger on any tag push.
workflow_dispatch: # Allows manual triggering of the workflow
jobs:
distribute-release:
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
### Set variables for the action.
env:
PULUMI_ORG: 'pulumi' # The Pulumi organization to publish the component to.
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.ref }} # Checkout the specific tag that triggered the workflow
fetch-depth: 0 # Ensures the build matches the git tag.
- name: Authenticate to Pulumi
uses: pulumi/auth-actions@v1
with:
organization: ${{ env.PULUMI_ORG }}
requested-token-type: urn:pulumi:token-type:access_token:organization
scope: admin
# Determine the version to use - either the triggered tag or latest tag for manual runs
- name: Determine Component Version
id: version
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
# For tag pushes, use the tag that triggered the workflow
VERSION="${{ github.ref_name }}"
echo "Using triggered tag: $VERSION"
else
# For manual runs, get the latest tag
VERSION=$(git tag --sort=-version:refname | head -1)
echo "Manual run: Using latest tag: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
# Publish if this is a tag push.
- name: Publish Component to Private Registry
if: github.event_name == 'push'
run: |
echo "Publishing latest component version to the ${{ env.PULUMI_ORG }} Pulumi org."
pulumi package publish https://github.com/${{ github.repository }} --publisher ${{ env.PULUMI_ORG }}
This workflow will be automatically triggered any time a tag is pushed. The easiest way to do that is via the GitHub web-ui, by clicking on “Releases” then “Draft a new Release”, which will give a form that includes a new semver tag, name of the release, and release notes. When the release is made it will push a tag to the repo, which will then trigger this workflow. It can also be run manually.
Similiarly to the testing workflow, we use a mix of Pulumi-specific GitHub Actions, as well as some off-the-shelf standard actions:
actions/checkout@v4
- check out the code into the Github runnerpulumi/auth-actions@v1
- authenticate with Pulumi Cloud
However, pulumi/actions
doesn’t support the publish
subcommand, so we set that step up manually via a run
step. Use the PULUMI_ORG
variable to set the --publisher
and the GitHub Actions-internal github.repository
variable to get the name of the repository.
GitHub Actions Triggers
Now that we have these workflows in place, you could set up your automation in a variety of ways. Consider configuring your workflows with different triggers based on your team’s needs:
Test Workflow Triggers
- On Push: Test every commit to main branches
- On Pull Request: Test all proposed changes
- Manual Dispatch: Allow on-demand testing
- Scheduled: Daily tests to catch environmental issues
- On Release: Final validation before publishing
Release Workflow Triggers
- On Release Published: Automatically publish when GitHub releases are created
- Manual Dispatch: Allow manual publishing with version specification
- Multi-repo automation: Coordinate releases by triggering the release workflow from another repo’s action
For more details on GitHub Actions triggers, see the GitHub Actions documentation.
Workflow Status Notifications
Consider configuring notifications for workflow failures, like this YAML stanza that notifies via Slack:
jobs:
slackNotification:
name: Slack Notification
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
Next Steps
Once your automated publishing workflow is established, consider these enhancements:
- Deployment Hooks: Set up Pulumi Cloud webhooks to trigger deployments when new component versions are published
- Version Compatibility Testing: Test new versions against existing consumer programs
- Progressive Rollouts: Implement canary releases and blue/green deployments for high-impact components
- Integration with Policies: Create Pulumi Crossguard policies that ensure only approved component versions are deployed
Learn More
Thank you for your feedback!
If you have a question about how to use Pulumi, reach out in Community Slack.
Open an issue on GitHub to report a problem or suggest an improvement.