diff --git a/.github/workflows/pr_notifications.yml b/.github/workflows/pr_notifications.yml index aded99d7..07700f58 100644 --- a/.github/workflows/pr_notifications.yml +++ b/.github/workflows/pr_notifications.yml @@ -2,114 +2,163 @@ name: PR Notifications on: pull_request_target: - branches: [main] - types: [opened, synchronize, edited, reopened, ready_for_review] + branches: [main, 'release/**'] + types: [opened, reopened, ready_for_review] + issue_comment: + types: [created] + workflow_dispatch: + inputs: + pr_number: + description: 'Номер PR' + required: true + type: string + mode: + description: 'Режим запуска' + required: true + default: full + type: choice + options: + - full + - notify-only permissions: contents: read pull-requests: write -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - jobs: + authorize-comment: + if: >- + github.event_name == 'issue_comment' && github.event.issue.pull_request && + (github.event.comment.body == '/review' || github.event.comment.body == + '/notify') + runs-on: ubuntu-latest + steps: + - name: Check Comment Author Permission + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMENT_AUTHOR: ${{ github.event.comment.user.login }} + shell: bash + run: | + PERMISSION=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$COMMENT_AUTHOR/permission" --jq '.permission') + if [[ "$PERMISSION" != "write" && "$PERMISSION" != "admin" ]]; then + echo "::error::Команды ревью доступны пользователям с правом записи в репозиторий." + exit 1 + fi + notify: + needs: authorize-comment + if: >- + !cancelled() && (github.event_name != 'issue_comment' || + needs.authorize-comment.result == 'success') runs-on: ubuntu-latest - if: github.event.pull_request.draft != true + concurrency: + group: + ${{ github.workflow }}-${{ inputs.pr_number || + github.event.pull_request.number || github.event.issue.number }} + cancel-in-progress: true env: - MM_USERS_JSON: "${{ secrets.MM_USERS }}" - PR_AUTHOR: "${{ github.event.pull_request.user.login }}" - GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + MM_USERS_JSON: ${{ secrets.MM_USERS }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: + ${{ inputs.pr_number || github.event.pull_request.number || + github.event.issue.number }} + MODE: + ${{ inputs.mode || (github.event_name == 'issue_comment' && + github.event.comment.body == '/notify' && 'notify-only') || 'full' }} steps: - - name: Timeout + - name: Wait for automatic reviewer assignment + if: + github.event_name == 'pull_request_target' && + github.event.pull_request.draft != true run: sleep 1m - - name: Get Available Reviewers - run: | - AVAILABLE_REVIEWERS_ENTRIES=$( - echo "$MM_USERS_JSON" | - jq -c --arg exclude "$PR_AUTHOR" 'to_entries | map(select(.key != $exclude))' - ) - echo "::add-mask::$AVAILABLE_REVIEWERS_ENTRIES" - echo "AVAILABLE_REVIEWERS_ENTRIES=$AVAILABLE_REVIEWERS_ENTRIES" >> $GITHUB_ENV - - name: Check Requested Reviewers - run: | - REQUESTED_REVIEWERS_LOGIN_LIST=$(gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json reviewRequests --jq '.reviewRequests | map(select(.login)) | map(.login)') - SELECTED_REVIEWER_ENTRY=$( - echo "$AVAILABLE_REVIEWERS_ENTRIES" | - jq -c --argjson requested "$REQUESTED_REVIEWERS_LOGIN_LIST" 'first(.[] | select(.key as $gh_login | $requested | index($gh_login)))' - ) - echo "::add-mask::$SELECTED_REVIEWER_ENTRY" - echo "SELECTED_REVIEWER_ENTRY=$SELECTED_REVIEWER_ENTRY" >> $GITHUB_ENV - - name: Check Last Review + + - name: Load Pull Request + id: pr + shell: bash run: | - if [ -z "$SELECTED_REVIEWER_ENTRY" ]; then - LAST_REVIEW=$(gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --json reviews --jq '.reviews | last') - LAST_REVIEWER_LOGIN=$(echo "$LAST_REVIEW" | jq -r '.author.login') - SELECTED_REVIEWER_ENTRY=$( - echo $AVAILABLE_REVIEWERS_ENTRIES | - jq -c --arg lastReviewerLogin "$LAST_REVIEWER_LOGIN" 'map(select(.key == $lastReviewerLogin)) | first' - ) - echo "::add-mask::$SELECTED_REVIEWER_ENTRY" - echo "LAST_REVIEW=$LAST_REVIEW" >> $GITHUB_ENV + if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::Укажите положительный номер PR." + exit 1 + fi - if [[ "$SELECTED_REVIEWER_ENTRY" != "null" ]]; then - echo "SELECTED_REVIEWER_ENTRY=$SELECTED_REVIEWER_ENTRY" >> $GITHUB_ENV - else - echo "Last review check failed" - fi + gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \ + --json number,url,author,headRefName,state,isDraft,reviewRequests,reviews \ + > "$RUNNER_TEMP/review-pr.json" + + if ! jq -e '.state == "OPEN" and (.isDraft | not)' "$RUNNER_TEMP/review-pr.json" > /dev/null; then + echo "::notice::Уведомления доступны только для открытых PR, готовых к ревью." + echo "ready=false" >> "$GITHUB_OUTPUT" else - echo "The reviewer was selected on the previous steps. Skip checking last review" + echo "ready=true" >> "$GITHUB_OUTPUT" fi - - name: Assign Reviewer + - name: Select Reviewer + id: reviewer + if: steps.pr.outputs.ready == 'true' + shell: bash run: | - if [ -z "$SELECTED_REVIEWER_ENTRY" ]; then - AVAILABLE_REVIEWERS_LENGTH=$(echo "$AVAILABLE_REVIEWERS_ENTRIES" | jq 'length') - SELECTED_REVIEWER_INDEX=$((RANDOM % AVAILABLE_REVIEWERS_LENGTH)) - SELECTED_REVIEWER_ENTRY=$(echo "$AVAILABLE_REVIEWERS_ENTRIES" | jq -c --argjson index "$SELECTED_REVIEWER_INDEX" '.[$index]') + PR_JSON="$RUNNER_TEMP/review-pr.json" + PR_AUTHOR=$(jq -r '.author.login' "$PR_JSON") + AVAILABLE_REVIEWERS=$(jq -c --arg author "$PR_AUTHOR" \ + 'to_entries | map(select(.key != $author))' <<< "$MM_USERS_JSON") + REQUESTED_REVIEWERS=$(jq -c '[.reviewRequests[] | .login // empty]' "$PR_JSON") + LAST_REVIEWER=$(jq -r '.reviews | last | .author.login // ""' "$PR_JSON") + + SELECTED_REVIEWER=$(jq -c --argjson requested "$REQUESTED_REVIEWERS" \ + 'map(select(.key as $login | $requested | index($login))) | first' <<< "$AVAILABLE_REVIEWERS") + HAS_REQUESTED_REVIEWER=false + if [[ "$SELECTED_REVIEWER" != "null" ]]; then + HAS_REQUESTED_REVIEWER=true else - echo "The reviewer was selected on the previous steps. Skip assigning a reviewer" + SELECTED_REVIEWER=$(jq -c --arg login "$LAST_REVIEWER" \ + 'map(select(.key == $login)) | first' <<< "$AVAILABLE_REVIEWERS") fi - REVIEWER_GH=$(echo "$SELECTED_REVIEWER_ENTRY" | jq -r '.key') - REVIEWER_MM=$(echo "$SELECTED_REVIEWER_ENTRY" | jq -r '.value') - - gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-reviewer "$REVIEWER_GH" + if [[ "$SELECTED_REVIEWER" == "null" ]]; then + if [[ "$MODE" == "notify-only" ]]; then + echo "::error::Нет текущего или последнего ревьюера из MM_USERS. Запустите режим full." + exit 1 + fi + REVIEWER_COUNT=$(jq 'length' <<< "$AVAILABLE_REVIEWERS") + if [[ "$REVIEWER_COUNT" -eq 0 ]]; then + echo "::error::В MM_USERS нет доступного ревьюера, кроме автора PR." + exit 1 + fi + SELECTED_REVIEWER=$(jq -c --argjson index "$((RANDOM % REVIEWER_COUNT))" \ + '.[$index]' <<< "$AVAILABLE_REVIEWERS") + fi + echo "::add-mask::$SELECTED_REVIEWER" + REVIEWER_GH=$(jq -r '.key' <<< "$SELECTED_REVIEWER") echo "::add-mask::$REVIEWER_GH" - echo "REVIEWER_GH=$REVIEWER_GH" >> $GITHUB_ENV + echo "$SELECTED_REVIEWER" > "$RUNNER_TEMP/review-recipient.json" - echo "::add-mask::$REVIEWER_MM" - echo "REVIEWER_MM=$REVIEWER_MM" >> $GITHUB_ENV - - name: Check Notification Need - run: | - PR_STATE=$(echo "$LAST_REVIEW" | jq -r '.state') - REVIEWER_LOGIN=$(echo "$LAST_REVIEW" | jq -r '.author.login') + if [[ "$GITHUB_EVENT_NAME" == "pull_request_target" && "$HAS_REQUESTED_REVIEWER" == "false" ]] && \ + jq -e --arg login "$REVIEWER_GH" \ + '.reviews | last | .state == "APPROVED" and .author.login == $login' "$PR_JSON" > /dev/null; then + echo "::notice::Ревьюер уже одобрил PR." + echo "notify=false" >> "$GITHUB_OUTPUT" + exit 0 + fi - if [[ "$PR_STATE" == "APPROVED" ]] && [[ "$REVIEWER_LOGIN" == "$REVIEWER_GH" ]]; then - NEED_NOTIFY=0 - else - NEED_NOTIFY=1 + if [[ "$MODE" == "full" ]]; then + gh pr edit "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --add-reviewer "$REVIEWER_GH" fi + echo "notify=true" >> "$GITHUB_OUTPUT" - echo "NEED_NOTIFY=$NEED_NOTIFY" >> $GITHUB_ENV - - name: "Notify Reviewer" + - name: Notify Reviewer + if: steps.reviewer.outputs.notify == 'true' + env: + REVIEW_CHAT_WEBHOOK: ${{ secrets.REVIEW_CHAT_WEBHOOK }} + shell: bash run: | - if [ "$NEED_NOTIFY" -ne 0 ]; then - BRANCH_URL="${{ github.event.pull_request.head.repo.html_url }}/tree/${{ github.event.pull_request.head.ref }}" - - MESSAGE="MR [!${{ github.event.pull_request.number }}](${{ github.event.pull_request.html_url }})" - MESSAGE+=" от [${{ github.event.pull_request.user.login }} ${USER_NAME}]" - MESSAGE+="(${{ github.event.pull_request.user.html_url }})" - MESSAGE+="(ветка: [${{ github.event.pull_request.head.ref }}](${BRANCH_URL})) готов на ревью.\n" - MESSAGE+="Выбран ревьюер: @${REVIEWER_MM}.\n" - MESSAGE+="Ссылка на MR: ${{ github.event.pull_request.html_url }}" - - curl -X POST \ + REVIEWER_MM=$(jq -r '.value' "$RUNNER_TEMP/review-recipient.json") + echo "::add-mask::$REVIEWER_MM" + jq --arg reviewer "$REVIEWER_MM" --arg server "$GITHUB_SERVER_URL" \ + '{text: ("PR [#\(.number)](\(.url)) от [\(.author.login)](\($server)/\(.author.login)) " + + "(ветка: \(.headRefName)) готов к ревью.\n" + + "Ревьюер: @\($reviewer).\nСсылка на PR: \(.url)")}' \ + "$RUNNER_TEMP/review-pr.json" | \ + curl --fail-with-body --silent --show-error \ -H "Content-Type: application/json" \ - -d "{\"text\": \"$MESSAGE\"}" \ - "${{ secrets.REVIEW_CHAT_WEBHOOK }}" - else - echo "Notification is not needed. Skip notification step" - fi + --data-binary @- "$REVIEW_CHAT_WEBHOOK"