Skip to content

gha: Add release branch sync workflow#7091

Open
vvoland wants to merge 1 commit into
docker:masterfrom
vvoland:gha-sync-release
Open

gha: Add release branch sync workflow#7091
vvoland wants to merge 1 commit into
docker:masterfrom
vvoland:gha-sync-release

Conversation

@vvoland

@vvoland vvoland commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Add a manually dispatched workflow for maintainers to sync a docker release branch to a selected docker release tag.

The job checks out the requested release branch, merges the tag with git merge --no-ff, checks the worktree content out from that tag, stages the result, and pushes the updated branch.

The actual push is guarded by the docker-releases GitHub environment which requires a manual approval.

@vvoland vvoland added this to the 29.7.0 milestone Jul 8, 2026
@vvoland vvoland self-assigned this Jul 8, 2026
@vvoland vvoland force-pushed the gha-sync-release branch 3 times, most recently from cc66332 to e802815 Compare July 8, 2026 10:04
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🔴 CRITICAL

Two high-severity bugs completely disable the master-branch safety guard in the Validate step (BRANCH vs $branch case mismatch + trailing space in the comparison string). Several medium-severity issues were also found: git checkout -- '*' won't resolve subdirectory conflicts, sed "//q" uses unescaped regex with slash-injection risk, incorrect boolean logic in conflict detection, missing xargs -r guard, and a DRY_RUN check that fires after merges have already been applied.

Comment thread .github/workflows/sync-release-branch.yml Outdated
Comment thread .github/workflows/sync-release-branch.yml Outdated
Comment thread scripts/unmerged-tags Outdated
Comment thread scripts/sync-branch
Comment thread .github/workflows/sync-release-branch.yml
Comment thread scripts/sync-branch
Comment thread .github/workflows/sync-release-branch.yml Outdated
Comment thread .github/workflows/sync-release-branch.yml

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

3 findings in 2 files — 1 high-severity (forced inline), 2 medium-severity (security + logic).

Comment thread scripts/unmerged-tags Outdated
Comment thread .github/workflows/sync-release-branch.yml
Comment thread scripts/unmerged-tags Outdated
@vvoland vvoland force-pushed the gha-sync-release branch from 14b8e3f to 0812cfb Compare July 8, 2026 13:48
@vvoland vvoland requested a review from docker-agent July 8, 2026 14:56

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🔴 CRITICAL

Two confirmed high-severity bugs in new code introduced by this PR.

Findings summary

  1. scripts/unmerged-tags:32 — Plain sort sorts version tags lexicographically, causing wrong tag ordering (e.g. docker-v29.10.0 before docker-v29.9.0). The awk exit-on-target logic then cuts the list at the wrong point, syncing the wrong set of tags.
  2. .github/workflows/sync-release-branch.yml:80scripts/sync-branch (which runs git merge) is called before the DRY_RUN check, so merges always execute even in dry-run mode. The dry_run flag only prevents the push, contradicting the stated intent of "print the tag range without merging or pushing".

Comment thread scripts/unmerged-tags Outdated
Comment thread .github/workflows/sync-release-branch.yml
Add a manually dispatched workflow for maintainers to sync a Docker
release branch to a selected release tag.

The sync-release-branch job checks out the release branch, computes
the list of unmerged tags up to the requested tag via
scripts/unmerged-tags, merges them in order via scripts/sync-branch
using git merge --no-ff (resolving conflicts by taking the tag's
content), then pushes the result to a temporary branch.

The push-release-branch job runs after manual approval via the
docker-releases environment. It verifies that neither the release
branch nor the temporary branch moved since the sync job ran before
force-advancing the release branch and deleting the temporary branch.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
@vvoland vvoland force-pushed the gha-sync-release branch from 0812cfb to 21f6308 Compare July 8, 2026 15:06
@vvoland vvoland requested a review from docker-agent July 8, 2026 15:08

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

4 medium-severity findings confirmed or likely in the newly added scripts and workflow.

Comment thread scripts/unmerged-tags
if [[ "$tag" == *"-rc."* ]]; then
echo "error: RC tags cannot be used as sync targets" >&2
exit 1
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] No validation that $tag matches expected pattern; a tag starting with - could be treated as a git flag

$tag (from inputs.tag, a user-supplied workflow dispatch string) is passed directly to git merge-base --is-ancestor "$tag" "$release_branch" without checking that it matches an expected pattern like ^docker-v[0-9]. If a user supplies a value starting with - (e.g., --help, -f), git may interpret it as a flag rather than a ref name. The RC filter only excludes strings containing -rc. — it does not catch leading hyphens or other unexpected patterns.

Although workflow_dispatch requires repository write access, a simple defensive validation at the top of the script (e.g., [[ "$tag" =~ ^docker-v[0-9] ]]) would prevent accidental or intentional misuse:

