|
| 1 | +name: Blocked/Stacked Pull Requests Automation |
| 2 | + |
| 3 | +on: |
| 4 | + pull_request_target: |
| 5 | + types: |
| 6 | + - opened |
| 7 | + - reopened |
| 8 | + - edited |
| 9 | + - synchronize |
| 10 | + workflow_dispatch: |
| 11 | + inputs: |
| 12 | + pr_id: |
| 13 | + description: Local Pull Request number to work on |
| 14 | + required: true |
| 15 | + type: number |
| 16 | + |
| 17 | +jobs: |
| 18 | + blocked_status: |
| 19 | + name: Check Blocked Status |
| 20 | + runs-on: ubuntu-latest |
| 21 | + |
| 22 | + steps: |
| 23 | + - name: Generate token |
| 24 | + id: generate-token |
| 25 | + uses: actions/create-github-app-token@v2 |
| 26 | + with: |
| 27 | + app-id: ${{ vars.PULL_REQUEST_APP_ID }} |
| 28 | + private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} |
| 29 | + |
| 30 | + - name: Setup From Dispatch Event |
| 31 | + if: github.event_name == 'workflow_dispatch' |
| 32 | + id: dispatch_event_setup |
| 33 | + env: |
| 34 | + GH_TOKEN: ${{ steps.generate-token.outputs.token }} |
| 35 | + PR_NUMBER: ${{ inputs.pr_id }} |
| 36 | + run: | |
| 37 | + # setup env for the rest of the workflow |
| 38 | + OWNER=$(dirname "${{ github.repository }}") |
| 39 | + REPO=$(basename "${{ github.repository }}") |
| 40 | + PR_JSON=$( |
| 41 | + gh api \ |
| 42 | + -H "Accept: application/vnd.github.raw+json" \ |
| 43 | + -H "X-GitHub-Api-Version: 2022-11-28" \ |
| 44 | + "/repos/$OWNER/$REPO/pulls/$PR_NUMBER" |
| 45 | + ) |
| 46 | + echo "PR_JSON=$PR_JSON" >> "$GITHUB_ENV" |
| 47 | +
|
| 48 | + - name: Setup Environment |
| 49 | + id: env_setup |
| 50 | + env: |
| 51 | + EVENT_PR_JSON: ${{ toJSON(github.event.pull_request) }} |
| 52 | + run: | |
| 53 | + # setup env for the rest of the workflow |
| 54 | + PR_JSON=${PR_JSON:-"$EVENT_PR_JSON"} |
| 55 | + { |
| 56 | + echo "REPO=$(jq -r '.base.repo.name' <<< "$PR_JSON")" |
| 57 | + echo "OWNER=$(jq -r '.base.repo.owner.login' <<< "$PR_JSON")" |
| 58 | + echo "PR_NUMBER=$(jq -r '.number' <<< "$PR_JSON")" |
| 59 | + echo "JOB_DATA=$(jq -c ' |
| 60 | + { |
| 61 | + "repo": .base.repo.name, |
| 62 | + "owner": .base.repo.owner.login, |
| 63 | + "repoUrl": .base.repo.html_url, |
| 64 | + "prNumber": .number, |
| 65 | + "prHeadSha": .head.sha, |
| 66 | + "prHeadLabel": .head.label, |
| 67 | + "prBody": .body, |
| 68 | + "prLabels": (reduce .labels[].name as $l ([]; . + [$l])) |
| 69 | + } |
| 70 | + ' <<< "$PR_JSON")" |
| 71 | + } >> "$GITHUB_ENV" |
| 72 | +
|
| 73 | +
|
| 74 | + - name: Find Blocked/Stacked PRs in body |
| 75 | + id: pr_ids |
| 76 | + run: | |
| 77 | + prs=$( |
| 78 | + jq -c ' |
| 79 | + .prBody as $body |
| 80 | + | ( |
| 81 | + $body | |
| 82 | + reduce ( |
| 83 | + . | scan("blocked (?:by|on):? #([0-9]+)") |
| 84 | + | map({ |
| 85 | + "type": "Blocked on", |
| 86 | + "number": ( . | tonumber ) |
| 87 | + }) |
| 88 | + ) as $i ([]; . + [$i[]]) |
| 89 | + ) as $bprs |
| 90 | + | ( |
| 91 | + $body | |
| 92 | + reduce ( |
| 93 | + . | scan("stacked on:? #([0-9]+)") |
| 94 | + | map({ |
| 95 | + "type": "Stacked on", |
| 96 | + "number": ( . | tonumber ) |
| 97 | + }) |
| 98 | + ) as $i ([]; . + [$i[]]) |
| 99 | + ) as $sprs |
| 100 | + | ($bprs + $sprs) as $prs |
| 101 | + | { |
| 102 | + "blocking": $prs, |
| 103 | + "numBlocking": ( $prs | length), |
| 104 | + } |
| 105 | + ' <<< "$JOB_DATA" |
| 106 | + ) |
| 107 | + echo "prs=$prs" >> "$GITHUB_OUTPUT" |
| 108 | +
|
| 109 | + - name: Collect Blocked PR Data |
| 110 | + id: blocking_data |
| 111 | + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 |
| 112 | + env: |
| 113 | + GH_TOKEN: ${{ steps.generate-token.outputs.token }} |
| 114 | + BLOCKING_PRS: ${{ steps.pr_ids.outputs.prs }} |
| 115 | + run: | |
| 116 | + blocked_pr_data=$( |
| 117 | + while read -r pr_data ; do |
| 118 | + gh api \ |
| 119 | + -H "Accept: application/vnd.github+json" \ |
| 120 | + -H "X-GitHub-Api-Version: 2022-11-28" \ |
| 121 | + "/repos/$OWNER/$REPO/pulls/$(jq -r '.number' <<< "$pr_data")" \ |
| 122 | + | jq -c --arg type "$(jq -r '.type' <<< "$pr_data")" \ |
| 123 | + ' |
| 124 | + . | { |
| 125 | + "type": $type, |
| 126 | + "number": .number, |
| 127 | + "merged": .merged, |
| 128 | + "labels": (reduce .labels[].name as $l ([]; . + [$l])), |
| 129 | + "basePrUrl": .html_url, |
| 130 | + "baseRepoName": .head.repo.name, |
| 131 | + "baseRepoOwner": .head.repo.owner.login, |
| 132 | + "baseRepoUrl": .head.repo.html_url, |
| 133 | + "baseSha": .head.sha, |
| 134 | + "baseRefName": .head.ref, |
| 135 | + } |
| 136 | + ' |
| 137 | + done < <(jq -c '.blocking[]' <<< "$BLOCKING_PRS") | jq -c -s |
| 138 | + ) |
| 139 | + { |
| 140 | + echo "data=$blocked_pr_data"; |
| 141 | + echo "all_merged=$(jq -r 'all(.[].merged; .)' <<< "$blocked_pr_data")"; |
| 142 | + echo "current_blocking=$(jq -c 'map( select( .merged | not ) | .number )' <<< "$blocked_pr_data" )"; |
| 143 | + } >> "$GITHUB_OUTPUT" |
| 144 | +
|
| 145 | + - name: Add 'blocked' Label is Missing |
| 146 | + id: label_blocked |
| 147 | + if: (fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'blocked') && !fromJSON(steps.blocking_data.outputs.all_merged) |
| 148 | + continue-on-error: true |
| 149 | + env: |
| 150 | + GH_TOKEN: ${{ steps.generate-token.outputs.token }} |
| 151 | + run: | |
| 152 | + gh -R ${{ github.repository }} issue edit --add-label 'blocked' "$PR_NUMBER" |
| 153 | +
|
| 154 | + - name: Remove 'blocked' Label if All Dependencies Are Merged |
| 155 | + id: unlabel_blocked |
| 156 | + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 && fromJSON(steps.blocking_data.outputs.all_merged) |
| 157 | + continue-on-error: true |
| 158 | + env: |
| 159 | + GH_TOKEN: ${{ steps.generate-token.outputs.token }} |
| 160 | + run: | |
| 161 | + gh -R ${{ github.repository }} issue edit --remove-label 'blocked' "$PR_NUMBER" |
| 162 | +
|
| 163 | + - name: Apply 'blocking' Label to Unmerged Dependencies |
| 164 | + id: label_blocking |
| 165 | + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 |
| 166 | + continue-on-error: true |
| 167 | + env: |
| 168 | + GH_TOKEN: ${{ steps.generate-token.outputs.token }} |
| 169 | + BLOCKING_ISSUES: ${{ steps.blocking_data.outputs.current_blocking }} |
| 170 | + run: | |
| 171 | + while read -r pr ; do |
| 172 | + gh -R ${{ github.repository }} issue edit --add-label 'blocking' "$pr" || true |
| 173 | + done < <(jq -c '.[]' <<< "$BLOCKING_ISSUES") |
| 174 | +
|
| 175 | + - name: Apply Blocking PR Status Check |
| 176 | + id: blocked_check |
| 177 | + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 |
| 178 | + continue-on-error: true |
| 179 | + env: |
| 180 | + GH_TOKEN: ${{ steps.generate-token.outputs.token }} |
| 181 | + BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }} |
| 182 | + run: | |
| 183 | + pr_head_sha=$(jq -r '.prHeadSha' <<< "$JOB_DATA") |
| 184 | + # create commit Status, overwrites previous identical context |
| 185 | + while read -r pr_data ; do |
| 186 | + DESC=$( |
| 187 | + jq -r ' "Blocking PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged"' <<< "$pr_data" |
| 188 | + ) |
| 189 | + gh api \ |
| 190 | + --method POST \ |
| 191 | + -H "Accept: application/vnd.github+json" \ |
| 192 | + -H "X-GitHub-Api-Version: 2022-11-28" \ |
| 193 | + "/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \ |
| 194 | + -f "state=$(jq -r 'if .merged then "success" else "failure" end' <<< "$pr_data")" \ |
| 195 | + -f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \ |
| 196 | + -f "description=$DESC" \ |
| 197 | + -f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")" |
| 198 | + done < <(jq -c '.[]' <<< "$BLOCKING_DATA") |
| 199 | +
|
| 200 | + - name: Context Comment |
| 201 | + id: generate-comment |
| 202 | + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 |
| 203 | + continue-on-error: true |
| 204 | + env: |
| 205 | + BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }} |
| 206 | + run: | |
| 207 | + COMMENT_PATH="$(pwd)/temp_comment_file.txt" |
| 208 | + echo '<h3>PR Dependencies :pushpin:</h3>' > "$COMMENT_PATH" |
| 209 | + echo >> "$COMMENT_PATH" |
| 210 | + pr_head_label=$(jq -r '.prHeadLabel' <<< "$JOB_DATA") |
| 211 | + while read -r pr_data ; do |
| 212 | + base_pr=$(jq -r '.number' <<< "$pr_data") |
| 213 | + base_ref_name=$(jq -r '.baseRefName' <<< "$pr_data") |
| 214 | + base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data") |
| 215 | + base_repo_name=$(jq -r '.baseRepoName' <<< "$pr_data") |
| 216 | + compare_url="https://github.com/$base_repo_owner/$base_repo_name/compare/$base_ref_name...$pr_head_label" |
| 217 | + status=$(jq -r 'if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged" end' <<< "$pr_data") |
| 218 | + type=$(jq -r '.type' <<< "$pr_data") |
| 219 | + echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH" |
| 220 | + done < <(jq -c '.[]' <<< "$BLOCKING_DATA") |
| 221 | +
|
| 222 | + { |
| 223 | + echo 'body<<EOF'; |
| 224 | + cat "${COMMENT_PATH}"; |
| 225 | + echo 'EOF'; |
| 226 | + } >> "$GITHUB_OUTPUT" |
| 227 | +
|
| 228 | + - name: 💬 PR Comment |
| 229 | + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 |
| 230 | + continue-on-error: true |
| 231 | + env: |
| 232 | + GH_TOKEN: ${{ steps.generate-token.outputs.token }} |
| 233 | + COMMENT_BODY: ${{ steps.generate-comment.outputs.body }} |
| 234 | + run: | |
| 235 | + gh -R ${{ github.repository }} issue comment "$PR_NUMBER" \ |
| 236 | + --body "$COMMENT_BODY" \ |
| 237 | + --create-if-none \ |
| 238 | + --edit-last |
| 239 | +
|
0 commit comments