Releasing Crossplane Extensions

Distributing Crossplane extensions

Crossplane provides a packaging specification for extending a Crossplane instance with APIs and business logic for composing resources.

Building a Crossplane extension involves creating OCI images in the xpkg format. Authors and maintainers of Crossplane extensions must push their packages to an OCI registry before users can reference and use them.

The release process for Crossplane extensions grew organically in the community and developed its own conventions and common configurations. Authors of these extensions should follow this guide to enable automation for building and pushing their packages as part of their git workflow.

This guide provides step-by-step instructions for configuring automated CI pipelines in GitHub Actions for pushing your Crossplane extensions to xpkg.crossplane.io, the main registry that the Crossplane community uses today.

Tip
For more information about Crossplane packages, review the xpkg concepts.

Typical workflow

A typical GitHub workflow definition to build and release an extension contains the following steps:

  1. Fetching the source repository
  2. Authenticating to a remote registry
  3. Building and packaging artifacts
  4. Pushing (publishing) the artifact
Warning
The supplied credentials for the remote registry require read and write access as upload requests to the registry specify push authorization scope.

Quickstart: Releasing a Provider to xpkg.crossplane.io

Prerequisites

Steps

  1. Create a new YAML file under .github/workflows. By convention, name this file publish-provider-package.yaml.

  2. Copy the following workflow definition into the file, replacing <REPOSITORY NAME> with the desired name of the repository in the registry.

     1name: Publish Provider Package
     2
     3on:
     4  workflow_dispatch:
     5    inputs:
     6      version:
     7        description: "Version string to use while publishing the package (e.g. v1.0.0-alpha.1)"
     8        default: ''
     9        required: false
    10      go-version:
    11        description: 'Go version to use if building needs to be done'
    12        default: '1.23'
    13        required: false
    14
    15jobs:
    16  publish-provider-package:
    17    uses: crossplane-contrib/provider-workflows/.github/workflows/publish-provider-non-family.yml@main
    18    with:
    19      repository: <REPOSITORY NAME>
    20      version: ${{ github.event.inputs.version }}
    21      go-version: ${{ github.event.inputs.go-version }}
    22      cleanup-disk: true
    23    secrets:
    24      GHCR_PAT: ${{ secrets.GITHUB_TOKEN }}
    
  3. Commit the workflow file to the default branch of the GitHub repository.

  4. The workflow should now be available to trigger via the GitHub UI in the Actions tab.

  5. Create a release branch with the release- prefix in the name in the GitHub UI. For example, release-0.1.

  6. Tag the desired commit on release branch with a valid semver release tag. For example, v0.1.0. By default, this is the inferred reference pushed to the registry.

  7. Manually run the workflow in the GitHub UI, targeting the release branch from step 5.

See branching conventions for more details on tagging practices and optionally overriding the inferred git tag version.

Quickstart: Releasing a Function to xpkg.crossplane.io

The template repository for functions provides a functional GitHub Action YAML file that pushes to xpkg.crossplane.io without extra configuration.

To build and push a new release to the registry:

  1. Cut a release branch with the release- prefix in the name in the GitHub UI. For example, release-0.1.
  2. Tag the desired commit on release branch with a valid semver release tag for a corresponding GitHub Release. For example, v0.1.0.
  3. Manually run the workflow in the GitHub UI, targeting the release branch from step 1. The workflow generates a default version string if user input isn’t provided.

See branching conventions for more details on tagging practices and optionally overriding the inferred git tag version.

Common Configuration

While the reusable workflows referenced in the quickstart guides are for convenience, users may choose to write their own custom GitHub Actions.

This and following sections provide more detailed information about common configuration options and conventions to implement the release process.

All workflows require references to credentials for a remote registry. Typically, users configure them as GitHub Actions Secrets, and the workflow performs authentication via thedocker/login-action action.

For example, adding the following step to a pipeline authenticates the job to ghcr.io using the workflow’s ephemeral GitHub OIDC token.

1      - name: Login to GHCR
2        uses: docker/login-action@v3
3        with:
4          registry: ghcr.io
5          username: ${{ github.repository_owner }}
6          password: ${{ secrets.GITHUB_TOKEN }}
Important

By default, the job’s OIDC token doesn’t have permission to write packages to ghcr.io. Permissions are configurable in the GitHub repository’s settings or declared explicitly in the workflow definition YAML file.

