Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,26 @@ jobs:
"$venv/bin/python" tools/wheel_smoke.py
done

crates-publish:
# The crates.io half of a `v*` release: the engine crates the cut stamped and
# the facade. It publishes from publish-crates.yml, not here, because that
# file is what crates.io's trusted publisher is bound to — and all three of
# its guards (its own ref check, the `crates` environment's branch policy,
# publish_crates.py's preflight) require `main`, which a tag ref is not. So
# the tag dispatches it on main, the same call the nightly makes (#1073).
# The release commit IS main's head when the tags are cut; the publish skips
# any version crates.io already has, so a re-dispatch is harmless.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, 'rc') && !contains(github.ref_name, 'dev')
name: crates.io (dispatch publish-crates)
runs-on: ubuntu-latest
permissions:
actions: write # dispatch publish-crates.yml
steps:
- name: dispatch publish-crates (execute=true) on main
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run publish-crates.yml --repo "$GITHUB_REPOSITORY" --ref main -f execute=true

pypi-publish:
# Trusted-publishing flow (no API token). The PyPI trusted publisher
# is configured against this repo + this workflow + the `pypi`
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Every change listed here declares itself breaking — at `0.x` a breaking change

## [Unreleased]

### Fixed

- **A `v*` release tag now publishes to crates.io, as it already did to PyPI.** The release run dispatches the crates.io publish for the engine crates the cut stamped and the facade, so a release no longer needs a by-hand `publish-crates.yml` dispatch. `cut-release.sh X --ship` cuts every product tag on the merged release commit in one command, refusing to run off `main`, off the `release: X` commit, or on a facade stamped another version, and skipping a tag that already exists.

## [0.15.0] — 2026-10-12

### Added
Expand Down
16 changes: 11 additions & 5 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,13 @@ cut for X` commit when anything was owed, then stamps the product as `release: X
The engine cut is in the product cut because the crates.io facade resolves its
engine deps from the registry: a product release over an engine fix that was never
bumped would publish a facade that cannot see the fix. `--plan` lists the engine
bumps beside the product number. After the release PR merges, the nightly's
publish dispatch ships every crate stamped ahead of crates.io, the facade
included (#1073). The old two-tier `cut-release.sh` and its
bumps beside the product number. After the release PR merges, run
`tools/release/cut-release.sh X --ship` on the merged `main`: it cuts every
product tag on the release commit, skipping one that exists. The tags publish
everything — the `v*` release run builds PyPI **and** dispatches
`publish-crates.yml`, which ships every crate stamped ahead of crates.io, the
facade included, exactly as PyPI and npm ship from their tags. The nightly's
publish dispatch (#1073) is the safety net. The old two-tier `cut-release.sh` and its
`--skip engine` escape hatch are gone with the lockstep: skipping the engine
existed to avoid burning eleven lockstep versions on a browser-only fix, and
per-crate versioning dissolves the dilemma — a browser-only fix simply bumps no
Expand Down Expand Up @@ -164,8 +168,10 @@ tools/release/bump-version.sh product minor # or: patch · --new-version 0.6.
git push -u origin release/0.6.0
gh pr create -B main -t "release: 0.6.0" -b "version bump" # merge once CI is green

# 3. On the merged main, cut EVERY product tag — release.yml builds + publishes from them:
git switch main && git pull
# 3. On the merged main, cut EVERY product tag — the tags publish PyPI, npm AND crates.io:
git switch main && git pull --ff-only
tools/release/cut-release.sh 0.6.0 --ship
# which is, by hand (release.yml builds + publishes from the tags):
gh release create v0.6.0 --title v0.6.0 --generate-notes # wheels + sdist → PyPI, CLI → GH release
gh release create node-v0.6.0 --title node-v0.6.0 --generate-notes # npm addon + @laterite/native-*
git tag --no-sign wasm-v0.6.0 && git push origin wasm-v0.6.0 # npm @laterite/ags4-wasm (browser)
Expand Down
6 changes: 5 additions & 1 deletion changelog.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"changed": [],
"deprecated": [],
"removed": [],
"fixed": [],
"fixed": [
{
"text": "**A `v*` release tag now publishes to crates.io, as it already did to PyPI.** The release run dispatches the crates.io publish for the engine crates the cut stamped and the facade, so a release no longer needs a by-hand `publish-crates.yml` dispatch. `cut-release.sh X --ship` cuts every product tag on the merged release commit in one command, refusing to run off `main`, off the `release: X` commit, or on a facade stamped another version, and skipping a tag that already exists."
}
],
"security": []
},
"releases": [
Expand Down
79 changes: 65 additions & 14 deletions tools/release/cut-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,24 @@
# tree (bare `path` deps, no version), and the registry is only what
# `laterite-duckdb` and direct Rust consumers see.
#
# WHAT IT DOES NOT DO — same as `bump-version.sh`, for the same reasons:
# * No tag, no push. The bump lands on `main` via a reviewed release PR; the
# publish tags are cut after it merges. See RELEASING.md.
# * No publish. The registries are append-only and the publishes are
# environment-gated on purpose. This prints the sequence; a human runs it.
# * It does not touch `laterite-duckdb`. Separate repo, downstream of the
# crates.io index; printed as a follow-up, never done.
# TWO STEPS, because crates.io and the tags can only take what is on `main`:
# 1. On a release branch: the engine cut + the product stamp, as commits for a
# reviewed release PR. No tag, no push.
# 2. After that PR merges, on `main` with `--ship`: every product tag, on the
# release commit. The tags start every publish: `v*` builds PyPI and
# dispatches crates.io (the engine crates the cut stamped + the facade),
# `node-v*` / `wasm-v*` build npm — all in release.yml. A tag that already
# exists is skipped, so a re-run after a partial failure finishes the job.
#
# WHAT IT STILL DOES NOT DO:
# * Approve anything. The `pypi` / `npm` environments stay a human's click.
# * Touch `laterite-duckdb`. Separate repo, downstream of the crates.io index;
# printed as a follow-up, never done.
#
# Usage:
# tools/release/cut-release.sh 0.12.0 # stamp the product
# tools/release/cut-release.sh 0.12.0 # step 1: stamp (release branch)
# tools/release/cut-release.sh 0.12.0 --plan # print the plan and stop
# tools/release/cut-release.sh 0.12.0 --ship # step 2: every tag (merged main)
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
Expand All @@ -44,13 +51,15 @@ die() {
exit 1
}

[ "$#" -ge 1 ] || die "usage: cut-release.sh <version> [--plan] (see header)"
[ "$#" -ge 1 ] || die "usage: cut-release.sh <version> [--plan|--ship] (see header)"

VERSION="$1"; shift
PLAN_ONLY=0
SHIP=0
while [ "$#" -gt 0 ]; do
case "$1" in
--plan) PLAN_ONLY=1 ;;
--ship) SHIP=1 ;;
--skip) die "--skip is gone with the engine tier (#781) — engine crates bump per-crate via bump_crate.py" ;;
*) die "unknown argument '$1'" ;;
esac
Expand All @@ -64,6 +73,48 @@ echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([a-z0-9.]*)?$' \
|| die "'$VERSION' does not look like a version (expected e.g. 0.12.0)"

