Skip to content

Commit 9bc3c69

Browse files
author
am-aurora
committed
Add loci-analysis workflow from overlay
1 parent e8323ca commit 9bc3c69

10 files changed

Lines changed: 743 additions & 0 deletions
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(dirname "$0")"
5+
6+
# Fetches and processes upstream PRs, outputting selected PRs to /tmp/pulls.ndjson
7+
#
8+
# Required environment variables:
9+
# UPSTREAM_REPO - upstream repo (e.g., "openssl/openssl")
10+
# UPSTREAM_DEFAULT - upstream default branch name
11+
# GH_TOKEN - GitHub token for API calls
12+
# GITHUB_ACTOR - actor for git config
13+
# GITHUB_OUTPUT - path to output file
14+
#
15+
# Optional environment variables (for scheduled mode):
16+
# UPSTREAM_PR_LOOKBACK_DAYS - how far back to look for PRs
17+
# MAX_UPSTREAM_PRS - max PRs to process
18+
#
19+
# Optional environment variables (for manual mode):
20+
# PR_URL - specific PR URL to mirror
21+
# BASE_SHA - upstream main commit SHA to use as base (overrides merge-base)
22+
23+
git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" 2>/dev/null || true
24+
git fetch upstream "${UPSTREAM_DEFAULT}:refs/remotes/upstream/${UPSTREAM_DEFAULT}"
25+
git fetch origin overlay:refs/remotes/origin/overlay || true
26+
27+
git config user.name "${GITHUB_ACTOR}"
28+
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
29+
30+
# If BASE_SHA is provided without PR_URL, just create the loci/main branch and exit
31+
if [ -n "${BASE_SHA:-}" ] && [ -z "${PR_URL:-}" ]; then
32+
echo "Base SHA mode: creating loci/main branch for ${BASE_SHA}"
33+
if loci_main_branch=$(bash "$SCRIPT_DIR/sync-loci-main.sh" "$BASE_SHA"); then
34+
echo "Branch ${loci_main_branch} already exists and is up-to-date."
35+
else
36+
echo "Created/updated ${loci_main_branch}."
37+
fi
38+
echo "prs_to_sync=no" >> "$GITHUB_OUTPUT"
39+
exit 0
40+
fi
41+
42+
> /tmp/pulls.ndjson
43+
selected_pulls_count=0
44+
manual_mode=0
45+
max_pulls="${MAX_UPSTREAM_PRS:-2}"
46+
47+
if [ -n "${PR_URL:-}" ]; then
48+
manual_mode=1
49+
echo "Manual mode: processing PR from URL: ${PR_URL}"
50+
51+
if [[ ! "$PR_URL" =~ ^https://github.com/([^/]+/[^/]+)/pull/([0-9]+)$ ]]; then
52+
echo "::error::Invalid PR URL format. Expected: https://github.com/owner/repo/pull/123"
53+
exit 1
54+
fi
55+
56+
pr_repo="${BASH_REMATCH[1]}"
57+
manual_pr_num="${BASH_REMATCH[2]}"
58+
59+
if [ "$pr_repo" != "$UPSTREAM_REPO" ]; then
60+
echo "::error::PR repo (${pr_repo}) does not match UPSTREAM_REPO (${UPSTREAM_REPO})"
61+
exit 1
62+
fi
63+
64+
pulls=$(gh api "repos/${UPSTREAM_REPO}/pulls/${manual_pr_num}" | jq -s '.')
65+
else
66+
lookback_days="${UPSTREAM_PR_LOOKBACK_DAYS:-7}"
67+
cutoff=$(date -u -d "${lookback_days} days ago" +%Y-%m-%dT%H:%M:%SZ)
68+
per_page=20
69+
page=1
70+
71+
echo "Searching for ${max_pulls} valid pull requests targeting ${UPSTREAM_DEFAULT}, updated since ${cutoff}."
72+
fi
73+
74+
while true; do
75+
if [ "$manual_mode" -eq 0 ]; then
76+
pulls=$(gh api "repos/${UPSTREAM_REPO}/pulls?state=open&base=${UPSTREAM_DEFAULT}&sort=updated&direction=desc&per_page=${per_page}&page=${page}" 2>/dev/null || echo "[]")
77+
page_pulls_count=$(echo "$pulls" | jq 'length')
78+
79+
if [ "$page_pulls_count" -eq 0 ]; then
80+
echo "Pull requests exhausted on page ${page}. Stopping."
81+
break
82+
fi
83+
echo "Processing page ${page} (${page_pulls_count} pull requests)"
84+
fi
85+
86+
while read -r pr; do
87+
pull_num=$(jq -r '.number' <<<"$pr")
88+
pull_head_sha=$(jq -r '.head.sha' <<<"$pr")
89+
pull_head_ref=$(jq -r '.head.ref' <<<"$pr")
90+
91+
is_draft=$(jq -r '.draft' <<<"$pr")
92+
if [ "$is_draft" = "true" ]; then
93+
if [ "$manual_mode" -eq 1 ]; then
94+
echo "::notice::PR #${pull_num} is a draft PR. Proceeding anyway (manual mode)."
95+
else
96+
echo " PR #${pull_num}: is a draft. Skipping."
97+
continue
98+
fi
99+
fi
100+
101+
# Skip cutoff check in manual mode
102+
if [ "$manual_mode" -eq 0 ]; then
103+
updated_at=$(jq -r '.updated_at' <<<"$pr")
104+
created_at=$(jq -r '.created_at' <<<"$pr")
105+
if [[ "$updated_at" < "$cutoff" && "$created_at" < "$cutoff" ]]; then
106+
continue
107+
fi
108+
fi
109+
110+
# Sanitize branch name: replace / with -, truncate to 50 chars
111+
sanitized_branch=$(echo "${pull_head_ref}" | tr '/' '-' | cut -c1-50)
112+
loci_pr_branch="loci/pr-${pull_num}-${sanitized_branch}"
113+
114+
# Fetch pull request head for merge-base computation
115+
git fetch upstream "refs/pull/${pull_num}/head:refs/remotes/upstream/pr/${pull_num}" 2>/dev/null || \
116+
git fetch upstream "${pull_head_sha}:refs/remotes/upstream/pr/${pull_num}" 2>/dev/null || true
117+
118+
# Determine merge-base: use BASE_SHA if provided, otherwise compute it
119+
if [ -n "${BASE_SHA:-}" ]; then
120+
merge_base="${BASE_SHA}"
121+
echo " PR #${pull_num}: using provided BASE_SHA as merge-base: ${merge_base}"
122+
else
123+
merge_base=$(git merge-base "${pull_head_sha}" "refs/remotes/upstream/${UPSTREAM_DEFAULT}" 2>/dev/null || true)
124+
if [ -z "${merge_base}" ]; then
125+
echo " PR #${pull_num}: could not compute merge-base. Skipping."
126+
if [ "$manual_mode" -eq 1 ]; then
127+
echo "::error::Could not compute merge-base for manually specified PR"
128+
exit 1
129+
fi
130+
continue
131+
fi
132+
echo " PR #${pull_num}: computed merge-base: ${merge_base}"
133+
fi
134+
135+
short_merge_base="${merge_base:0:7}"
136+
137+
# Create or update base branch if needed (must happen before conflict check when using loci base)
138+
if loci_main_branch=$(bash "$SCRIPT_DIR/sync-loci-main.sh" "$merge_base"); then
139+
: # Branch already up-to-date
140+
else
141+
# Branch was created/updated — push pending branch and skip PR creation
142+
if [ "$manual_mode" -eq 0 ]; then
143+
pending_branch="loci/pending-pr-${pull_num}-${sanitized_branch}"
144+
echo " PR #${pull_num}: ${loci_main_branch} just triggered to create/update. Pushing pending branch: ${pending_branch}."
145+
146+
if git show-ref --verify --quiet "refs/remotes/upstream/pr/${pull_num}"; then
147+
git branch --no-track -f "${pending_branch}" "refs/remotes/upstream/pr/${pull_num}"
148+
else
149+
git fetch upstream "refs/pull/${pull_num}/head:refs/heads/${pending_branch}" || \
150+
git fetch upstream "${pull_head_sha}:refs/heads/${pending_branch}"
151+
fi
152+
153+
git push origin "refs/heads/${pending_branch}:refs/heads/${pending_branch}" --force
154+
selected_pulls_count=$((selected_pulls_count + 1))
155+
echo " PR #${pull_num}: added as pending (${selected_pulls_count})."
156+
if [ "$selected_pulls_count" -ge "$max_pulls" ]; then
157+
echo "Quota of ${max_pulls} reached, stopping."
158+
break 2
159+
fi
160+
continue
161+
else
162+
echo " PR #${pull_num}: created/updated ${loci_main_branch}. Continuing with PR."
163+
fi
164+
fi
165+
166+
# Check for merge conflicts - against loci/main-* when BASE_SHA provided, otherwise upstream default
167+
if [ -n "${BASE_SHA:-}" ]; then
168+
conflict_target="refs/heads/${loci_main_branch}"
169+
conflict_target_name="$loci_main_branch"
170+
else
171+
conflict_target="refs/remotes/upstream/${UPSTREAM_DEFAULT}"
172+
conflict_target_name="upstream ${UPSTREAM_DEFAULT}"
173+
fi
174+
175+
if ! git merge-tree --write-tree --merge-base "${merge_base}" "${pull_head_sha}" "${conflict_target}" &>/dev/null; then
176+
echo " PR #${pull_num}: has conflicts with ${conflict_target_name}. Skipping."
177+
if [ "$manual_mode" -eq 1 ]; then
178+
echo "::error::PR has merge conflicts with ${conflict_target_name}"
179+
exit 1
180+
fi
181+
continue
182+
fi
183+
184+
origin_sha=$(git ls-remote --heads origin "refs/heads/${loci_pr_branch}" | cut -f1 || true)
185+
if [ -n "${origin_sha}" ] && [ "${origin_sha}" = "${pull_head_sha}" ]; then
186+
echo " PR #${pull_num}: already up-to-date."
187+
# In manual mode, still add it (user explicitly requested); in scheduled mode, skip
188+
if [ "$manual_mode" -eq 0 ]; then
189+
echo " Skipping."
190+
continue
191+
else
192+
echo " Adding anyway (manual mode)."
193+
fi
194+
fi
195+
196+
# Determine if we should target loci/main-* (only when base_sha explicitly provided)
197+
if [ -n "${BASE_SHA:-}" ]; then
198+
use_loci_base=1
199+
else
200+
use_loci_base=0
201+
fi
202+
203+
# Select pull request
204+
jq -c \
205+
--arg pull_number "$pull_num" \
206+
--arg pull_head_sha "$pull_head_sha" \
207+
--arg loci_pr_branch "$loci_pr_branch" \
208+
--arg short_merge_base "$short_merge_base" \
209+
--arg loci_main_branch "$loci_main_branch" \
210+
--argjson use_loci_base "$use_loci_base" \
211+
'{
212+
pull_number: $pull_number,
213+
title: .title,
214+
body: (.body // ""),
215+
pull_head_sha: $pull_head_sha,
216+
loci_pr_branch: $loci_pr_branch,
217+
short_merge_base: $short_merge_base,
218+
loci_main_branch: $loci_main_branch,
219+
use_loci_base: $use_loci_base
220+
}' <<<"$pr" >> /tmp/pulls.ndjson
221+
222+
selected_pulls_count=$((selected_pulls_count + 1))
223+
echo " PR #${pull_num}: added (${selected_pulls_count})."
224+
225+
if [ "$manual_mode" -eq 0 ] && [ "$selected_pulls_count" -ge "$max_pulls" ]; then
226+
echo "Quota of ${max_pulls} reached, stopping."
227+
break 2
228+
fi
229+
done < <(echo "$pulls" | jq -c '.[]' 2>/dev/null)
230+
231+
# In manual mode, we only process one PR, so break after first iteration
232+
if [ "$manual_mode" -eq 1 ]; then
233+
break
234+
fi
235+
236+
page=$((page + 1))
237+
done
238+
239+
if [ "$selected_pulls_count" -eq 0 ]; then
240+
latest_upstream_sha=$(git rev-parse "refs/remotes/upstream/${UPSTREAM_DEFAULT}")
241+
echo "No PRs found. Syncing latest upstream ${UPSTREAM_DEFAULT} (${latest_upstream_sha})."
242+
if loci_main_branch=$(bash "$SCRIPT_DIR/sync-loci-main.sh" "$latest_upstream_sha"); then
243+
echo "${loci_main_branch} already up-to-date."
244+
else
245+
echo "Created/updated ${loci_main_branch}."
246+
fi
247+
echo "prs_to_sync=no" >> "$GITHUB_OUTPUT"
248+
else
249+
echo "prs_to_sync=yes" >> "$GITHUB_OUTPUT"
250+
echo "Selected ${selected_pulls_count} upstream PRs to process"
251+
fi
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env bash
2+
# Checks a mirror PR for an insightful loci summary and notifies the original PR author.
3+
#
4+
# Usage:
5+
# notify-insightful-prs.sh <mirror_pr_number> <sticky_comment_id>
6+
#
7+
# Requires env: GITHUB_REPOSITORY, GH_TOKEN
8+
9+
set -euo pipefail
10+
11+
NOTIFICATION_MARKER='<!-- loci-notification -->'
12+
13+
mirror_pr_number="${1:?mirror PR number required}"
14+
sticky_comment_id="${2:?sticky comment ID required}"
15+
16+
pr_body=$(gh pr view "$mirror_pr_number" --repo "$GITHUB_REPOSITORY" --json body --jq '.body // ""')
17+
18+
source_pr_path=$(echo "$pr_body" \
19+
| grep -o '<!-- loci-source-pr: [^>]* -->' \
20+
| head -1 \
21+
| sed 's/<!-- loci-source-pr: //;s/ -->//')
22+
23+
if [ -z "$source_pr_path" ]; then
24+
echo "PR #${mirror_pr_number}: No loci-source-pr marker found, skipping."
25+
exit 0
26+
fi
27+
28+
source_repo=$(echo "$source_pr_path" | cut -d/ -f1-2)
29+
source_pr_number=$(echo "$source_pr_path" | cut -d/ -f4)
30+
sticky_comment_body=$(gh api "repos/${GITHUB_REPOSITORY}/issues/comments/${sticky_comment_id}" --jq '.body')
31+
32+
echo "PR comment body: ${sticky_comment_body}."
33+
34+
if ! echo "$sticky_comment_body" | grep -qE '!\[(Control Flow Graph|Flame Graph):'; then
35+
echo "PR #${mirror_pr_number}: Summary has no CFG/Flame Graph images, not insightful. Skipping."
36+
exit 0
37+
fi
38+
39+
echo "PR #${mirror_pr_number}: Insightful summary found. Source: ${source_repo}#${source_pr_number}"
40+
# comment_url="https://github.com/${GITHUB_REPOSITORY}/pull/${mirror_pr_number}#issuecomment-${sticky_comment_id}"
41+
# notification_body="🔎 Loci found performance insights on this PR. [View Latest Summary →](${comment_url}) ${NOTIFICATION_MARKER}"
42+
# existing_comment_id=$(gh api --paginate "repos/${source_repo}/issues/${source_pr_number}/comments" \
43+
# | jq -r "[.[] | select(.body | contains(\"${NOTIFICATION_MARKER}\"))] | last | .id // empty" \
44+
# 2>/dev/null || true)
45+
#
46+
# if [ -n "$existing_comment_id" ]; then
47+
# echo "Updating existing notification comment #${existing_comment_id} on ${source_repo}#${source_pr_number}."
48+
# gh api "repos/${source_repo}/issues/comments/${existing_comment_id}" \
49+
# --method PATCH \
50+
# --field body="$notification_body"
51+
# else
52+
# echo "Posting new notification comment on ${source_repo}#${source_pr_number}."
53+
# gh api "repos/${source_repo}/issues/${source_pr_number}/comments" \
54+
# --method POST \
55+
# --field body="$notification_body"
56+
# fi
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(dirname "$0")"
5+
source "$SCRIPT_DIR/utils.sh"
6+
7+
# List pending-pr branches, rename to pr, create mirror PRs
8+
pending_branches=$(git ls-remote --heads origin 'refs/heads/loci/pending-pr-*' | awk '{print $2}' | sed 's|refs/heads/||')
9+
if [ -z "$pending_branches" ]; then
10+
echo "No pending PR branches found."
11+
exit 0
12+
fi
13+
14+
while read -r pending_branch; do
15+
[[ "$pending_branch" =~ ^loci/pending-pr-([0-9]+)-(.+)$ ]] || continue
16+
num="${BASH_REMATCH[1]}"
17+
rest="${BASH_REMATCH[2]}"
18+
pr_branch="loci/pr-${num}-${rest}"
19+
20+
echo "::group::Promoting ${pending_branch}${pr_branch}"
21+
22+
# Rename: push as loci/pr-*, delete loci/pending-pr-*
23+
git fetch origin "${pending_branch}:refs/heads/${pending_branch}"
24+
git push origin "refs/heads/${pending_branch}:refs/heads/${pr_branch}" --force
25+
git push origin --delete "${pending_branch}"
26+
27+
# Create mirror PR (lib fetches upstream metadata internally)
28+
upsert_mirror_pr "$pr_branch" "main" "$num"
29+
30+
echo "::endgroup::"
31+
done <<<"$pending_branches"

.github/scripts/process-prs.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Creates/updates mirror branches and PRs from pulls.ndjson
5+
#
6+
# Required environment variables:
7+
# UPSTREAM_REPO - upstream repo (e.g., "openssl/openssl")
8+
# GH_TOKEN - GitHub token for API calls
9+
# GITHUB_REPOSITORY - current repo
10+
11+
SCRIPT_DIR="$(dirname "$0")"
12+
source "$SCRIPT_DIR/utils.sh"
13+
14+
git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" 2>/dev/null || true
15+
16+
while read -r line; do
17+
[ -z "$line" ] && continue
18+
19+
num=$(jq -r .pull_number <<<"$line")
20+
title=$(jq -r .title <<<"$line")
21+
body=$(jq -r .body <<<"$line")
22+
pull_head_sha=$(jq -r .pull_head_sha <<<"$line")
23+
loci_pr_branch=$(jq -r .loci_pr_branch <<<"$line")
24+
loci_main_branch=$(jq -r .loci_main_branch <<<"$line")
25+
use_loci_base=$(jq -r '.use_loci_base // 0' <<<"$line")
26+
27+
# Target loci/main-* only when base_sha was explicitly provided; otherwise target main
28+
if [ "$use_loci_base" -eq 1 ]; then
29+
target_base="$loci_main_branch"
30+
else
31+
target_base="main"
32+
fi
33+
34+
echo "::group::PR #${num}: ${loci_pr_branch} -> ${target_base}"
35+
36+
echo "Updating ${loci_pr_branch} to ${pull_head_sha}."
37+
if git show-ref --verify --quiet "refs/remotes/upstream/pr/${num}"; then
38+
git branch --no-track -f "${loci_pr_branch}" "refs/remotes/upstream/pr/${num}"
39+
else
40+
git fetch upstream "refs/pull/${num}/head:refs/heads/${loci_pr_branch}" || \
41+
git fetch upstream "${pull_head_sha}:refs/heads/${loci_pr_branch}"
42+
fi
43+
git push origin "refs/heads/${loci_pr_branch}:refs/heads/${loci_pr_branch}" --force
44+
45+
upsert_mirror_pr "$loci_pr_branch" "$target_base" "$num"
46+
47+
echo "::endgroup::"
48+
done < /tmp/pulls.ndjson

0 commit comments

Comments
 (0)