-
Notifications
You must be signed in to change notification settings - Fork 2.2k
gha: Add release branch sync workflow #7091
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,137 @@ | ||||||
| name: Sync Docker release branch | ||||||
|
|
||||||
| concurrency: | ||||||
| group: ${{ github.workflow }}-${{ github.ref_name }} | ||||||
| cancel-in-progress: false | ||||||
|
|
||||||
| permissions: | ||||||
| contents: read | ||||||
|
|
||||||
| on: | ||||||
| workflow_dispatch: | ||||||
| inputs: | ||||||
| tag: | ||||||
| description: Tag to sync from, for example docker-v29.6.0 | ||||||
| required: true | ||||||
| type: string | ||||||
| dry_run: | ||||||
| description: Merge but don't push | ||||||
| required: true | ||||||
| default: false | ||||||
| type: boolean | ||||||
|
|
||||||
| jobs: | ||||||
| sync-release-branch: | ||||||
| runs-on: ubuntu-24.04 | ||||||
| permissions: | ||||||
| contents: write | ||||||
| outputs: | ||||||
| base_sha: ${{ steps.sync.outputs.base_sha }} | ||||||
| has_changes: ${{ steps.sync.outputs.has_changes }} | ||||||
| temporary_branch: ${{ steps.sync.outputs.temporary_branch }} | ||||||
| temporary_sha: ${{ steps.sync.outputs.temporary_sha }} | ||||||
| timeout-minutes: 10 | ||||||
| steps: | ||||||
| - name: Checkout | ||||||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||||||
| with: | ||||||
| fetch-depth: 0 | ||||||
|
|
||||||
| - name: Validate | ||||||
| env: | ||||||
| BRANCH: ${{ github.ref_name }} | ||||||
| run: | | ||||||
| if [ "$BRANCH" = "master" ]; then | ||||||
| echo "::error::This workflow is expected to be run on a release branch, not master" | ||||||
| exit 1 | ||||||
| fi | ||||||
|
|
||||||
| - name: Configure git author | ||||||
| run: | | ||||||
| git config user.name "github-actions[bot]" | ||||||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||||||
|
|
||||||
| - name: Sync release branch to tag range | ||||||
| id: sync | ||||||
| env: | ||||||
| DRY_RUN: ${{ inputs.dry_run }} | ||||||
| RELEASE_BRANCH: ${{ github.ref_name }} | ||||||
| RUN_ATTEMPT: ${{ github.run_attempt }} | ||||||
| RUN_ID: ${{ github.run_id }} | ||||||
| TAG: ${{ inputs.tag }} | ||||||
| run: | | ||||||
| set -o pipefail | ||||||
| base_sha=$(git rev-parse "origin/$RELEASE_BRANCH") | ||||||
| temporary_branch="process/sync-release-branch/$RUN_ID-$RUN_ATTEMPT" | ||||||
| echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT" | ||||||
| echo "temporary_branch=$temporary_branch" >> "$GITHUB_OUTPUT" | ||||||
|
|
||||||
| tags_file=$(mktemp) | ||||||
| scripts/unmerged-tags \ | ||||||
| "$RELEASE_BRANCH" \ | ||||||
| "$TAG" \ | ||||||
| > "$tags_file" | ||||||
|
|
||||||
| echo >> "$GITHUB_STEP_SUMMARY" | ||||||
| echo "## Tags to sync" >> "$GITHUB_STEP_SUMMARY" | ||||||
| echo >> "$GITHUB_STEP_SUMMARY" | ||||||
| sed 's/^/- /' "$tags_file" >> "$GITHUB_STEP_SUMMARY" | ||||||
|
|
||||||
| xargs -r scripts/sync-branch < "$tags_file" | tee -a "$GITHUB_STEP_SUMMARY" | ||||||
|
vvoland marked this conversation as resolved.
vvoland marked this conversation as resolved.
|
||||||
|
|
||||||
| if [[ "$DRY_RUN" == "true" ]]; then | ||||||
|
vvoland marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] Dry-run mode still executes The If the intent is to allow a "preview without side effects" run (as the description states), the merge step should be gated: If instead the intent is "merge but don't push" (a trial merge to validate feasibility), the
|
||||||
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | ||||||
| exit 0 | ||||||
| fi | ||||||
|
|
||||||
| if [[ $(git rev-parse HEAD) == $(git rev-parse "origin/$RELEASE_BRANCH") ]]; then | ||||||
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | ||||||
| echo "No changes to push" | ||||||
| exit 0 | ||||||
| fi | ||||||
|
|
||||||
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | ||||||
| git push origin "HEAD:refs/heads/$temporary_branch" | ||||||
| echo "temporary_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | ||||||
|
|
||||||
| push-release-branch: | ||||||
| needs: sync-release-branch | ||||||
| if: ${{ !inputs.dry_run && needs.sync-release-branch.outputs.has_changes == 'true' }} | ||||||
| runs-on: ubuntu-24.04 | ||||||
| environment: docker-releases | ||||||
| permissions: | ||||||
| contents: write | ||||||
| timeout-minutes: 10 | ||||||
| steps: | ||||||
| - name: Checkout release | ||||||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||||||
| with: | ||||||
| fetch-depth: 0 | ||||||
|
|
||||||
| - name: Push release branch | ||||||
| env: | ||||||
| BASE_SHA: ${{ needs.sync-release-branch.outputs.base_sha }} | ||||||
| RELEASE_BRANCH: ${{ github.ref_name }} | ||||||
| TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }} | ||||||
| TEMPORARY_SHA: ${{ needs.sync-release-branch.outputs.temporary_sha }} | ||||||
| run: | | ||||||
| git fetch origin "$RELEASE_BRANCH" | ||||||
| current_sha=$(git rev-parse "origin/$RELEASE_BRANCH") | ||||||
| if [[ "$current_sha" != "$BASE_SHA" ]]; then | ||||||
| echo "$RELEASE_BRANCH changed from $BASE_SHA to $current_sha" | ||||||
| exit 1 | ||||||
| fi | ||||||
|
|
||||||
| git fetch origin "$TEMPORARY_BRANCH" | ||||||
| current_temporary_sha=$(git rev-parse FETCH_HEAD) | ||||||
| if [[ "$current_temporary_sha" != "$TEMPORARY_SHA" ]]; then | ||||||
| echo "$TEMPORARY_BRANCH changed from $TEMPORARY_SHA to $current_temporary_sha" | ||||||
| exit 1 | ||||||
| fi | ||||||
|
|
||||||
| git push origin "FETCH_HEAD:$RELEASE_BRANCH" | ||||||
|
|
||||||
| - name: Delete temporary branch | ||||||
| env: | ||||||
| TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }} | ||||||
| run: git push origin --delete "$TEMPORARY_BRANCH" | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||
| #!/usr/bin/env bash | ||||||||||
| # Merge the given release tags. | ||||||||||
| set -Eeuo pipefail | ||||||||||
|
|
||||||||||
| if [[ $# -lt 1 ]]; then | ||||||||||
| echo "usage: $0 [--dry-run] TAG..." >&2 | ||||||||||
| exit 1 | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| tags_to_sync=("$@") | ||||||||||
|
|
||||||||||
| for tag_to_sync in "${tags_to_sync[@]}"; do | ||||||||||
| if git merge --no-ff "$tag_to_sync"; then | ||||||||||
| continue | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| if ! git rev-parse --verify --quiet MERGE_HEAD > /dev/null || git diff --quiet --diff-filter=U; then | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] Wrong boolean operator The condition on line 17 is: Due to operator precedence this evaluates as: The guard's intent is to abort when something truly unexpected happened — i.e., when we are not in a merge state AND there are no unresolved conflicts. With The intent is better expressed with
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But if if git can auto-resolve everything the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're absolutely right — I misunderstood the merge behavior. If Looking at this again, the guard is only reached when
In case (1), My analysis was flawed — the existing condition correctly handles the expected failure modes. Thank you for catching that! |
||||||||||
| exit 1 | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| git checkout "$tag_to_sync" -- '.' | ||||||||||
| git add --all | ||||||||||
|
vvoland marked this conversation as resolved.
|
||||||||||
| git merge --continue | ||||||||||
| done | ||||||||||
|
|
||||||||||
| # Check that every tag is in the branch. | ||||||||||
|
vvoland marked this conversation as resolved.
|
||||||||||
| # This catches cases where a merge did not actually incorporate one of the | ||||||||||
| # requested release tags. | ||||||||||
| for tag_to_sync in "${tags_to_sync[@]}"; do | ||||||||||
| if ! git merge-base --is-ancestor "$tag_to_sync" HEAD; then | ||||||||||
| echo "tag $tag_to_sync is not contained in $(git rev-parse --abbrev-ref HEAD)" >&2 | ||||||||||
| exit 1 | ||||||||||
| fi | ||||||||||
| done | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| #!/usr/bin/env bash | ||
| set -Eeuo pipefail | ||
|
|
||
| if [[ $# -ne 2 ]]; then | ||
| echo "usage: $0 <RELEASE_BRANCH> <TAG>" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| release_branch="$1" | ||
| tag="$2" | ||
|
|
||
| if [[ "$tag" == *"-rc."* ]]; then | ||
| echo "error: RC tags cannot be used as sync targets" >&2 | ||
| exit 1 | ||
| fi | ||
|
vvoland marked this conversation as resolved.
|
||
|
|
||
| if ! git rev-parse --verify --quiet "refs/tags/$tag" > /dev/null; then | ||
| echo "error: Tag $tag does not exist" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Return early the requested tag is already merged into release branch. | ||
| if git merge-base --is-ancestor "$tag" "$release_branch"; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| if git rev-parse --verify --quiet upstream/master > /dev/null 2>&1; then | ||
| master="upstream/master" | ||
| else | ||
| master="origin/master" | ||
| fi | ||
|
|
||
| # Get all docker release tags merged into master but not into release branch | ||
| git tag --merged "$master" --no-merged "$release_branch" \ | ||
| | grep '^v' \ | ||
| | grep -v -- "-rc." \ | ||
| | sort -V \ | ||
| | awk -v tag="$tag" '{print} $0==tag{exit}' | ||
|
vvoland marked this conversation as resolved.
|
||
Uh oh!
There was an error while loading. Please reload this page.