Skip to content

Commit e4f2c21

Browse files
mrecachinasCopilot
andcommitted
Harden closed PR branch cleanup
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b9a13bb commit e4f2c21

1 file changed

Lines changed: 0 additions & 169 deletions

File tree

Lines changed: 0 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +0,0 @@
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-
delete_branch() {
63-
local branch="$1"
64-
local encoded_branch
65-
66-
encoded_branch="$(encode_ref "${branch}")"
67-
if gh api -X DELETE "repos/${REPOSITORY}/git/refs/heads/${encoded_branch}" --silent; then
68-
echo "Deleted branch ${branch}."
69-
elif gh api "repos/${REPOSITORY}/git/ref/heads/${encoded_branch}" --silent >/dev/null 2>&1; then
70-
echo "::error::Failed to delete existing branch ${branch}."
71-
return 1
72-
else
73-
echo "Branch ${branch} is already absent."
74-
fi
75-
}
76-
77-
process_pr() {
78-
local advisory_file_pages base_ref base_repo expected_staging_branch head_ref head_repo
79-
local pr_json pr_number="$1" state
80-
expected_staging_branch="${2:-}"
81-
82-
if ! is_pr_number "${pr_number}"; then
83-
echo "::error::Unexpected pull request number: ${pr_number}"
84-
return 1
85-
fi
86-
87-
pr_json="$(gh api "repos/${REPOSITORY}/pulls/${pr_number}")"
88-
state="$(jq -r '.state' <<<"${pr_json}")"
89-
base_ref="$(jq -r '.base.ref' <<<"${pr_json}")"
90-
base_repo="$(jq -r '.base.repo.full_name' <<<"${pr_json}")"
91-
head_ref="$(jq -r '.head.ref // empty' <<<"${pr_json}")"
92-
head_repo="$(jq -r '.head.repo.full_name // empty' <<<"${pr_json}")"
93-
94-
if [[ "${state}" != "closed" ]]; then
95-
echo "Pull request ${pr_number} is ${state}, not closed; skipping."
96-
return 0
97-
fi
98-
99-
if [[ "${base_repo}" != "${REPOSITORY}" ]]; then
100-
echo "Pull request ${pr_number} targets ${base_repo}, not ${REPOSITORY}; skipping."
101-
return 0
102-
fi
103-
104-
if [[ -n "${expected_staging_branch}" && "${base_ref}" != "${expected_staging_branch}" ]]; then
105-
echo "Pull request ${pr_number} no longer targets ${expected_staging_branch}; skipping."
106-
return 0
107-
fi
108-
109-
if ! is_staging_branch_for_pr "${base_ref}" "${pr_number}"; then
110-
echo "Pull request ${pr_number} base branch ${base_ref} is not its advisory improvement branch; skipping."
111-
return 0
112-
fi
113-
114-
if [[ "${head_repo}" != "${REPOSITORY}" ]]; then
115-
echo "Pull request ${pr_number} head repo is ${head_repo}, not ${REPOSITORY}; skipping."
116-
return 0
117-
fi
118-
119-
advisory_file_pages="$(gh api --paginate "repos/${REPOSITORY}/pulls/${pr_number}/files?per_page=100" \
120-
--jq 'any(.[]; .filename | startswith("advisories/"))')"
121-
if ! grep -qx 'true' <<<"${advisory_file_pages}"; then
122-
echo "Pull request ${pr_number} does not modify advisories/; skipping."
123-
return 0
124-
fi
125-
126-
delete_branch "${base_ref}"
127-
if [[ "${head_ref}" == "${base_ref}" ]]; then
128-
return 0
129-
fi
130-
131-
if ! is_deletable_branch "${head_ref}"; then
132-
echo "Head branch ${head_ref} is not a valid deletable Git branch; leaving it in place."
133-
return 0
134-
fi
135-
136-
delete_branch "${head_ref}"
137-
}
138-
139-
collect_reconciliation_targets() {
140-
local branch branches
141-
142-
branches="$(gh api --paginate "repos/${REPOSITORY}/branches?per_page=100" --jq '.[].name')"
143-
while IFS= read -r branch; do
144-
if [[ "${branch}" =~ ^[^/]+/advisory-improvement-([0-9]+)$ ]] &&
145-
git check-ref-format "refs/heads/${branch}" >/dev/null; then
146-
printf '%s\t%s\n' "${BASH_REMATCH[1]}" "${branch}"
147-
fi
148-
done <<<"${branches}"
149-
}
150-
151-
if [[ "${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
152-
PR_NUMBER="${WORKFLOW_RUN_PR_NUMBER}"
153-
if ! is_pr_number "${PR_NUMBER:-}"; then
154-
echo "No pull request number was provided; skipping."
155-
exit 0
156-
fi
157-
process_pr "${PR_NUMBER}"
158-
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
159-
process_pr "${DISPATCH_PR_NUMBER}"
160-
else
161-
TARGETS="$(collect_reconciliation_targets)"
162-
if [[ -z "${TARGETS}" ]]; then
163-
echo "No staging branches need reconciliation."
164-
exit 0
165-
fi
166-
while IFS=$'\t' read -r PR_NUMBER STAGING_BRANCH; do
167-
process_pr "${PR_NUMBER}" "${STAGING_BRANCH}"
168-
done <<<"${TARGETS}"
169-
fi

0 commit comments

Comments
 (0)