Skip to content

Commit 5acb7ef

Browse files
mrecachinasCopilot
andauthored
Restore branch cleanup writer content
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e4f2c21 commit 5acb7ef

1 file changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
name: Delete PR staging and head branches writer
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Delete PR staging and head branches"]
6+
types: [completed]
7+
schedule:
8+
- cron: "5-55/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+
contents: write
18+
pull-requests: read
19+
20+
jobs:
21+
delete-staging-and-head-branches:
22+
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request') }}
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Delete staging and head branches
26+
env:
27+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28+
REPOSITORY: ${{ github.repository }}
29+
WORKFLOW_RUN_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
30+
DISPATCH_PR_NUMBER: ${{ inputs.pr_number }}
31+
run: |
32+
set -euo pipefail
33+
34+
is_pr_number() {
35+
[[ "$1" =~ ^[0-9]+$ ]]
36+
}
37+
38+
is_staging_branch_for_pr() {
39+
local branch="$1"
40+
local pr_number="$2"
41+
local prefix suffix
42+
43+
is_pr_number "${pr_number}" || return 1
44+
git check-ref-format "refs/heads/${branch}" >/dev/null || return 1
45+
46+
suffix="/advisory-improvement-${pr_number}"
47+
[[ "${branch}" == *"${suffix}" ]] || return 1
48+
prefix="${branch%"${suffix}"}"
49+
[[ -n "${prefix}" && "${prefix}" != */* ]]
50+
}
51+
52+
is_deletable_branch() {
53+
local branch="$1"
54+
[[ -n "${branch}" && "${branch}" != "main" ]] || return 1
55+
git check-ref-format "refs/heads/${branch}" >/dev/null
56+
}
57+
58+
encode_ref() {
59+
jq -rn --arg value "$1" '$value | @uri'
60+
}
61+
62+
request_ref() {
63+
local body_file="$1"
64+
local method="$2"
65+
local encoded_branch="$3"
66+
local ref_path="git/refs"
67+
68+
if [[ "${method}" == "GET" ]]; then
69+
ref_path="git/ref"
70+
fi
71+
72+
curl --silent --show-error \
73+
--request "${method}" \
74+
--output "${body_file}" \
75+
--write-out '%{http_code}' \
76+
--header "Accept: application/vnd.github+json" \
77+
--header "Authorization: Bearer ${GH_TOKEN}" \
78+
--header "X-GitHub-Api-Version: 2022-11-28" \
79+
"https://api.github.com/repos/${REPOSITORY}/${ref_path}/heads/${encoded_branch}"
80+
}
81+
82+
delete_branch() {
83+
local branch="$1"
84+
local expected_sha="${2:-}"
85+
local body_file current_sha encoded_branch status
86+
87+
encoded_branch="$(encode_ref "${branch}")"
88+
body_file="$(mktemp)"
89+
if ! status="$(request_ref "${body_file}" GET "${encoded_branch}")"; then
90+
rm -f "${body_file}"
91+
echo "::error::Failed to inspect branch ${branch}."
92+
return 1
93+
fi
94+
95+
if [[ "${status}" == "404" ]]; then
96+
rm -f "${body_file}"
97+
echo "Branch ${branch} is already absent."
98+
return 0
99+
fi
100+
if [[ "${status}" != "200" ]]; then
101+
cat "${body_file}" >&2
102+
rm -f "${body_file}"
103+
echo "::error::Failed to inspect branch ${branch}: GitHub API returned ${status}."
104+
return 1
105+
fi
106+
107+
current_sha="$(jq -er '.object.sha' "${body_file}")"
108+
if [[ -n "${expected_sha}" && "${current_sha}" != "${expected_sha}" ]]; then
109+
rm -f "${body_file}"
110+
echo "::error::Head branch ${branch} now points to ${current_sha}, not ${expected_sha}; leaving it and the staging branch in place."
111+
return 1
112+
fi
113+
114+
if ! status="$(request_ref "${body_file}" DELETE "${encoded_branch}")"; then
115+
rm -f "${body_file}"
116+
echo "::error::Failed to delete branch ${branch}."
117+
return 1
118+
fi
119+
120+
if [[ "${status}" == "204" ]]; then
121+
rm -f "${body_file}"
122+
echo "Deleted branch ${branch}."
123+
return 0
124+
fi
125+
if [[ "${status}" == "404" ]]; then
126+
rm -f "${body_file}"
127+
echo "Branch ${branch} was already absent when deletion was attempted."
128+
return 0
129+
fi
130+
131+
cat "${body_file}" >&2
132+
rm -f "${body_file}"
133+
echo "::error::Failed to delete branch ${branch}: GitHub API returned ${status}."
134+
return 1
135+
}
136+
137+
process_pr() {
138+
local advisory_file_pages base_ref base_repo expected_staging_branch head_ref head_repo head_sha
139+
local pr_json pr_number="$1" state
140+
expected_staging_branch="${2:-}"
141+
142+
if ! is_pr_number "${pr_number}"; then
143+
echo "::error::Unexpected pull request number: ${pr_number}"
144+
return 1
145+
fi
146+
147+
pr_json="$(gh api "repos/${REPOSITORY}/pulls/${pr_number}")"
148+
state="$(jq -r '.state' <<<"${pr_json}")"
149+
base_ref="$(jq -r '.base.ref' <<<"${pr_json}")"
150+
base_repo="$(jq -r '.base.repo.full_name' <<<"${pr_json}")"
151+
head_ref="$(jq -r '.head.ref // empty' <<<"${pr_json}")"
152+
head_repo="$(jq -r '.head.repo.full_name // empty' <<<"${pr_json}")"
153+
head_sha="$(jq -r '.head.sha // empty' <<<"${pr_json}")"
154+
155+
if [[ "${state}" != "closed" ]]; then
156+
echo "Pull request ${pr_number} is ${state}, not closed; skipping."
157+
return 0
158+
fi
159+
160+
if [[ "${base_repo}" != "${REPOSITORY}" ]]; then
161+
echo "Pull request ${pr_number} targets ${base_repo}, not ${REPOSITORY}; skipping."
162+
return 0
163+
fi
164+
165+
if [[ -n "${expected_staging_branch}" && "${base_ref}" != "${expected_staging_branch}" ]]; then
166+
echo "Pull request ${pr_number} no longer targets ${expected_staging_branch}; skipping."
167+
return 0
168+
fi
169+
170+
if ! is_staging_branch_for_pr "${base_ref}" "${pr_number}"; then
171+
echo "Pull request ${pr_number} base branch ${base_ref} is not its advisory improvement branch; skipping."
172+
return 0
173+
fi
174+
175+
if [[ "${head_repo}" != "${REPOSITORY}" ]]; then
176+
echo "Pull request ${pr_number} head repo is ${head_repo}, not ${REPOSITORY}; skipping."
177+
return 0
178+
fi
179+
180+
if [[ ! "${head_sha}" =~ ^[0-9a-f]{40}$ ]]; then
181+
echo "::error::Pull request ${pr_number} has an unexpected head SHA: ${head_sha}"
182+
return 1
183+
fi
184+
185+
advisory_file_pages="$(gh api --paginate "repos/${REPOSITORY}/pulls/${pr_number}/files?per_page=100" \
186+
--jq 'any(.[]; .filename | startswith("advisories/"))')"
187+
if ! grep -qx 'true' <<<"${advisory_file_pages}"; then
188+
echo "Pull request ${pr_number} does not modify advisories/; skipping."
189+
return 0
190+
fi
191+
192+
if [[ "${head_ref}" == "${base_ref}" ]]; then
193+
delete_branch "${base_ref}" "${head_sha}"
194+
return 0
195+
fi
196+
197+
if ! is_deletable_branch "${head_ref}"; then
198+
echo "::error::Head branch ${head_ref} is not a valid deletable Git branch."
199+
return 1
200+
fi
201+
202+
delete_branch "${head_ref}" "${head_sha}"
203+
delete_branch "${base_ref}"
204+
}
205+
206+
collect_reconciliation_targets() {
207+
local branch branches
208+
209+
branches="$(gh api --paginate "repos/${REPOSITORY}/branches?per_page=100" --jq '.[].name')"
210+
while IFS= read -r branch; do
211+
if [[ "${branch}" =~ ^[^/]+/advisory-improvement-([0-9]+)$ ]] &&
212+
git check-ref-format "refs/heads/${branch}" >/dev/null; then
213+
printf '%s\t%s\n' "${BASH_REMATCH[1]}" "${branch}"
214+
fi
215+
done <<<"${branches}"
216+
}
217+
218+
if [[ "${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
219+
PR_NUMBER="${WORKFLOW_RUN_PR_NUMBER}"
220+
if ! is_pr_number "${PR_NUMBER:-}"; then
221+
echo "No pull request number was provided; skipping."
222+
exit 0
223+
fi
224+
process_pr "${PR_NUMBER}"
225+
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
226+
process_pr "${DISPATCH_PR_NUMBER}"
227+
else
228+
TARGETS="$(collect_reconciliation_targets)"
229+
if [[ -z "${TARGETS}" ]]; then
230+
echo "No staging branches need reconciliation."
231+
exit 0
232+
fi
233+
while IFS=$'\t' read -r PR_NUMBER STAGING_BRANCH; do
234+
process_pr "${PR_NUMBER}" "${STAGING_BRANCH}"
235+
done <<<"${TARGETS}"
236+
fi

0 commit comments

Comments
 (0)