ci: sync release commit back to development

This commit is contained in:
enoch85
2026-03-30 18:12:50 +00:00
parent e4d280e304
commit 98133bb40e
5 changed files with 188 additions and 63 deletions
@@ -15,6 +15,7 @@ Promotes the current `development` branch state to `main` for release.
- If checks or branch protection still block the merge, release automation comments on this PR with the blocker.
- If the flow succeeds, release automation posts a final summary comment with the merge, sync-back, and build results.
- The manual release workflow starts only after the post-merge sync-back is complete.
- `Release 5 - Publish` finishes with a second sync-back so the semantic-release commit on `main` is merged back into `development`.
- Trigger the final release from `main` with `REF=main ./release.sh release`.
## Test Plan
@@ -24,3 +25,4 @@ Promotes the current `development` branch state to `main` for release.
- [ ] Approve this PR to trigger `Release 2 - Queue Push PR To Main` and `Release 2.5 - Execute Push PR To Main`
- [ ] Confirm the PR was squash-merged into `main`, sync-back completed, and `Release 4 - Build Main` finished
- [ ] Run `REF=main ./release.sh release`
- [ ] Confirm `Release 5 - Publish` synced the release commit from `main` back into `development`
+147 -55
View File
@@ -3,6 +3,13 @@ name: "Release 3 - Sync Back"
on:
workflow_dispatch:
inputs:
mode:
type: choice
description: Sync strategy to run
options:
- reconcile
- post-release
default: reconcile
dry_run:
type: boolean
description: Preview sync-back without pushing changes
@@ -13,6 +20,10 @@ on:
type: string
required: false
default: development
mode:
type: string
required: false
default: reconcile
dry_run:
type: boolean
required: false
@@ -30,6 +41,7 @@ env:
MAIN_BRANCH: main
DEVELOPMENT_BRANCH: development
RECONCILE_COMMIT_MESSAGE: "chore: reconcile with main after squash-merge"
POST_RELEASE_COMMIT_MESSAGE: "chore: sync release changes back from main"
jobs:
guard_manual_run:
@@ -80,65 +92,145 @@ jobs:
run: |
set -euo pipefail
git fetch "$REMOTE"
shortstat="$(git diff --shortstat "$REMOTE/$MAIN_BRANCH" "$REMOTE/$DEVELOPMENT_BRANCH")"
if [ -n "$shortstat" ]; then
echo "$shortstat" >&2
echo "Branches do not have identical content. Squash-merge the PR first." >&2
exit 1
sync_mode='${{ inputs.mode }}'
if [ -z "$sync_mode" ]; then
sync_mode='${{ github.event.inputs.mode }}'
fi
merge_base="$(git merge-base "$REMOTE/$MAIN_BRANCH" "$REMOTE/$DEVELOPMENT_BRANCH")"
preview="$(git merge-tree "$merge_base" "$REMOTE/$MAIN_BRANCH" "$REMOTE/$DEVELOPMENT_BRANCH")"
if [ -n "$preview" ]; then
printf '%s\n' "$preview" >&2
echo "Reconciliation preview is not clean. Resolve branch differences before syncing back." >&2
exit 1
if [ -z "$sync_mode" ]; then
sync_mode='reconcile'
fi
if [ "$DRY_RUN" = "true" ]; then
{
echo "## Sync Back"
echo
echo "- Mode: dry run"
echo "- Status: branches have identical content and the merge preview is clean"
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git checkout "$DEVELOPMENT_BRANCH"
git pull --ff-only "$REMOTE" "$DEVELOPMENT_BRANCH"
if git show-ref --verify --quiet "refs/heads/$MAIN_BRANCH"; then
git checkout "$MAIN_BRANCH"
else
git checkout -b "$MAIN_BRANCH" "$REMOTE/$MAIN_BRANCH"
fi
git pull --ff-only "$REMOTE" "$MAIN_BRANCH"
git checkout "$DEVELOPMENT_BRANCH"
git merge "$MAIN_BRANCH" --no-ff -m "$RECONCILE_COMMIT_MESSAGE"
git push "$REMOTE" "$DEVELOPMENT_BRANCH"
git checkout "$MAIN_BRANCH"
git merge "$DEVELOPMENT_BRANCH" --ff-only
git push "$REMOTE" "$MAIN_BRANCH"
git fetch "$REMOTE"
ahead_dev="$(git rev-list --count "$REMOTE/$MAIN_BRANCH..$REMOTE/$DEVELOPMENT_BRANCH")"
ahead_main="$(git rev-list --count "$REMOTE/$DEVELOPMENT_BRANCH..$REMOTE/$MAIN_BRANCH")"
case "$sync_mode" in
reconcile)
shortstat="$(git diff --shortstat "$REMOTE/$MAIN_BRANCH" "$REMOTE/$DEVELOPMENT_BRANCH")"
if [ -n "$shortstat" ]; then
echo "$shortstat" >&2
echo "Branches do not have identical content. Squash-merge the PR first." >&2
exit 1
fi
if [ "$ahead_dev" != "0" ] || [ "$ahead_main" != "0" ]; then
echo "Branches are still diverged after sync-back." >&2
exit 1
fi
merge_base="$(git merge-base "$REMOTE/$MAIN_BRANCH" "$REMOTE/$DEVELOPMENT_BRANCH")"
preview="$(git merge-tree "$merge_base" "$REMOTE/$MAIN_BRANCH" "$REMOTE/$DEVELOPMENT_BRANCH")"
if [ -n "$preview" ]; then
printf '%s\n' "$preview" >&2
echo "Reconciliation preview is not clean. Resolve branch differences before syncing back." >&2
exit 1
fi
{
echo "## Sync Back"
echo
echo "- Mode: apply"
echo "- development_ahead_of_main=$ahead_dev"
echo "- main_ahead_of_development=$ahead_main"
} >> "$GITHUB_STEP_SUMMARY"
if [ "$DRY_RUN" = "true" ]; then
{
echo "## Sync Back"
echo
echo "- Strategy: reconcile"
echo "- Mode: dry run"
echo "- Status: branches have identical content and the merge preview is clean"
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git checkout "$DEVELOPMENT_BRANCH"
git pull --ff-only "$REMOTE" "$DEVELOPMENT_BRANCH"
if git show-ref --verify --quiet "refs/heads/$MAIN_BRANCH"; then
git checkout "$MAIN_BRANCH"
else
git checkout -b "$MAIN_BRANCH" "$REMOTE/$MAIN_BRANCH"
fi
git pull --ff-only "$REMOTE" "$MAIN_BRANCH"
git checkout "$DEVELOPMENT_BRANCH"
git merge "$MAIN_BRANCH" --no-ff -m "$RECONCILE_COMMIT_MESSAGE"
git push "$REMOTE" "$DEVELOPMENT_BRANCH"
git checkout "$MAIN_BRANCH"
git merge "$DEVELOPMENT_BRANCH" --ff-only
git push "$REMOTE" "$MAIN_BRANCH"
git fetch "$REMOTE"
ahead_dev="$(git rev-list --count "$REMOTE/$MAIN_BRANCH..$REMOTE/$DEVELOPMENT_BRANCH")"
ahead_main="$(git rev-list --count "$REMOTE/$DEVELOPMENT_BRANCH..$REMOTE/$MAIN_BRANCH")"
if [ "$ahead_dev" != "0" ] || [ "$ahead_main" != "0" ]; then
echo "Branches are still diverged after sync-back." >&2
exit 1
fi
{
echo "## Sync Back"
echo
echo "- Strategy: reconcile"
echo "- Mode: apply"
echo "- development_ahead_of_main=$ahead_dev"
echo "- main_ahead_of_development=$ahead_main"
} >> "$GITHUB_STEP_SUMMARY"
;;
post-release)
ahead_dev_before="$(git rev-list --count "$REMOTE/$MAIN_BRANCH..$REMOTE/$DEVELOPMENT_BRANCH")"
ahead_main_before="$(git rev-list --count "$REMOTE/$DEVELOPMENT_BRANCH..$REMOTE/$MAIN_BRANCH")"
git checkout "$DEVELOPMENT_BRANCH"
git pull --ff-only "$REMOTE" "$DEVELOPMENT_BRANCH"
if [ "$ahead_main_before" = "0" ]; then
{
echo "## Sync Back"
echo
echo "- Strategy: post-release"
echo "- Mode: $([ "$DRY_RUN" = "true" ] && echo dry\ run || echo apply)"
echo "- Status: development already contains main"
echo "- development_ahead_of_main=$ahead_dev_before"
echo "- main_ahead_of_development=$ahead_main_before"
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if ! git merge --no-commit --no-ff "$REMOTE/$MAIN_BRANCH"; then
git merge --abort || true
echo "Post-release sync-back hit merge conflicts. Resolve the branch drift before retrying." >&2
exit 1
fi
if [ "$DRY_RUN" = "true" ]; then
git merge --abort
{
echo "## Sync Back"
echo
echo "- Strategy: post-release"
echo "- Mode: dry run"
echo "- Status: main can be merged back into development cleanly"
echo "- development_ahead_of_main=$ahead_dev_before"
echo "- main_ahead_of_development=$ahead_main_before"
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git commit -m "$POST_RELEASE_COMMIT_MESSAGE"
git push "$REMOTE" "$DEVELOPMENT_BRANCH"
git fetch "$REMOTE"
ahead_dev_after="$(git rev-list --count "$REMOTE/$MAIN_BRANCH..$REMOTE/$DEVELOPMENT_BRANCH")"
ahead_main_after="$(git rev-list --count "$REMOTE/$DEVELOPMENT_BRANCH..$REMOTE/$MAIN_BRANCH")"
if [ "$ahead_main_after" != "0" ]; then
echo "main is still ahead of development after post-release sync-back." >&2
exit 1
fi
{
echo "## Sync Back"
echo
echo "- Strategy: post-release"
echo "- Mode: apply"
echo "- development_ahead_of_main=$ahead_dev_after"
echo "- main_ahead_of_development=$ahead_main_after"
} >> "$GITHUB_STEP_SUMMARY"
;;
*)
echo "Unknown sync mode: $sync_mode" >&2
exit 1
;;
esac
+13
View File
@@ -88,6 +88,19 @@ jobs:
npx semantic-release
fi
sync_back_release_commit:
name: Sync release commit back to development
needs: semantic-release
if: ${{ needs.semantic-release.result == 'success' && github.event.inputs.dry-run-semantic-release != 'true' }}
uses: ./.github/workflows/release_3_sync_back.yml
permissions:
contents: write
secrets: inherit
with:
ref: development
mode: post-release
dry_run: false
build-docker-image:
if: github.event.inputs.with-docker-images == 'true'
name: Build ${{ matrix.platform }}
+4 -1
View File
@@ -168,13 +168,14 @@ When adding new UI text, please try to adhere to the following guidelines:
| `development` | Default branch. All PRs target here. Granular commit history. |
| `main` | Stable release branch. Updated via squash-merge PRs from `development`. |
Maintainers should promote `development` to `main` through the release PR workflow. After the release PR is approved, automation will squash-merge it into `main`, sync the branches, and run `Release 4 - Build Main`.
Maintainers should promote `development` to `main` through the release PR workflow. After the release PR is approved, automation will squash-merge it into `main`, sync the branches, and run `Release 4 - Build Main`. After `Release 5 - Publish` creates the release commit on `main`, automation runs a second sync-back so `development` does not stay behind `main`.
```bash
./release.sh prepare-pr
# Approve the release PR to trigger Release 2 and Release 2.5
# Wait for the PR summary comment confirming merge, sync-back, and Build Main
REF=main ./release.sh release
# Release 5 finishes with a post-publish sync-back into development
```
If branch sync needs to be rerun manually, use:
@@ -182,6 +183,8 @@ If branch sync needs to be rerun manually, use:
```bash
./release.sh sync-back --dry-run
./release.sh sync-back
./release.sh sync-back --post-release --dry-run
./release.sh sync-back --post-release
```
The workflow-only fallback is [release_3_sync_back.yml](.github/workflows/release_3_sync_back.yml).
+22 -7
View File
@@ -8,7 +8,7 @@ usage() {
cat <<'EOF'
Usage:
./release.sh prepare-pr
./release.sh sync-back [--dry-run]
./release.sh sync-back [--post-release] [--dry-run]
./release.sh release [--dry-run] [--with-docker-images]
Environment:
@@ -28,12 +28,27 @@ run_prepare_pr() {
}
run_sync_back() {
if [[ "${1:-}" == "--dry-run" ]]; then
gh workflow run release_3_sync_back.yml --ref "$REF" -f dry_run=true
return
fi
local mode="reconcile"
local dry_run="false"
gh workflow run release_3_sync_back.yml --ref "$REF"
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
dry_run="true"
;;
--post-release)
mode="post-release"
;;
*)
usage >&2
echo "Error: unknown sync-back option: $1" >&2
exit 1
;;
esac
shift
done
gh workflow run release_3_sync_back.yml --ref "$REF" -f mode="$mode" -f dry_run="$dry_run"
}
run_release() {
@@ -69,7 +84,7 @@ main() {
;;
sync-back)
shift
run_sync_back "${1:-}"
run_sync_back "$@"
;;
release)
shift