Skip to content

Commit 9ba113b

Browse files
authored
Merge pull request #7091 from vvoland/gha-sync-release
gha: Add release branch sync workflow
2 parents a86b9aa + 182f56f commit 9ba113b

3 files changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: Sync Docker release branch
2+
3+
concurrency:
4+
group: ${{ github.workflow }}-${{ github.ref_name }}
5+
cancel-in-progress: false
6+
7+
permissions:
8+
contents: read
9+
10+
on:
11+
workflow_dispatch:
12+
inputs:
13+
tag:
14+
description: Tag to sync from, for example v29.6.0
15+
required: true
16+
type: string
17+
dry_run:
18+
description: Merge but don't push
19+
required: true
20+
default: false
21+
type: boolean
22+
23+
jobs:
24+
sync-release-branch:
25+
runs-on: ubuntu-24.04
26+
permissions:
27+
contents: write
28+
outputs:
29+
base_sha: ${{ steps.sync.outputs.base_sha }}
30+
has_changes: ${{ steps.sync.outputs.has_changes }}
31+
temporary_branch: ${{ steps.sync.outputs.temporary_branch }}
32+
temporary_sha: ${{ steps.sync.outputs.temporary_sha }}
33+
timeout-minutes: 10
34+
steps:
35+
- name: Checkout
36+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
37+
with:
38+
fetch-depth: 0
39+
40+
- name: Validate
41+
env:
42+
BRANCH: ${{ github.ref_name }}
43+
run: |
44+
if [ "$BRANCH" = "master" ]; then
45+
echo "::error::This workflow is expected to be run on a release branch, not master"
46+
exit 1
47+
fi
48+
49+
- name: Configure git author
50+
run: |
51+
git config user.name "github-actions[bot]"
52+
git config user.email "github-actions[bot]@users.noreply.github.com"
53+
54+
- name: Sync release branch to tag range
55+
id: sync
56+
env:
57+
DRY_RUN: ${{ inputs.dry_run }}
58+
RELEASE_BRANCH: ${{ github.ref_name }}
59+
RUN_ATTEMPT: ${{ github.run_attempt }}
60+
RUN_ID: ${{ github.run_id }}
61+
TAG: ${{ inputs.tag }}
62+
run: |
63+
set -o pipefail
64+
base_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
65+
temporary_branch="process/sync-release-branch/$RUN_ID-$RUN_ATTEMPT"
66+
echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
67+
echo "temporary_branch=$temporary_branch" >> "$GITHUB_OUTPUT"
68+
69+
tags_file=$(mktemp)
70+
scripts/unmerged-tags \
71+
"$RELEASE_BRANCH" \
72+
"$TAG" \
73+
> "$tags_file"
74+
75+
echo >> "$GITHUB_STEP_SUMMARY"
76+
echo "## Tags to sync" >> "$GITHUB_STEP_SUMMARY"
77+
echo >> "$GITHUB_STEP_SUMMARY"
78+
sed 's/^/- /' "$tags_file" >> "$GITHUB_STEP_SUMMARY"
79+
80+
xargs -r scripts/sync-branch < "$tags_file" | tee -a "$GITHUB_STEP_SUMMARY"
81+
82+
if [[ "$DRY_RUN" == "true" ]]; then
83+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
84+
exit 0
85+
fi
86+
87+
if [[ $(git rev-parse HEAD) == $(git rev-parse "origin/$RELEASE_BRANCH") ]]; then
88+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
89+
echo "No changes to push"
90+
exit 0
91+
fi
92+
93+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
94+
git push origin "HEAD:refs/heads/$temporary_branch"
95+
echo "temporary_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
96+
97+
push-release-branch:
98+
needs: sync-release-branch
99+
if: ${{ !inputs.dry_run && needs.sync-release-branch.outputs.has_changes == 'true' }}
100+
runs-on: ubuntu-24.04
101+
environment: docker-releases
102+
permissions:
103+
contents: write
104+
timeout-minutes: 10
105+
steps:
106+
- name: Checkout release
107+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
108+
with:
109+
fetch-depth: 0
110+
111+
- name: Push release branch
112+
env:
113+
BASE_SHA: ${{ needs.sync-release-branch.outputs.base_sha }}
114+
RELEASE_BRANCH: ${{ github.ref_name }}
115+
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
116+
TEMPORARY_SHA: ${{ needs.sync-release-branch.outputs.temporary_sha }}
117+
run: |
118+
git fetch origin "$RELEASE_BRANCH"
119+
current_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
120+
if [[ "$current_sha" != "$BASE_SHA" ]]; then
121+
echo "$RELEASE_BRANCH changed from $BASE_SHA to $current_sha"
122+
exit 1
123+
fi
124+
125+
git fetch origin "$TEMPORARY_BRANCH"
126+
current_temporary_sha=$(git rev-parse FETCH_HEAD)
127+
if [[ "$current_temporary_sha" != "$TEMPORARY_SHA" ]]; then
128+
echo "$TEMPORARY_BRANCH changed from $TEMPORARY_SHA to $current_temporary_sha"
129+
exit 1
130+
fi
131+
132+
git push origin "FETCH_HEAD:$RELEASE_BRANCH"
133+
134+
- name: Delete temporary branch
135+
env:
136+
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
137+
run: git push origin --delete "$TEMPORARY_BRANCH"

