This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/doris-skills.git


The following commit(s) were added to refs/heads/main by this push:
     new da8171a  [opt] opt review skill to post comment once psas (#7)
da8171a is described below

commit da8171aa6fbf29753a7307ba8bd2296f2c5a4638
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Mon Aug 17 23:28:25 2026 +0800

    [opt] opt review skill to post comment once psas (#7)
---
 README.md                                          |   6 +-
 skills/doris-repo-review/SKILL.md                  |  80 ++++++-
 .../references/pr-comment-format.md                | 121 ++++++++++
 .../doris-repo-review/scripts/post-pass-comment.sh | 252 +++++++++++++++++++++
 4 files changed, 449 insertions(+), 10 deletions(-)

diff --git a/README.md b/README.md
index 598efc7..0ff7565 100644
--- a/README.md
+++ b/README.md
@@ -34,10 +34,12 @@ skill from its `description`, so in practice you describe 
the problem and the ri
 
 | Skill | What it does | Use it when |
 |---|---|---|
-| [`doris-repo-review`](skills/doris-repo-review/) | Contributor-side PR 
review of a local `apache/doris` clone, running the same flow as the CI Code 
Review Runner: worktree alignment, risk scan, subagent fan-out, shared ledger, 
anchored EN + ZH review documents | Reviewing an `apache/doris` pull request 
locally, the way the pipeline would |
+| [`doris-repo-review`](skills/doris-repo-review/) | Contributor-side PR 
review of a local `apache/doris` clone, running the same flow as the CI Code 
Review Runner: worktree alignment, risk scan, subagent fan-out, shared ledger, 
anchored EN + ZH review documents, and — on a passing review — one 
machine-readable PASS comment on the PR | Reviewing an `apache/doris` pull 
request locally, the way the pipeline would |
 
 These skills read a Doris **source** checkout and never touch a cluster; the 
cluster-side skills
-never touch repository source. Nothing here builds Doris, runs tests, or 
writes to GitHub.
+never touch repository source. Nothing here builds Doris or runs tests. The 
one GitHub write in
+the whole set is `doris-repo-review`'s PASS comment — posted only when the 
review finds no
+`Blocker` and no `Major`, and only after the user has seen the exact body and 
approved it.
 
 ## Install
 
diff --git a/skills/doris-repo-review/SKILL.md 
b/skills/doris-repo-review/SKILL.md
index 6667d22..c460e38 100644
--- a/skills/doris-repo-review/SKILL.md
+++ b/skills/doris-repo-review/SKILL.md
@@ -1,6 +1,6 @@
 ---
 name: doris-repo-review
-description: Given a PR URL (`/doris-repo-review 
https://github.com/apache/doris/pull/66807`), first check whether the current 
directory's branch and commit match that PR, and if they do not, align the 
current directory to the PR head without disturbing local work (refuse to 
switch when tracked files are modified and hand the decision back to the user); 
then review it with the same pipeline apache/doris CI runs (Code Review Runner) 
- main-agent risk scan, 1-3 full-review subagents plus r [...]
+description: Given a PR URL (`/doris-repo-review 
https://github.com/apache/doris/pull/66807`), first check whether the current 
directory's branch and commit match that PR, and if they do not, align the 
current directory to the PR head without disturbing local work (refuse to 
switch when tracked files are modified and hand the decision back to the user); 
then review it with the same pipeline apache/doris CI runs (Code Review Runner) 
- main-agent risk scan, 1-3 full-review subagents plus r [...]
 ---
 
 # Local pipeline-style Doris code review
@@ -35,6 +35,7 @@ $S/align-to-pr.sh <PR> --check                      # 
diagnosis only, changes no
 $S/align-to-pr.sh <PR> --out "$CTX/align.env"       # step 1: align the 
current directory
 $S/prepare-review-context.sh --ctx "$CTX" --align "$CTX/align.env"   # step 2: 
gather context
 python3 $S/verify-anchors.py --ctx "$CTX" --doc <en> --doc <zh>      # step 9: 
verify anchors
+$S/post-pass-comment.sh --ctx "$CTX" --model <id> ... --dry-run      # step 
10: PASS comment
 ```
 
 | File | Purpose |
@@ -42,8 +43,10 @@ python3 $S/verify-anchors.py --ctx "$CTX" --doc <en> --doc 
<zh>      # step 9: v
 | `scripts/align-to-pr.sh` | Resolve the PR, diagnose how the current 
directory relates to it, align it to the PR head |
 | `scripts/prepare-review-context.sh` | Produce the authoritative diff, 
new-side line ranges, required AGENTS.md list, existing comments, ledger 
skeleton |
 | `scripts/verify-anchors.py` | Check that every `path:line` anchor really 
exists and that both documents expose the same finding IDs |
+| `scripts/post-pass-comment.sh` | Render and post the machine-readable PASS 
comment; refuses everything that is not a pass |
 | `references/prompts.md` | Subagent prompt templates (CI wording, carried 
over verbatim) |
 | `references/doc-templates.md` | Templates for both documents, anchor format, 
verdict rule |
+| `references/pr-comment-format.md` | The `doris-repo-review/v1` comment 
schema, field meanings, and how a program reads it back |
 
 Requirements: `git`, an authenticated `gh` CLI, `jq`, and `python3`. The clone 
must have full
 history (`git fetch --unshallow` on a shallow one), because the authoritative 
diff is a three-dot
@@ -59,8 +62,13 @@ diff from the merge base.
    never go hunting for some other directory to work in. Every switch is
    `git checkout --detach`, so branch refs stay exactly where they were.
 2. **Read-only review.** Do not build, do not run tests, do not modify any 
source file in the
-   repository. The only writes allowed are the two documents under 
`review-docs/` and the context
-   directory `$CTX`. **Post nothing to GitHub.**
+   repository. The only local writes allowed are the two documents under 
`review-docs/` and the
+   context directory `$CTX`.
+   **Exactly one thing may ever be written to GitHub**: the PASS comment of 
step 10, only when the
+   verdict is APPROVE, only through `post-pass-comment.sh`, and only after the 
user has seen the
+   rendered body and said go. A REQUEST_CHANGES review posts nothing. No 
inline comments, no
+   review submission, no labels, no edits to the PR body - and never a comment 
on any PR other
+   than the one being reviewed.
 3. **The diff has exactly one source**: `$CTX/pr.diff` and 
`$CTX/pr_changed_files.txt`. Do not
    reach for `gh pr diff`, the web UI, or a hand-rolled `git diff` to get the 
change list - a
    different way of fetching it means a different base.
@@ -312,7 +320,50 @@ unchanged context lines - which is usually where a 
miscomputed line number shows
 
 ---
 
-## 10. Closing report
+## 10. Post the PASS comment to the PR
+
+**Only when the verdict is APPROVE** (no `Blocker`, no `Major`) and 
`verify-anchors.py` has
+passed. A REQUEST_CHANGES review posts nothing at all - say so in the closing 
report and stop.
+
+Write the notes, dry-run, get a go, post:
+
+```bash
+# At most 5 bullets, each anchored where it can be. Skip the file when there 
is nothing to say.
+cat > "$CTX/pr-comment-notes.md" <<'EOF'
+- `fe/fe-core/src/main/java/org/apache/doris/X.java:214` — <what the 
maintainer should know>
+EOF
+
+$S/post-pass-comment.sh --ctx "$CTX" \
+    --model "<exact model id of this session>" --effort 
"${CLAUDE_EFFORT:-unknown}" \
+    --findings <blocker>,<major>,<minor>,<nit> \
+    --rounds <r> --converged <true|false> \
+    --notes-file "$CTX/pr-comment-notes.md" \
+    --dry-run
+```
+
+- **`--model` is the exact model id of the session doing the review** 
(`claude-opus-5[1m]`,
+  `gpt-5.6-sol`, …), taken from what this session was told about itself - 
never a guess, never a
+  bare family name. `--effort` comes from `$CLAUDE_EFFORT`. The comment is a 
public, signed
+  statement about who reviewed the code; both fields are what make it 
auditable.
+- The dry run runs **every** precondition and prints the exact body. **Show 
that body to the user
+  and wait for a go**, then re-run the identical command **without 
`--dry-run`**. Never post
+  without that confirmation.
+- The script refuses to post when: any `Blocker`/`Major` is present; the live 
PR head no longer
+  equals the reviewed commit; the PR is not open (`--allow-closed` overrides); 
the notes are
+  malformed or more than five; `converged: false` came without a note. A 
refusal is a real signal
+  - relay it, do not work around it.
+- **The PR head moved** means the author pushed during the review: the review 
is stale, so re-run
+  the whole skill instead of posting.
+- An earlier v1 comment from the same account for the **same** commit is 
edited in place; a new
+  commit gets a new comment, so each push leaves exactly one record.
+- Findings counts, rounds and `converged` must match the documents written in 
step 9. The counts
+  are of *accepted* findings, not of candidates.
+- `references/pr-comment-format.md` holds the schema, the field meanings, and 
the parser snippet.
+  **Never hand-write or hand-edit this comment** - the format is a contract 
other programs read.
+
+---
+
+## 11. Closing report
 
 Tell the user:
 
@@ -320,15 +371,17 @@ Tell the user:
    severity, and whether the rounds converged.
 2. The `branch check` / `commit check` results - especially `ahead:N` 
(unpushed commits that were
    not reviewed).
-3. **Where the current directory now stands**: with `ALIGN_MODE=switched` it 
is detached on the PR
+3. **What happened to the PASS comment**: the URL when one was posted or 
updated, or the reason
+   nothing was posted (the verdict was REQUEST_CHANGES, the PR head moved, the 
user said no).
+4. **Where the current directory now stands**: with `ALIGN_MODE=switched` it 
is detached on the PR
    head, and `git checkout <PREV_REF>` restores it. **Do not switch back 
automatically** - the user
    may still want to read the code.
-4. `review-docs/` **is not gitignored in the doris repository**, so **do not 
commit it
+5. `review-docs/` **is not gitignored in the doris repository**, so **do not 
commit it
    automatically**; leave that to the user.
 
 ---
 
-## 11. Mapping to the CI pipeline
+## 12. Mapping to the CI pipeline
 
 | CI (code-review-runner.yml) | Local |
 |---|---|
@@ -342,11 +395,12 @@ Tell the user:
 | Single-file ledger with sections | A `ledger/` directory, one file per owner 
|
 | Main risk scan → 1-3 full-review subagents + risk-focused → merge → ≤3 
rounds | Identical |
 | `gh pr review` / Reviews API posting inline comments | **Two `review-docs/` 
documents (EN + ZH) with `path:line` anchors** |
+| CI's review verdict is visible on the PR itself | On a pass, one 
`doris-repo-review/v1` comment from the local account (commit sha, timestamp, 
model, findings, notes); on REQUEST_CHANGES, nothing - the documents stay local 
|
 | 60-minute timeout | No hard timeout, but likewise do not let one round turn 
into unbounded digging |
 
 ---
 
-## 12. Common traps
+## 13. Common traps
 
 - **The local checkout is ahead of the PR head.** `commit check: ahead:N` 
means there are unpushed
   commits and **they are not part of the review**. This is the easiest thing 
for a reader to
@@ -370,3 +424,13 @@ Tell the user:
   dropped.
 - **Do not commit `review-docs/`.** The doris repository does not ignore it, 
and an automatic commit
   would slip it into the PR.
+- **The PASS comment is public and signed with the user's name.** It goes to a 
public Apache PR
+  from their GitHub account, so it is posted only after they have seen the 
exact body. Treat a
+  script refusal as final rather than something to route around, and never 
"tidy up" the rendered
+  body by hand - a program reads it.
+- **A pass is not a merge approval.** The comment states that a local pipeline 
review found no
+  Blocker and no Major on one specific commit. It carries no CI signal and no 
Apache sign-off, and
+  the `<sub>` disclaimer line says exactly that - keep it.
+- **Counts drift between the documents and the comment.** `--findings` must be 
the accepted
+  findings of step 9, not the candidate count from the ledger; re-count from 
the written documents
+  before posting.
diff --git a/skills/doris-repo-review/references/pr-comment-format.md 
b/skills/doris-repo-review/references/pr-comment-format.md
new file mode 100644
index 0000000..a26488e
--- /dev/null
+++ b/skills/doris-repo-review/references/pr-comment-format.md
@@ -0,0 +1,121 @@
+# PASS comment format (`doris-repo-review/v1`)
+
+When a review passes, the skill posts **one comment** on the PR from the 
locally authenticated
+`gh` account. The comment is written by `scripts/post-pass-comment.sh`, never 
typed by hand: the
+agent supplies the numbers and the notes, the script owns the layout. Anything 
that reads the
+comment back — a script, a dashboard, another agent — depends on that layout 
being fixed.
+
+## Layout
+
+Three layers, each with one job:
+
+| Layer | Content | Consumer |
+|---|---|---|
+| HTML markers `<!-- doris-repo-review:v1:begin -->` / `:end` | anchor for 
finding and updating the comment | programs |
+| fenced `yaml` block | every structured field | programs (`yaml.safe_load`), 
and humans |
+| markdown body | status heading, `Notes for maintainers`, disclaimer | humans 
|
+
+````markdown
+<!-- doris-repo-review:v1:begin -->
+### Local pipeline review — ✅ PASS
+
+```yaml
+schema: doris-repo-review/v1
+status: PASS
+pr: apache/doris#66807
+commit: 7f3a91c4e0b25d8a6c1f04b93e27ad5810cf6b42
+base: 0b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293
+reviewed_at: 2026-08-17T21:22+08:00
+reviewer: morningman
+model: claude-opus-5[1m]
+effort: max
+findings: {blocker: 0, major: 0, minor: 2, nit: 1}
+rounds: 2
+converged: true
+```
+
+**Notes for maintainers**
+
+- 
`fe/fe-core/src/main/java/org/apache/doris/mysql/authenticate/ldap/LdapManager.java:214`
 — the
+  empty-password fast path is covered by unit tests only; an LDAP regression 
case is worth adding
+  before this is backported to `branch-3.0`.
+- The behaviour change is gated by `ldap_authentication_enabled`, so no 
rolling-upgrade path is
+  required.
+
+<sub>Reviewed locally with the `doris-repo-review` pipeline (a local port of
+`.github/workflows/code-review-runner.yml`). This is not a CI status 
check.</sub>
+<!-- doris-repo-review:v1:end -->
+````
+
+## Fields
+
+| Field | Type | Source |
+|---|---|---|
+| `schema` | const `doris-repo-review/v1` | the script; bump it if any field 
changes meaning |
+| `status` | `PASS` | the only value ever posted; `CHANGES_REQUESTED` is 
reserved, not used |
+| `pr` | `owner/repo#N` | `meta.env` |
+| `commit` | 40-hex | `HEAD_SHA` — the exact commit reviewed |
+| `base` | 40-hex | `BASE_SHA` — with `commit` this reproduces the reviewed 
diff |
+| `reviewed_at` | ISO-8601, minute precision, with offset | when the comment 
was rendered |
+| `reviewer` | GitHub login | `gh api user`, falling back to GraphQL `viewer` 
and `gh auth status` |
+| `model` | exact model id | passed with `--model`, e.g. `claude-opus-5[1m]`, 
`gpt-5.6-sol` |
+| `effort` | reasoning effort | `--effort`, default `$CLAUDE_EFFORT` |
+| `findings` | inline map | counts per severity; `blocker` and `major` are 
always 0 in a PASS |
+| `rounds` | int | convergence rounds actually run (1-3) |
+| `converged` | bool | `false` means the 3-round cap was hit with candidates 
still open |
+
+Rules the script enforces, so they cannot drift:
+
+- **PASS only.** Any `blocker` or `major` above zero is a `REQUEST_CHANGES` 
review and the script
+  refuses to post — a failing review leaves no trace on GitHub.
+- **The PR head must not have moved.** The live `head.sha` is re-read and must 
equal `commit`,
+  otherwise the comment would sign off a commit nobody reviewed.
+- **`converged: false` requires at least one note** saying what was left 
unexamined.
+- **At most 5 notes**, each a `- ` bullet (2-space indented continuation lines 
allowed). Longer
+  material belongs in the local review documents.
+- **Same commit ⇒ update in place.** An earlier v1 comment by the same account 
carrying the same
+  `commit` is edited via `PATCH`; a different commit gets a new comment, so 
one push leaves one
+  record.
+- **Minute precision on `reviewed_at` is deliberate.** A YAML timestamp needs 
seconds, so
+  `2026-08-17T21:22+08:00` stays a plain string in every parser instead of 
turning into a date
+  object in some of them. Do not add seconds.
+
+## Reading it back
+
+```python
+import re, yaml, json, subprocess
+
+comments = json.loads(subprocess.check_output(
+    ["gh", "api", "repos/apache/doris/issues/66807/comments", "--paginate"]))
+
+for c in comments:
+    m = re.search(r"<!-- doris-repo-review:v1:begin -->.*?```yaml\n(.*?)```", 
c["body"], re.S)
+    if not m:
+        continue
+    d = yaml.safe_load(m.group(1))
+    print(d["status"], d["commit"], d["reviewed_at"], d["model"], d["effort"])
+    # "has the current head been reviewed and passed?"
+    passed = d["status"] == "PASS" and d["commit"] == head_sha
+```
+
+`grep`-level check, when a full parse is overkill:
+
+```bash
+gh api repos/apache/doris/issues/66807/comments --paginate \
+  --jq '.[] | select(.body | test("doris-repo-review:v1:begin")) | .body' \
+  | grep -E '^(status|commit|model|reviewed_at):'
+```
+
+## Notes for maintainers — what belongs there
+
+The comment is not a substitute for the review documents; it is the receipt. A 
note earns its
+place only when a maintainer would act differently without it:
+
+- a residual risk that did not reach `Minor`, with a `path:line` anchor;
+- coverage the review could not reach (no build, no cluster, no test run);
+- a backport or upgrade consideration the PR itself does not state;
+- with `converged: false`, what was still open when the round cap hit.
+
+Not this: restating what the PR does, listing every `Minor`/`Nit` (they live 
in the documents),
+praise, or anything that reads as an official Apache sign-off. `_None._` is a 
perfectly good
+notes section.
diff --git a/skills/doris-repo-review/scripts/post-pass-comment.sh 
b/skills/doris-repo-review/scripts/post-pass-comment.sh
new file mode 100755
index 0000000..fcfd6a0
--- /dev/null
+++ b/skills/doris-repo-review/scripts/post-pass-comment.sh
@@ -0,0 +1,252 @@
+#!/usr/bin/env bash
+# Render and post the machine-readable PASS comment of a doris-repo-review run.
+#
+# Usage:
+#   post-pass-comment.sh --ctx <dir> --model <id> [options]
+#
+#   --ctx <dir>            review context directory (must contain meta.env)
+#   --model <id>           exact model id of the reviewing agent, e.g. 
claude-opus-5[1m]
+#   --effort <s>           reasoning effort (default: $CLAUDE_EFFORT, else 
"unknown")
+#   --findings b,m,mi,n    blocker,major,minor,nit counts (default 0,0,0,0)
+#   --rounds <n>           convergence rounds actually run (default 1)
+#   --converged true|false (default true)
+#   --notes-file <f>       markdown bullet list for "Notes for maintainers"
+#   --dry-run              run every precondition, render the body, post 
nothing
+#   --force-new            always create a new comment, never update in place
+#   --allow-closed         allow posting on a non-open PR
+#
+# Only a PASS is ever posted: blocker and major counts must both be 0, which is
+# the same verdict rule the review documents use. The body layout is fixed here
+# on purpose - the agent supplies the notes, never the format.
+#
+# Rendered body: <ctx>/pr-comment.md   Posted URL: <ctx>/pr-comment.url
+set -euo pipefail
+
+CTX=""
+MODEL=""
+EFFORT="${CLAUDE_EFFORT:-unknown}"
+FINDINGS="0,0,0,0"
+ROUNDS="1"
+CONVERGED="true"
+NOTES_FILE=""
+DRY_RUN=0
+FORCE_NEW=0
+ALLOW_CLOSED=0
+
+while [ $# -gt 0 ]; do
+    case "$1" in
+        --ctx)          CTX="$2"; shift 2 ;;
+        --model)        MODEL="$2"; shift 2 ;;
+        --effort)       EFFORT="$2"; shift 2 ;;
+        --findings)     FINDINGS="$2"; shift 2 ;;
+        --rounds)       ROUNDS="$2"; shift 2 ;;
+        --converged)    CONVERGED="$2"; shift 2 ;;
+        --notes-file)   NOTES_FILE="$2"; shift 2 ;;
+        --dry-run)      DRY_RUN=1; shift ;;
+        --force-new)    FORCE_NEW=1; shift ;;
+        --allow-closed) ALLOW_CLOSED=1; shift ;;
+        -h|--help)      sed -n '2,25p' "$0"; exit 0 ;;
+        *) echo "unknown argument: $1" >&2; exit 2 ;;
+    esac
+done
+
+BEGIN_MARKER='<!-- doris-repo-review:v1:begin -->'
+END_MARKER='<!-- doris-repo-review:v1:end -->'
+SCHEMA='doris-repo-review/v1'
+
+# ------------------------------------------------------------------ 
preconditions
+[ -n "$CTX" ] || { echo "ERROR: --ctx is required." >&2; exit 2; }
+META="$CTX/meta.env"
+[ -f "$META" ] || { echo "ERROR: $META not found - run 
prepare-review-context.sh first." >&2; exit 2; }
+[ -n "$MODEL" ] || { echo "ERROR: --model is required; state the exact model 
id, never a guess." >&2; exit 2; }
+command -v gh >/dev/null 2>&1 || { echo "ERROR: gh CLI is required." >&2; exit 
1; }
+command -v jq >/dev/null 2>&1 || { echo "ERROR: jq is required." >&2; exit 1; }
+command -v python3 >/dev/null 2>&1 || { echo "ERROR: python3 is required." 
>&2; exit 1; }
+
+read_meta() { sed -n "s/^$1=//p" "$META" | head -n 1; }
+UPSTREAM_REPO="$(read_meta UPSTREAM_REPO)"
+PR_NUMBER="$(read_meta PR_NUMBER)"
+PR_URL="$(read_meta PR_URL)"
+BASE_SHA="$(read_meta BASE_SHA)"
+HEAD_SHA="$(read_meta HEAD_SHA)"
+REPO_ROOT="$(read_meta REPO_ROOT)"
+
+[ -n "$PR_NUMBER" ] || { echo "ERROR: meta.env has no PR_NUMBER - this review 
is not attached to a PR." >&2; exit 2; }
+[ -n "$UPSTREAM_REPO" ] || { echo "ERROR: meta.env has no UPSTREAM_REPO." >&2; 
exit 2; }
+[ -n "$PR_URL" ] || 
PR_URL="https://github.com/${UPSTREAM_REPO}/pull/${PR_NUMBER}";
+
+case "$CONVERGED" in true|false) ;; *) echo "ERROR: --converged takes true or 
false." >&2; exit 2 ;; esac
+[[ "$ROUNDS" =~ ^[0-9]+$ ]] || { echo "ERROR: --rounds takes a number." >&2; 
exit 2; }
+IFS=',' read -r F_BLOCKER F_MAJOR F_MINOR F_NIT <<<"$FINDINGS"
+for v in "$F_BLOCKER" "$F_MAJOR" "$F_MINOR" "$F_NIT"; do
+    [[ "$v" =~ ^[0-9]+$ ]] || { echo "ERROR: --findings takes 
blocker,major,minor,nit as four numbers." >&2; exit 2; }
+done
+
+# The verdict rule: any Blocker or Major means REQUEST_CHANGES, and a
+# REQUEST_CHANGES review posts nothing at all.
+if [ "$F_BLOCKER" -gt 0 ] || [ "$F_MAJOR" -gt 0 ]; then
+    echo "ERROR: $F_BLOCKER blocker(s) and $F_MAJOR major(s) mean 
REQUEST_CHANGES." >&2
+    echo "       This skill posts a comment only when the review passes. 
Nothing was posted." >&2
+    exit 1
+fi
+
+# ------------------------------------------------------------------------- 
notes
+NOTES_BODY="_None._"
+NOTE_COUNT=0
+if [ -n "$NOTES_FILE" ]; then
+    [ -f "$NOTES_FILE" ] || { echo "ERROR: --notes-file not found: 
$NOTES_FILE" >&2; exit 2; }
+    if grep -q . "$NOTES_FILE"; then
+        if grep -q 'doris-repo-review:v1' "$NOTES_FILE"; then
+            echo "ERROR: the notes file contains a v1 marker; that would break 
comment detection." >&2
+            exit 2
+        fi
+        while IFS= read -r line; do
+            [ -n "${line//[[:space:]]/}" ] || continue
+            case "$line" in
+                "- "*) NOTE_COUNT=$((NOTE_COUNT + 1)) ;;
+                "  "*) : ;;   # continuation of the previous bullet
+                *) echo "ERROR: every note line must be a '- ' bullet or a 
2-space continuation: $line" >&2; exit 2 ;;
+            esac
+        done < "$NOTES_FILE"
+        [ "$NOTE_COUNT" -le 5 ] || { echo "ERROR: at most 5 notes; got 
$NOTE_COUNT. Keep the rest in the review document." >&2; exit 2; }
+        NOTES_BODY="$(cat "$NOTES_FILE")"
+    fi
+fi
+if [ "$CONVERGED" = "false" ] && [ "$NOTE_COUNT" -eq 0 ]; then
+    echo "ERROR: converged=false needs at least one note saying what was left 
open." >&2
+    exit 2
+fi
+
+# ------------------------------------------------------------------ live PR 
state
+PR_TSV="$(gh api "repos/${UPSTREAM_REPO}/pulls/${PR_NUMBER}" --jq '[.head.sha, 
.state] | @tsv')" || {
+    echo "ERROR: cannot read ${UPSTREAM_REPO}#${PR_NUMBER}." >&2; exit 1; }
+IFS=$'\t' read -r LIVE_HEAD_SHA LIVE_STATE <<<"$PR_TSV"
+
+if [ "$LIVE_HEAD_SHA" != "$HEAD_SHA" ]; then
+    echo "ERROR: the PR head moved since the review." >&2
+    echo "       reviewed : $HEAD_SHA" >&2
+    echo "       PR now at: $LIVE_HEAD_SHA" >&2
+    echo "       Posting would sign off a commit that was never reviewed. 
Re-run the review." >&2
+    exit 1
+fi
+if [ "$LIVE_STATE" != "open" ] && [ "$ALLOW_CLOSED" = "0" ]; then
+    echo "ERROR: ${UPSTREAM_REPO}#${PR_NUMBER} is '$LIVE_STATE', not open. 
Pass --allow-closed to post anyway." >&2
+    exit 1
+fi
+
+if [ -n "$REPO_ROOT" ] && [ -d "$REPO_ROOT/.git" ]; then
+    LOCAL_HEAD="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || true)"
+    if [ -n "$LOCAL_HEAD" ] && [ "$LOCAL_HEAD" != "$HEAD_SHA" ]; then
+        echo "NOTE: $REPO_ROOT is no longer parked on the reviewed commit (now 
$LOCAL_HEAD)." >&2
+        echo "      That is fine if you switched back after the review; the 
comment still refers to $HEAD_SHA." >&2
+    fi
+fi
+
+# Three ways to name the posting account: the REST /user endpoint has been 
seen answering 503
+# while the rest of the API is healthy, and on an error gh still prints the 
error JSON on stdout,
+# so each candidate is validated as a real login before it is accepted.
+try_login() {
+    local out
+    out="$("$@" 2>/dev/null || true)"
+    out="$(printf '%s' "$out" | tr -d '[:space:]')"
+    [[ "$out" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,38}$ ]] || return 1
+    printf '%s' "$out"
+}
+auth_status_login() {
+    gh auth status 2>&1 | sed -n 's/.*Logged in to github.com account \([^ 
]*\).*/\1/p' | head -n 1
+}
+REVIEWER="$(try_login gh api user --jq .login || true)"
+[ -n "$REVIEWER" ] || REVIEWER="$(try_login gh api graphql -f 
"query={viewer{login}}" --jq .data.viewer.login || true)"
+[ -n "$REVIEWER" ] || REVIEWER="$(try_login auth_status_login || true)"
+[ -n "$REVIEWER" ] || { echo "ERROR: cannot determine which GitHub account gh 
is logged in as." >&2; exit 1; }
+REVIEWED_AT="$(python3 -c "import datetime; 
print(datetime.datetime.now().astimezone().isoformat(timespec='minutes'))")"
+
+# ------------------------------------------------------------------ render 
body
+FENCE='```'
+DISCLAIMER='<sub>Reviewed locally with the `doris-repo-review` pipeline (a 
local port of `.github/workflows/code-review-runner.yml`). This is not a CI 
status check.</sub>'
+BODY_FILE="$CTX/pr-comment.md"
+cat > "$BODY_FILE" <<EOF
+${BEGIN_MARKER}
+### Local pipeline review — ✅ PASS
+
+${FENCE}yaml
+schema: ${SCHEMA}
+status: PASS
+pr: ${UPSTREAM_REPO}#${PR_NUMBER}
+commit: ${HEAD_SHA}
+base: ${BASE_SHA}
+reviewed_at: ${REVIEWED_AT}
+reviewer: ${REVIEWER}
+model: ${MODEL}
+effort: ${EFFORT}
+findings: {blocker: ${F_BLOCKER}, major: ${F_MAJOR}, minor: ${F_MINOR}, nit: 
${F_NIT}}
+rounds: ${ROUNDS}
+converged: ${CONVERGED}
+${FENCE}
+
+**Notes for maintainers**
+
+${NOTES_BODY}
+
+${DISCLAIMER}
+${END_MARKER}
+EOF
+
+# ------------------------------------------- find an earlier comment of this 
tool
+# Own comments only - another account's comment cannot be edited anyway. A 
comment for THIS
+# commit wins wherever it sits in the thread; otherwise the newest one is kept 
just to report
+# which commit the previous run signed off on.
+SAME_ID=""
+LAST_ID=""
+LAST_COMMIT=""
+while IFS=$'\t' read -r cid clogin ccommit; do
+    [ -n "$cid" ] || continue
+    [ "$clogin" = "$REVIEWER" ] || continue
+    ccommit="${ccommit#commit: }"
+    LAST_ID="$cid"
+    LAST_COMMIT="$ccommit"
+    [ "$ccommit" = "$HEAD_SHA" ] && SAME_ID="$cid"
+done < <(gh api "repos/${UPSTREAM_REPO}/issues/${PR_NUMBER}/comments" 
--paginate \
+            --jq '.[] | select(.body | test("doris-repo-review:v1:begin")) | [ 
.id, .user.login, ([.body | scan("commit: [0-9a-f]{40}")] | .[0] // "") ] | 
@tsv' 2>/dev/null || true)
+
+ACTION="create"
+EXISTING_ID=""
+if [ -n "$SAME_ID" ] && [ "$FORCE_NEW" = "0" ]; then
+    ACTION="update"
+    EXISTING_ID="$SAME_ID"
+fi
+
+echo "=== doris-repo-review PASS comment ==="
+echo "pr        : ${PR_URL}"
+echo "commit    : ${HEAD_SHA}"
+echo "reviewer  : ${REVIEWER}"
+echo "model     : ${MODEL} (effort ${EFFORT})"
+echo "findings  : blocker=${F_BLOCKER} major=${F_MAJOR} minor=${F_MINOR} 
nit=${F_NIT}"
+echo "notes     : ${NOTE_COUNT}"
+if [ "$ACTION" = "update" ]; then
+    echo "action    : UPDATE the existing comment ${EXISTING_ID} (same commit)"
+elif [ -n "$LAST_ID" ]; then
+    echo "action    : CREATE a new comment (the previous one, ${LAST_ID}, was 
for ${LAST_COMMIT:-an unknown commit})"
+else
+    echo "action    : CREATE a new comment"
+fi
+echo "body file : ${BODY_FILE}"
+echo 
"--------------------------------------------------------------------------"
+cat "$BODY_FILE"
+echo 
"--------------------------------------------------------------------------"
+
+if [ "$DRY_RUN" = "1" ]; then
+    echo "dry run - nothing was posted."
+    exit 0
+fi
+
+if [ "$ACTION" = "update" ]; then
+    RESP="$(jq -n --rawfile body "$BODY_FILE" '{body: $body}' \
+        | gh api --method PATCH 
"repos/${UPSTREAM_REPO}/issues/comments/${EXISTING_ID}" --input -)"
+else
+    RESP="$(jq -n --rawfile body "$BODY_FILE" '{body: $body}' \
+        | gh api --method POST 
"repos/${UPSTREAM_REPO}/issues/${PR_NUMBER}/comments" --input -)"
+fi
+COMMENT_URL="$(printf '%s' "$RESP" | jq -r '.html_url')"
+printf '%s\n' "$COMMENT_URL" > "$CTX/pr-comment.url"
+echo "posted    : ${COMMENT_URL}"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to