Skip to content

chore(release): bump version to 2.5.15 (#1273) #172

chore(release): bump version to 2.5.15 (#1273)

chore(release): bump version to 2.5.15 (#1273) #172

Workflow file for this run

name: Autonomous Release
on:
push:
branches: [main]
paths:
- Cargo.toml
- pyproject.toml
workflow_dispatch:
inputs:
publish:
description: "Publish GitHub Release and PyPI after building"
required: false
type: boolean
default: false
permissions:
contents: write
attestations: write
id-token: write
concurrency:
group: release-auto-${{ github.ref_name }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
jobs:
prepare:
name: Prepare release
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
should_build: ${{ steps.prepare.outputs.should_build }}
should_publish_github: ${{ steps.prepare.outputs.should_publish_github }}
should_publish_pypi: ${{ steps.prepare.outputs.should_publish_pypi }}
version: ${{ steps.prepare.outputs.version }}
project_version: ${{ steps.prepare.outputs.project_version }}
commit_sha: ${{ steps.prepare.outputs.commit_sha }}
release_ref: ${{ steps.prepare.outputs.release_ref }}
steps:
- uses: actions/checkout@v6
- name: Check release eligibility
id: prepare
shell: bash
env:
PUBLISH_REQUESTED: ${{ inputs.publish }}
run: |
set -euo pipefail
cargo_version="$(
python - <<'PY'
import tomllib
with open("Cargo.toml", "rb") as f:
print(tomllib.load(f)["workspace"]["package"]["version"])
PY
)"
pyproject_version="$(
python - <<'PY'
import tomllib
with open("pyproject.toml", "rb") as f:
print(tomllib.load(f)["project"]["version"])
PY
)"
if [ "$cargo_version" != "$pyproject_version" ]; then
echo "Cargo.toml version ($cargo_version) does not match pyproject.toml version ($pyproject_version)" >&2
exit 1
fi
version="v${cargo_version}"
commit_sha="$(git rev-parse HEAD)"
release_ref="${commit_sha}"
tag_exists="false"
tag_sha=""
if tag_sha="$(git ls-remote --exit-code --tags origin "refs/tags/${version}" | awk '{print $1}' | tail -n1)"; then
tag_exists="true"
release_ref="${version}"
fi
pypi_json="$(mktemp)"
if curl -fsSL --connect-timeout 10 --max-time 30 https://pypi.org/pypi/fbuild/json -o "$pypi_json"; then
latest="$(python -c 'import json,sys; print(json.load(open(sys.argv[1]))["info"]["version"])' "$pypi_json")"
pypi_file_count="$(python -c 'import json,sys; data=json.load(open(sys.argv[1])); print(len(data.get("releases", {}).get(sys.argv[2], [])))' "$pypi_json" "$cargo_version")"
else
latest="0.0.0"
pypi_file_count="0"
fi
should_build="false"
should_publish_github="false"
should_publish_pypi="false"
newest="$(printf '%s\n%s\n' "$latest" "$cargo_version" | sort -V | tail -n1)"
# One wheel per PLATFORMS entry in ci/publish.py — derive the
# expected count from the SOT instead of hardcoding it, so
# matrix/lane changes can't silently break this gate
# (a stale hardcoded 4 bit the 2.3.22-2.3.24 window).
expected_wheels="$(python -c 'import ci.publish; print(len(ci.publish.PLATFORMS))')"
if [ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ] \
&& [ "$tag_exists" != "true" ] \
&& [ "$newest" = "$cargo_version" ] \
&& [ "$pypi_file_count" -lt "$expected_wheels" ]; then
should_build="true"
should_publish_github="true"
should_publish_pypi="true"
fi
# Manual dispatch is always at least a build. Publishing is an
# explicit opt-in checkbox so release dry-runs are the default.
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
should_build="true"
release_ref="${commit_sha}"
should_publish_github="false"
should_publish_pypi="false"
if [ "${PUBLISH_REQUESTED:-false}" = "true" ]; then
should_publish_github="true"
should_publish_pypi="true"
if [ "$tag_exists" = "true" ]; then
release_ref="${version}"
fi
fi
fi
{
echo "version=${version}"
echo "project_version=${cargo_version}"
echo "commit_sha=${commit_sha}"
echo "release_ref=${release_ref}"
echo "should_build=${should_build}"
echo "should_publish_github=${should_publish_github}"
echo "should_publish_pypi=${should_publish_pypi}"
} >> "$GITHUB_OUTPUT"
echo "Candidate version: ${version}"
echo "Latest PyPI version: ${latest}"
echo "PyPI files for ${cargo_version}: ${pypi_file_count}"
echo "Tag exists: ${tag_exists}"
echo "Tag SHA: ${tag_sha:-<none>}"
echo "Release ref: ${release_ref}"
echo "Should build: ${should_build}"
echo "Publish requested: ${PUBLISH_REQUESTED:-false}"
echo "Should publish GitHub release: ${should_publish_github}"
echo "Should publish PyPI: ${should_publish_pypi}"
build:
name: Native build (${{ matrix.target }})
needs: prepare
if: needs.prepare.outputs.should_build == 'true'
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
runner: ubuntu-latest
binary_ext: ""
linux_cross: false
macos_cross: false
- target: aarch64-unknown-linux-musl
runner: ubuntu-latest
binary_ext: ""
linux_cross: true
macos_cross: false
# macOS lanes were restored once setup-soldr could install the
# published soldr v0.8.8 release. They were dropped when every usable soldr
# version still pointed the Apple SDK fetch at the retired
# manifest branch (HTTP 404); the "setup-soldr version
# --json incompatibility" that blocked bumping soldr was a
# misdiagnosis — soldr v0.7.85/v0.7.87 shipped silent
# binaries. v0.8.0 is the known-good floor for the Apple
# SDK URL fix. The action reference may follow v0 updates, while
# every fbuild workflow pins the binary version explicitly.
#
# aarch64-apple-darwin cross-compiles from the Linux
# runner via soldr + cargo-zigbuild + soldr's managed
# Apple SDK — no mac-host dependency in the matrix.
- target: aarch64-apple-darwin
runner: ubuntu-latest
binary_ext: ""
linux_cross: false
macos_cross: false
mac_cross_linux: true
# x86_64-apple-darwin shares the mac_cross_linux template
# branch — soldr's apple_sdk fetcher auto-picks
# `darwin-x86_64` (thin-x86_64) when the target triple is
# `x86_64-apple-darwin`, and `cargo zigbuild --target
# x86_64-apple-darwin` resolves the SDKROOT + frameworks
# identically to the arm64 lane. Closes Lane 3 of soldr#1006.
- target: x86_64-apple-darwin
runner: ubuntu-latest
binary_ext: ""
linux_cross: false
macos_cross: false
mac_cross_linux: true
# Windows x86_64 now uses the same Linux -> MSVC soldr build path
# as aarch64-pc-windows-msvc. The native windows-latest release
# lane repeatedly exceeded the 45 minute release-binary step while
# compiling fbuild-cli, blocking the v2.4.0 publish. soldr v0.8.0's
# clang-cl shim fixes the earlier cargo-xwin/ring header routing
# issue that forced native Windows on the 2.3.12 cut.
- target: x86_64-pc-windows-msvc
runner: ubuntu-latest
binary_ext: ".exe"
linux_cross: false
macos_cross: false
# ARM64 uses the same Linux -> MSVC soldr build path as x86_64.
# Keep this release lane: soldr's clang shim routes ring + cc-rs
# through clang-cl for the MSVC target while its internal xwin path
# supplies the Windows SDK.
- target: aarch64-pc-windows-msvc
runner: ubuntu-latest
binary_ext: ".exe"
linux_cross: false
macos_cross: false
uses: ./.github/workflows/template_native_build.yml
with:
target: ${{ matrix.target }}
runner: ${{ matrix.runner }}
binary_ext: ${{ matrix.binary_ext }}
linux_cross: ${{ matrix.linux_cross }}
macos_cross: ${{ matrix.macos_cross }}
mac_cross_linux: ${{ matrix.mac_cross_linux == true }}
ref: ${{ needs.prepare.outputs.release_ref }}
publish:
name: Publish GitHub release
needs: [prepare, build]
if: needs.prepare.outputs.should_publish_github == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.release_ref }}
- name: Download native artifacts
uses: actions/download-artifact@v8
with:
pattern: binaries-*
path: artifacts
- name: Package release archives
shell: bash
run: |
set -euo pipefail
version="${{ needs.prepare.outputs.version }}"
mkdir -p dist pkg
for artifact_dir in artifacts/binaries-*; do
[ -d "$artifact_dir" ] || continue
target="${artifact_dir#artifacts/binaries-}"
package_name="fbuild-${version}-${target}"
package_dir="pkg/${package_name}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp -a "$artifact_dir"/. "$package_dir"/
if [[ "$target" == *windows* ]]; then
(cd pkg && zip -r "../dist/${package_name}.zip" "$package_name")
else
tar -C pkg -czf "dist/${package_name}.tar.gz" "$package_name"
fi
done
cd dist
sha256sum fbuild-* > "fbuild-${version}-SHA256SUMS.txt"
- name: Attest release artifacts
uses: actions/attest-build-provenance@v4
with:
subject-checksums: dist/fbuild-${{ needs.prepare.outputs.version }}-SHA256SUMS.txt
- name: Create draft release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
version="${{ needs.prepare.outputs.version }}"
gh release create "$version" dist/* \
--draft \
--generate-notes \
--target "${{ needs.prepare.outputs.release_ref }}" \
--title "$version"
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release edit "${{ needs.prepare.outputs.version }}" --draft=false
build-pypi:
name: Build PyPI wheels
needs: [prepare, build]
if: needs.prepare.outputs.should_publish_pypi == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.release_ref }}
- name: Download native artifacts
uses: actions/download-artifact@v8
with:
pattern: binaries-*
path: dist/_tmp
- name: Build wheels
shell: bash
run: |
set -euo pipefail
python - <<'PY'
import shutil
import ci.publish as publish
tmp = publish.DIST_DIR / "_tmp"
missing = []
for artifact_name, subdir in publish.ARTIFACT_MAP.items():
src = tmp / artifact_name
if not src.exists():
missing.append(artifact_name)
continue
dest = publish.DIST_DIR / subdir
if dest.exists():
shutil.rmtree(dest)
dest.mkdir(parents=True)
for path in src.iterdir():
if path.is_file():
target = dest / path.name
shutil.copy2(path, target)
if not path.name.endswith(".exe"):
target.chmod(0o755)
if missing:
raise SystemExit(f"Missing native artifacts: {', '.join(missing)}")
meta = publish.read_project_meta()
wheels = publish.build_all_wheels(*meta)
print(f"Built {len(wheels)} wheel(s)")
PY
- name: Smoke test Linux x86_64 wheel
timeout-minutes: 5
shell: bash
run: |
set -euo pipefail
python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade --timeout 60 pip
python -m pip install --timeout 60 dist/wheels/*manylinux_2_17_x86_64*.whl
fbuild --version
python -c "import fbuild; print(fbuild.__version__)"
- name: Upload PyPI distributions
uses: actions/upload-artifact@v7
with:
name: pypi-fbuild
path: dist/wheels/*.whl
if-no-files-found: error
publish-pypi:
name: Publish PyPI
needs: [prepare, build-pypi]
if: needs.prepare.outputs.should_publish_pypi == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
environment: pypi
permissions:
contents: read
id-token: write
steps:
- name: Download PyPI distributions
uses: actions/download-artifact@v8
with:
name: pypi-fbuild
path: dist
- name: List distributions
shell: bash
run: ls -lh dist
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
packages-dir: dist
attestations: true
skip-existing: true
- name: Verify all wheels visible on PyPI
shell: bash
env:
PYPI_VERSION: ${{ needs.prepare.outputs.project_version }}
run: |
set -euo pipefail
# Derive the expected count from the wheels this run actually
# built (downloaded into dist/ above) instead of hardcoding
# it — a stale hardcoded 4 broke this gate when the release
# matrix changed (2.3.22-2.3.24 window).
expected="$(find dist -name '*.whl' | wc -l)"
if [ "$expected" -lt 1 ]; then
echo "No wheels found in dist/ — nothing to verify" >&2
exit 1
fi
deadline=$(( $(date +%s) + 300 ))
while :; do
count="$(curl -fsSL --connect-timeout 5 --max-time 10 "https://pypi.org/pypi/fbuild/${PYPI_VERSION}/json" \
| python -c 'import json,sys; print(len(json.load(sys.stdin).get("urls",[])))' 2>/dev/null \
|| echo 0)"
echo "PyPI reports ${count}/${expected} files for fbuild ${PYPI_VERSION}"
if [ "$count" -ge "$expected" ]; then
echo "All wheels visible on PyPI"
exit 0
fi
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "Timed out waiting for ${expected} wheels on PyPI (saw ${count})" >&2
exit 1
fi
sleep 15
done