From 623b0e29d812419270b47e5a65fb59d2ba7880e0 Mon Sep 17 00:00:00 2001 From: Jose Tomas Robles Hahn Date: Fri, 11 Sep 2026 13:57:11 -0300 Subject: [PATCH] feat: Request reviews from the authors of released changes - A release or publication pull request gathers the work of several people, and nothing told any of them that their changes were about to reach production. - It is an input so that a VCS repository whose releases do not warrant it can turn it off, and it defaults to being on, because the common case is wanting the authors to know. - The authors are read from the GitHub API rather than from the Git history, because only GitHub can resolve the email address of a commit to a GitHub user. The commits are paginated, as a release easily exceeds the size of a single page. - A review is requested from one user at a time, because GitHub rejects the whole request when a single one of the users cannot be a reviewer, for instance because they have left the organization, and a release must not fail over that. - In 'Release and Deploy to Production' the step runs last, so that an unreachable GitHub API cannot cost the release and deployment checklist, which is worth more than the review requests. - The header comment of each workflow no longer lists the steps, which the step names already carry, and says instead what the pull request it opens is for, which none of them does. Co-Authored-By: Claude Opus 5 --- .../task-release-and-deploy-to-prod.yaml | 80 ++++++++++++++++--- .../workflows/task-release-and-publish.yaml | 77 +++++++++++++++--- 2 files changed, 136 insertions(+), 21 deletions(-) diff --git a/.github/workflows/task-release-and-deploy-to-prod.yaml b/.github/workflows/task-release-and-deploy-to-prod.yaml index f6d1a5c..6b167fa 100644 --- a/.github/workflows/task-release-and-deploy-to-prod.yaml +++ b/.github/workflows/task-release-and-deploy-to-prod.yaml @@ -1,8 +1,7 @@ # GitHub Actions Workflow for 'Release and Deploy to Production' Task # -# It creates a release VCS branch with the changes that are not yet in the production VCS reference, -# opens a draft GitHub pull request for it, and comments a release and deployment checklist on that -# pull request. When there is nothing to release, it ends quietly. +# It opens a GitHub pull request to create a release and deploy it to production. It ends quietly +# when there is nothing to release. name: "Task: Release and Deploy to Production" @@ -27,6 +26,13 @@ on: kind: release kind: deploy description: Labels of the GitHub Pull Request, One per Line + request_reviews_from_change_authors: + type: boolean + required: false + default: true + description: >- + Whether to request a review of the GitHub pull request from every GitHub user who + authored one of the commits included in it. pre_deployment_checklist_items: type: string @@ -120,6 +126,7 @@ jobs: echo "changelog_file_path=${changelog_file_path:?}" >> "$GITHUB_OUTPUT" - name: Create GitHub Pull Request + id: create_github_pull_request if: ${{ steps.check_changes.outputs.has_changes == 'true' }} run: | label_args=() @@ -129,14 +136,18 @@ jobs: fi done <<< "$GITHUB_PULL_REQUEST_LABELS" - gh pr create \ - --base "$PRODUCTION_VCS_REF" \ - --head "$RELEASE_VCS_REF" \ - --draft \ - --title 'Release and deploy to production' \ - --body-file "$CHANGELOG_FILE_PATH" \ - --assignee "$RELEASE_ASSIGNEE" \ - "${label_args[@]}" + github_pull_request_url="$( + gh pr create \ + --base "$PRODUCTION_VCS_REF" \ + --head "$RELEASE_VCS_REF" \ + --draft \ + --title 'Release and deploy to production' \ + --body-file "$CHANGELOG_FILE_PATH" \ + --assignee "$RELEASE_ASSIGNEE" \ + "${label_args[@]}" + )" + + echo "url=${github_pull_request_url:?}" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ github.token }} RELEASE_ASSIGNEE: ${{ github.actor }} @@ -174,3 +185,50 @@ jobs: GH_TOKEN: ${{ github.token }} PRE_DEPLOYMENT_CHECKLIST_ITEMS: ${{ inputs.pre_deployment_checklist_items }} POST_DEPLOYMENT_CHECKLIST_ITEMS: ${{ inputs.post_deployment_checklist_items }} + + - name: Request Reviews from Change Authors + if: >- + ${{ steps.check_changes.outputs.has_changes == 'true' + && inputs.request_reviews_from_change_authors }} + run: | + github_pull_request_number="$( + gh pr view "$GITHUB_PULL_REQUEST_URL" --json number --jq '.number' + )" + + # The author of a commit is read from the GitHub API rather than from the Git history, + # because only GitHub can resolve the email address of a commit to a GitHub user. The + # commits are paginated, as a release easily exceeds the size of a single page. + echo "Getting the authors of the commits of pull request #$github_pull_request_number…" + commit_authors="$( + gh api --paginate \ + "repos/${GITHUB_REPOSITORY:?}/pulls/${github_pull_request_number:?}/commits" \ + --jq '.[].author.login // empty' \ + | sort -u + )" + + reviewers=() + while IFS= read -r commit_author; do + # A bot cannot be a reviewer, and neither can the pull request's own author, which is + # the bot of GitHub Actions because the pull request is created with its token. + case "$commit_author" in + '' | *'[bot]') continue ;; + esac + reviewers+=("$commit_author") + done <<< "$commit_authors" + + if [ "${#reviewers[@]}" -eq 0 ]; then + echo 'There is nobody to request a review from.' + exit 0 + fi + + # A review is requested from one user at a time, because GitHub rejects the whole request + # when a single one of the users cannot be a reviewer, for instance because they no longer + # have access to the VCS repository. A review that cannot be requested is not fatal. + for reviewer in "${reviewers[@]}"; do + echo "Requesting a review from '$reviewer'…" + gh pr edit "$GITHUB_PULL_REQUEST_URL" --add-reviewer "$reviewer" \ + || echo "::warning::Could not request a review from '$reviewer'." + done + env: + GH_TOKEN: ${{ github.token }} + GITHUB_PULL_REQUEST_URL: ${{ steps.create_github_pull_request.outputs.url }} diff --git a/.github/workflows/task-release-and-publish.yaml b/.github/workflows/task-release-and-publish.yaml index b41fe57..5002f72 100644 --- a/.github/workflows/task-release-and-publish.yaml +++ b/.github/workflows/task-release-and-publish.yaml @@ -1,7 +1,7 @@ # GitHub Actions Workflow for 'Release and Publish' Task # -# Once a release pull request has been merged into the staging VCS reference, it creates a -# publication VCS branch and opens a draft GitHub pull request that publishes the release. +# It opens a GitHub pull request to publish a previously created release. It must be called from a +# merged release pull request, whose title, head VCS reference and assignee it derives its own from. name: "Task: Release and Publish" @@ -20,6 +20,13 @@ on: task kind: publish description: Labels of the GitHub Pull Request, One per Line + request_reviews_from_change_authors: + type: boolean + required: false + default: true + description: >- + Whether to request a review of the GitHub pull request from every GitHub user who + authored one of the commits included in it. permissions: {} @@ -72,6 +79,7 @@ jobs: echo "CREATE_RELEASE_GITHUB_PULL_REQUEST_DESCRIPTION=${create_release_github_pull_request_description:?}" >> "$GITHUB_ENV" - name: Create GitHub Pull Request for Publication + id: create_github_pull_request run: | label_args=() while IFS= read -r label; do @@ -80,14 +88,63 @@ jobs: fi done <<< "$GITHUB_PULL_REQUEST_LABELS" - gh pr create \ - --base "$RELEASE_VCS_REF" \ - --head "$CREATE_RELEASE_VCS_REF" \ - --draft \ - --title "$CREATE_RELEASE_GITHUB_PULL_REQUEST_TITLE" \ - --body "$CREATE_RELEASE_GITHUB_PULL_REQUEST_DESCRIPTION" \ - --assignee "$RELEASE_ASSIGNEE" \ - "${label_args[@]}" + github_pull_request_url="$( + gh pr create \ + --base "$RELEASE_VCS_REF" \ + --head "$CREATE_RELEASE_VCS_REF" \ + --draft \ + --title "$CREATE_RELEASE_GITHUB_PULL_REQUEST_TITLE" \ + --body "$CREATE_RELEASE_GITHUB_PULL_REQUEST_DESCRIPTION" \ + --assignee "$RELEASE_ASSIGNEE" \ + "${label_args[@]}" + )" + + echo "url=${github_pull_request_url:?}" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ github.token }} GITHUB_PULL_REQUEST_LABELS: ${{ inputs.github_pull_request_labels }} + + - name: Request Reviews from Change Authors + if: ${{ inputs.request_reviews_from_change_authors }} + run: | + github_pull_request_number="$( + gh pr view "$GITHUB_PULL_REQUEST_URL" --json number --jq '.number' + )" + + # The author of a commit is read from the GitHub API rather than from the Git history, + # because only GitHub can resolve the email address of a commit to a GitHub user. The + # commits are paginated, as a release easily exceeds the size of a single page. + echo "Getting the authors of the commits of pull request #$github_pull_request_number…" + commit_authors="$( + gh api --paginate \ + "repos/${GITHUB_REPOSITORY:?}/pulls/${github_pull_request_number:?}/commits" \ + --jq '.[].author.login // empty' \ + | sort -u + )" + + reviewers=() + while IFS= read -r commit_author; do + # A bot cannot be a reviewer, and neither can the pull request's own author, which is + # the bot of GitHub Actions because the pull request is created with its token. + case "$commit_author" in + '' | *'[bot]') continue ;; + esac + reviewers+=("$commit_author") + done <<< "$commit_authors" + + if [ "${#reviewers[@]}" -eq 0 ]; then + echo 'There is nobody to request a review from.' + exit 0 + fi + + # A review is requested from one user at a time, because GitHub rejects the whole request + # when a single one of the users cannot be a reviewer, for instance because they no longer + # have access to the VCS repository. A review that cannot be requested is not fatal. + for reviewer in "${reviewers[@]}"; do + echo "Requesting a review from '$reviewer'…" + gh pr edit "$GITHUB_PULL_REQUEST_URL" --add-reviewer "$reviewer" \ + || echo "::warning::Could not request a review from '$reviewer'." + done + env: + GH_TOKEN: ${{ github.token }} + GITHUB_PULL_REQUEST_URL: ${{ steps.create_github_pull_request.outputs.url }}