Skip to content

Commit cdc096e

Browse files
authored
Merge pull request #9051 from github/copilot/prt-migration-20260811-advisory-database
Migrate pull request automation away from pull_request_target
2 parents bdae56e + 5acb7ef commit cdc096e

4 files changed

Lines changed: 485 additions & 32 deletions

File tree

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,29 @@
11
name: Create PR staging branch
22

33
on:
4-
pull_request_target:
4+
pull_request:
55
branches: [main]
66
types: [opened, synchronize, reopened, edited]
77
paths:
88
- "advisories/**"
9-
workflow_dispatch:
109

1110
permissions:
12-
contents: write # Required to create and push branches
13-
pull-requests: write # Required to edit PR base branch
11+
contents: read
1412

1513
jobs:
16-
ensure-base-is-staging:
14+
signal:
1715
runs-on: ubuntu-latest
1816
steps:
19-
- uses: actions/checkout@v6
20-
- name: ensure base is staging
21-
env:
22-
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
23-
PR_NUMBER: ${{ github.event.pull_request.number }}
24-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25-
run: |
26-
set -xeo pipefail
27-
BRANCH_NAME="$PR_AUTHOR"/advisory-improvement-"$PR_NUMBER"
28-
git checkout -b "$BRANCH_NAME"
29-
git push origin "$BRANCH_NAME"
30-
gh pr edit --repo ${{ github.repository }} $PR_NUMBER --base "$BRANCH_NAME"
17+
- name: Record pull request signal
18+
env:
19+
PR_NUMBER: ${{ github.event.pull_request.number }}
20+
run: |
21+
mkdir -p workflow-signal
22+
printf '%s\n' "${PR_NUMBER}" > workflow-signal/pr_number.txt
23+
- name: Upload pull request signal
24+
uses: actions/upload-artifact@v4
25+
with:
26+
name: create-staging-pr-number
27+
path: workflow-signal/pr_number.txt
28+
retention-days: 1
29+
if-no-files-found: error
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
name: Create PR staging branch writer
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Create PR staging branch"]
6+
types: [completed]
7+
schedule:
8+
- cron: "*/10 * * * *"
9+
workflow_dispatch:
10+
inputs:
11+
pr_number:
12+
description: Pull request number to process
13+
required: true
14+
type: number
15+
16+
permissions:
17+
actions: read
18+
contents: write
19+
pull-requests: write
20+
21+
jobs:
22+
ensure-base-is-staging:
23+
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request') }}
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Ensure base is staging
27+
env:
28+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29+
REPOSITORY: ${{ github.repository }}
30+
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
31+
WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
32+
WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
33+
WORKFLOW_RUN_HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
34+
WORKFLOW_RUN_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
35+
DISPATCH_PR_NUMBER: ${{ inputs.pr_number }}
36+
run: |
37+
set -euo pipefail
38+
39+
is_pr_number() {
40+
[[ "$1" =~ ^[0-9]+$ ]]
41+
}
42+
43+
recover_pr_number_from_artifact() {
44+
local run_id="$1"
45+
local artifact_count artifact_dir artifact_id artifact_json artifact_size entries pr_number
46+
47+
is_pr_number "${run_id}" || return 1
48+
49+
if ! artifact_json="$(gh api "repos/${REPOSITORY}/actions/runs/${run_id}/artifacts" \
50+
--jq '[.artifacts[] | select(.name == "create-staging-pr-number" and .expired == false)]')"; then
51+
return 1
52+
fi
53+
artifact_count="$(jq -r 'length' <<<"${artifact_json}")"
54+
[[ "${artifact_count}" == "1" ]] || return 1
55+
56+
artifact_id="$(jq -r '.[0].id' <<<"${artifact_json}")"
57+
artifact_size="$(jq -r '.[0].size_in_bytes' <<<"${artifact_json}")"
58+
is_pr_number "${artifact_id}" || return 1
59+
is_pr_number "${artifact_size}" || return 1
60+
(( artifact_size <= 4096 )) || return 1
61+
62+
artifact_dir="workflow-run-artifacts/create-staging-${run_id}"
63+
rm -rf "${artifact_dir}"
64+
mkdir -p "${artifact_dir}"
65+
if ! gh api "repos/${REPOSITORY}/actions/artifacts/${artifact_id}/zip" > "${artifact_dir}/artifact.zip"; then
66+
rm -rf "${artifact_dir}"
67+
return 1
68+
fi
69+
if ! entries="$(unzip -Z1 "${artifact_dir}/artifact.zip" 2>/dev/null)"; then
70+
rm -rf "${artifact_dir}"
71+
return 1
72+
fi
73+
if [[ "${entries}" != "pr_number.txt" ]]; then
74+
rm -rf "${artifact_dir}"
75+
return 1
76+
fi
77+
if ! pr_number="$(unzip -p "${artifact_dir}/artifact.zip" pr_number.txt 2>/dev/null | head -c 32)"; then
78+
rm -rf "${artifact_dir}"
79+
return 1
80+
fi
81+
rm -rf "${artifact_dir}"
82+
83+
is_pr_number "${pr_number}" || return 1
84+
printf '%s\n' "${pr_number}"
85+
}
86+
87+
recover_pr_number_from_head_sha() {
88+
local head_sha="$1"
89+
local head_repository="$2"
90+
local matches pulls_json
91+
92+
[[ "${head_sha}" =~ ^[0-9a-fA-F]{40}$ ]] || return 1
93+
[[ -n "${head_repository}" && "${head_repository}" != "null" ]] || return 1
94+
95+
if ! pulls_json="$(gh api -H "Accept: application/vnd.github+json" "repos/${REPOSITORY}/commits/${head_sha}/pulls")"; then
96+
return 1
97+
fi
98+
matches="$(jq -r --arg head_sha "${head_sha}" --arg head_repository "${head_repository}" '
99+
[
100+
.[]
101+
| select(.state == "open")
102+
| select(.base.ref == "main")
103+
| select(.head.sha == $head_sha)
104+
| select(.head.repo.full_name == $head_repository)
105+
| .number
106+
]
107+
' <<<"${pulls_json}")"
108+
109+
[[ "$(jq -r 'length' <<<"${matches}")" == "1" ]] || return 1
110+
jq -r '.[0]' <<<"${matches}"
111+
}
112+
113+
encode_ref() {
114+
jq -rn --arg value "$1" '$value | @uri'
115+
}
116+
117+
process_pr() {
118+
local advisory_file_pages base_ref base_repo branch_name encoded_branch head_ref head_repo head_sha
119+
local main_sha pr_author pr_json pr_number="$1" state
120+
121+
if ! is_pr_number "${pr_number}"; then
122+
echo "::error::Unexpected pull request number: ${pr_number}"
123+
return 1
124+
fi
125+
126+
pr_json="$(gh api "repos/${REPOSITORY}/pulls/${pr_number}")"
127+
state="$(jq -r '.state' <<<"${pr_json}")"
128+
base_ref="$(jq -r '.base.ref' <<<"${pr_json}")"
129+
base_repo="$(jq -r '.base.repo.full_name' <<<"${pr_json}")"
130+
pr_author="$(jq -r '.user.login' <<<"${pr_json}")"
131+
head_sha="$(jq -r '.head.sha // empty' <<<"${pr_json}")"
132+
head_repo="$(jq -r '.head.repo.full_name // empty' <<<"${pr_json}")"
133+
head_ref="$(jq -r '.head.ref // empty' <<<"${pr_json}")"
134+
135+
if [[ "${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
136+
if [[ ! "${WORKFLOW_RUN_HEAD_SHA:-}" =~ ^[0-9a-fA-F]{40}$ ||
137+
-z "${WORKFLOW_RUN_HEAD_REPOSITORY:-}" ||
138+
"${WORKFLOW_RUN_HEAD_REPOSITORY}" == "null" ||
139+
-z "${WORKFLOW_RUN_HEAD_BRANCH:-}" ||
140+
"${WORKFLOW_RUN_HEAD_BRANCH}" == "null" ]]; then
141+
echo "::error::The workflow run is missing trusted head identity metadata."
142+
return 1
143+
fi
144+
if [[ "${head_sha}" != "${WORKFLOW_RUN_HEAD_SHA}" ||
145+
"${head_repo}" != "${WORKFLOW_RUN_HEAD_REPOSITORY}" ||
146+
"${head_ref}" != "${WORKFLOW_RUN_HEAD_BRANCH}" ]]; then
147+
echo "::error::Pull request ${pr_number} does not match the triggering workflow run."
148+
return 1
149+
fi
150+
fi
151+
152+
if [[ "${state}" != "open" ]]; then
153+
echo "Pull request ${pr_number} is ${state}; skipping."
154+
return 0
155+
fi
156+
157+
if [[ "${base_ref}" != "main" ]]; then
158+
echo "Pull request ${pr_number} base is ${base_ref}, not main; skipping."
159+
return 0
160+
fi
161+
162+
if [[ "${base_repo}" != "${REPOSITORY}" ]]; then
163+
echo "Pull request ${pr_number} targets ${base_repo}, not ${REPOSITORY}; skipping."
164+
return 0
165+
fi
166+
167+
advisory_file_pages="$(gh api --paginate "repos/${REPOSITORY}/pulls/${pr_number}/files?per_page=100" \
168+
--jq 'any(.[]; .filename | startswith("advisories/"))')"
169+
if ! grep -qx 'true' <<<"${advisory_file_pages}"; then
170+
echo "Pull request ${pr_number} does not modify advisories/; skipping."
171+
return 0
172+
fi
173+
174+
branch_name="${pr_author}/advisory-improvement-${pr_number}"
175+
if ! git check-ref-format "refs/heads/${branch_name}" >/dev/null; then
176+
echo "::error::Unexpected staging branch name: ${branch_name}"
177+
return 1
178+
fi
179+
encoded_branch="$(encode_ref "${branch_name}")"
180+
181+
if gh api "repos/${REPOSITORY}/git/ref/heads/${encoded_branch}" --silent >/dev/null 2>&1; then
182+
echo "Staging branch ${branch_name} already exists."
183+
else
184+
main_sha="$(gh api "repos/${REPOSITORY}/git/ref/heads/main" --jq '.object.sha')"
185+
if gh api -X POST "repos/${REPOSITORY}/git/refs" \
186+
-f ref="refs/heads/${branch_name}" \
187+
-f sha="${main_sha}" \
188+
--silent; then
189+
echo "Created staging branch ${branch_name} from main."
190+
elif gh api "repos/${REPOSITORY}/git/ref/heads/${encoded_branch}" --silent >/dev/null 2>&1; then
191+
echo "Staging branch ${branch_name} was created by another run."
192+
else
193+
echo "::error::Failed to create staging branch ${branch_name}."
194+
return 1
195+
fi
196+
fi
197+
198+
gh api -X PATCH "repos/${REPOSITORY}/pulls/${pr_number}" \
199+
-f base="${branch_name}" \
200+
--silent
201+
echo "Retargeted pull request ${pr_number} to ${branch_name}."
202+
}
203+
204+
if [[ "${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
205+
if is_pr_number "${WORKFLOW_RUN_PR_NUMBER:-}"; then
206+
PR_NUMBERS="${WORKFLOW_RUN_PR_NUMBER}"
207+
elif PR_NUMBERS="$(recover_pr_number_from_artifact "${WORKFLOW_RUN_ID:-}")"; then
208+
echo "Recovered pull request number ${PR_NUMBERS} from signal artifact."
209+
elif PR_NUMBERS="$(recover_pr_number_from_head_sha "${WORKFLOW_RUN_HEAD_SHA:-}" "${WORKFLOW_RUN_HEAD_REPOSITORY:-}")"; then
210+
echo "Recovered pull request number ${PR_NUMBERS} from workflow_run head SHA."
211+
else
212+
echo "No pull request number could be recovered; skipping."
213+
exit 0
214+
fi
215+
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
216+
PR_NUMBERS="${DISPATCH_PR_NUMBER}"
217+
else
218+
PR_NUMBERS="$(gh api --paginate "repos/${REPOSITORY}/pulls?state=open&base=main&per_page=100" --jq '.[].number')"
219+
if [[ -z "${PR_NUMBERS}" ]]; then
220+
echo "No open pull requests targeting main need reconciliation."
221+
exit 0
222+
fi
223+
fi
224+
225+
while IFS= read -r PR_NUMBER; do
226+
[[ -n "${PR_NUMBER}" ]] || continue
227+
process_pr "${PR_NUMBER}"
228+
done <<<"${PR_NUMBERS}"
Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,18 @@
11
name: Delete PR staging and head branches
22

33
on:
4-
pull_request_target:
4+
pull_request:
55
branches: ["*/advisory-improvement-*"]
66
types: [closed]
77
paths:
88
- "advisories/**"
9-
workflow_dispatch:
109

1110
permissions:
12-
contents: write # Required to delete branches
11+
contents: read
1312

1413
jobs:
15-
delete-staging-and-head-branches:
16-
if: ${{ !github.event.pull_request.head.repo.fork }}
14+
signal:
1715
runs-on: ubuntu-latest
1816
steps:
19-
- uses: actions/checkout@v6
20-
- name: Delete staging and head branches
21-
env:
22-
STAGING_BRANCH: ${{ github.event.pull_request.base.ref }}
23-
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
24-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25-
run: |
26-
set -xeo pipefail
27-
git push origin --delete --force $STAGING_BRANCH
28-
git push origin --delete --force $HEAD_BRANCH
17+
- name: Record pull request signal
18+
run: echo "Delete staging and head branches signal received."

0 commit comments

Comments
 (0)