diff --git a/.github/workflows/git-commit-lint.yaml b/.github/workflows/git-commit-lint.yaml index 012a8ee..ab97685 100644 --- a/.github/workflows/git-commit-lint.yaml +++ b/.github/workflows/git-commit-lint.yaml @@ -45,7 +45,7 @@ jobs: - name: Set Up Node.js id: set_up_nodejs - uses: actions/setup-node@v6.4.0 + uses: actions/setup-node@v7.0.0 with: node-version: "lts/*" diff --git a/.github/workflows/task-release-and-deploy-to-prod.yaml b/.github/workflows/task-release-and-deploy-to-prod.yaml index f6d1a5c..835fa7e 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 @@ -85,6 +91,7 @@ jobs: run: | echo "Counting commits of '$RELEASE_VCS_REF' that are not in '$PRODUCTION_VCS_REF'…" change_count="$(git rev-list --count "${PRODUCTION_VCS_REF}..${RELEASE_VCS_REF}" --)" + echo "change_count=${change_count:?}" >> "$GITHUB_OUTPUT" if [ "${change_count:?}" -gt 0 ]; then echo "There are $change_count changes to release." @@ -92,6 +99,15 @@ jobs: else echo 'There is nothing to release. The remaining steps will be skipped.' echo 'has_changes=false' >> "$GITHUB_OUTPUT" + + { + echo '# Release and Deploy to Production' + echo + echo 'Nothing to release.' + echo + echo "- **From:** \`$RELEASE_VCS_REF\`" + echo "- **Into:** \`$PRODUCTION_VCS_REF\`" + } >> "$GITHUB_STEP_SUMMARY" fi - name: Prepare Release @@ -120,6 +136,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,16 +146,31 @@ 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" + + { + echo '# Release and Deploy to Production' + echo + echo "$github_pull_request_url" + echo + echo "- **Changes:** $CHANGE_COUNT" + echo "- **From:** \`$RELEASE_VCS_REF\`" + echo "- **Into:** \`$PRODUCTION_VCS_REF\`" + } >> "$GITHUB_STEP_SUMMARY" env: GH_TOKEN: ${{ github.token }} + CHANGE_COUNT: ${{ steps.check_changes.outputs.change_count }} RELEASE_ASSIGNEE: ${{ github.actor }} CHANGELOG_FILE_PATH: ${{ steps.generate_changelog.outputs.changelog_file_path }} GITHUB_PULL_REQUEST_LABELS: ${{ inputs.github_pull_request_labels }} @@ -174,3 +206,63 @@ 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" + + # 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. + requested_reviewers=() + rejected_reviewers=() + for reviewer in "${reviewers[@]}"; do + echo "Requesting a review from '$reviewer'…" + if gh pr edit "$GITHUB_PULL_REQUEST_URL" --add-reviewer "$reviewer"; then + requested_reviewers+=("@$reviewer") + else + echo "::warning::Could not request a review from '$reviewer'." + rejected_reviewers+=("@$reviewer") + fi + done + + { + if [ "${#requested_reviewers[@]}" -gt 0 ]; then + echo "- **Reviewers:** ${requested_reviewers[*]}" + else + echo '- **Reviewers:** nobody' + fi + + if [ "${#rejected_reviewers[@]}" -gt 0 ]; then + echo "- **Reviewers GitHub rejected:** ${rejected_reviewers[*]}" + fi + } >> "$GITHUB_STEP_SUMMARY" + 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..d000a8b 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,86 @@ 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" + + { + echo '# Release and Publish' + echo + echo "$github_pull_request_url" + echo + echo "- **From:** \`$CREATE_RELEASE_VCS_REF\`" + echo "- **Into:** \`$RELEASE_VCS_REF\`" + echo "- **Release:** $PREPARE_RELEASE_GITHUB_PULL_REQUEST_HTML_URL" + } >> "$GITHUB_STEP_SUMMARY" 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" + + # 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. + requested_reviewers=() + rejected_reviewers=() + for reviewer in "${reviewers[@]}"; do + echo "Requesting a review from '$reviewer'…" + if gh pr edit "$GITHUB_PULL_REQUEST_URL" --add-reviewer "$reviewer"; then + requested_reviewers+=("@$reviewer") + else + echo "::warning::Could not request a review from '$reviewer'." + rejected_reviewers+=("@$reviewer") + fi + done + + { + if [ "${#requested_reviewers[@]}" -gt 0 ]; then + echo "- **Reviewers:** ${requested_reviewers[*]}" + else + echo '- **Reviewers:** nobody' + fi + + if [ "${#rejected_reviewers[@]}" -gt 0 ]; then + echo "- **Reviewers GitHub rejected:** ${rejected_reviewers[*]}" + fi + } >> "$GITHUB_STEP_SUMMARY" + env: + GH_TOKEN: ${{ github.token }} + GITHUB_PULL_REQUEST_URL: ${{ steps.create_github_pull_request.outputs.url }}