From a65ced1598751d5838859a7c9a7b15b85ab310c2 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:01:58 -0700 Subject: [PATCH] ci: add a workflow to detect and check dependencies for blocked pull requests Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .github/workflows/blocked_prs.yml | 236 ++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 .github/workflows/blocked_prs.yml diff --git a/.github/workflows/blocked_prs.yml b/.github/workflows/blocked_prs.yml new file mode 100644 index 000000000..8f060dbb6 --- /dev/null +++ b/.github/workflows/blocked_prs.yml @@ -0,0 +1,236 @@ +name: Blocked/Stacked Pull Requests Automation + +on: + pull_request_target: + types: + - opened + - edited + workflow_dispatch: + inputs: + pr_id: + description: Local Pull Request number to work on + required: true + type: number + +jobs: + block_status: + name: Check Blocked Status + runs-on: ubuntu-latest + steps: + - name: Setup From Pull Request Vent + if: ${{ github.event_name != 'workflow_dispatch' }} + id: pr_event_setup + env: + REPO_L: ${{ github.event.pull_request.base.repo.name }} + OWNER_L: ${{ github.event.pull_request.base.repo.owner.login }} + REPO_URL_L: $ {{ github.event.pull_request.base.repo.html_url }} + PR_HEAD_SHA_L: ${{ github.event.pull_request.head.sha }} + PR_NUMBER_L: ${{ github.event.pull_request.number }} + PR_HEAD_LABEL_L: ${{ github.event.pull_request.head.label }} + PR_BODY_L: ${{ github.event.pull_request.body }} + PR_LABLES_L: ${{ github.event.pull_request.labels }} + run: | + # setup env for the rest of the workflow + { + echo "REPO=$REPO_L" + echo "OWNER=$OWNER_L" + echo "REPO_URL=$REPO_URL_L" + echo "PR_NUMBER=$PR_NUMBER_L" + echo "PR_HEAD_SHA=$PR_HEAD_SHA_L" + echo "PR_HEAD_LABEL=$PR_HEAD_LABEL_L" + echo "PR_BODY=$PR_BODY_L" + echo "PR_LABELS=$(jq 'reduce .[].name as $l ([]; . + [$l])' <<< "$PR_LABELS_L" )" + } >> "$GITHUB_ENV" + + - name: Setup From Dispatch Event + if: ${{ github.event_name == 'workflow_dispatch' }} + id: dispatch_event_setup + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER_REPO_L: ${{ github.repository }} + OWNER_L: ${{ github.repository_owner }} + REPO_URL_L: $ {{ github.repositoryUrl }} + PR_NUMBER_L: ${{ inputs.pr_id }} + run: | + # setup env for the rest of the workflow + owner_prefix="$OWNER_L/" + REPO_L="${OWNER_REPO_L#"$owner_prefix"}" + PR_L=$( + gh api \ + -H "Accept: application/vnd.github.raw+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$OWNER_L/$REPO_L/pulls/$PR_NUMBER_L" + ) + PR_HEAD_SHA_L=$(jq -r '.head.sha' <<< "$PR_L") + PR_HEAD_LABEL_L=$(jq -r '.head.label' <<< "$PR_L") + PR_BODY_L=$(jq -r '.body' <<< "$PR_L") + PR_LABELS_L=$(jq '.labels' <<< "$PR_L") + { + echo "REPO=$REPO_L" + echo "OWNER=$OWNER_L" + echo "REPO_URL=$REPO_URL_L" + echo "PR_NUMBER=$PR_NUMBER_L" + echo "PR_HEAD_SHA=$PR_HEAD_SHA_L" + echo "PR_HEAD_LABEL=$PR_HEAD_LABEL_L" + echo "PR_BODY=$PR_BODY_L" + echo "PR_LABELS=$(jq 'reduce .[].name as $l ([]; . + [$l])' <<< "$PR_LABELS_L" )" + } >> "$GITHUB_ENV" + + + - name: Find Blocked/Stacked PRs in body + id: pr_ids + run: | + PRS=$( + jq ' + . as $body + | ( + $body | scan("blocked (?by)|(?on):? #(?[0-9]+)") + | map({ + "type": "Blocked on", + "number": ( . | tonumber ) + }) + ) as $bprs + | ( + $body | scan("stacked on:? #(?[0-9]+)") + | map({ + "type": "Stacked on", + "number": ( . | tonumber ) + }) + ) as $sprs + | ($bprs + $sprs) as $prs + | { + "blocking": $prs, + "numBlocking": ( $prs | length), + } + ' <<< "$PR_BODY" + ) + echo "prs=$PRS" >> "$GITHUB_OUPUT" + + - name: Collect Blocked PR Data + id: blocked_data + if: ${{ fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BLOCKED_PR_DATA=$( + while read -r PR ; do + gh api \ + -H "Accept: application/vnd.github.raw+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$OWNER/$REPO/pulls/$(jq -r '.number' <<< "$PR")" \ + | jq --arg type "$(jq -r '.type' <<< "$PR")" \ + ' + . | { + "type": $type, + "number": .number, + "merged": .merged, + "labels": (reduce .labels[].name as $l ([]; . + [$l])), + "basePrUrl": .html_url, + "baseRepoName": .head.repo.name, + "baseRepoOwner": .head.repo.owner.login, + "baseRepoUrl": .head.repo.html_url, + "baseSha": .head.sha, + "baseRefName": .head.ref, + } + ' + done < <(jq -c '.blocking[]' <<< "${{steps.pr_ids.outputs.prs}}") | jq -s + ) + echo "state=$BLOCKED_PR_DATA" >> "$GITHUB_OUPUT" + echo "all_merged=$(jq 'all(.[].merged; .)' <<< "$BLOCKED_PR_DATA")" + + - name: Apply Blocked Label if Missing + id: label_blocked + if: ${{ fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 && !contains(fromJSON(env.PR_LABELS), 'blocked') && !fromJSON(steps.blocked_data.outputs.all_merged) }} + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$OWNER/$REPO/issues/$PR_NUMBER/labels" \ + -f "labels[]=blocked" + + - name: Remove 'blocked' Label if All Dependencies Are Merged + id: unlabel_blocked + if: ${{ fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 && fromJSON(steps.blocked_data.outputs.all_merged) }} + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api \ + --method DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$OWNER/$REPO/issues/$PR_NUMBER/labels/blocked" + + - name: Apply 'blocking' Label to Dependencies if Missing + id: label_blocking + if: ${{ fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 }} + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # label pr dependencies with 'blocking' if not already + while read -r PR_DATA ; do + if jq -e 'all(.labels[]; . != "blocking")' <<< "$PR_DATA" > /dev/null ; then + PR=$(jq -r '.number' <<< "$PR_DATA") + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$OWNER/$REPO/issues/$PR/labels" \ + -f "labels[]=blocking" + fi + done < <(jq -c '.[]' <<< "${{steps.blocked_data.outputs.state}}") + + - name: Apply Blocking PR Status Check + id: blocked_check + if: ${{ fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 }} + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # create commit Status, overwrites previous identical context + while read -r PR_DATA ; do + DESC=$( + jq -r ' "Blocking PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged"' <<< "$PR_DATA" + ) + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/${OWNER}/${REPO}/statuses/${PR_HEAD_SHA}" \ + -f "state=$(jq -r 'if .merged then "success" else "failure" end' <<< "$PR_DATA")" \ + -f "target_url=$(jq -r '.basePrUrl' <<< "$PR_DATA" )" \ + -f "description=$DESC" \ + -f "context=continuous-integration/blocked-pr-check:$(jq '.number' <<< "$PR_DATA")" + done < <(jq -c '.[]' <<< "${{steps.blocked_data.outputs.state}}") + + - name: Context Comment + id: blocked_comment + if: ${{ fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 }} + continue-on-error: true + run: | + COMMENT_PATH="$(pwd)/temp_comment_file.txt" + touch "$COMMENT_PATH" + echo "" > "$COMMENT_PATH" + while read -r PR_DATA ; do + BASE_PR=$(jq '.number' <<< "$PR_DATA") + BASE_REF_NAME=$(jq '.baseRefName' <<< "$PR_DATA") + COMPARE_URL="$REPO_URL/compare/$BASE_REF_NAME...$PR_HEAD_LABEL" + STATUS=$(jq 'if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged" end' <<< "$PR_DATA") + TYPE=$(jq -r '.type' <<< "$PR_DATA") + echo " - $TYPE #$BASE_PR $STATUS [(compare)]($COMPARE_URL)" >> "$COMMENT_PATH" + done < <(jq -c '.[]' <<< "${{steps.blocked_data.outputs.state}}") + echo "file_path=${COMMENT_PATH}" >> "$GITHUB_OUTPUT" + + - name: 💬 PR Comment + if: ${{ fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 }} + continue-on-error: true + uses: spicyparrot/pr-comment-action@v1.0.0 + with: + comment: "### PR Dependencies :pushpin:" + comment_path: ${{ steps.blocked_comment.outputs.file_path }} + comment_id: "block_pr_dependencies"