branch="$(git branch --show-current)"

# --- step 2: ship the merged release. ----------------------------------------
if [ "$SHIP" -eq 1 ]; then
[ "$branch" = "main" ] || die "--ship runs on main, after the release PR merges"
[ -z "$(git status --porcelain)" ] || die "working tree is dirty — commit or stash first"
git fetch -q origin main
head="$(git rev-parse HEAD)"
[ "$head" = "$(git rev-parse origin/main)" ] \
|| die "main is not origin/main — 'git pull --ff-only' first"
# The tags must sit on the release commit itself, never on a later merge.
case "$(git log -1 --format=%s)" in
"release: $VERSION"*) ;;
*) die "main's head is not the merged 'release: $VERSION' commit — tag that commit by hand, or ship before anything else merges" ;;
esac
facade="$(sed -n 's/^version = "\(.*\)"/\1/p' rust-packages/laterite/Cargo.toml | head -1)"
[ "$facade" = "$VERSION" ] || die "the facade is stamped $facade, not $VERSION"

for tag in "v$VERSION" "node-v$VERSION" "wasm-v$VERSION"; do
if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null; then
echo "tag $tag: already on origin, skipped"
continue
fi
case "$tag" in
# v* / node-v* are releases, cut server-side (lightweight, so they match
# the release.yml triggers). The target is the full SHA: the API refuses
# an abbreviated one.
v*|node-v*) gh release create "$tag" --target "$head" --title "$tag" --generate-notes ;;
# --no-sign: tag.gpgsign would make a bare 'git tag' a signed annotated tag.
wasm-v*) git tag --no-sign "$tag" "$head" && git push origin "$tag" ;;
esac
echo "tag $tag: cut on $head"
done

echo
echo "=== left for a human ==="
echo " 1. Approve the 'pypi' / 'npm' environments on the release runs:"
echo " gh run list --workflow release.yml --limit 6"
echo " 2. The DuckDB extension (its own repo) pins ENGINE crates — update its"
echo " pins once crates.io shows the new versions, then cut it on its own number."
exit 0
fi

[ "$branch" != "main" ] || die "refuse to cut on main — 'git switch -c release/$VERSION' first"
[ -z "$(git status --porcelain)" ] || die "working tree is dirty — commit or stash first"

Expand Down Expand Up @@ -118,11 +169,11 @@ tools/release/bump-version.sh product --new-version "$VERSION"

cat <<EOF

=== engine crates ===
=== after the release PR merges ===

The engine cut above stamped every crate owed a bump. After the release PR
merges, each crate (and the facade) ahead of the registry is published by the
nightly's publish dispatch (#1073), or at once with:
gh workflow run publish-crates.yml --ref main -f execute=true
On the merged main, one command cuts every product tag; the tags publish
PyPI, npm and crates.io (the engine crates and the facade):
git switch main && git pull --ff-only
tools/release/cut-release.sh $VERSION --ship

EOF
Loading