1. Docs
  2. Pulumi IDP
  3. Get Started
  4. Publishing Components from GitHub Actions

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

    Development Workflow Overview

    The recommended workflow for developing and publishing components follows these stages:

    1. Local Development: Author your component using a local development workflow
    2. Repository Setup: Create a GitHub repository with proper documentation
    3. Testing Infrastructure: Write comprehensive unit and integration tests
    4. Automated Testing: Set up GitHub Actions for continuous testing
    5. Versioned Releases: Create semantic versioned releases on GitHub
    6. 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:

    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 runner
    • pulumi/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

      Guide to Secure Infrastructure Automation