scripts/sync-branch

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env bash
2+
# Merge the given release tags.
3+
set -Eeuo pipefail
4+
5+
if [[ $# -lt 1 ]]; then
6+
echo "usage: $0 TAG..." >&2
7+
exit 1
8+
fi
9+
10+
tags_to_sync=("$@")
11+
12+
for tag_to_sync in "${tags_to_sync[@]}"; do
13+
if git merge --no-ff "$tag_to_sync"; then
14+
continue
15+
fi
16+
17+
if ! git rev-parse --verify --quiet MERGE_HEAD > /dev/null || git diff --quiet --diff-filter=U; then
18+
exit 1
19+
fi
20+
21+
git checkout "$tag_to_sync" -- '.'
22+
git add --all
23+
git merge --continue
24+
done
25+
26+
# Check that every tag is in the branch.
27+
# This catches cases where a merge did not actually incorporate one of the
28+
# requested release tags.
29+
for tag_to_sync in "${tags_to_sync[@]}"; do
30+
if ! git merge-base --is-ancestor "$tag_to_sync" HEAD; then
31+
echo "tag $tag_to_sync is not contained in $(git rev-parse --abbrev-ref HEAD)" >&2
32+
exit 1
33+
fi
34+
done

scripts/unmerged-tags

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env bash
2+
set -Eeuo pipefail
3+
4+
if [[ $# -ne 2 ]]; then
5+
echo "usage: $0 <RELEASE_BRANCH> <TAG>" >&2
6+
exit 1
7+
fi
8+
9+
release_branch="$1"
10+
tag="$2"
11+
12+
if [[ "$tag" == *"-rc."* ]]; then
13+
echo "error: RC tags cannot be used as sync targets" >&2
14+
exit 1
15+
fi
16+
17+
if [[ "$tag" != v* ]]; then
18+
echo "error: Tag must start with 'v' (e.g. v29.6.0)" >&2
19+
exit 1
20+
fi
21+
22+
if ! git rev-parse --verify --quiet "refs/tags/$tag" > /dev/null; then
23+
echo "error: Tag $tag does not exist" >&2
24+
exit 1
25+
fi
26+
27+
# Return early the requested tag is already merged into release branch.
28+
if git merge-base --is-ancestor "$tag" "$release_branch"; then
29+
exit 0
30+
fi
31+
32+
if git rev-parse --verify --quiet upstream/master > /dev/null 2>&1; then
33+
master="upstream/master"
34+
else
35+
master="origin/master"
36+
fi
37+
38+
if ! git merge-base --is-ancestor "$tag" "$master"; then
39+
echo "error: Tag $tag is not in $master" >&2
40+
exit 1
41+
fi
42+
43+
# Get all docker release tags merged into master but not into release branch
44+
git tag --merged "$master" --no-merged "$release_branch" \
45+
| grep '^v' \
46+
| grep -v -- "-rc." \
47+
| sort -V \
48+
| awk -v tag="$tag" '{print} $0==tag{exit}'

0 commit comments

Comments
 (0)