Skip to content

Commit e369d7a

Browse files
Alex SouthwellAlex Southwell
authored andcommitted
Add post-merge PR comment with merge-specific run links.
When a PR merges to main/master, poll for CircleCI and GitHub Actions runs created for that merge commit SHA and comment direct links on the merged PR.
1 parent e920751 commit e369d7a

File tree

1 file changed

+170
-0
lines changed

1 file changed

+170
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
name: Comment Post-Merge Run Links
2+
3+
on:
4+
pull_request_target:
5+
types:
6+
- closed
7+
8+
permissions:
9+
contents: read
10+
pull-requests: write
11+
12+
jobs:
13+
comment-run-links:
14+
name: Comment links after merge
15+
if: github.event.pull_request.merged == true && (github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'master')
16+
runs-on: ${{ fromJSON(vars.GHA_RUNS_ON_JSON || '["self-hosted","linux"]') }}
17+
steps:
18+
- name: Resolve run links for this merge commit
19+
id: resolve-links
20+
env:
21+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22+
CIRCLECI_TOKEN: ${{ secrets.CIRCLECI_TOKEN }}
23+
REPO_OWNER: ${{ github.repository_owner }}
24+
REPO_NAME: ${{ github.event.repository.name }}
25+
RAW_MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
26+
RAW_BASE_REF: ${{ github.event.pull_request.base.ref }}
27+
MAX_ATTEMPTS: "36"
28+
SLEEP_SECONDS: "10"
29+
WORKFLOW_NAME_REGEX: ${{ vars.WORKFLOW_NAME_REGEX || 'pulumi|deploy|build' }}
30+
WORKFLOW_OWNER_REPO: ${{ vars.WORKFLOW_OWNER_REPO || github.repository }}
31+
CIRCLECI_PROJECT_SLUG: ${{ vars.CIRCLECI_PROJECT_SLUG || '' }}
32+
run: |
33+
set -euo pipefail
34+
35+
echo "workflow_url=" >> "$GITHUB_OUTPUT"
36+
echo "circleci_url=" >> "$GITHUB_OUTPUT"
37+
echo "notes=" >> "$GITHUB_OUTPUT"
38+
echo "merge_sha=n/a" >> "$GITHUB_OUTPUT"
39+
echo "base_ref=n/a" >> "$GITHUB_OUTPUT"
40+
41+
# Validate event values before using them in API requests.
42+
if [[ "${RAW_BASE_REF}" != "main" && "${RAW_BASE_REF}" != "master" ]]; then
43+
echo "notes=Unsupported base branch; expected main/master." >> "$GITHUB_OUTPUT"
44+
exit 0
45+
fi
46+
if [[ ! "${RAW_MERGE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then
47+
echo "notes=No valid merge commit SHA available on this PR event." >> "$GITHUB_OUTPUT"
48+
exit 0
49+
fi
50+
51+
BASE_REF="${RAW_BASE_REF}"
52+
MERGE_SHA="${RAW_MERGE_SHA}"
53+
echo "merge_sha=${MERGE_SHA}" >> "$GITHUB_OUTPUT"
54+
echo "base_ref=${BASE_REF}" >> "$GITHUB_OUTPUT"
55+
56+
attempt=1
57+
workflow_url=""
58+
while [[ $attempt -le ${MAX_ATTEMPTS} ]]; do
59+
runs_json="$(gh api "/repos/${WORKFLOW_OWNER_REPO}/actions/runs?event=push&head_sha=${MERGE_SHA}&per_page=50" 2>/dev/null || true)"
60+
workflow_url="$(RUNS_JSON="${runs_json}" BASE_REF="${BASE_REF}" WORKFLOW_NAME_REGEX="${WORKFLOW_NAME_REGEX}" python3 - <<'PY_INNER'
61+
import json
62+
import os
63+
import re
64+
65+
raw = os.environ.get("RUNS_JSON") or "{}"
66+
base_ref = os.environ.get("BASE_REF") or ""
67+
name_re = os.environ.get("WORKFLOW_NAME_REGEX") or "pulumi|deploy|build"
68+
69+
try:
70+
data = json.loads(raw)
71+
except Exception:
72+
print("")
73+
raise SystemExit(0)
74+
75+
runs = data.get("workflow_runs") or []
76+
if not runs:
77+
print("")
78+
raise SystemExit(0)
79+
80+
def branch_ok(run):
81+
branch = (run.get("head_branch") or "")
82+
return (not base_ref) or (branch == base_ref)
83+
84+
regex = re.compile(name_re, re.IGNORECASE)
85+
filtered = [r for r in runs if branch_ok(r)]
86+
preferred = [r for r in filtered if regex.search(r.get("name") or "")]
87+
88+
candidate = None
89+
if preferred:
90+
candidate = sorted(preferred, key=lambda x: x.get("run_number", 0), reverse=True)[0]
91+
elif filtered:
92+
candidate = sorted(filtered, key=lambda x: x.get("run_number", 0), reverse=True)[0]
93+
94+
print((candidate or {}).get("html_url", ""))
95+
PY_INNER
96+
)"
97+
if [[ -n "${workflow_url}" ]]; then
98+
break
99+
fi
100+
sleep "${SLEEP_SECONDS}"
101+
attempt=$(( attempt + 1 ))
102+
done
103+
104+
if [[ -n "${workflow_url}" ]]; then
105+
echo "workflow_url=${workflow_url}" >> "$GITHUB_OUTPUT"
106+
fi
107+
108+
circleci_slug="${CIRCLECI_PROJECT_SLUG}"
109+
if [[ -z "${circleci_slug}" ]]; then
110+
circleci_slug="${REPO_OWNER}/${REPO_NAME}"
111+
fi
112+
113+
if [[ -z "${CIRCLECI_TOKEN:-}" ]]; then
114+
echo "notes=CircleCI token not configured; skipping CircleCI run lookup." >> "$GITHUB_OUTPUT"
115+
exit 0
116+
fi
117+
118+
attempt=1
119+
circleci_url=""
120+
while [[ $attempt -le ${MAX_ATTEMPTS} ]]; do
121+
pipelines_json="$(curl -fsSL -H "Circle-Token: ${CIRCLECI_TOKEN}" "https://circleci.com/api/v2/project/gh/${circleci_slug}/pipeline?branch=${BASE_REF}" 2>/dev/null || true)"
122+
circleci_url="$(PIPELINES_JSON="${pipelines_json}" CIRCLECI_SLUG="${circleci_slug}" MERGE_SHA="${MERGE_SHA}" python3 - <<'PY_INNER'
123+
import json
124+
import os
125+
126+
raw = os.environ.get("PIPELINES_JSON") or "{}"
127+
slug = os.environ.get("CIRCLECI_SLUG") or ""
128+
merge_sha = os.environ.get("MERGE_SHA") or ""
129+
130+
try:
131+
data = json.loads(raw)
132+
except Exception:
133+
print("")
134+
raise SystemExit(0)
135+
136+
for item in data.get("items") or []:
137+
vcs = item.get("vcs") or {}
138+
if (vcs.get("revision") or "") == merge_sha:
139+
number = item.get("number")
140+
if number is not None:
141+
print(f"https://app.circleci.com/pipelines/github/{slug}/{number}")
142+
raise SystemExit(0)
143+
144+
print("")
145+
PY_INNER
146+
)"
147+
if [[ -n "${circleci_url}" ]]; then
148+
break
149+
fi
150+
sleep "${SLEEP_SECONDS}"
151+
attempt=$(( attempt + 1 ))
152+
done
153+
154+
if [[ -n "${circleci_url}" ]]; then
155+
echo "circleci_url=${circleci_url}" >> "$GITHUB_OUTPUT"
156+
fi
157+
158+
- name: Add run links to merged PR
159+
uses: peter-evans/create-or-update-comment@v4
160+
with:
161+
issue-number: ${{ github.event.pull_request.number }}
162+
body: |
163+
Merged to `${{ steps.resolve-links.outputs.base_ref }}`.
164+
165+
Follow-on runs for merge commit `${{ steps.resolve-links.outputs.merge_sha }}`:
166+
167+
- CircleCI: ${{ steps.resolve-links.outputs.circleci_url || 'Not found within polling window.' }}
168+
- GitHub Actions workflow: ${{ steps.resolve-links.outputs.workflow_url || 'Not found within polling window.' }}
169+
170+
${{ steps.resolve-links.outputs.notes }}

0 commit comments

Comments
 (0)