Release Build #15
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
| name: Release Build | |
| on: | |
| schedule: | |
| - cron: '0 0 1,15 * *' # Runs at 00:00 on the 1st and 15th of each month (UTC) | |
| workflow_dispatch: | |
| inputs: | |
| release_version: | |
| description: 'Release version (e.g., 0.9.0 or 0.9.0rc1)' | |
| required: false | |
| type: string | |
| skip_version_checks: | |
| description: 'Skip version verification steps' | |
| required: false | |
| type: boolean | |
| default: false | |
| skip_smoke_tests: | |
| description: 'Skip smoke tests (recommended when promoting tested RC to stable)' | |
| required: false | |
| type: boolean | |
| default: false | |
| jobs: | |
| release-build: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| test_branch: ${{ steps.create_test_branch.outputs.test_branch }} | |
| release_branch: ${{ steps.create_release_branch.outputs.release_branch }} | |
| release_version: ${{ steps.determine_version.outputs.release_version }} | |
| new_commit_sha: ${{ steps.create_test_branch.outputs.new_commit_sha }} | |
| previous_release_branch: ${{ steps.find_previous_release_branch.outputs.previous_release_branch }} | |
| previous_minor_release_branch: ${{ steps.find_previous_release_branch.outputs.previous_minor_release_branch }} | |
| steps: | |
| - name: Clone repository | |
| uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python 3.10 | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: '3.10' | |
| - name: Install the latest version of uv | |
| uses: astral-sh/setup-uv@v4 | |
| with: | |
| version: "latest" | |
| python-version: '3.10' | |
| - name: Find latest release from PyPI | |
| id: find_previous_release_branch | |
| run: | | |
| # Get the latest version from PyPI using JSON API | |
| LATEST_PYPI_VERSION=$(curl -s https://pypi.org/pypi/skypilot/json | jq -r .info.version) | |
| echo "Latest PyPI version: ${LATEST_PYPI_VERSION}" | |
| # Determine the base branch for PyPI version | |
| PREVIOUS_RELEASE_BRANCH="releases/${LATEST_PYPI_VERSION}" | |
| echo "previous_release_branch=${PREVIOUS_RELEASE_BRANCH}" >> $GITHUB_OUTPUT | |
| echo "Determined previous release branch: ${PREVIOUS_RELEASE_BRANCH}" | |
| # Output the latest PyPI version for subsequent steps | |
| echo "latest_pypi_version=${LATEST_PYPI_VERSION}" >> $GITHUB_OUTPUT | |
| # Find the latest version of the previous minor version | |
| MAJOR=$(echo $LATEST_PYPI_VERSION | cut -d. -f1) | |
| MINOR=$(echo $LATEST_PYPI_VERSION | cut -d. -f2) | |
| PREVIOUS_MINOR=$((MINOR - 1)) | |
| PREVIOUS_MINOR_LATEST_VERSION=$(curl -s https://pypi.org/pypi/skypilot/json | jq -r ".releases | keys[]" | grep "^${MAJOR}\.${PREVIOUS_MINOR}\." | sort -V | tail -n 1) | |
| if [ -n "${PREVIOUS_MINOR_LATEST_VERSION}" ]; then | |
| PREVIOUS_MINOR_RELEASE_BRANCH="releases/${PREVIOUS_MINOR_LATEST_VERSION}" | |
| echo "previous_minor_release_branch=${PREVIOUS_MINOR_RELEASE_BRANCH}" >> $GITHUB_OUTPUT | |
| echo "Determined previous minor release branch: ${PREVIOUS_MINOR_RELEASE_BRANCH}" | |
| else | |
| echo "Could not determine previous minor release branch." | |
| echo "previous_minor_release_branch=" >> $GITHUB_OUTPUT | |
| fi | |
| # Determine release version based on trigger type | |
| - name: Determine release version | |
| id: determine_version | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ github.event.inputs.release_version }}" ]; then | |
| # Manual trigger with input version provided | |
| RELEASE_VERSION="${{ github.event.inputs.release_version }}" | |
| echo "Using manually specified version: ${RELEASE_VERSION}" | |
| else | |
| # Scheduled trigger or manual trigger without version - extract version from latest release and increment patch | |
| LATEST_VERSION="${{ steps.find_previous_release_branch.outputs.latest_pypi_version }}" | |
| # Split version into parts | |
| MAJOR=$(echo $LATEST_VERSION | cut -d. -f1) | |
| MINOR=$(echo $LATEST_VERSION | cut -d. -f2) | |
| PATCH=$(echo $LATEST_VERSION | cut -d. -f3) | |
| # Always increment patch version | |
| NEW_PATCH=$((PATCH + 1)) | |
| RELEASE_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" | |
| echo "Incrementing from ${LATEST_VERSION} to ${RELEASE_VERSION}" | |
| fi | |
| echo "release_version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT | |
| - name: Verify release version > latest PyPI version | |
| id: verify_version | |
| if: ${{ !github.event.inputs.skip_version_checks }} | |
| run: | | |
| RELEASE_VERSION="${{ steps.determine_version.outputs.release_version }}" | |
| echo "Validated release version: ${RELEASE_VERSION}" | |
| # Get the latest version from PyPI using JSON API | |
| LATEST_PYPI_VERSION="${{ steps.find_previous_release_branch.outputs.latest_pypi_version }}" | |
| echo "Latest PyPI version: ${LATEST_PYPI_VERSION}" | |
| # Parse latest PyPI version | |
| PYPI_MAJOR=$(echo $LATEST_PYPI_VERSION | cut -d. -f1) | |
| PYPI_MINOR=$(echo $LATEST_PYPI_VERSION | cut -d. -f2) | |
| PYPI_PATCH=$(echo $LATEST_PYPI_VERSION | cut -d. -f3) | |
| # Calculate expected next patch version | |
| NEXT_PATCH_VERSION="${PYPI_MAJOR}.${PYPI_MINOR}.$((PYPI_PATCH + 1))" | |
| echo "Expected next patch version: ${NEXT_PATCH_VERSION}" | |
| # Check if the determined release version is the expected next patch version | |
| if [ "${RELEASE_VERSION}" = "${NEXT_PATCH_VERSION}" ]; then | |
| echo "Success: Version check passed. Determined version ${RELEASE_VERSION} is the expected next patch version." | |
| else | |
| echo "Error: Determined release version ${RELEASE_VERSION} must be the next patch version (${NEXT_PATCH_VERSION}) compared to the latest PyPI version ${LATEST_PYPI_VERSION}." | |
| exit 1 | |
| fi | |
| - name: Create release branch | |
| id: create_release_branch | |
| run: | | |
| RELEASE_VERSION="${{ steps.determine_version.outputs.release_version }}" | |
| BRANCH_NAME="releases/${RELEASE_VERSION}" | |
| # Configure git | |
| git config --local user.email "[email protected]" | |
| git config --local user.name "GitHub Action" | |
| # Check if branch already exists | |
| if git ls-remote --exit-code --heads origin ${BRANCH_NAME} > /dev/null 2>&1; then | |
| echo "Error: Release branch ${BRANCH_NAME} already exists. Please manually delete the branch first and then rerun the workflow." | |
| exit 1 | |
| fi | |
| # Create release branch | |
| git checkout -b ${BRANCH_NAME} | |
| echo "Created release branch: ${BRANCH_NAME}" | |
| echo "release_branch=${BRANCH_NAME}" >> $GITHUB_OUTPUT | |
| git push -f origin ${BRANCH_NAME} | |
| echo "Pushed release branch: ${BRANCH_NAME}" | |
| - name: Set release version and create test branch | |
| id: create_test_branch | |
| run: | | |
| RELEASE_VERSION="${{ steps.determine_version.outputs.release_version }}" | |
| BRANCH_NAME="releases/${RELEASE_VERSION}" | |
| # Checkout the base release branch | |
| echo "Checking out the release branch ${BRANCH_NAME}..." | |
| git checkout ${BRANCH_NAME} | |
| # Make version changes | |
| echo "Updating __version__ in sky/__init__.py to ${RELEASE_VERSION}..." | |
| sed -i "s/__version__ = '.*'/__version__ = '${RELEASE_VERSION}'/g" sky/__init__.py | |
| sed -i "s/image: berkeleyskypilot\/skypilot:.*/image: berkeleyskypilot\/skypilot:${RELEASE_VERSION}/g" charts/skypilot/values.yaml | |
| # Create the test branch from the *current* state (base branch with version bump) | |
| TEST_BRANCH="test_releases/${RELEASE_VERSION}" | |
| echo "Creating test branch ${TEST_BRANCH}..." | |
| git checkout -b ${TEST_BRANCH} | |
| # Commit the version change on the new test branch | |
| git add sky/__init__.py | |
| git add charts/skypilot/values.yaml | |
| git commit -m "Release ${RELEASE_VERSION}" | |
| # Get the new commit SHA from the test branch | |
| NEW_COMMIT_SHA=$(git rev-parse HEAD) | |
| echo "new_commit_sha=${NEW_COMMIT_SHA}" >> $GITHUB_OUTPUT | |
| echo "New commit SHA on ${TEST_BRANCH}: ${NEW_COMMIT_SHA}" | |
| # Push the new test branch | |
| echo "Pushing ${TEST_BRANCH}..." | |
| git push -f origin ${TEST_BRANCH} | |
| echo "test_branch=${TEST_BRANCH}" >> $GITHUB_OUTPUT | |
| smoke-tests: | |
| needs: release-build | |
| if: | | |
| always() && | |
| needs.release-build.result == 'success' && | |
| github.event.inputs.skip_smoke_tests != 'true' | |
| uses: ./.github/workflows/buildkite-trigger-wait.yml | |
| with: | |
| commit: ${{ needs.release-build.outputs.new_commit_sha }} | |
| branch: ${{ needs.release-build.outputs.test_branch }} | |
| message: "Release ${{ needs.release-build.outputs.release_version }}" | |
| pipeline: "full-smoke-tests-run" | |
| timeout_minutes: 240 | |
| wait: true | |
| fail_on_buildkite_failure: true | |
| secrets: | |
| BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }} | |
| quicktest-core: | |
| needs: release-build | |
| if: | | |
| always() && | |
| needs.release-build.result == 'success' && | |
| github.event.inputs.skip_smoke_tests != 'true' | |
| uses: ./.github/workflows/buildkite-trigger-wait.yml | |
| with: | |
| commit: ${{ needs.release-build.outputs.new_commit_sha }} | |
| branch: ${{ needs.release-build.outputs.test_branch }} | |
| message: "Release ${{ needs.release-build.outputs.release_version }}" | |
| pipeline: "quicktest-core" | |
| build_env_vars: '{"ARGS": "--base-branch ${{ needs.release-build.outputs.previous_release_branch }}"}' | |
| timeout_minutes: 180 | |
| wait: true | |
| fail_on_buildkite_failure: true | |
| secrets: | |
| BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }} | |
| quicktest-core-previous-minor: | |
| needs: release-build | |
| if: | | |
| always() && | |
| needs.release-build.result == 'success' && | |
| github.event.inputs.skip_smoke_tests != 'true' | |
| uses: ./.github/workflows/buildkite-trigger-wait.yml | |
| with: | |
| commit: ${{ needs.release-build.outputs.new_commit_sha }} | |
| branch: ${{ needs.release-build.outputs.test_branch }} | |
| message: "Release ${{ needs.release-build.outputs.release_version }} (vs previous minor)" | |
| pipeline: "quicktest-core" | |
| build_env_vars: '{"ARGS": "--base-branch ${{ needs.release-build.outputs.previous_minor_release_branch }}"}' | |
| timeout_minutes: 180 | |
| wait: true | |
| fail_on_buildkite_failure: true | |
| secrets: | |
| BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }} | |
| smoke-tests-remote-server-kubernetes: | |
| needs: release-build | |
| if: | | |
| always() && | |
| needs.release-build.result == 'success' && | |
| github.event.inputs.skip_smoke_tests != 'true' | |
| uses: ./.github/workflows/buildkite-trigger-wait.yml | |
| with: | |
| commit: ${{ needs.release-build.outputs.new_commit_sha }} | |
| branch: ${{ needs.release-build.outputs.test_branch }} | |
| message: "Release ${{ needs.release-build.outputs.release_version }} --remote-server --kubernetes" | |
| pipeline: "smoke-tests" | |
| build_env_vars: '{"ARGS": "--remote-server --kubernetes"}' | |
| timeout_minutes: 60 | |
| wait: true | |
| fail_on_buildkite_failure: true | |
| sleep_seconds: 1800 # 30-minute delay to reduce buildkite resource usage | |
| secrets: | |
| BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }} | |
| release-tests: | |
| needs: release-build | |
| if: | | |
| always() && | |
| needs.release-build.result == 'success' && | |
| github.event.inputs.skip_smoke_tests != 'true' | |
| uses: ./.github/workflows/buildkite-trigger-wait.yml | |
| with: | |
| commit: ${{ needs.release-build.outputs.new_commit_sha }} | |
| branch: ${{ needs.release-build.outputs.test_branch }} | |
| message: "Release ${{ needs.release-build.outputs.release_version }}" | |
| pipeline: "release" | |
| wait: false | |
| fail_on_buildkite_failure: false | |
| secrets: | |
| BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }} | |
| create-pr: | |
| needs: [release-build, smoke-tests, quicktest-core, quicktest-core-previous-minor, smoke-tests-remote-server-kubernetes, release-tests] | |
| if: always() && needs.release-build.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 0 | |
| - name: Create release branch and PR | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT_FOR_RELEASE }} | |
| TEST_BRANCH: ${{ needs.release-build.outputs.test_branch }} | |
| RELEASE_BRANCH: ${{ needs.release-build.outputs.release_branch }} | |
| RELEASE_VERSION: ${{ needs.release-build.outputs.release_version }} | |
| SKIP_SMOKE_TESTS: ${{ github.event.inputs.skip_smoke_tests }} | |
| run: | | |
| # Configure git | |
| git config --local user.email "[email protected]" | |
| git config --local user.name "GitHub Action" | |
| # Detect if this is an RC promotion | |
| SOURCE_BRANCH="${{ github.ref_name }}" | |
| IS_RC_PROMOTION="false" | |
| if [[ "$SOURCE_BRANCH" =~ ^releases/.*rc[0-9]+$ ]]; then | |
| IS_RC_PROMOTION="true" | |
| RC_VERSION=$(echo "$SOURCE_BRANCH" | sed 's/releases\///') | |
| fi | |
| # Build PR body based on whether tests were skipped | |
| if [ "$SKIP_SMOKE_TESTS" == "true" ]; then | |
| if [ "$IS_RC_PROMOTION" == "true" ]; then | |
| PR_BODY="## Promote RC to Stable Release ${RELEASE_VERSION} | |
| **Source:** \`$SOURCE_BRANCH\` (RC version: $RC_VERSION) | |
| **Target:** Stable release \`${RELEASE_VERSION}\` | |
| ⚠️ **Smoke tests were SKIPPED** - This release is being promoted from a tested RC. | |
| ### Pre-release Testing | |
| This version was previously tested as release candidate \`$RC_VERSION\` and deemed stable by early adopters. | |
| ### Changes in this PR | |
| - Updated \`sky/__init__.py\`: \`$RC_VERSION\` → \`${RELEASE_VERSION}\` | |
| - Updated \`charts/skypilot/values.yaml\`: Docker image tag \`$RC_VERSION\` → \`${RELEASE_VERSION}\`" | |
| else | |
| PR_BODY="Release ${RELEASE_VERSION} | |
| ⚠️ **Smoke tests were SKIPPED** - Please ensure manual testing was performed." | |
| fi | |
| else | |
| # Normal release with test results | |
| PR_BODY="Release ${RELEASE_VERSION} | |
| Buildkite Test Links: | |
| - [Full Smoke Tests](https://buildkite.com/skypilot-1/full-smoke-tests-run/builds/${{ needs.smoke-tests.outputs.build_number }}) - $([ "${{ needs.smoke-tests.outputs.build_status }}" == "success" ] && echo "✅ Success" || echo "❌ Failed") | |
| - [Quicktest Core](https://buildkite.com/skypilot-1/quicktest-core/builds/${{ needs.quicktest-core.outputs.build_number }}) - $([ "${{ needs.quicktest-core.outputs.build_status }}" == "success" ] && echo "✅ Success" || echo "❌ Failed") | |
| - [Quicktest Core (vs Previous Minor)](https://buildkite.com/skypilot-1/quicktest-core/builds/${{ needs.quicktest-core-previous-minor.outputs.build_number }}) - $([ "${{ needs.quicktest-core-previous-minor.outputs.build_status }}" == "success" ] && echo "✅ Success" || echo "❌ Failed") | |
| - [Smoke Tests Remote Server Kubernetes](https://buildkite.com/skypilot-1/smoke-tests/builds/${{ needs.smoke-tests-remote-server-kubernetes.outputs.build_number }}) - $([ "${{ needs.smoke-tests-remote-server-kubernetes.outputs.build_status }}" == "success" ] && echo "✅ Success" || echo "❌ Failed") | |
| - [Release Tests](https://buildkite.com/skypilot-1/release/builds/${{ needs.release-tests.outputs.build_number }}) - ⏳ (not waiting for completion) | |
| *Release Tests may take up to 24 hours to complete and might fail due to resource constraints.*" | |
| fi | |
| echo "Creating PR from ${TEST_BRANCH} to ${RELEASE_BRANCH}" | |
| gh pr create --base ${RELEASE_BRANCH} --head ${TEST_BRANCH} \ | |
| --title "Release ${RELEASE_VERSION}" \ | |
| --body "${PR_BODY}" | |
| - name: Summary | |
| if: always() | |
| env: | |
| SKIP_SMOKE_TESTS: ${{ github.event.inputs.skip_smoke_tests }} | |
| run: | | |
| if [ "$SKIP_SMOKE_TESTS" == "true" ]; then | |
| SOURCE_BRANCH="${{ github.ref_name }}" | |
| if [[ "$SOURCE_BRANCH" =~ ^releases/.*rc[0-9]+$ ]]; then | |
| RC_VERSION=$(echo "$SOURCE_BRANCH" | sed 's/releases\///') | |
| cat <<EOF >> "$GITHUB_STEP_SUMMARY" | |
| # Release ${{ needs.release-build.outputs.release_version }} | |
| ## RC Promotion | |
| Promoting from \`$RC_VERSION\` to stable version \`${{ needs.release-build.outputs.release_version }}\` | |
| ⚠️ **Smoke tests were SKIPPED** - This release was promoted from a tested RC. | |
| EOF | |
| else | |
| cat <<EOF >> "$GITHUB_STEP_SUMMARY" | |
| # Release ${{ needs.release-build.outputs.release_version }} | |
| ⚠️ **Smoke tests were SKIPPED** - Please ensure manual testing was performed. | |
| EOF | |
| fi | |
| else | |
| cat <<EOF >> "$GITHUB_STEP_SUMMARY" | |
| # Release ${{ needs.release-build.outputs.release_version }} | |
| ## Buildkite Test Links | |
| - [Full Smoke Tests](https://buildkite.com/skypilot-1/full-smoke-tests-run/builds/${{ needs.smoke-tests.outputs.build_number }}) - $([ "${{ needs.smoke-tests.outputs.build_status }}" == "success" ] && echo "✅ Success" || echo "❌ Failed") | |
| - [Quicktest Core](https://buildkite.com/skypilot-1/quicktest-core/builds/${{ needs.quicktest-core.outputs.build_number }}) - $([ "${{ needs.quicktest-core.outputs.build_status }}" == "success" ] && echo "✅ Success" || echo "❌ Failed") | |
| - [Quicktest Core (vs Previous Minor)](https://buildkite.com/skypilot-1/quicktest-core/builds/${{ needs.quicktest-core-previous-minor.outputs.build_number }}) - $([ "${{ needs.quicktest-core-previous-minor.outputs.build_status }}" == "success" ] && echo "✅ Success" || echo "❌ Failed") | |
| - [Smoke Tests Remote Server Kubernetes](https://buildkite.com/skypilot-1/smoke-tests/builds/${{ needs.smoke-tests-remote-server-kubernetes.outputs.build_number }}) - $([ "${{ needs.smoke-tests-remote-server-kubernetes.outputs.build_status }}" == "success" ] && echo "✅ Success" || echo "❌ Failed") | |
| - [Release Tests](https://buildkite.com/skypilot-1/release/builds/${{ needs.release-tests.outputs.build_number }}) - ⏳ (not waiting for completion) | |
| *Release Tests may take up to 24 hours to complete and might fail due to resource constraints.* | |
| EOF | |
| fi |