From a62731bd6e0d2b52f9a8165a764c0fe43d7d457c Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 22 Jul 2026 10:21:11 +0300 Subject: [PATCH] feat: publish review check on reviewed HEAD Adds a dedicated `review` check for the exact pull request HEAD Keeps manual review commands reported separately from the default branch job Documents the new review check behavior in contributing guidelines --- .github/workflows/pr-review.yml | 135 +++++++++++++++++++++++++++++--- CONTRIBUTING.md | 5 ++ 2 files changed, 130 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index edeb5caf..b04d54ae 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -17,12 +17,16 @@ concurrency: jobs: review: + # Manual comment workflows run on the default branch, so their native job + # check cannot represent the reviewed PR HEAD. Publish that check explicitly. + name: automation if: | github.event_name == 'pull_request_target' || (github.event_name == 'issue_comment' && github.event.issue.pull_request && github.event.comment.user.login != 'openchamber-bot[bot]' && (github.event.comment.body == '/oc-review' || startsWith(github.event.comment.body, '/oc-review ') || github.event.comment.body == '@openchamber-bot review' || startsWith(github.event.comment.body, '@openchamber-bot review '))) || (github.event_name == 'pull_request_review_comment' && github.event.comment.user.login != 'openchamber-bot[bot]' && (github.event.comment.body == '/oc-review' || startsWith(github.event.comment.body, '/oc-review ') || github.event.comment.body == '@openchamber-bot review' || startsWith(github.event.comment.body, '@openchamber-bot review '))) runs-on: ubuntu-latest permissions: + checks: write contents: read pull-requests: write issues: write @@ -32,21 +36,17 @@ jobs: with: fetch-depth: 1 - - name: Generate review app token - id: app-token - uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 - with: - app-id: ${{ secrets.OC_REVIEW_APP_ID }} - private-key: ${{ secrets.OC_REVIEW_APP_PRIVATE_KEY }} - - name: Resolve pull request context id: pr env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_TOKEN: ${{ github.token }} EVENT_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} run: | pr_json="$(gh pr view "$EVENT_PR_NUMBER" --json number,url,author,baseRefName,headRefName,headRefOid,headRepositoryOwner,isDraft)" - echo "number=$(printf '%s' "$pr_json" | jq -r '.number')" >> "$GITHUB_OUTPUT" + { + echo "number=$(printf '%s' "$pr_json" | jq -r '.number')" + echo "head_sha=$(printf '%s' "$pr_json" | jq -r '.headRefOid')" + } >> "$GITHUB_OUTPUT" if [ "$(printf '%s' "$pr_json" | jq -r '.isDraft')" = "true" ]; then echo "draft=true" >> "$GITHUB_OUTPUT" @@ -59,10 +59,43 @@ jobs: echo "author=$(printf '%s' "$pr_json" | jq -r '.author.login')" echo "base_ref=$(printf '%s' "$pr_json" | jq -r '.baseRefName')" echo "head_ref=$(printf '%s' "$pr_json" | jq -r '.headRefName')" - echo "head_sha=$(printf '%s' "$pr_json" | jq -r '.headRefOid')" echo "head_repo_owner=$(printf '%s' "$pr_json" | jq -r '.headRepositoryOwner.login')" } >> "$GITHUB_OUTPUT" + - name: Start review check + id: review-check + env: + GH_TOKEN: ${{ github.token }} + REVIEW_HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + run: | + check_run="$(jq -n \ + --arg head_sha "$REVIEW_HEAD_SHA" \ + --arg details_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + '{ + name: "review", + head_sha: $head_sha, + status: "in_progress", + details_url: $details_url, + output: { + title: "OpenChamber review in progress", + summary: "Reviewing the current pull request HEAD." + } + }')" + + check_id="$(printf '%s' "$check_run" | gh api \ + --method POST \ + "repos/${GITHUB_REPOSITORY}/check-runs" \ + --input - \ + --jq '.id')" + echo "id=$check_id" >> "$GITHUB_OUTPUT" + + - name: Generate review app token + id: app-token + uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 + with: + app-id: ${{ secrets.OC_REVIEW_APP_ID }} + private-key: ${{ secrets.OC_REVIEW_APP_PRIVATE_KEY }} + - name: Clear review status for draft if: steps.pr.outputs.draft == 'true' env: @@ -246,6 +279,7 @@ jobs: Required reviewed HEAD: $REVIEW_HEAD_SHA" - name: Verify and enforce review verdict + id: verdict if: always() && steps.pr.outputs.draft == 'false' && steps.safety.outputs.safe == 'true' env: GH_TOKEN: ${{ steps.app-token.outputs.token }} @@ -366,3 +400,84 @@ jobs: echo "Review verdict is $verdict; only pass satisfies this check." >&2 exit 1 fi + + - name: Publish review check + if: always() && steps.review-check.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + CHECK_RUN_ID: ${{ steps.review-check.outputs.id }} + PR_NUMBER: ${{ steps.pr.outputs.number }} + REVIEW_HEAD_SHA: ${{ steps.pr.outputs.head_sha }} + DRAFT: ${{ steps.pr.outputs.draft }} + VERDICT_OUTCOME: ${{ steps.verdict.outcome }} + run: | + current_head="$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid')" + review_label="$(gh pr view "$PR_NUMBER" --json labels --jq '[.labels[].name | select(startswith("review:"))] | first // ""')" + + set_review_status() { + local target_label="$1" + local remove_args=() + + while IFS= read -r label; do + case "$label" in + review:*) remove_args+=(--remove-label "$label") ;; + esac + done < <(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name') + + if [ -n "$target_label" ]; then + gh pr edit "$PR_NUMBER" "${remove_args[@]}" --add-label "$target_label" + elif [ "${#remove_args[@]}" -gt 0 ]; then + gh pr edit "$PR_NUMBER" "${remove_args[@]}" + fi + + review_label="$target_label" + } + + if [ "$current_head" = "$REVIEW_HEAD_SHA" ]; then + if [ "$DRAFT" = "true" ]; then + set_review_status "" + elif [ "$VERDICT_OUTCOME" != "success" ]; then + case "$review_label" in + review:needs-evidence|review:blocked|review:human-required|review:automation-failed) ;; + *) set_review_status "review:automation-failed" ;; + esac + fi + fi + + if [ "$current_head" != "$REVIEW_HEAD_SHA" ]; then + conclusion="cancelled" + title="Review superseded by a newer HEAD" + summary="The pull request HEAD moved before this review completed." + elif [ "$DRAFT" = "true" ]; then + conclusion="neutral" + title="Review skipped for draft pull request" + summary="Mark the pull request ready for review to start the readiness check." + elif [ "$VERDICT_OUTCOME" = "success" ] && [ "$review_label" = "review:ready" ]; then + conclusion="success" + title="OpenChamber review passed" + summary="The reviewed HEAD is ready for maintainer review." + else + conclusion="failure" + title="OpenChamber review did not pass" + summary="Current readiness state: ${review_label:-review:automation-failed}." + fi + + check_run="$(jq -n \ + --arg conclusion "$conclusion" \ + --arg title "$title" \ + --arg summary "$summary" \ + --arg details_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + '{ + status: "completed", + conclusion: $conclusion, + details_url: $details_url, + output: { + title: $title, + summary: $summary + } + }')" + + printf '%s' "$check_run" | gh api \ + --method PATCH \ + "repos/${GITHUB_REPOSITORY}/check-runs/${CHECK_RUN_ID}" \ + --input - >/dev/null diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b8ed365..f746f3e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -219,6 +219,11 @@ the previous readiness label before it starts, and only `review:ready` means the pull request is ready to enter the maintainer review queue. Draft pull requests have no readiness label. +The workflow publishes a separate `review` check on the exact reviewed HEAD; +only `review:ready` passes it. The `automation` job reports workflow execution +independently so a manual `/oc-review` result can update readiness without +being attached to the default-branch commit that triggered the command. + Each completed review creates a new comment tied to its reviewed HEAD so the conversation remains chronological. Previous review comments are not rewritten.