Skip to content

Commit cbbe863

Browse files
chore: Add script and workflow to sync upstream OTel project (#122)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 043b0a9 commit cbbe863

File tree

3 files changed

+353
-1
lines changed

3 files changed

+353
-1
lines changed

.github/scripts/sync-upstream.sh

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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+
DRY_RUN=false
27+
28+
usage() {
29+
cat <<'EOF'
30+
Usage:
31+
.github/scripts/sync-upstream.sh [options]
32+
33+
Options:
34+
--upstream-remote <name> Upstream git remote name (default: upstream)
35+
--upstream-url <url> Upstream remote URL (required when remote does not exist)
36+
--upstream-branch <name> Upstream branch name (default: main)
37+
--upstream-commit <sha> Sync to this specific commit instead of branch tip
38+
--base-branch <name> Local base branch name (default: main)
39+
--sync-branch <name> Sync working branch (default: auto-generated)
40+
--resume Continue after manual conflict resolution
41+
--skip-pr Do not create pull request automatically
42+
--dry-run Do everything except push and create PR (local validation).
43+
Uses current branch as base so script stays available.
44+
-h, --help Show this help message
45+
46+
Typical flow:
47+
1) Start sync:
48+
.github/scripts/sync-upstream.sh --upstream-url <url>
49+
50+
2) Sync to a specific upstream commit:
51+
.github/scripts/sync-upstream.sh --upstream-url <url> \\
52+
--upstream-commit <sha-or-ref>
53+
54+
3) If conflict occurs, resolve conflicts, then:
55+
git add <resolved-files>
56+
git commit # finish the merge commit
57+
.github/scripts/sync-upstream.sh --resume --sync-branch <branch>
58+
(or /tmp/sync-upstream.sh if script not on current branch)
59+
EOF
60+
}
61+
62+
while [[ $# -gt 0 ]]; do
63+
case "$1" in
64+
--upstream-remote)
65+
UPSTREAM_REMOTE="$2"
66+
shift 2
67+
;;
68+
--upstream-url)
69+
UPSTREAM_URL="$2"
70+
shift 2
71+
;;
72+
--upstream-branch)
73+
UPSTREAM_BRANCH="$2"
74+
shift 2
75+
;;
76+
--upstream-commit)
77+
UPSTREAM_COMMIT="$2"
78+
shift 2
79+
;;
80+
--base-branch)
81+
BASE_BRANCH="$2"
82+
shift 2
83+
;;
84+
--sync-branch)
85+
SYNC_BRANCH="$2"
86+
shift 2
87+
;;
88+
--resume)
89+
RESUME=true
90+
shift
91+
;;
92+
--skip-pr)
93+
SKIP_PR=true
94+
shift
95+
;;
96+
--dry-run)
97+
DRY_RUN=true
98+
shift
99+
;;
100+
-h|--help)
101+
usage
102+
exit 0
103+
;;
104+
*)
105+
echo "Unknown option: $1"
106+
usage
107+
exit 1
108+
;;
109+
esac
110+
done
111+
112+
cd "$REPO_ROOT"
113+
114+
require_command() {
115+
if ! command -v "$1" >/dev/null 2>&1; then
116+
echo "Missing required command: $1"
117+
exit 1
118+
fi
119+
}
120+
121+
is_merge_in_progress() {
122+
[[ -f .git/MERGE_HEAD ]]
123+
}
124+
125+
ensure_clean_worktree() {
126+
if [[ -n $(git status --porcelain) ]]; then
127+
echo "Worktree is not clean. Please commit or stash changes first."
128+
exit 1
129+
fi
130+
}
131+
132+
ensure_remote() {
133+
if git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then
134+
return
135+
fi
136+
if [[ -z "$UPSTREAM_URL" ]]; then
137+
echo "Remote '$UPSTREAM_REMOTE' not found. Please provide --upstream-url."
138+
exit 1
139+
fi
140+
git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL"
141+
}
142+
143+
# Resolved ref to merge (either a specific commit or branch tip)
144+
get_upstream_target() {
145+
if [[ -n "$UPSTREAM_COMMIT" ]]; then
146+
git rev-parse --verify "$UPSTREAM_COMMIT"
147+
else
148+
echo "${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}"
149+
fi
150+
}
151+
152+
push_branch() {
153+
if [[ "$DRY_RUN" == true ]]; then
154+
echo "[DRY-RUN] Would push branch: $SYNC_BRANCH -> origin"
155+
return
156+
fi
157+
git push -u origin "$SYNC_BRANCH"
158+
}
159+
160+
create_pr() {
161+
if [[ "$SKIP_PR" == true ]] || [[ "$DRY_RUN" == true ]]; then
162+
if [[ "$DRY_RUN" == true ]]; then
163+
echo "[DRY-RUN] Would create PR for branch: $SYNC_BRANCH -> $BASE_BRANCH"
164+
fi
165+
return
166+
fi
167+
require_command gh
168+
local title upstream_desc
169+
upstream_desc="${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}"
170+
if [[ -n "$UPSTREAM_COMMIT" ]]; then
171+
upstream_desc="commit $(git rev-parse --short "$UPSTREAM_COMMIT") (from ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH})"
172+
fi
173+
title="chore: sync ${upstream_desc} into ${BASE_BRANCH}"
174+
local body_file
175+
body_file=$(mktemp)
176+
trap 'rm -f "$body_file"' EXIT
177+
cat <<EOF >"$body_file"
178+
## Summary
179+
- Merge upstream \`${upstream_desc}\` into \`${BASE_BRANCH}\`
180+
- Preserve upstream commit history for incremental future syncs
181+
182+
## Notes
183+
- This PR can be merged into \`${BASE_BRANCH}\` without conflicts (conflicts were resolved during the upstream merge)
184+
- Use **merge commit** (not squash) to preserve upstream commit granularity
185+
EOF
186+
187+
if gh pr view "$SYNC_BRANCH" >/dev/null 2>&1; then
188+
echo "PR for branch $SYNC_BRANCH already exists, skipping creation."
189+
return
190+
fi
191+
192+
gh pr create --base "$BASE_BRANCH" --head "$SYNC_BRANCH" --title "$title" --body-file "$body_file"
193+
}
194+
195+
# ── Main ──────────────────────────────────────────────────────────────
196+
197+
require_command git
198+
199+
if [[ "$DRY_RUN" == true ]]; then
200+
echo "=== DRY-RUN mode: will not push or create PR ==="
201+
fi
202+
203+
ensure_remote
204+
git fetch origin "$BASE_BRANCH"
205+
git fetch "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH"
206+
# When --upstream-commit is set, ensure we have that commit (might need full fetch)
207+
if [[ -n "$UPSTREAM_COMMIT" ]]; then
208+
if ! git rev-parse --verify "$UPSTREAM_COMMIT" >/dev/null 2>&1; then
209+
echo "Commit $UPSTREAM_COMMIT not found. Fetching all upstream refs..."
210+
git fetch "$UPSTREAM_REMOTE"
211+
fi
212+
if ! git rev-parse --verify "$UPSTREAM_COMMIT" >/dev/null 2>&1; then
213+
echo "Error: commit $UPSTREAM_COMMIT does not exist in $UPSTREAM_REMOTE."
214+
exit 1
215+
fi
216+
fi
217+
218+
if [[ "$RESUME" == true ]]; then
219+
# ── Resume after manual conflict resolution ──
220+
if [[ -z "$SYNC_BRANCH" ]]; then
221+
SYNC_BRANCH=$(git branch --show-current)
222+
fi
223+
git checkout "$SYNC_BRANCH"
224+
225+
if is_merge_in_progress; then
226+
echo "Merge is still in progress (MERGE_HEAD exists)."
227+
echo "Please finish the merge commit first:"
228+
echo " git add <resolved-files>"
229+
echo " git commit"
230+
echo "Then re-run:"
231+
echo " .github/scripts/sync-upstream.sh --resume --sync-branch $SYNC_BRANCH"
232+
exit 2
233+
fi
234+
235+
echo "Merge completed. Proceeding to finalize."
236+
237+
else
238+
# ── Start a new sync ──
239+
ensure_clean_worktree
240+
241+
ORIG_BRANCH=$(git branch --show-current) # save for dry-run cleanup hint
242+
if [[ -z "$SYNC_BRANCH" ]]; then
243+
SYNC_BRANCH="sync/upstream-$(date -u +%Y%m%d-%H%M%S)"
244+
fi
245+
246+
# Dry-run: use current branch as base so sync script stays available
247+
if [[ "$DRY_RUN" == true ]]; then
248+
BASE_REF="HEAD"
249+
echo "Creating sync branch: $SYNC_BRANCH (from current branch $ORIG_BRANCH)"
250+
else
251+
BASE_REF="origin/$BASE_BRANCH"
252+
echo "Creating sync branch: $SYNC_BRANCH (from origin/$BASE_BRANCH)"
253+
fi
254+
git checkout -B "$SYNC_BRANCH" "$BASE_REF"
255+
256+
UPSTREAM_TARGET=$(get_upstream_target)
257+
if [[ -n "$UPSTREAM_COMMIT" ]]; then
258+
echo "Merging upstream commit $(git rev-parse --short "$UPSTREAM_TARGET") into $SYNC_BRANCH ..."
259+
else
260+
echo "Merging ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH} into $SYNC_BRANCH ..."
261+
fi
262+
if ! git merge "$UPSTREAM_TARGET" \
263+
--no-edit \
264+
-m "Merge upstream $(git rev-parse --short "$UPSTREAM_TARGET") into ${SYNC_BRANCH}"; then
265+
266+
echo ""
267+
echo "════════════════════════════════════════════════════════════"
268+
echo " Merge conflict detected."
269+
echo ""
270+
echo " Please resolve conflicts manually:"
271+
echo " 1) Fix conflicting files"
272+
echo " 2) git add <resolved-files>"
273+
echo " 3) git commit # finishes the merge commit"
274+
echo " 4) Re-run this script:"
275+
echo " .github/scripts/sync-upstream.sh \\"
276+
echo " --resume --sync-branch $SYNC_BRANCH"
277+
echo "════════════════════════════════════════════════════════════"
278+
exit 2
279+
fi
280+
281+
echo "Merge completed without conflicts."
282+
fi
283+
284+
# ── Finalize: push, create PR ──
285+
286+
push_branch
287+
create_pr
288+
289+
echo ""
290+
if [[ "$DRY_RUN" == true ]]; then
291+
echo "DRY-RUN complete. Sync branch exists locally: $SYNC_BRANCH"
292+
echo "To discard: git checkout ${ORIG_BRANCH:-$BASE_BRANCH} && git branch -D $SYNC_BRANCH"
293+
echo "To push manually: git push -u origin $SYNC_BRANCH"
294+
else
295+
echo "Done. Sync branch: $SYNC_BRANCH"
296+
fi
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Sync upstream
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+
24+
permissions:
25+
contents: write
26+
pull-requests: write
27+
28+
jobs:
29+
sync:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
with:
34+
fetch-depth: 0
35+
36+
- name: Configure git identity
37+
run: |
38+
git config user.name "github-actions[bot]"
39+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
40+
41+
- name: Run upstream sync
42+
env:
43+
SKIP_PR: ${{ inputs.skip_pr }}
44+
GH_TOKEN: ${{ github.token }}
45+
run: |
46+
chmod +x .github/scripts/sync-upstream.sh
47+
cmd=".github/scripts/sync-upstream.sh \
48+
--upstream-url ${{ inputs.upstream_url }} \
49+
--upstream-branch ${{ inputs.upstream_branch }} \
50+
--base-branch ${{ inputs.base_branch }} \
51+
--sync-branch sync/upstream-${{ github.run_id }}"
52+
53+
if [[ "$SKIP_PR" == "true" ]]; then
54+
cmd="$cmd --skip-pr"
55+
fi
56+
57+
eval "$cmd"

util/opentelemetry-util-genai/README-loongsuite.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,4 +622,3 @@ OpenTelemetry GenAI Utils 的设计文档: `Design Document <https://docs.google
622622
* `OpenTelemetry Project <https://opentelemetry.io/>`_
623623
* `OpenTelemetry GenAI Semantic Conventions <https://opentelemetry.io/docs/specs/semconv/gen-ai/>`_
624624
* `LoongSuite OpenTelemetry Python Agent <https://github.com/loongsuite/loongsuite-python-agent>`_
625-

0 commit comments

Comments
 (0)