Files
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> f1973ddb36 chore: update current ZStack target to v2.2.3
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-04 15:11:09 +08:00

234 lines
8.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# sync-upstream: move both forks onto a new upstream release.
#
# For each repo it fetches upstream, cuts a fresh v<version>-lofyer branch from
# the target tag, and replays our fork-only commits onto it. Conflicts are left
# in the worktree for you to resolve (upstream frequently reflows docs or
# deletes files we keep), then re-run with --continue.
#
# Usage:
# scripts/sync-upstream --list # show available upstream versions
# scripts/sync-upstream v2.2.3 # bump both forks to v2.2.3
# scripts/sync-upstream v2.2.3 --dry-run # show the plan only
# scripts/sync-upstream --continue # resume after resolving conflicts
#
# Notes:
# - Refuses to run with dirty worktrees (except untracked files, which are
# yours and are left alone).
# - The tag must exist in BOTH upstreams, so frontend and backend never end up
# on different releases.
# - Nothing is pushed. Review, then push each repo yourself.
#
set -o errexit
set -o nounset
set -o pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
TARGET=""
LIST=0
DRY_RUN=0
CONTINUE=0
while [[ $# -gt 0 ]]; do
case "${1}" in
--list) LIST=1 ;;
--dry-run) DRY_RUN=1 ;;
--continue) CONTINUE=1 ;;
-h|--help) sed -n '2,24p' "${0}"; exit 0 ;;
v*) TARGET="${1}" ;;
*) fail "unknown argument: ${1}" ;;
esac
shift
done
require_cmd git
require_repos
# Ensure an 'upstream' remote exists and points at GitHub.
ensure_upstream_remote() {
local repo="${1}" url="${2}"
if git -C "${repo}" remote get-url upstream >/dev/null 2>&1; then
local current
current="$(git -C "${repo}" remote get-url upstream)"
if [[ "${current}" != "${url}" ]]; then
log " updating upstream remote: ${current} -> ${url}"
git -C "${repo}" remote set-url upstream "${url}"
fi
else
log " adding upstream remote -> ${url}"
git -C "${repo}" remote add upstream "${url}"
fi
}
fetch_upstream() {
local repo="${1}" url="${2}" name="${3}"
log "fetching upstream for ${name}"
ensure_upstream_remote "${repo}" "${url}"
git -C "${repo}" fetch --tags --prune upstream >/dev/null 2>&1
}
# Stable release tags only (drop rc/alpha/beta), newest last.
list_versions() {
git -C "${1}" tag --list 'v*' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V
}
# --- resume path -------------------------------------------------------------
if [[ "${CONTINUE}" == "1" ]]; then
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
name="${spec%%:*}"; repo="${spec#*:}"
if [[ -d "${repo}/.git/sequencer" ]] || [[ -f "${repo}/.git/CHERRY_PICK_HEAD" ]]; then
step "Resuming cherry-pick in ${name}"
if git -C "${repo}" diff --name-only --diff-filter=U | grep -q .; then
git -C "${repo}" diff --name-only --diff-filter=U | sed 's/^/ conflict: /'
fail "${name} still has unresolved conflicts; resolve and 'git add' them first"
fi
GIT_EDITOR=true git -C "${repo}" cherry-pick --continue || true
# The repo's lint hooks can stash/restore and upset the sequencer; if the
# commit landed, clear the leftover state so the next pick can proceed.
if [[ -d "${repo}/.git/sequencer" ]] && ! git -C "${repo}" diff --name-only --diff-filter=U | grep -q .; then
git -C "${repo}" cherry-pick --quit 2>/dev/null || true
fi
log "${name}: resumed; re-run sync-upstream with the target version to replay remaining commits"
fi
done
exit 0
fi
# --- list path ---------------------------------------------------------------
fetch_upstream "${UI_REPO}" "${UI_UPSTREAM_URL}" "frontend"
fetch_upstream "${BACKEND_REPO}" "${BACKEND_UPSTREAM_URL}" "backend"
if [[ "${LIST}" == "1" ]]; then
step "Upstream stable releases present in BOTH repos"
comm -12 <(list_versions "${UI_REPO}") <(list_versions "${BACKEND_REPO}") | tail -15
step "Current"
log "frontend: $(repo_branch "${UI_REPO}")"
log "backend : $(repo_branch "${BACKEND_REPO}")"
exit 0
fi
[[ -n "${TARGET}" ]] || fail "no target version given (try --list)"
# --- preflight ---------------------------------------------------------------
step "Preflight"
CURRENT_VERSION="$(resolve_workspace_version)"
log "current version: ${CURRENT_VERSION}"
log "target version : ${TARGET}"
[[ "${TARGET}" != "${CURRENT_VERSION}" ]] || fail "already on ${TARGET}"
NEW_BRANCH="${TARGET}-lofyer"
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
name="${spec%%:*}"; repo="${spec#*:}"
git -C "${repo}" rev-parse -q --verify "refs/tags/${TARGET}" >/dev/null \
|| fail "${name}: tag ${TARGET} not found upstream"
# Tracked modifications block a clean branch cut; untracked files are the
# user's own work and are preserved across checkout.
if ! git -C "${repo}" diff-index --quiet HEAD -- 2>/dev/null; then
git -C "${repo}" status --short | grep -v '^??' | sed 's/^/ /'
fail "${name}: worktree has uncommitted changes; commit or stash them first"
fi
git -C "${repo}" rev-parse -q --verify "refs/heads/${NEW_BRANCH}" >/dev/null \
&& fail "${name}: branch ${NEW_BRANCH} already exists"
done
log "both repos are clean and ${TARGET} exists in both upstreams"
# Fork-only commits = on our branch but not in the new upstream tag, oldest first.
# Fork-only commits: on our branch, not reachable from the new tag, and not
# present on any upstream ref.
#
# `--cherry-pick --right-only` alone is not enough: upstream commits that were
# rebased or squashed into the new release get a different patch-id and would be
# replayed again. So we additionally drop anything that any upstream branch or
# tag contains, which is what actually distinguishes our work from upstream's.
fork_commits() {
local repo="${1}" old_branch="${2}" c
while read -r c; do
[[ -n "${c}" ]] || continue
# Skip commits reachable from any upstream remote branch or upstream tag.
if git -C "${repo}" branch -r --contains "${c}" 2>/dev/null | grep -q '^\s*upstream/'; then
continue
fi
if git -C "${repo}" tag --contains "${c}" 2>/dev/null | grep -q .; then
continue
fi
echo "${c}"
done < <(git -C "${repo}" log --reverse --no-merges --format='%H' \
"${TARGET}..${old_branch}" --cherry-pick --right-only)
}
# --- plan --------------------------------------------------------------------
step "Plan"
declare -A PICKS
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
name="${spec%%:*}"; repo="${spec#*:}"
old_branch="$(repo_branch "${repo}")"
mapfile -t commits < <(fork_commits "${repo}" "${old_branch}")
PICKS["${name}"]="${commits[*]:-}"
log "${name}: ${old_branch} -> ${NEW_BRANCH}, replaying ${#commits[@]} fork commit(s)"
for c in "${commits[@]:-}"; do
[[ -n "${c}" ]] && echo " $(git -C "${repo}" log --format='%h %s' -1 "${c}")"
done
done
if [[ "${DRY_RUN}" == "1" ]]; then
step "Dry run"
log "no changes made"
exit 0
fi
# --- execute -----------------------------------------------------------------
for spec in "frontend:${UI_REPO}" "backend:${BACKEND_REPO}"; do
name="${spec%%:*}"; repo="${spec#*:}"
step "Updating ${name}"
git -C "${repo}" checkout -b "${NEW_BRANCH}" "${TARGET}"
log " cut ${NEW_BRANCH} from ${TARGET}"
read -r -a commits <<< "${PICKS[${name}]}"
for c in "${commits[@]:-}"; do
[[ -n "${c}" ]] || continue
subject="$(git -C "${repo}" log --format='%h %s' -1 "${c}")"
if git -C "${repo}" cherry-pick "${c}" >/dev/null 2>&1; then
log " picked ${subject}"
else
# A lint/pre-commit hook may have committed successfully while still
# returning non-zero; treat "no conflicts left" as success.
if git -C "${repo}" diff --name-only --diff-filter=U | grep -q .; then
warn " CONFLICT while picking ${subject}"
git -C "${repo}" diff --name-only --diff-filter=U | sed 's/^/ /'
cat >&2 <<EOF
Resolve the conflict in ${repo}, then:
git -C ${repo} add <files>
$(dirname "${BASH_SOURCE[0]}")/sync-upstream --continue
$(dirname "${BASH_SOURCE[0]}")/sync-upstream ${TARGET} # replay the rest
EOF
exit 1
fi
git -C "${repo}" cherry-pick --quit 2>/dev/null || true
log " picked ${subject} (hook noise ignored)"
fi
done
done
step "Done"
log "both forks are on ${NEW_BRANCH}"
cat <<EOF
Next steps:
1. Verify the build: scripts/build-image
2. Review the history: git -C ${UI_REPO} log --oneline ${TARGET}..HEAD
git -C ${BACKEND_REPO} log --oneline ${TARGET}..HEAD
3. Push both repos: git -C ${UI_REPO} push -u origin ${NEW_BRANCH}
git -C ${BACKEND_REPO} push -u origin ${NEW_BRANCH}
4. Update the version table in AGENTS.md
EOF