Conda Publish #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Conda Publish | |
| # Chained off Auto-publish rather than triggered by `release: published`. | |
| # | |
| # Auto-publish creates the GitHub release with the default GITHUB_TOKEN, and | |
| # GitHub deliberately does not start new workflow runs from events raised that | |
| # way -- so the release trigger never once fired, and every conda package up to | |
| # 0.0.11 was published by hand. workflow_run is the documented way out: it fires | |
| # on the *workflow* finishing, not on the release event. | |
| # | |
| # The chain is also an ordering requirement, not just a convenience: the recipe | |
| # builds from `tag: v${{ version }}`, so the tag Auto-publish creates has to | |
| # exist before rattler-build can fetch the source. | |
| on: | |
| workflow_run: | |
| workflows: ["Auto-publish"] | |
| types: [completed] | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| force_build: | |
| description: 'Force build even if the version did not change' | |
| required: false | |
| default: 'false' | |
| type: boolean | |
| jobs: | |
| check: | |
| name: Decide whether there is a release to build | |
| runs-on: ubuntu-latest | |
| # Auto-publish runs on every push to main and succeeds whether or not it | |
| # published anything, so its success is not by itself a new release. | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| github.event.workflow_run.conclusion == 'success' | |
| outputs: | |
| should_publish: ${{ steps.decide.outputs.should_publish }} | |
| version: ${{ steps.decide.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| # The commit Auto-publish ran on, plus its parent, so the version can | |
| # be compared against what main held before the push. | |
| ref: ${{ github.event.workflow_run.head_sha || github.ref }} | |
| fetch-depth: 2 | |
| - name: Compare the packaged version with the previous commit | |
| id: decide | |
| run: | | |
| set -euo pipefail | |
| # `rust/Cargo.toml` since 0.1.0: the version moved there and the recipe | |
| # reads it from there too (`load_from_file`), so this is the same number | |
| # the package will be named with. `[workspace.package] version` is the | |
| # first `version = "..."` line in the file. Auto-publish decides the same | |
| # way -- keep the two in step. | |
| # | |
| # Note what is deliberately *not* here: Auto-publish also skips a version | |
| # that is already tagged, because it is the workflow that creates the tag. | |
| # By the time this one runs, the tag exists on purpose. | |
| read_version() { sed -n 's/^version = "\(.*\)"/\1/p' "$1" | head -1; } | |
| version=$(read_version rust/Cargo.toml) | |
| if [ -z "$version" ]; then | |
| echo "could not read a version from rust/Cargo.toml" >&2 | |
| exit 1 | |
| fi | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| if [ "${{ inputs.force_build }}" = "true" ]; then | |
| echo "forced: building $version regardless of whether it changed" | |
| echo "should_publish=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| git show HEAD^:rust/Cargo.toml > /tmp/previous-cargo.toml 2>/dev/null || : > /tmp/previous-cargo.toml | |
| previous=$(read_version /tmp/previous-cargo.toml) | |
| if [ "$version" = "$previous" ]; then | |
| echo "version is still ${version:-unset}; nothing to publish" | |
| echo "should_publish=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "version went from ${previous:-none} to $version; publishing" | |
| echo "should_publish=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| build: | |
| name: Build conda package | |
| needs: check | |
| if: needs.check.outputs.should_publish == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event.workflow_run.head_sha || github.ref }} | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Wait for the release tag to exist | |
| # Auto-publish pushes the tag inside the run that triggers this one, so | |
| # it is normally already there. Retrying keeps a slow tag push from | |
| # failing the build with a confusing rattler-build fetch error. | |
| run: | | |
| set -euo pipefail | |
| tag="v${{ needs.check.outputs.version }}" | |
| for attempt in $(seq 1 10); do | |
| if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then | |
| echo "$tag exists" | |
| exit 0 | |
| fi | |
| echo "waiting for $tag (attempt $attempt)" | |
| sleep 15 | |
| done | |
| echo "$tag never appeared; the recipe builds from that tag, so stopping" >&2 | |
| exit 1 | |
| - name: Build conda package | |
| uses: prefix-dev/rattler-build-action@v0.2.39 | |
| env: | |
| # The other half of the recipe's `repo_url` context var: unset, the | |
| # recipe defaults to upstream, which is the defect this fixes -- a | |
| # fork's build silently fetching and publishing upstream's source | |
| # under the fork's name. `github.repository` is automatically correct | |
| # for any fork with zero configuration (and resolves to | |
| # `blooop/devlaunch` on upstream itself, i.e. unchanged behaviour), | |
| # which is why this is a workflow expression and not a repository | |
| # variable somebody has to remember to set. | |
| DEVLAUNCH_SOURCE_REPO: https://github.com/${{ github.repository }} | |
| with: | |
| recipe-path: conda.recipe/recipe.yaml | |
| # `--experimental` is what enables the recipe's `load_from_file`, which | |
| # is how its version comes from `rust/Cargo.toml` instead of being a | |
| # second number to bump. It gates that jinja function only; nothing about | |
| # the package it produces is experimental. | |
| # | |
| # The recipe compiles Rust now, with `${{ compiler('rust') }}` from | |
| # conda-forge -- no toolchain step is needed here, and none should be | |
| # added: the toolchain is a build dependency resolved from the channels | |
| # like any other, which is what makes the build reproducible from the | |
| # recipe alone. | |
| # | |
| # `--channel https://prefix.dev/blooop` here is deliberately unrelated | |
| # to DEVLAUNCH_SOURCE_REPO above: it resolves build DEPENDENCIES | |
| # (the compilers) from where they are maintained, which is a | |
| # different thing from where this run's source comes from or where | |
| # its output gets published (settled by 96d27f2; do not change this | |
| # to a fork's own channel). | |
| build-args: --experimental --channel conda-forge --channel https://prefix.dev/blooop --no-include-recipe | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: conda-packages | |
| path: output/ | |
| if-no-files-found: error | |
| publish: | |
| name: Publish to prefix.dev | |
| needs: [check, build] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: conda-packages | |
| path: output/ | |
| - name: Setup pixi | |
| uses: prefix-dev/setup-pixi@v0.10.2 | |
| with: | |
| run-install: false | |
| - name: Upload to prefix.dev | |
| env: | |
| PIXI_TOKEN: ${{ secrets.PIXI_TOKEN }} | |
| # Where this repository publishes, as a repository *variable* rather | |
| # than a line of code. A fork configures `CONDA_CHANNEL` in its own | |
| # settings and needs no diff here; unset, it is upstream's channel, so | |
| # upstream behaves exactly as before. | |
| # | |
| # A variable and not a secret: a channel name is not one, and a secret | |
| # would be invisible in the log of a job whose whole subject is where | |
| # the package went. | |
| CONDA_CHANNEL: ${{ vars.CONDA_CHANNEL || 'blooop' }} | |
| run: | | |
| for pkg in $(find output -type f \( -name "*.conda" -o -name "*.tar.bz2" \)); do | |
| echo "Uploading ${pkg}" | |
| # Captured rather than streamed, because the reason a failure is | |
| # tolerable has to be read before it is tolerated. | |
| if out=$(pixi exec rattler-build upload prefix \ | |
| -c "${CONDA_CHANNEL}" \ | |
| --api-key "$PIXI_TOKEN" \ | |
| "${pkg}" 2>&1); then | |
| echo "Successfully uploaded ${pkg}" | |
| elif printf '%s' "$out" | grep -qiE "already exist"; then | |
| # The one tolerable failure: a re-run after a half-finished release | |
| # re-uploads what is already there, and that is not an error. | |
| echo "${pkg} is already in the channel" | |
| else | |
| # Everything else is a release that published nothing. This used to | |
| # print "may already exist" for any failure and exit 0, so a wrong | |
| # channel name, a bad token or an expired one all produced a green | |
| # build and an empty channel -- discovered later, by an install that | |
| # could not resolve. | |
| echo "$out" >&2 | |
| echo "::error::upload of ${pkg} failed and not because it exists" >&2 | |
| exit 1 | |
| fi | |
| done |