Suggested change
fi
if [[ "$tag" == *"-rc."* ]]; then
echo "error: RC tags cannot be used as sync targets" >&2
exit 1
fi
if [[ ! "$tag" =~ ^docker-v[0-9] ]]; then
echo "error: tag must match docker-v<version> format, got: $tag" >&2
exit 1
fi
Confidence Score
🟡 moderate 62/100

Comment thread scripts/unmerged-tags
| grep '^docker-v' \
| grep -v -- "-rc." \
| sort -V \
| awk -v tag="$tag" '{print} $0==tag{exit}'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] If the requested tag is not in git tag output, awk emits all tags instead of stopping

The awk command '{print} $0==tag{exit}' prints every line until it matches $tag, then exits. If $tag is not present in the filtered/sorted output — e.g., because it was not yet merged into master, or due to a typo — awk falls off the end and emits all pending tags. This causes scripts/sync-branch to merge every outstanding release tag into the branch rather than just the intended range.

The early-exit guard at line 18 only handles the case where the tag is already merged into the release branch; it does not catch the case where the tag doesn't appear in the master-merged list. Adding a post-pipeline check (e.g., tracking whether the tag was seen) would prevent this silent over-sync:

Suggested change
| awk -v tag="$tag" '{print} $0==tag{exit}'
| awk -v tag="$tag" 'found || {print} $0==tag{found=1; exit} END{if (!found) {print "error: tag " tag " not found in tag list" > "/dev/stderr"; exit 1}}'

Or equivalently, validate after the pipeline that the output contains the expected tag.

Confidence Score
🟢 strong 100/100

Comment thread scripts/sync-branch
exit 1
fi

git checkout "$tag_to_sync" -- '*'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] Glob '*' in git checkout does not match dotfiles — .dockerignore, .github/, etc. are skipped

git checkout "$tag_to_sync" -- '*' passes the literal asterisk * as a git pathspec. Like shell globs, git's pathspec * does not match files or directories whose names begin with ., so hidden files such as .dockerignore, .gitignore, and everything under .github/ would not be checked out from the tag when resolving a merge conflict. This means those files retain the conflict-state version, and the subsequent git add --all + git merge --continue would commit the wrong content for those paths.

Use . (current directory) to check out all files including dotfiles:

Suggested change
git checkout "$tag_to_sync" -- '*'
git checkout "$tag_to_sync" -- .
Confidence Score
🟢 strong 97/100

Comment thread scripts/sync-branch
continue
fi

if ! git rev-parse --verify --quiet MERGE_HEAD > /dev/null || git diff --quiet --diff-filter=U; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] Wrong boolean operator || vs && in merge-conflict guard may cause unexpected exit

The condition on line 17 is:

if ! git rev-parse --verify --quiet MERGE_HEAD > /dev/null || git diff --quiet --diff-filter=U; then
    exit 1
fi

Due to operator precedence this evaluates as:

(NOT in_merge_state) OR (no_unresolved_conflicts)

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 condition is also true when MERGE_HEAD is absent alone, or when there are no unresolved conflicts alone. The problematic edge case: if a merge fails and MERGE_HEAD is present but auto-resolution leaves no U-status files (e.g., all conflicts were trivially resolved but merge still exited non-zero), git diff --quiet --diff-filter=U exits 0 (no output), making the overall condition FALSE OR TRUE = TRUE → the script exits 1 even though the state is a valid in-progress merge.

The intent is better expressed with && — exit only when BOTH conditions indicate an unexpected state:

Suggested change
if ! git rev-parse --verify --quiet MERGE_HEAD > /dev/null || git diff --quiet --diff-filter=U; then
if ! git rev-parse --verify --quiet MERGE_HEAD > /dev/null && git diff --quiet --diff-filter=U; then
Confidence Score
🟡 moderate 75/100

xargs -r scripts/sync-branch < "$tags_file" | tee -a "$GITHUB_STEP_SUMMARY"
if [[ "$DRY_RUN" == "true" ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] Dry-run mode still executes git merge operations — it only skips the push

The dry_run input is described as "Print the tag range without merging or pushing," but xargs -r scripts/sync-branch < "$tags_file" on line 80 runs unconditionally — scripts/sync-branch performs real git merge --no-ff operations. The dry-run check on line 82 only prevents the subsequent git push. This means a dry-run invocation performs irreversible (locally) merges in the runner's working tree, which is discarded, but the behavior contradicts the documented intent.

If the intent is to allow a "preview without side effects" run (as the description states), the merge step should be gated:

if [[ "$DRY_RUN" != "true" ]]; then
    xargs -r scripts/sync-branch < "$tags_file" | tee -a "$GITHUB_STEP_SUMMARY"
fi

If instead the intent is "merge but don't push" (a trial merge to validate feasibility), the dry_run description should be updated to say "Merge locally but do not push" to avoid confusion.

Confidence Score
🟡 moderate 75/100

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants