|
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