Skip to content

Conda Publish

Conda Publish #2

Workflow file for this run

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