Skip to content

docs: the README still said the tab flattened a branch's slashes #1296

docs: the README still said the tab flattened a branch's slashes

docs: the README still said the tab flattened a branch's slashes #1296

Workflow file for this run

name: Auto-publish
# The first half of a release: PyPI, the tag, and the GitHub release. The second
# half is `conda-publish.yml`, which chains off *this workflow finishing* -- see
# its header for why it has to be a chain and not a `release:` trigger. Both
# halves read the same version from the same file, so one tag ships both channels
# (docs/rust-rewrite-plan.md cutover check 4).
#
# This used to be one step, `etils-actions/pypi-auto-publish`, which read the
# version out of `pyproject.toml` and built the project with a generic
# `python -m build`. Neither survives 0.1.0: the artifact is two compiled Rust
# binaries built by maturin from `packaging/wheel`, and the version lives in
# `rust/Cargo.toml`. What that action did is spelled out below instead -- detect a
# version bump, build, publish to PyPI, tag, write the release from the changelog
# -- with the same trigger and the same two secrets it used (`PYPI_API_TOKEN` and
# the default `GITHUB_TOKEN`). No new secret is involved.
on: [push, workflow_dispatch]
jobs:
publish-job:
# Only publish on `main` branch
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions: # Don't forget permissions
contents: write
steps:
- uses: actions/checkout@v7
with:
# This commit and its parent: a release is a version that changed, so
# deciding takes both. `fetch-tags` so the idempotence check below can
# see whether this version was already tagged.
fetch-depth: 2
fetch-tags: true
- name: Decide whether there is a release to publish
id: decide
run: |
set -euo pipefail
# `[workspace.package] version` is the first `version = "..."` line in
# the file; the crates inherit it with `version.workspace = true`.
# `conda-publish.yml` reads it exactly this way -- keep the two in step.
read_version() { sed -n 's/^version = "\(.*\)"/\1/p' "$1" | head -1; }
version=$(read_version rust/Cargo.toml)
if [ -z "$version" ]; then
echo "::error::could not read a version from rust/Cargo.toml"
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
git show HEAD^:rust/Cargo.toml > /tmp/previous-cargo.toml 2>/dev/null || : > /tmp/previous-cargo.toml
previous=$(read_version /tmp/previous-cargo.toml)
# Already tagged. Existence is asked of the ref alone, never of the commit
# behind it: this is a depth-2 clone, so a tag pointing anywhere but here
# may have no commit object to resolve, and a failed resolve must not read
# as "free to publish".
if git rev-parse -q --verify "refs/tags/v$version" >/dev/null; then
tagged_at=$(git rev-parse -q --verify "refs/tags/v$version^{commit}" || true)
if [ -n "$tagged_at" ] && [ "$tagged_at" = "$(git rev-parse HEAD)" ]; then
# The tag is on this very commit: a re-run of the workflow over the
# release it already published. `gh release create --target
# $GITHUB_SHA` is what puts it here, and idempotence is the point.
echo "v$version was published from this commit; nothing to publish"
echo "publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$version" != "$previous" ]; then
# This push moved the version *onto* a number somebody else already
# released. Two branches cut the same one, which git merges with no
# conflict because the same version line on both sides is one edit
# (blooop/devlaunch#591). Refusing to publish is right either way; the
# difference is that this says why, instead of reporting the same
# "nothing to publish" an ordinary push gets -- which is how #591 sat
# merged and released nowhere with nothing red.
#
# Narrow on purpose. It fires only when the version changed against the
# first parent, and #591's own merge did not: both sides were already
# 0.36.0, so there was no bump left to see by then. That case is caught
# a step earlier, by `scripts/version_untaken.py` on the pull request.
echo "::error::this push moves the version to $version, but v$version is already tagged at ${tagged_at:-another commit}. Two branches cut the same release; bump to an unused version and re-file the CHANGELOG entry under it."
exit 1
fi
# A later push that did not touch the version. Nothing to do, and saying
# so beats a failed upload.
echo "v$version is already tagged; nothing to publish"
echo "publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$version" = "$previous" ]; then
echo "version is still ${version:-unset}; nothing to publish"
echo "publish=false" >> "$GITHUB_OUTPUT"
else
echo "version went from ${previous:-none} to $version; publishing"
echo "publish=true" >> "$GITHUB_OUTPUT"
fi
- name: Extract the release notes from CHANGELOG.md
if: steps.decide.outputs.publish == 'true'
run: |
set -euo pipefail
version='${{ steps.decide.outputs.version }}'
awk -v heading="## [$version]" '
index($0, heading) == 1 { found = 1; next }
found && /^## / { exit }
found { print }
' CHANGELOG.md > /tmp/release-notes.md
if ! grep -q '[^[:space:]]' /tmp/release-notes.md; then
echo "::error::CHANGELOG.md has no '## [$version]' section; a release nobody can read is not a release"
exit 1
fi
cat /tmp/release-notes.md
- name: Build the linux-64 bin-wheel
if: steps.decide.outputs.publish == 'true'
uses: PyO3/maturin-action@v1
with:
# `packaging/wheel/pyproject.toml` is the published project: maturin
# bin bindings over `rust/dl`, whose two bin targets are `dl` and (behind
# the `wheel` feature) `aid`.
working-directory: packaging/wheel
# Built in a manylinux container rather than on the runner, because the
# tag is computed from the glibc the binaries actually got linked
# against: a plain `ubuntu-latest` build is `manylinux_2_39`, which pip
# then refuses to install on anything older than the runner --
# Ubuntu 22.04 included. 2_28 reaches back to Ubuntu 20.04 and RHEL 8,
# and is what the conda package's `__glibc >=2.17` floor makes moot on
# the other channel.
manylinux: '2_28'
rust-toolchain: '1.97.1'
# Pinned, and kept in lockstep with `ci.yml`'s packaging job. The
# action's default is `latest`, which is how maturin 1.14 arrived
# unbidden and broke the build (1.14 rejects a `readme` path resolving
# outside the metadata root; see the symlink note in
# `packaging/wheel/pyproject.toml`). A pin means the maturin a release
# is cut with only ever moves when someone edits these two files.
maturin-version: v1.14.1
# `--locked`: the dependency versions are rust/Cargo.lock's, not
# whatever crates.io holds on release day. Same reason the conda recipe
# passes it.
args: --release --locked --out dist
- name: Check the wheel before it goes anywhere
if: steps.decide.outputs.publish == 'true'
run: |
set -euo pipefail
version='${{ steps.decide.outputs.version }}'
# Exactly one wheel, and it is the two binaries at the version this
# release is being cut at. A mismatch means the version was read from one
# place and the binaries built from another, which is the failure mode
# single-sourcing exists to remove -- so it stops here, before PyPI.
count=$(ls -1 packaging/wheel/dist/*.whl | wc -l)
if [ "$count" != 1 ]; then
echo "::error::expected exactly one wheel, found $count"
ls -l packaging/wheel/dist/
exit 1
fi
wheel=$(ls -1 packaging/wheel/dist/*.whl)
python3 scripts/check_wheel.py "$wheel" --version "$version"
# And the binaries in it run, and agree about who they are.
python3 -m venv /tmp/wheel-check
/tmp/wheel-check/bin/pip install --quiet "$wheel"
test "$(/tmp/wheel-check/bin/dl --version)" = "dl $version"
test "$(/tmp/wheel-check/bin/aid --version)" = "aid $version"
- name: Publish to PyPI
if: steps.decide.outputs.publish == 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: packaging/wheel/dist
# A re-run after a half-finished release re-uploads nothing and carries
# on to the tag, rather than failing on PyPI's 400 for a file that is
# already there.
skip-existing: true
- name: Tag the release and write it up
if: steps.decide.outputs.publish == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
version='${{ steps.decide.outputs.version }}'
# Last, deliberately: this tag is what `conda-publish.yml` waits for and
# what its recipe fetches its source from, so it must not exist until
# the wheel of the same version does.
gh release create "v$version" \
--target "$GITHUB_SHA" \
--title "v$version" \
--notes-file /tmp/release-notes.md \
packaging/wheel/dist/*.whl