Prow Merge release-* Branch to rhoai-staging Branch #8
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: Prow Merge release-* Branch to rhoai-staging Branch | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| confirm_pyproject_sync_policy: | |
| description: | | |
| Developer Note: release-* -> rhoai-staging auto-sync does not auto-propagate all pyproject.toml updates as-is. | |
| If pyproject.toml or runtimes/*/pyproject.toml changes are needed in rhoai-staging, open a separate manual sync PR. | |
| After manual pyproject sync, regenerate lock files with `make lock`. | |
| Acknowledgement: I understand and will manually sync pyproject.toml changes when needed. | |
| required: false | |
| type: boolean | |
| default: false | |
| # Prevent concurrent sync runs for the same source branch. | |
| # We queue instead of canceling to avoid losing a manually-triggered run. | |
| concurrency: | |
| group: sync-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| UPSTREAM_URL: "https://github.com/opendatahub-io/MLServer.git" | |
| RELEASE_BRANCH: ${{ github.ref_name }} | |
| TARGET_BRANCH: rhoai-staging | |
| jobs: | |
| auto-merge: | |
| # Only release-* branches are allowed to trigger this sync flow. | |
| if: startsWith(github.ref, 'refs/heads/release-') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Validate manual pyproject sync acknowledgement | |
| if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.confirm_pyproject_sync_policy != 'true' }} | |
| run: | | |
| echo "Please acknowledge pyproject.toml manual sync policy before running this workflow." | |
| exit 1 | |
| - name: Checkout repository | |
| # Pinned from actions/checkout v6 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| ref: master | |
| fetch-depth: 0 | |
| - name: Fetch required branches | |
| run: | | |
| set -o nounset | |
| set -o pipefail | |
| # Fetch both branch tips used throughout validation and merge. | |
| git fetch origin "$TARGET_BRANCH" | |
| git fetch origin $RELEASE_BRANCH | |
| # Fail early with clear logs if expected refs are missing. | |
| if ! git rev-parse --verify "origin/$TARGET_BRANCH" >/dev/null 2>&1; then | |
| echo "Required branch not found: origin/$TARGET_BRANCH" | |
| exit 1 | |
| fi | |
| if ! git rev-parse --verify "origin/$RELEASE_BRANCH" >/dev/null 2>&1; then | |
| echo "Required branch not found: origin/$RELEASE_BRANCH" | |
| exit 1 | |
| fi | |
| - name: Validate Sync Status | |
| id: validate | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -o nounset | |
| set -o pipefail | |
| # Block duplicate in-flight sync PRs from the same release branch. | |
| EXISTING_PR=$(gh pr list \ | |
| --base "$TARGET_BRANCH" \ | |
| --json number,headRefName \ | |
| --jq ".[] | select(.headRefName | startswith(\"sync/${RELEASE_BRANCH}\")) | .number" \ | |
| | head -n1) | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "PR #$EXISTING_PR already exists for syncing $RELEASE_BRANCH to $TARGET_BRANCH." | |
| echo "Please close any outstanding syncs from $RELEASE_BRANCH to $TARGET_BRANCH before proceeding." | |
| exit 1 | |
| fi | |
| # Decide whether a sync is needed after applying the same policy rules | |
| # used in the merge step (excluded files and keep-from-rhoai files). | |
| MEANINGFUL_CHANGES=false | |
| while IFS= read -r file; do | |
| [ -z "$file" ] && continue | |
| # Always excluded from sync | |
| if [[ "$file" == ".tekton/mlserver-push.yaml" ]] || \ | |
| [[ "$file" == ".github/workflows/prow-merge-release-to-staging.yml" ]] || \ | |
| [[ "$file" == ".github/workflows/create-and-bump-tag.yml" ]]; then | |
| continue | |
| fi | |
| # Root-level metadata kept from rhoai-staging by policy. | |
| # NOTE: bare filenames are root-path matches; runtime files are handled | |
| # by explicit runtimes/* patterns below. | |
| if [[ "$file" == "pyproject.toml" ]] || \ | |
| [[ "$file" == "poetry.lock" ]] || \ | |
| [[ "$file" == "docs/conf.py" ]] || \ | |
| [[ "$file" == "mlserver/version.py" ]] || \ | |
| [[ "$file" == runtimes/*/pyproject.toml ]] || \ | |
| [[ "$file" == runtimes/*/poetry.lock ]]; then | |
| continue | |
| fi | |
| # Runtime version files are branch-owned when they already exist on | |
| # $TARGET_BRANCH. New runtime version files on release are meaningful | |
| # and should be synced into $TARGET_BRANCH. | |
| if [[ "$file" == runtimes/*/*/version.py ]]; then | |
| if git cat-file -e "origin/$TARGET_BRANCH:$file" 2>/dev/null; then | |
| continue | |
| fi | |
| fi | |
| MEANINGFUL_CHANGES=true | |
| break | |
| done < <(git diff --name-only "origin/$TARGET_BRANCH..origin/$RELEASE_BRANCH") | |
| if [ "$MEANINGFUL_CHANGES" = true ]; then | |
| echo "Meaningful changes detected between $RELEASE_BRANCH and $TARGET_BRANCH. Proceeding with PR creation." | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "No meaningful changes to sync from $RELEASE_BRANCH to $TARGET_BRANCH after applying sync policy." | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Sync ${{ github.ref_name }} to ${{ env.TARGET_BRANCH }} | |
| if: steps.validate.outputs.has_changes == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -o nounset | |
| set -o pipefail | |
| # Generate sync branch name with timestamp | |
| SYNC_BRANCH="sync/${RELEASE_BRANCH}-$(date +%Y-%m-%d-%H%M%S)" | |
| # Configure git | |
| git config user.name "Sync Bot" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Create new sync branch from origin/$TARGET_BRANCH. | |
| git checkout -b "$SYNC_BRANCH" "origin/$TARGET_BRANCH" | |
| # Start merge without committing yet. This lets us apply policy changes | |
| # first so the final PR contains one deterministic merge commit. | |
| set +e | |
| git merge --no-commit --no-ff -X theirs "origin/$RELEASE_BRANCH" | |
| MERGE_EXIT=$? | |
| set -e | |
| # Distinguish expected conflict state (MERGE_HEAD exists) from fatal | |
| # merge failures where no merge state was created. | |
| if [ "$MERGE_EXIT" -ne 0 ] && ! git rev-parse -q --verify MERGE_HEAD >/dev/null; then | |
| echo "Merge failed unexpectedly with exit code $MERGE_EXIT and no MERGE_HEAD." | |
| exit 1 | |
| fi | |
| # Always keep these files in target-branch shape (or absent if absent on target branch). | |
| FILES_TO_EXCLUDE=( | |
| ".tekton/mlserver-push.yaml" | |
| ".github/workflows/prow-merge-release-to-staging.yml" | |
| ".github/workflows/create-and-bump-tag.yml" | |
| ) | |
| for file in "${FILES_TO_EXCLUDE[@]}"; do | |
| if git cat-file -e "origin/$TARGET_BRANCH:$file" 2>/dev/null; then | |
| # File exists on target branch, restore it | |
| echo "Restoring $file from $TARGET_BRANCH" | |
| git restore --source "origin/$TARGET_BRANCH" --staged --worktree -- "$file" | |
| else | |
| # File doesn't exist on target branch, remove it | |
| echo "Removing $file (not present on $TARGET_BRANCH)" | |
| git rm -f --ignore-unmatch "$file" | |
| fi | |
| done | |
| # Apply metadata ownership policy: | |
| # - Keep root/runtime dependency metadata from target branch. | |
| # - Keep existing runtime version.py from target branch. | |
| # - For newly introduced runtime version.py on release, keep release version. | |
| # This mirrors files managed by hack/update-version.sh. | |
| # Iterate remote-ref branch diff (not merge-index state) for deterministic policy. | |
| while IFS= read -r file; do | |
| [ -z "$file" ] && continue | |
| case "$file" in | |
| # Dependency metadata and version tracking files. | |
| pyproject.toml|poetry.lock|docs/conf.py|mlserver/version.py|runtimes/*/pyproject.toml|runtimes/*/poetry.lock) | |
| if git cat-file -e "origin/$TARGET_BRANCH:$file" 2>/dev/null; then | |
| echo "Keeping $file from $TARGET_BRANCH" | |
| git restore --source "origin/$TARGET_BRANCH" --staged --worktree -- "$file" | |
| else | |
| echo "Removing $file (not present on $TARGET_BRANCH)" | |
| git rm -f --ignore-unmatch "$file" | |
| fi | |
| ;; | |
| runtimes/*/*/version.py) | |
| if git cat-file -e "origin/$TARGET_BRANCH:$file" 2>/dev/null; then | |
| echo "Keeping $file from $TARGET_BRANCH" | |
| git restore --source "origin/$TARGET_BRANCH" --staged --worktree -- "$file" | |
| else | |
| echo "::warning::Runtime version file '$file' exists only on release branch. Keeping release version to allow new runtime sync." | |
| # If this file is unmerged, resolve conflict by taking release side. | |
| if [ -n "$(git ls-files -u -- "$file")" ]; then | |
| git checkout --theirs -- "$file" | |
| fi | |
| git add "$file" | |
| fi | |
| ;; | |
| esac | |
| done < <(git diff --name-only "origin/$TARGET_BRANCH..origin/$RELEASE_BRANCH") | |
| # Do not continue if any unresolved conflicts remain after policy handling. | |
| if git rev-parse -q --verify MERGE_HEAD >/dev/null; then | |
| UNRESOLVED_CONFLICTS=$(git diff --name-only --diff-filter=U) | |
| if [ -n "$UNRESOLVED_CONFLICTS" ]; then | |
| echo "Merge returned exit code: $MERGE_EXIT" | |
| echo "Unresolved merge conflicts remain:" | |
| echo "$UNRESOLVED_CONFLICTS" | |
| echo "Please resolve conflicts manually and fix the merge conflict." | |
| exit 1 | |
| fi | |
| fi | |
| # No-op check: | |
| # - first diff checks worktree vs index | |
| # - second diff checks index vs HEAD (merge result before commit) | |
| # If both are clean, policy neutralized the merge and we can stop. | |
| if git diff --quiet && git diff --quiet --cached; then | |
| echo "No effective changes remain after sync policy; skipping push and PR creation." | |
| if git rev-parse -q --verify MERGE_HEAD >/dev/null; then | |
| git merge --abort || true | |
| fi | |
| exit 0 | |
| fi | |
| # Finalize exactly one merge commit for this sync PR. | |
| if git rev-parse -q --verify MERGE_HEAD >/dev/null; then | |
| git commit --no-edit | |
| else | |
| echo "Expected MERGE_HEAD to exist before final commit, but it was missing." | |
| exit 1 | |
| fi | |
| # Push the sync branch | |
| git push origin "$SYNC_BRANCH" | |
| # Create new PR | |
| gh pr create \ | |
| --base "$TARGET_BRANCH" \ | |
| --head "$SYNC_BRANCH" \ | |
| --title "Sync $RELEASE_BRANCH to $TARGET_BRANCH" \ | |
| --body "$(cat <<EOF | |
| ## Description | |
| - Automated sync of changes from $RELEASE_BRANCH to $TARGET_BRANCH | |
| - Part of RHOAI release preparation | |
| ## Changes | |
| - All changes from $RELEASE_BRANCH | |
| - Any changes to the following files are excluded: | |
| - \`.tekton/mlserver-push.yaml\` | |
| - \`.github/workflows/prow-merge-release-to-staging.yml\` | |
| - \`.github/workflows/create-and-bump-tag.yml\` | |
| - Version and dependency metadata files are preserved from \`$TARGET_BRANCH\` by sync policy. | |
| 🤖 Generated with GitHub Actions | |
| EOF | |
| )" |