Skip to content

Make Orchestrator::evaluate take `const Expression * instead of Exp… #190

Make Orchestrator::evaluate take `const Expression * instead of Exp…

Make Orchestrator::evaluate take `const Expression * instead of Exp… #190

Workflow file for this run

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: release
# Trigger on every push to main (not just version.txt changes). The validate
# job compares version.txt against the latest GitHub release and skips the
# rest of the pipeline if the version has already been released. This means
# a CI-fix commit after a failed release attempt will automatically retry the
# release without needing a manual dispatch or a version bump.
#
# The release pipeline does NOT rebuild — it waits for the per-push CI
# workflows triggered by the same commit (lint-version, linux, mac, wheels,
# sdist, linux-sdk, packaging_mac) to succeed, downloads their artifacts,
# publishes wheels + sdist to TestPyPI, then creates the v<version> tag and
# a GitHub Release.
#
# If any sibling workflow fails the release stops — no upload, no tag.
on:
push:
branches: [main]
workflow_dispatch:
inputs:
ref:
description: 'Commit SHA to release (default: latest main)'
required: false
default: ''
permissions:
contents: write # signed-tag push, GitHub Release creation
actions: read # download-artifact from sibling workflow runs
id-token: write # OIDC for trusted PyPI/TestPyPI publishing
# Never cancel an in-flight release — they end in tag creation + PyPI upload,
# both irreversible. A second push must wait for the first to finish.
concurrency:
group: release
cancel-in-progress: false
env:
PROJECT_NAME: rebalancer
# Workflows that must succeed for this SHA before we publish.
REQUIRED_WORKFLOWS: "lint-version.yml getdeps_linux.yml getdeps_mac.yml wheels.yml sdist.yml linux-sdk.yml packaging_mac.yml"
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
version: ${{ steps.read.outputs.version }}
skip: ${{ steps.read.outputs.skip }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.ref || github.sha }}
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- run: pip install packaging
- name: Lint version.txt
# --skip-index-check: the pre-flight step in publish owns the
# already-on-TestPyPI check; skipping it here avoids a permanent block
# after a partial upload when release is triggered via workflow_dispatch.
run: python3 tools/lint-version.py --skip-index-check
- name: Check version against latest release
id: read
run: |
VERSION=$(cat version.txt)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
LATEST_TAG=$(gh release view --repo "${{ github.repository }}" \
--json tagName --jq '.tagName' 2>/dev/null || echo "")
LATEST_VERSION="${LATEST_TAG#v}"
# lint-version.py already rejected v < latest, so the only cases here are:
# v > latest — new release requested → proceed
# v == latest — no bump this push → skip gracefully
IS_NEWER=$(python3 -c "from packaging.version import Version; v=Version('$VERSION'); l=Version('$LATEST_VERSION') if '$LATEST_VERSION' else Version('0'); print('true' if v>l else 'false')")
if [ "$IS_NEWER" = "true" ]; then
echo "v$VERSION > latest release '${LATEST_VERSION:-none}' — will release"
echo "skip=false" >> "$GITHUB_OUTPUT"
else
echo "v$VERSION == latest release '${LATEST_VERSION:-none}' — no bump, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
find-release-sha:
needs: validate
if: needs.validate.outputs.skip == 'false'
runs-on: ubuntu-latest
timeout-minutes: 90
outputs:
qualifying_sha: ${{ steps.scan.outputs.qualifying_sha }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.ref || github.sha }}
fetch-depth: 0
- name: Find oldest qualifying SHA
id: scan
run: |
set -euo pipefail
# Most recent commit that touched version.txt — lower bound of the search.
base_sha=$(git log --format=%H -1 -- version.txt)
if [ -z "$base_sha" ]; then
echo "::error::version.txt has no git history"
exit 1
fi
echo "base_sha=$base_sha"
# HEAD is the commit that triggered this release run. On a fresh version
# bump its sibling workflows may still be running, so we retry the scan
# until they settle or the job times out.
head_sha=$(git rev-parse HEAD)
deadline=$(( $(date +%s) + 80 * 60 )) # 80 min; job timeout is 90 min
while true; do
# All commits from base_sha through HEAD, oldest first.
mapfile -t commits < <(git log --reverse --format=%H "${base_sha}^..HEAD")
echo "Scanning ${#commits[@]} commits (oldest first)"
qualifying_sha=""
for sha in "${commits[@]}"; do
all_pass=true
for wf in $REQUIRED_WORKFLOWS; do
run_json=$(gh run list \
--repo "${{ github.repository }}" \
--workflow "$wf" \
--commit "$sha" \
--limit 1 \
--json status,conclusion 2>/dev/null || echo '[]')
status=$(echo "$run_json" | jq -r '.[0].status // "missing"')
conclusion=$(echo "$run_json" | jq -r '.[0].conclusion // ""')
if [ "$status" != "completed" ] || [ "$conclusion" != "success" ]; then
echo " $sha: $wf not qualifying (status=$status conclusion=$conclusion)"
all_pass=false
break
fi
done
if [ "$all_pass" = "true" ]; then
qualifying_sha="$sha"
echo "✅ Qualifying SHA: $sha"
break
fi
done
[ -n "$qualifying_sha" ] && break
# No qualifying SHA yet. Check whether any required workflow on HEAD is
# still pending (in_progress, queued, or not yet triggered). If so, wait
# and retry — this handles the common case where the release fires before
# sibling CI workflows finish on a fresh version-bump commit. If all are
# already completed and nothing qualifies, there is no point waiting.
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "::error::Timed out after 80 min — no qualifying SHA found in [${base_sha}..HEAD]"
exit 1
fi
any_pending=false
for wf in $REQUIRED_WORKFLOWS; do
run_json=$(gh run list \
--repo "${{ github.repository }}" \
--workflow "$wf" \
--commit "$head_sha" \
--limit 1 \
--json status 2>/dev/null || echo '[]')
status=$(echo "$run_json" | jq -r '.[0].status // "missing"')
if [ "$status" != "completed" ]; then
echo " $wf is $status on HEAD — will retry"
any_pending=true
break
fi
done
if [ "$any_pending" = "false" ]; then
echo "::error::All workflows on HEAD completed but no qualifying SHA found in [${base_sha}..HEAD]"
exit 1
fi
echo "Waiting 60s for pending workflows to complete..."
sleep 60
done
echo "qualifying_sha=$qualifying_sha" >> "$GITHUB_OUTPUT"
publish-testpypi:
needs: [validate, find-release-sha]
if: needs.validate.outputs.skip == 'false'
runs-on: ubuntu-latest
timeout-minutes: 30
environment: testpypi # configure trusted publisher on test.pypi.org
env:
SHA: ${{ needs.find-release-sha.outputs.qualifying_sha }}
VERSION: ${{ needs.validate.outputs.version }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.find-release-sha.outputs.qualifying_sha }}
fetch-depth: 0
- name: Download wheel artifacts (linux + macos)
run: |
mkdir -p pypi_dist
for wf in wheels.yml sdist.yml linux-sdk.yml packaging_mac.yml; do
run_id=$(gh run list \
--repo "${{ github.repository }}" \
--workflow "$wf" \
--commit "$SHA" \
--limit 1 \
--json databaseId --jq '.[0].databaseId')
echo "Downloading artifacts from $wf run $run_id"
gh run download "$run_id" --repo "${{ github.repository }}" --dir _stage
done
# pypi_dist/ gets only Python distributions (.whl, sdist .tar.gz) for upload.
# Homebrew bottles (*.bottle.tar.gz) are excluded — they are not valid PyPI dists.
find _stage -type f -name '*.whl' -exec cp -v {} pypi_dist/ \;
find _stage -type f -name '*.tar.gz' ! -name '*.bottle.tar.gz' \
-exec cp -v {} pypi_dist/ \;
echo "=== PyPI dist ===" && ls -la pypi_dist/
# Validate every file before touching either index. PyPI has no atomic
# upload API — files land one at a time with no rollback — so any
# rejection mid-upload leaves a partial release on the index. We treat a
# partial upload as unrecoverable: skip-existing is intentionally NOT set,
# so a re-run after a partial upload will fail rather than silently
# completing a mismatched set. The operator must bump the version or yank
# the partial release before retrying. Checks:
# - size: PyPI rejects files > 100 MB (test.pypi.org same limit)
# - existing: abort if this version is already (partially) on TestPyPI
# - metadata: twine check catches malformed wheels/sdists
- name: Pre-flight check PyPI distributions
run: |
pip install --quiet twine requests
echo "=== size check (limit: 100 MB per file) ==="
python3 - <<'PYEOF'
import os, sys, requests
limit = 100 * 1024 * 1024
bad = [
(f, sz)
for f in os.listdir("pypi_dist")
for sz in [os.path.getsize(os.path.join("pypi_dist", f))]
if sz > limit
]
for f, sz in bad:
print(f"::error::pypi_dist/{f} is {sz/1e6:.1f} MB — exceeds PyPI 100 MB per-file limit")
if bad:
sys.exit(1)
print(f"All {len(os.listdir('pypi_dist'))} files are under 100 MB.")
version = os.environ["VERSION"]
for index, label in [
(f"https://test.pypi.org/pypi/rebalancer/{version}/json", "TestPyPI"),
(f"https://pypi.org/pypi/rebalancer/{version}/json", "PyPI"),
]:
r = requests.get(index, timeout=10)
if r.status_code == 200:
existing = sorted(u["filename"] for u in r.json()["urls"])
print(f"::error::v{version} already has files on {label}: {existing}")
print("::error::Bump the version or yank the partial release before retrying.")
sys.exit(1)
print(f"v{version} not yet on {label} — safe to upload.")
PYEOF
echo "=== twine check ==="
twine check --strict pypi_dist/*
- name: Publish wheels + sdist to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
packages-dir: pypi_dist
publish-pypi:
needs: [validate, find-release-sha, publish-testpypi]
if: needs.validate.outputs.skip == 'false'
runs-on: ubuntu-latest
timeout-minutes: 30
environment: pypi # configure trusted publisher on pypi.org
env:
SHA: ${{ needs.find-release-sha.outputs.qualifying_sha }}
VERSION: ${{ needs.validate.outputs.version }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.find-release-sha.outputs.qualifying_sha }}
fetch-depth: 0
- name: Download wheel artifacts (linux + macos)
run: |
mkdir -p dist pypi_dist
for wf in wheels.yml sdist.yml linux-sdk.yml packaging_mac.yml; do
run_id=$(gh run list \
--repo "${{ github.repository }}" \
--workflow "$wf" \
--commit "$SHA" \
--limit 1 \
--json databaseId --jq '.[0].databaseId')
echo "Downloading artifacts from $wf run $run_id"
gh run download "$run_id" --repo "${{ github.repository }}" --dir _stage
done
find _stage -type f -name '*.whl' -exec cp -v {} pypi_dist/ \;
find _stage -type f -name '*.tar.gz' ! -name '*.bottle.tar.gz' \
-exec cp -v {} pypi_dist/ \;
find _stage -type f \( -name '*.whl' -o -name '*.tar.gz' \
-o -name '*.deb' -o -name '*.rpm' \
-o -name '*.bottle.tar.gz' \) \
-exec cp -v {} dist/ \;
echo "=== PyPI dist ===" && ls -la pypi_dist/
echo "=== Full dist ===" && ls -la dist/
- name: Publish wheels + sdist to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: pypi_dist
- name: Create signed tag
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
TAG="v${VERSION}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::tag $TAG already exists — refusing to overwrite"
exit 1
fi
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
- name: Create GitHub Release with all artifacts
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ env.VERSION }}
name: v${{ env.VERSION }}
generate_release_notes: true
files: |
dist/*.whl
dist/*.tar.gz
dist/*.deb
dist/*.rpm