|
| 1 | +--- |
| 2 | +title: Releasing Crossplane Extensions |
| 3 | +weight: 80 |
| 4 | +description: "Configuring build pipelines for Crossplane extensions with GitHub |
| 5 | +Actions" |
| 6 | +--- |
| 7 | + |
| 8 | +## Distributing Crossplane extensions |
| 9 | + |
| 10 | +Crossplane provides a packaging specification for extending a Crossplane |
| 11 | +instance with APIs and business logic for composing resources. |
| 12 | + |
| 13 | +Building a Crossplane extension involves creating OCI images in the [xpkg] |
| 14 | +format. Authors and maintainers of Crossplane extensions must push their |
| 15 | +packages to an OCI registry before users can reference and use them. |
| 16 | + |
| 17 | +The release process for Crossplane extensions grew organically in the community |
| 18 | +and developed its own conventions and common configurations. Authors of these |
| 19 | +extensions should follow this guide to enable automation for building |
| 20 | +and pushing their packages as part of their git workflow. |
| 21 | + |
| 22 | +This guide provides step-by-step instructions for configuring automated |
| 23 | +CI pipelines in GitHub Actions for pushing your Crossplane extensions to |
| 24 | +`xpkg.crossplane.io`, the main registry that the Crossplane community |
| 25 | +uses today. |
| 26 | + |
| 27 | +{{< hint "tip" >}} |
| 28 | +For more information about Crossplane packages, review the |
| 29 | +[xpkg concepts]({{<ref "../concepts/packages" >}}). |
| 30 | +{{< /hint >}} |
| 31 | + |
| 32 | +## Typical workflow |
| 33 | + |
| 34 | +A typical GitHub workflow definition to build and release an extension |
| 35 | +contains the following steps: |
| 36 | + |
| 37 | +1. Fetching the source repository |
| 38 | +2. Authenticating to a remote registry |
| 39 | +3. Building and packaging artifacts |
| 40 | +4. Pushing (publishing) the artifact |
| 41 | + |
| 42 | +{{< hint "warning" >}} |
| 43 | +The supplied credentials for the remote registry require read and write access |
| 44 | +as upload requests to the registry specify `push` authorization scope. |
| 45 | +{{< /hint >}} |
| 46 | + |
| 47 | +## Quickstart: Releasing a Provider to `xpkg.crossplane.io` |
| 48 | + |
| 49 | +### Prerequisites |
| 50 | + |
| 51 | +- A GitHub repository, for example created from the |
| 52 | +[Upjet template](https://github.com/crossplane/upjet-provider-template) |
| 53 | + |
| 54 | +### Steps |
| 55 | + |
| 56 | +1. Create a new YAML file under `.github/workflows`. By convention, name this |
| 57 | +file `publish-provider-package.yaml`. |
| 58 | +2. Copy the following workflow definition into the file, replacing |
| 59 | +`<REPOSITORY NAME>` with the desired name of the repository in the registry. |
| 60 | + |
| 61 | + ```yaml |
| 62 | + name: Publish Provider Package |
| 63 | + |
| 64 | + on: |
| 65 | + workflow_dispatch: |
| 66 | + inputs: |
| 67 | + version: |
| 68 | + description: "Version string to use while publishing the package (e.g. v1.0.0-alpha.1)" |
| 69 | + default: '' |
| 70 | + required: false |
| 71 | + go-version: |
| 72 | + description: 'Go version to use if building needs to be done' |
| 73 | + default: '1.23' |
| 74 | + required: false |
| 75 | + |
| 76 | + jobs: |
| 77 | + publish-provider-package: |
| 78 | + uses: crossplane-contrib/provider-workflows/.github/workflows/publish-provider-non-family.yml@main |
| 79 | + with: |
| 80 | + repository: <REPOSITORY NAME> |
| 81 | + version: ${{ github.event.inputs.version }} |
| 82 | + go-version: ${{ github.event.inputs.go-version }} |
| 83 | + cleanup-disk: true |
| 84 | + secrets: |
| 85 | + GHCR_PAT: ${{ secrets.GITHUB_TOKEN }} |
| 86 | + ``` |
| 87 | + |
| 88 | +3. Commit the workflow file to the default branch of the GitHub repository. |
| 89 | +4. The workflow should now be available to trigger via the GitHub UI in the |
| 90 | +`Actions` tab. |
| 91 | +5. Create a release branch with the `release-` prefix in the name in the GitHub UI. For example, `release-0.1`. |
| 92 | +6. Tag the desired commit on release branch with a valid semver release tag. |
| 93 | +For example, `v0.1.0`. By default, this is the inferred reference pushed to the registry. |
| 94 | +7. Manually run the workflow in the GitHub UI, targeting the release branch from step 5. |
| 95 | + |
| 96 | +See [branching conventions](#branching-conventions) for more details on tagging |
| 97 | +practices and optionally overriding the inferred git tag version. |
| 98 | + |
| 99 | +## Quickstart: Releasing a Function to `xpkg.crossplane.io` |
| 100 | + |
| 101 | +The template repository for [functions] provides a functional GitHub Action |
| 102 | +YAML file that pushes to `xpkg.crossplane.io` without extra configuration. |
| 103 | + |
| 104 | +To build and push a new release to the registry: |
| 105 | + |
| 106 | +1. Cut a release branch with the `release-` prefix in the name in the GitHub UI. For example, `release-0.1`. |
| 107 | +2. Tag the desired commit on release branch with a valid semver release tag for a corresponding |
| 108 | +GitHub Release. For example, `v0.1.0`. |
| 109 | +3. Manually run the workflow in the GitHub UI, targeting the release branch from step 1. |
| 110 | +The workflow generates a default version string if user input isn't provided. |
| 111 | + |
| 112 | +See [branching conventions](#branching-conventions) for more details on tagging |
| 113 | +practices and optionally overriding the inferred git tag version. |
| 114 | + |
| 115 | +## Common Configuration |
| 116 | + |
| 117 | +While the reusable workflows referenced in the quickstart guides are for |
| 118 | +convenience, users may choose to write their own custom GitHub Actions. |
| 119 | + |
| 120 | +This and following sections provide more detailed information |
| 121 | +about common configuration options and conventions to implement the release |
| 122 | +process. |
| 123 | + |
| 124 | +All workflows require references to credentials for a remote registry. |
| 125 | +Typically, users configure them as [GitHub Actions Secrets], and the workflow |
| 126 | +performs authentication via the`docker/login-action` |
| 127 | +[action](http://github.com/docker/login-action). |
| 128 | + |
| 129 | +For example, adding the following step to a pipeline authenticates |
| 130 | +the job to `ghcr.io` using the workflow's ephemeral GitHub OIDC token. |
| 131 | + |
| 132 | +```yaml |
| 133 | + - name: Login to GHCR |
| 134 | + uses: docker/login-action@v3 |
| 135 | + with: |
| 136 | + registry: ghcr.io |
| 137 | + username: ${{ github.repository_owner }} |
| 138 | + password: ${{ secrets.GITHUB_TOKEN }} |
| 139 | +``` |
| 140 | +
|
| 141 | +{{< hint "important" >}} |
| 142 | +By default, the job's OIDC token doesn't have permission to write packages |
| 143 | +to `ghcr.io`. Permissions are configurable in the GitHub repository's settings |
| 144 | +or declared |
| 145 | +[explicitly](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token) |
| 146 | +in the workflow definition YAML file. |
| 147 | + |
| 148 | +Writing packages requires a `permissions` block with `packages: write` if it |
| 149 | +isn't configured elsewhere for the repository. |
| 150 | +{{< /hint >}} |
| 151 | + |
| 152 | +For other registries, it's still best practice to reference credentials as |
| 153 | +custom Secret variables. For example: |
| 154 | + |
| 155 | +```yaml |
| 156 | + - name: Login to Another Registry |
| 157 | + uses: docker/login-action@v3 |
| 158 | + with: |
| 159 | + registry: my-registry.io |
| 160 | + username: ${{ env.REGISTRY_USER }} |
| 161 | + password: ${{ secrets.REGISTRY_PASSWORD }} |
| 162 | +``` |
| 163 | + |
| 164 | +## Branching conventions |
| 165 | + |
| 166 | +Repositories for Crossplane extensions follow similar branching conventions |
| 167 | +to upstream Crossplane, where the release process assumes the workflow |
| 168 | +executing in branches with the `release-*` prefix. `main` is often included, |
| 169 | +though a conventional release process would not build and push off of tags on |
| 170 | +`main`. |
| 171 | + |
| 172 | +```yaml |
| 173 | +on: |
| 174 | + push: |
| 175 | + branches: |
| 176 | + - main |
| 177 | + - release-* |
| 178 | +``` |
| 179 | + |
| 180 | +For example, when releasing `v0.1.0` of an extension, the conventional |
| 181 | +process is to cut a release branch `release-0.1` at the git commit |
| 182 | +where it builds from, and tag it as `v0.1.0`. |
| 183 | + |
| 184 | +{{< hint "note" >}} |
| 185 | +Some custom workflows may accept an explicit input for the remote reference instead of |
| 186 | +inferring it from a git ref. The [`ci.yml`](https://github.com/crossplane-contrib/function-python/blob/main/.github/workflows/ci.yml) |
| 187 | +file for `crossplane-contrib/function-python` is a good example. |
| 188 | +{{< /hint >}} |
| 189 | + |
| 190 | +## Configuring workflows for function packages |
| 191 | + |
| 192 | +Function workflow definitions differ based on the base language the |
| 193 | +function implementation uses. For example, a Python function requires |
| 194 | +a Python environment in the GitHub Action runner: |
| 195 | + |
| 196 | +```yaml |
| 197 | + - name: Setup Python |
| 198 | + uses: actions/setup-python@v5 |
| 199 | + with: |
| 200 | + python-version: ${{ env.PYTHON_VERSION }} |
| 201 | +
|
| 202 | + - name: Setup Hatch |
| 203 | + run: pipx install hatch==1.7.0 |
| 204 | +
|
| 205 | + - name: Lint |
| 206 | + run: hatch run lint:check |
| 207 | +``` |
| 208 | + |
| 209 | +While the template repository provides a working pipeline definition, users may |
| 210 | +choose to customize their environment with different tooling. |
| 211 | + |
| 212 | +Functions also require a runtime image of the core business logic to |
| 213 | +build and embed into the Function package. The default workflow definition |
| 214 | +builds for two platforms: `linux/amd64` and `linux/arm64`. |
| 215 | + |
| 216 | +```yaml |
| 217 | + - name: Build Runtime |
| 218 | + id: image |
| 219 | + uses: docker/build-push-action@v6 |
| 220 | + with: |
| 221 | + context: . |
| 222 | + platforms: linux/${{ matrix.arch }} |
| 223 | + cache-from: type=gha |
| 224 | + cache-to: type=gha,mode=max |
| 225 | + target: image |
| 226 | + build-args: |
| 227 | + PYTHON_VERSION=${{ env.PYTHON_VERSION }} |
| 228 | + outputs: type=docker,dest=runtime-${{ matrix.arch }}.tar |
| 229 | +``` |
| 230 | + |
| 231 | +## Configuring workflows for provider packages |
| 232 | + |
| 233 | +Providers, unlike Functions, use custom `make` targets in the [build submodule] |
| 234 | +for building and pushing Crossplane Provider packages. |
| 235 | + |
| 236 | +Configuring the workflow for a specific registry involves two steps: |
| 237 | + |
| 238 | +1. Updating the registry variables in the top-level `Makefile`. |
| 239 | +2. Referencing GitHub Actions Secrets for authorized credentials to the |
| 240 | +registry. |
| 241 | + |
| 242 | +### Configure target registry |
| 243 | + |
| 244 | +The provider template repository includes a top-level [`Makefile`](https://github.com/crossplane/upjet-provider-template/blob/main/Makefile). |
| 245 | +Edit the following variables to define the target registry: |
| 246 | + |
| 247 | +1. `XPKG_REG_ORGS` - a space-delimited list of target repositories. |
| 248 | +2. `XPKG_REG_ORGS_NO_PROMOTE` - for registries that don't use or infer |
| 249 | +channel tags. |
| 250 | + |
| 251 | +For example, the following dual-pushes to `xpkg.crossplane.io` as well as |
| 252 | +`index.docker.io`: |
| 253 | + |
| 254 | +```make |
| 255 | +XPKG_REG_ORGS ?= xpkg.crossplane.io/crossplane-contrib index.docker.io/crossplanecontrib |
| 256 | +
|
| 257 | +XPKG_REG_ORGS_NO_PROMOTE ?= xpkg.crossplane.io/crossplane-contrib |
| 258 | +``` |
| 259 | + |
| 260 | +## Reusable workflows |
| 261 | + |
| 262 | +The [crossplane-contrib/provider-workflows] repository provide reusable |
| 263 | +workflow definitions that are callable from a custom CI pipeline. |
| 264 | + |
| 265 | +For example, the following snippet references the callable workflow to |
| 266 | +build and push the `provider-kubernetes` package to `xpkg.crossplane.io`: |
| 267 | + |
| 268 | +```yaml |
| 269 | +jobs: |
| 270 | + publish-provider-package: |
| 271 | + uses: crossplane-contrib/provider-workflows/.github/workflows/publish-provider-non-family.yml@main |
| 272 | + with: |
| 273 | + repository: provider-kubernetes |
| 274 | + version: ${{ github.event.inputs.version }} |
| 275 | + go-version: ${{ github.event.inputs.go-version }} |
| 276 | + cleanup-disk: true |
| 277 | + secrets: |
| 278 | + GHCR_PAT: ${{ secrets.GITHUB_TOKEN }} |
| 279 | +``` |
| 280 | + |
| 281 | +{{< hint "tip" >}} |
| 282 | +The reusable workflows referenced here publish to `ghcr.io` by default. |
| 283 | +Ensure that the default GitHub Actions OIDC token inherits the |
| 284 | +`packages: write` permission. |
| 285 | +{{< /hint >}} |
| 286 | + |
| 287 | +## Troubleshooting |
| 288 | + |
| 289 | +{{< expand "Why is my workflow is failing with a 404 error code?" >}} |
| 290 | +Ensure the target repository exists in the registry. You need to create |
| 291 | +it if it doesn't already exist. |
| 292 | +{{</expand >}} |
| 293 | + |
| 294 | +{{< expand "Why is my workflow failing with a 401 error code?" >}} |
| 295 | +Ensure the credentials used during the registry login step has authorization to |
| 296 | +pull and push, and that the `{{ secrets.* }}` variable substitutions match |
| 297 | +what's configured in GitHub. |
| 298 | +{{</expand >}} |
| 299 | + |
| 300 | +<!-- Named Links --> |
| 301 | +[xpkg]: https://github.com/crossplane/crossplane/blob/main/contributing/specifications/xpkg.md |
| 302 | +[functions]: https://github.com/crossplane/function-template-go/blob/main/.github/workflows/ci.yml |
| 303 | +[GitHub Actions Secrets]: https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions |
| 304 | +[build submodule]: https://github.com/crossplane/build |
| 305 | +[crossplane-contrib/provider-workflows]: https://github.com/crossplane-contrib/provider-workflows/blob/main/.github/workflows |
0 commit comments