Skip to content

Commit da9aebb

Browse files
Add script and workflow to sync upstream OTel code
- sync-upstream-with-rebase.sh: merge upstream via git merge - sync-upstream.yml: workflow_dispatch to run sync and create PR - util-genai: upstream version mapping scripts and README table Change-Id: I43242f6866d8ba6c2ea50f95737f1bf243890efa Co-developed-by: Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 043b0a9 commit da9aebb

7 files changed

Lines changed: 684 additions & 3 deletions

File tree

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Sync upstream changes into a local branch via "git merge" (not rebase).
4+
#
5+
# Why merge instead of rebase?
6+
# - After merge, the sync branch is a direct descendant of the local base
7+
# branch, so merging the sync branch back into main is always clean.
8+
# - Upstream commits are preserved in the merge history; next time we run
9+
# "git merge upstream/main", Git automatically skips already-merged commits.
10+
# - Conflicts only need to be resolved once (during the merge), not twice
11+
# (once during rebase, once when merging back).
12+
13+
set -euo pipefail
14+
15+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
16+
REPO_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd)
17+
18+
UPSTREAM_REMOTE="upstream"
19+
UPSTREAM_URL=""
20+
UPSTREAM_BRANCH="main"
21+
UPSTREAM_COMMIT="" # if set, merge this commit instead of upstream/branch tip
22+
BASE_BRANCH="main"
23+
SYNC_BRANCH=""
24+
RESUME=false
25+
SKIP_PR=false
26+
SKIP_MAPPING_UPDATE=false
27+
28+
MAPPING_FILE="$REPO_ROOT/util/opentelemetry-util-genai/upstream_version_map.json"
29+
VERSION_FILE="$REPO_ROOT/util/opentelemetry-util-genai/src/opentelemetry/util/genai/version.py"
30+
UPDATE_MAPPING_SCRIPT="$REPO_ROOT/util/opentelemetry-util-genai/scripts/update_upstream_version_map.py"
31+
GENERATE_TABLE_SCRIPT="$REPO_ROOT/util/opentelemetry-util-genai/scripts/generate_version_mapping_table.py"
32+
README_FILE="$REPO_ROOT/util/opentelemetry-util-genai/README-loongsuite.rst"
33+
34+
usage() {
35+
cat <<'EOF'
36+
Usage:
37+
.github/scripts/sync-upstream-with-rebase.sh [options]
38+
39+
Options:
40+
--upstream-remote <name> Upstream git remote name (default: upstream)
41+
--upstream-url <url> Upstream remote URL (required when remote does not exist)
42+
--upstream-branch <name> Upstream branch name (default: main)
43+
--upstream-commit <sha> Sync to this specific commit instead of branch tip
44+
--base-branch <name> Local base branch name (default: main)
45+
--sync-branch <name> Sync working branch (default: auto-generated)
46+
--resume Continue after manual conflict resolution
47+
--skip-pr Do not create pull request automatically
48+
--skip-mapping-update Do not update util-genai upstream mapping files
49+
-h, --help Show this help message
50+
51+
Typical flow:
52+
1) Start sync:
53+
.github/scripts/sync-upstream-with-rebase.sh --upstream-url <url>
54+
55+
2) Sync to a specific upstream commit:
56+
.github/scripts/sync-upstream-with-rebase.sh --upstream-url <url> \\
57+
--upstream-commit <sha-or-ref>
58+
59+
3) If conflict occurs, resolve conflicts, then:
60+
git add <resolved-files>
61+
git commit # finish the merge commit
62+
.github/scripts/sync-upstream-with-rebase.sh --resume --sync-branch <branch>
63+
(or /tmp/sync-upstream-with-rebase.sh if script not on current branch)
64+
EOF
65+
}
66+
67+
while [[ $# -gt 0 ]]; do
68+
case "$1" in
69+
--upstream-remote)
70+
UPSTREAM_REMOTE="$2"
71+
shift 2
72+
;;
73+
--upstream-url)
74+
UPSTREAM_URL="$2"
75+
shift 2
76+
;;
77+
--upstream-branch)
78+
UPSTREAM_BRANCH="$2"
79+
shift 2
80+
;;
81+
--upstream-commit)
82+
UPSTREAM_COMMIT="$2"
83+
shift 2
84+
;;
85+
--base-branch)
86+
BASE_BRANCH="$2"
87+
shift 2
88+
;;
89+
--sync-branch)
90+
SYNC_BRANCH="$2"
91+
shift 2
92+
;;
93+
--resume)
94+
RESUME=true
95+
shift
96+
;;
97+
--skip-pr)
98+
SKIP_PR=true
99+
shift
100+
;;
101+
--skip-mapping-update)
102+
SKIP_MAPPING_UPDATE=true
103+
shift
104+
;;
105+
-h|--help)
106+
usage
107+
exit 0
108+
;;
109+
*)
110+
echo "Unknown option: $1"
111+
usage
112+
exit 1
113+
;;
114+
esac
115+
done
116+
117+
cd "$REPO_ROOT"
118+
119+
require_command() {
120+
if ! command -v "$1" >/dev/null 2>&1; then
121+
echo "Missing required command: $1"
122+
exit 1
123+
fi
124+
}
125+
126+
is_merge_in_progress() {
127+
[[ -f .git/MERGE_HEAD ]]
128+
}
129+
130+
ensure_clean_worktree() {
131+
if [[ -n $(git status --porcelain) ]]; then
132+
echo "Worktree is not clean. Please commit or stash changes first."
133+
exit 1
134+
fi
135+
}
136+
137+
ensure_remote() {
138+
if git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then
139+
return
140+
fi
141+
if [[ -z "$UPSTREAM_URL" ]]; then
142+
echo "Remote '$UPSTREAM_REMOTE' not found. Please provide --upstream-url."
143+
exit 1
144+
fi
145+
git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL"
146+
}
147+
148+
# Resolved ref to merge (either a specific commit or branch tip)
149+
get_upstream_target() {
150+
if [[ -n "$UPSTREAM_COMMIT" ]]; then
151+
git rev-parse --verify "$UPSTREAM_COMMIT"
152+
else
153+
echo "${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}"
154+
fi
155+
}
156+
157+
extract_upstream_version() {
158+
local target
159+
target=$(get_upstream_target)
160+
git show "${target}:util/opentelemetry-util-genai/src/opentelemetry/util/genai/version.py" \
161+
| python3 -c 'import re,sys; m=re.search(r"__version__\s*=\s*\"([^\"]+)\"", sys.stdin.read()); print(m.group(1) if m else "unknown")'
162+
}
163+
164+
update_mapping_and_readme() {
165+
local target upstream_commit upstream_version ref_type ref_val
166+
target=$(get_upstream_target)
167+
upstream_commit=$(git rev-parse "$target")
168+
upstream_version=$(extract_upstream_version)
169+
170+
if [[ -n "$UPSTREAM_COMMIT" ]]; then
171+
ref_type="commit"
172+
ref_val="$upstream_commit"
173+
else
174+
ref_type="branch"
175+
ref_val="$UPSTREAM_BRANCH"
176+
fi
177+
178+
python3 "$UPDATE_MAPPING_SCRIPT" \
179+
--mapping-file "$MAPPING_FILE" \
180+
--version-file "$VERSION_FILE" \
181+
--upstream-version "$upstream_version" \
182+
--upstream-commit "$upstream_commit" \
183+
--upstream-ref-type "$ref_type" \
184+
--upstream-ref "$ref_val"
185+
186+
python3 "$GENERATE_TABLE_SCRIPT" --mapping "$MAPPING_FILE" --readme "$README_FILE"
187+
188+
if ! git diff --quiet -- "$MAPPING_FILE" "$README_FILE"; then
189+
git add "$MAPPING_FILE" "$README_FILE"
190+
git commit -m "chore(util-genai): update upstream mapping after sync"
191+
fi
192+
}
193+
194+
push_branch() {
195+
git push -u origin "$SYNC_BRANCH"
196+
}
197+
198+
create_pr() {
199+
if [[ "$SKIP_PR" == true ]]; then
200+
return
201+
fi
202+
require_command gh
203+
local title upstream_desc
204+
upstream_desc="${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}"
205+
if [[ -n "$UPSTREAM_COMMIT" ]]; then
206+
upstream_desc="commit $(git rev-parse --short "$UPSTREAM_COMMIT") (from ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH})"
207+
fi
208+
title="chore: sync ${upstream_desc} into ${BASE_BRANCH}"
209+
local body_file
210+
body_file=$(mktemp)
211+
trap 'rm -f "$body_file"' EXIT
212+
cat <<EOF >"$body_file"
213+
## Summary
214+
- Merge upstream \`${upstream_desc}\` into \`${BASE_BRANCH}\`
215+
- Preserve upstream commit history for incremental future syncs
216+
- Auto-update util-genai upstream version mapping
217+
218+
## Notes
219+
- This PR can be merged into \`${BASE_BRANCH}\` without conflicts (conflicts were resolved during the upstream merge)
220+
- Use **merge commit** (not squash) to preserve upstream commit granularity
221+
EOF
222+
223+
if gh pr view "$SYNC_BRANCH" >/dev/null 2>&1; then
224+
echo "PR for branch $SYNC_BRANCH already exists, skipping creation."
225+
return
226+
fi
227+
228+
gh pr create --base "$BASE_BRANCH" --head "$SYNC_BRANCH" --title "$title" --body-file "$body_file"
229+
}
230+
231+
# ── Main ──────────────────────────────────────────────────────────────
232+
233+
require_command git
234+
require_command python3
235+
236+
ensure_remote
237+
git fetch origin "$BASE_BRANCH"
238+
git fetch "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH"
239+
# When --upstream-commit is set, ensure we have that commit (might need full fetch)
240+
if [[ -n "$UPSTREAM_COMMIT" ]]; then
241+
if ! git rev-parse --verify "$UPSTREAM_COMMIT" >/dev/null 2>&1; then
242+
echo "Commit $UPSTREAM_COMMIT not found. Fetching all upstream refs..."
243+
git fetch "$UPSTREAM_REMOTE"
244+
fi
245+
if ! git rev-parse --verify "$UPSTREAM_COMMIT" >/dev/null 2>&1; then
246+
echo "Error: commit $UPSTREAM_COMMIT does not exist in $UPSTREAM_REMOTE."
247+
exit 1
248+
fi
249+
fi
250+
251+
if [[ "$RESUME" == true ]]; then
252+
# ── Resume after manual conflict resolution ──
253+
if [[ -z "$SYNC_BRANCH" ]]; then
254+
SYNC_BRANCH=$(git branch --show-current)
255+
fi
256+
git checkout "$SYNC_BRANCH"
257+
258+
if is_merge_in_progress; then
259+
echo "Merge is still in progress (MERGE_HEAD exists)."
260+
echo "Please finish the merge commit first:"
261+
echo " git add <resolved-files>"
262+
echo " git commit"
263+
echo "Then re-run:"
264+
echo " .github/scripts/sync-upstream-with-rebase.sh --resume --sync-branch $SYNC_BRANCH"
265+
exit 2
266+
fi
267+
268+
echo "Merge completed. Proceeding to finalize."
269+
270+
else
271+
# ── Start a new sync ──
272+
ensure_clean_worktree
273+
274+
if [[ -z "$SYNC_BRANCH" ]]; then
275+
SYNC_BRANCH="sync/upstream-$(date -u +%Y%m%d-%H%M%S)"
276+
fi
277+
278+
echo "Creating sync branch: $SYNC_BRANCH (from origin/$BASE_BRANCH)"
279+
git checkout -B "$SYNC_BRANCH" "origin/$BASE_BRANCH"
280+
281+
UPSTREAM_TARGET=$(get_upstream_target)
282+
if [[ -n "$UPSTREAM_COMMIT" ]]; then
283+
echo "Merging upstream commit $(git rev-parse --short "$UPSTREAM_TARGET") into $SYNC_BRANCH ..."
284+
else
285+
echo "Merging ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH} into $SYNC_BRANCH ..."
286+
fi
287+
if ! git merge "$UPSTREAM_TARGET" \
288+
--no-edit \
289+
-m "Merge upstream $(git rev-parse --short "$UPSTREAM_TARGET") into ${SYNC_BRANCH}"; then
290+
291+
echo ""
292+
echo "════════════════════════════════════════════════════════════"
293+
echo " Merge conflict detected."
294+
echo ""
295+
echo " Please resolve conflicts manually:"
296+
echo " 1) Fix conflicting files"
297+
echo " 2) git add <resolved-files>"
298+
echo " 3) git commit # finishes the merge commit"
299+
echo " 4) Re-run this script:"
300+
echo " .github/scripts/sync-upstream-with-rebase.sh \\"
301+
echo " --resume --sync-branch $SYNC_BRANCH"
302+
echo "════════════════════════════════════════════════════════════"
303+
exit 2
304+
fi
305+
306+
echo "Merge completed without conflicts."
307+
fi
308+
309+
# ── Finalize: update mapping, push, create PR ──
310+
311+
if [[ "$SKIP_MAPPING_UPDATE" == false ]]; then
312+
update_mapping_and_readme
313+
fi
314+
315+
push_branch
316+
create_pr
317+
318+
echo ""
319+
echo "Done. Sync branch: $SYNC_BRANCH"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Sync upstream with rebase
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
upstream_url:
7+
description: "Upstream repository URL"
8+
required: true
9+
default: "https://github.com/open-telemetry/opentelemetry-python-contrib.git"
10+
upstream_branch:
11+
description: "Upstream branch to sync from"
12+
required: true
13+
default: "main"
14+
base_branch:
15+
description: "Target base branch in this repo"
16+
required: true
17+
default: "main"
18+
skip_pr:
19+
description: "Skip creating pull request"
20+
required: false
21+
default: false
22+
type: boolean
23+
skip_mapping_update:
24+
description: "Skip util-genai mapping update"
25+
required: false
26+
default: false
27+
type: boolean
28+
29+
permissions:
30+
contents: write
31+
pull-requests: write
32+
33+
jobs:
34+
sync:
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
with:
39+
fetch-depth: 0
40+
41+
- name: Configure git identity
42+
run: |
43+
git config user.name "github-actions[bot]"
44+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
45+
46+
- name: Run upstream sync
47+
env:
48+
SKIP_PR: ${{ inputs.skip_pr }}
49+
SKIP_MAPPING_UPDATE: ${{ inputs.skip_mapping_update }}
50+
GH_TOKEN: ${{ github.token }}
51+
run: |
52+
chmod +x .github/scripts/sync-upstream-with-rebase.sh
53+
cmd=".github/scripts/sync-upstream-with-rebase.sh \
54+
--upstream-url ${{ inputs.upstream_url }} \
55+
--upstream-branch ${{ inputs.upstream_branch }} \
56+
--base-branch ${{ inputs.base_branch }} \
57+
--sync-branch sync/upstream-${{ github.run_id }}"
58+
59+
if [[ "$SKIP_PR" == "true" ]]; then
60+
cmd="$cmd --skip-pr"
61+
fi
62+
if [[ "$SKIP_MAPPING_UPDATE" == "true" ]]; then
63+
cmd="$cmd --skip-mapping-update"
64+
fi
65+
66+
eval "$cmd"

0 commit comments

Comments
 (0)