Writing packages requires a permissions block with packages: write if it isn’t configured elsewhere for the repository.

For other registries, it’s still best practice to reference credentials as custom Secret variables. For example:

1      - name: Login to Another Registry
2        uses: docker/login-action@v3
3        with:
4          registry: my-registry.io
5          username: ${{ env.REGISTRY_USER }}
6          password: ${{ secrets.REGISTRY_PASSWORD }}

Branching conventions

Repositories for Crossplane extensions follow similar branching conventions to upstream Crossplane, where the release process assumes the workflow executing in branches with the release-* prefix. main is often included, though a conventional release process would not build and push off of tags on main.

1on:
2  push:
3    branches:
4      - main
5      - release-*

For example, when releasing v0.1.0 of an extension, the conventional process is to cut a release branch release-0.1 at the git commit where it builds from, and tag it as v0.1.0.

Note
Some custom workflows may accept an explicit input for the remote reference instead of inferring it from a git ref. The ci.yml file for crossplane-contrib/function-python is a good example.

Configuring workflows for function packages

Function workflow definitions differ based on the base language the function implementation uses. For example, a Python function requires a Python environment in the GitHub Action runner:

 1      - name: Setup Python
 2        uses: actions/setup-python@v5
 3        with:
 4          python-version: ${{ env.PYTHON_VERSION }}
 5
 6      - name: Setup Hatch
 7        run: pipx install hatch==1.7.0
 8
 9      - name: Lint
10        run: hatch run lint:check

While the template repository provides a working pipeline definition, users may choose to customize their environment with different tooling.

Functions also require a runtime image of the core business logic to build and embed into the Function package. The default workflow definition builds for two platforms: linux/amd64 and linux/arm64.

 1      - name: Build Runtime
 2        id: image
 3        uses: docker/build-push-action@v6
 4        with:
 5          context: .
 6          platforms: linux/${{ matrix.arch }}
 7          cache-from: type=gha
 8          cache-to: type=gha,mode=max
 9          target: image
10          build-args:
11            PYTHON_VERSION=${{ env.PYTHON_VERSION }}
12          outputs: type=docker,dest=runtime-${{ matrix.arch }}.tar

Configuring workflows for provider packages

Providers, unlike Functions, use custom make targets in the build submodule for building and pushing Crossplane Provider packages.

Configuring the workflow for a specific registry involves two steps:

  1. Updating the registry variables in the top-level Makefile.
  2. Referencing GitHub Actions Secrets for authorized credentials to the registry.

Configure target registry

The provider template repository includes a top-level Makefile. Edit the following variables to define the target registry:

  1. XPKG_REG_ORGS - a space-delimited list of target repositories.
  2. XPKG_REG_ORGS_NO_PROMOTE - for registries that don’t use or infer channel tags.

For example, the following dual-pushes to xpkg.crossplane.io as well as index.docker.io:

1XPKG_REG_ORGS ?= xpkg.crossplane.io/crossplane-contrib index.docker.io/crossplanecontrib
2
3XPKG_REG_ORGS_NO_PROMOTE ?= xpkg.crossplane.io/crossplane-contrib

Reusable workflows

The crossplane-contrib/provider-workflows repository provide reusable workflow definitions that are callable from a custom CI pipeline.

For example, the following snippet references the callable workflow to build and push the provider-kubernetes package to xpkg.crossplane.io:

 1jobs:
 2  publish-provider-package:
 3    uses: crossplane-contrib/provider-workflows/.github/workflows/publish-provider-non-family.yml@main
 4    with:
 5      repository: provider-kubernetes
 6      version: ${{ github.event.inputs.version }}
 7      go-version: ${{ github.event.inputs.go-version }}
 8      cleanup-disk: true
 9    secrets:
10      GHCR_PAT: ${{ secrets.GITHUB_TOKEN }}
Tip
The reusable workflows referenced here publish to ghcr.io by default. Ensure that the default GitHub Actions OIDC token inherits the packages: write permission.

Troubleshooting

Ensure the target repository exists in the registry. You need to create it if it doesn’t already exist.

Ensure the credentials used during the registry login step has authorization to pull and push, and that the {{ secrets.* }} variable substitutions match what’s configured in GitHub.