mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
Replace the static comment-pending + comment-results two-job pattern
with a live-updating comment system that polls the GitHub Actions API
every 15s, re-assembles the review comment from whatever results are
available, and upserts it via the <!-- hermes-ci-review-bot --> marker.
The comment updates in real time as each job finishes — no waiting for
the full pipeline.
Every CI job that wants to appear in the review comment emits a
review_status output — a JSON array of objects, each with a source
and a results array:
[
{
"source": "review-label-gate",
"results": [
{"kind": "action_required", "title": "...", "summary": "...",
"how_to_fix": "..."},
{"kind": "info", "title": "...", "summary": "..."}
]
},
{
"source": "ci timing",
"results": [
{"kind": "warning", "title": "CI timings", "summary": "...",
"detail": "...", "link": "..."}
]
}
]
One job can emit multiple results of different kinds. The source field
is used to exclude the corresponding job from the synthesized error
list (case-insensitive, hyphen-normalized matching against GitHub
Actions job display names).
| job | source | kind (on failure) | section |
|----------------------------|--------------------------|---------------------------|----------------------|
| review-labels | review label gate | action_required / info | Action required |
| lockfile-diff | lockfile-diff | action_required | Action required |
| ci-timings | ci timing | warning / info | Warnings |
| supply-chain scan | supply chain | error / (none) | Job failures |
| supply-chain dep-bounds | supply chain | action_required / (none) | Action required |
| osv-scanner | osv scan | warning / (none) | Warnings |
| uv-lockfile-check | uv.lock check | action_required / (none) | Action required |
| history-check | unrelated histories | action_required | Action required |
| contributor-check | contributor attribution | action_required | Action required |
Jobs that find nothing emit [] (empty array) — no noise info items.
A single comment-live job polls the GitHub Actions API every 15s,
classifies jobs into (completed, pending), assembles the comment, and
upserts it. Merges review_status outputs from all needs jobs via
toJSON(needs.*.outputs.review_status), and downloads the ci-timings
artifact when it becomes available. Shows commit SHA + message below
the header.
The assembler has ZERO job-specific knowledge. It just:
1. collect_from_statuses() — flattens all nested status objects into ReviewItems
2. collect_failed_jobs() — synthesizes errors for failed jobs with no declared status
3. _attach_job_urls() — fills in per-job log links for ALL items
4. render_comment() — groups by severity, renders with group headers
Each item shows links inline next to the title: View report (job-emitted
URL) and View job (auto-attached logs link). Each info item is its own
collapsible <details> block.
# ૮ >ﻌ< ა ci review
running on abc1234 — commit message first line
## ❌ Job failures
### {title} · [View job](url)
{summary}
## ⚠️ Action required
### {title} · [View job](url)
{summary}
**How to fix:**
{how_to_fix}
## ⚠️ Warnings
### {title} · [View report](url) · [View job](url)
{summary}
{detail}
<details><summary>{title}</summary>
{content}
</details>
Still running 3 jobs: ci-timings, docker
- test_assemble_review_comment.py (48 tests): collect_from_statuses,
collect_failed_jobs with exclude_sources, _attach_job_urls,
render_comment (group headers, inline links, commit info, per-item
details, pending footer), assemble integration
- test_live_comment.py (16 tests): classify_jobs pure function
- test_timings_report.py (10 tests): generate_review_status nested format
- test_lockfile_diff.py (6 tests)
- test_classify_changes.py (32 tests, pre-existing)
132 lines
6.1 KiB
YAML
132 lines
6.1 KiB
YAML
name: uv.lock check
|
|
|
|
# Verify uv.lock is in sync with pyproject.toml. Blocking check — PRs
|
|
# that modify pyproject.toml without regenerating uv.lock (or vice versa)
|
|
# must not merge, because the Docker build's `uv sync --frozen` step will
|
|
# fail on a stale lockfile and we'd rather catch it here than in the
|
|
# docker workflow on main.
|
|
#
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# IMPORTANT: this check runs against the MERGED state, not just your branch
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
#
|
|
# For `pull_request` events, GitHub checks out `refs/pull/<N>/merge` by
|
|
# default — a synthetic commit that merges your PR branch into the CURRENT
|
|
# state of `main`. That means the pyproject.toml evaluated here is
|
|
# `main's pyproject.toml + your PR's changes to pyproject.toml`, not just
|
|
# what's on your branch.
|
|
#
|
|
# Failure mode this creates: if `main` has advanced since you branched
|
|
# (e.g. someone merged a PR that added a dep to pyproject.toml + its
|
|
# corresponding uv.lock entries), your branch's uv.lock is missing those
|
|
# new entries. `uv lock --check` resolves against the merged pyproject
|
|
# and sees a lockfile that doesn't cover all the current deps → fails
|
|
# with "The lockfile at uv.lock needs to be updated."
|
|
#
|
|
# This can be confusing: `uv lock --check` passes locally (your branch
|
|
# is internally consistent) but fails in CI (merged state isn't).
|
|
#
|
|
# Fix is to sync your branch with main and regenerate the lockfile:
|
|
#
|
|
# git fetch origin main
|
|
# git rebase origin/main # or merge, whatever the repo prefers
|
|
# uv lock # regenerates uv.lock against new pyproject.toml
|
|
# git add uv.lock
|
|
# git commit -m "chore: refresh uv.lock after rebase onto main"
|
|
# git push --force-with-lease # if you rebased
|
|
#
|
|
# If you also changed pyproject.toml in your PR, `uv lock` handles that
|
|
# at the same time — one regeneration covers both your changes and the
|
|
# drift from main.
|
|
#
|
|
# This is the correct behavior! The check is protecting main's Docker
|
|
# build: a post-merge build would see the same merged state and fail
|
|
# the same way. Better to catch it here than after merge.
|
|
|
|
on:
|
|
workflow_call:
|
|
outputs:
|
|
review_status:
|
|
description: "JSON review status for the review-status aggregator"
|
|
value: ${{ jobs.check.outputs.review_status }}
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: uv-lockfile-check-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
check:
|
|
name: uv lock --check
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
outputs:
|
|
review_status: ${{ steps.verify.outputs.review_status }}
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Install uv
|
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0
|
|
|
|
# `uv lock --check` re-resolves the project from pyproject.toml and
|
|
# compares the result to uv.lock, exiting non-zero if they disagree.
|
|
# No network writes, no file modifications.
|
|
#
|
|
# On PRs this runs against the merge commit (see comment at the top
|
|
# of this file) — failures often mean "your branch is behind main,
|
|
# rebase and regenerate uv.lock."
|
|
- name: Verify uv.lock is up-to-date
|
|
id: verify
|
|
run: |
|
|
# uv lock --check re-resolves against PyPI (network). Retry so a
|
|
# registry blip doesn't read as "lockfile stale". A genuinely stale
|
|
# lockfile fails all attempts (deterministic), costing only seconds.
|
|
ok=false
|
|
for i in 1 2 3; do
|
|
if uv lock --check; then
|
|
ok=true
|
|
break
|
|
fi
|
|
[ "$i" = 3 ] && break
|
|
echo "::warning::uv lock --check failed (attempt $i); retrying in 10s"
|
|
sleep 10
|
|
done
|
|
if [ "$ok" != true ]; then
|
|
cat <<'EOF' >> "$GITHUB_STEP_SUMMARY"
|
|
## ❌ uv.lock is out of sync with pyproject.toml
|
|
|
|
**If this is a PR:** this check runs against the merged state
|
|
(your branch + current `main`), not just your branch. If
|
|
`uv lock --check` passes locally, your branch is likely behind
|
|
`main` — recent changes to `pyproject.toml` on `main` aren't
|
|
reflected in your branch's `uv.lock` yet.
|
|
|
|
To fix, sync with main and regenerate the lockfile:
|
|
|
|
```bash
|
|
git fetch origin main
|
|
git rebase origin/main # or `git merge origin/main`
|
|
uv lock # regenerate against new pyproject.toml
|
|
git add uv.lock
|
|
git commit -m "chore: refresh uv.lock after syncing with main"
|
|
git push --force-with-lease # drop --force-with-lease if you merged
|
|
```
|
|
|
|
**If you only changed pyproject.toml:** run `uv lock` locally
|
|
and commit the result.
|
|
|
|
This check is blocking because the Docker image build uses
|
|
`uv sync --frozen --extra all`, which rejects stale lockfiles
|
|
— catching it here avoids a ~15 min failed docker run
|
|
on `main` post-merge.
|
|
EOF
|
|
echo "::error title=uv.lock out of sync::Run \`uv lock\` locally and commit the result. If on a PR, sync with main first."
|
|
review_status='[{"source":"uv.lock check","results":[{"kind":"action_required","title":"uv.lock out of sync","summary":"uv.lock is out of sync with pyproject.toml.","how_to_fix":"Run `uv lock` locally and commit the result. If on a PR, sync with main first:\n```\ngit fetch origin main\ngit rebase origin/main\nuv lock\ngit add uv.lock\ngit commit -m \"chore: refresh uv.lock\"\n```\n"}]}]'
|
|
echo "review_status=${review_status}" >> "$GITHUB_OUTPUT"
|
|
exit 1
|
|
fi
|
|
review_status='[]'
|
|
echo "review_status=${review_status}" >> "$GITHUB_OUTPUT"
|