Make Orchestrator::evaluate take `const Expression * instead of Exp…
#190
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |