mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
ci: poll review statuses from artifacts dynamically
The live comment poller previously got its review status payloads from two sources: (1) REVIEW_STATUSES env var, frozen at comment-live job start from needs.*.outputs.review_status, and (2) a single ci-timings artifact fetched at the end. This meant status details (error messages, action_required items, etc.) only appeared in the comment after all jobs finished, even though job pass/fail was visible in real-time. Now every status-producing workflow_call uploads a small review-status artifact (review-status-<name>) as soon as it completes. The poller enumerates all review-status-* artifacts across the orchestrator run and all sub-workflow runs every cycle, downloads each, and merges them into the comment. Statuses appear as soon as each job finishes, not just at the end. Changes: - live_comment.py: replace _fetch_artifact_statuses (single artifact via gh CLI) with fetch_all_review_statuses (enumerate all review-status-* artifacts via API across all runs, download + parse each). Remove review_statuses_json parameter and --review-statuses- file CLI arg. Remove subprocess import (no longer shells out to gh). - ci.yml: remove REVIEW_STATUSES env var, inline Python merger, and --review-statuses-file arg from the comment-live step. Rename ci-timings-review-status artifact to review-status-ci-timings. - 8 workflow_call files: add a write review-status.json + upload artifact step after each review_status output is produced. - test_live_comment.py: add tests for _parse_status_file (with/without prefix, empty, invalid, nonexistent, non-list) and _merge_statuses.
This commit is contained in:
parent
18481742ee
commit
7144eb4900
11 changed files with 352 additions and 108 deletions
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
|
|
@ -196,47 +196,10 @@ jobs:
|
|||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message }}
|
||||
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/pull/${{ github.event.pull_request.number }}/commits/${{ github.event.pull_request.head.sha }}
|
||||
# Structured review statuses from workflow_call jobs.
|
||||
# Each job outputs a JSON array of {source, results: [...]} objects
|
||||
# that the assembler renders directly — no hardcoded job-name
|
||||
# matching. We merge all available outputs into one array.
|
||||
REVIEW_STATUSES: ${{ toJSON(needs.*.outputs.review_status) }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
|
||||
# REVIEW_STATUSES is a JSON array of strings (some may be empty
|
||||
# when a job was skipped). Parse each string and merge into one
|
||||
# flat array for the assembler.
|
||||
python3 - <<'PYEOF'
|
||||
import json, os, sys
|
||||
|
||||
raw = os.environ.get("REVIEW_STATUSES", "")
|
||||
merged = []
|
||||
if raw:
|
||||
try:
|
||||
arr = json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
arr = []
|
||||
for item in arr:
|
||||
if not item:
|
||||
continue
|
||||
try:
|
||||
statuses = json.loads(item)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
continue
|
||||
if isinstance(statuses, list):
|
||||
merged.extend(statuses)
|
||||
|
||||
# Write merged array to a temp file the poller reads.
|
||||
with open("/tmp/review_statuses.json", "w") as f:
|
||||
json.dump(merged, f)
|
||||
print(f"Merged {len(merged)} review status entries")
|
||||
PYEOF
|
||||
|
||||
python3 scripts/ci/live_comment.py \
|
||||
--interval 15 \
|
||||
--timeout 2100 \
|
||||
--review-statuses-file /tmp/review_statuses.json
|
||||
--timeout 2100
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Gate: runs after everything. ``if: always()`` ensures it reports a
|
||||
|
|
@ -304,8 +267,9 @@ jobs:
|
|||
# report with a gantt chart + per-step breakdown. The report is uploaded
|
||||
# as an artifact and a markdown summary is written to $GITHUB_STEP_SUMMARY.
|
||||
#
|
||||
# The live comment poller can read the standalone review-status artifact
|
||||
# after the HTML report is uploaded, so its link points straight at that report.
|
||||
# The live comment poller dynamically fetches all review-status-* artifacts
|
||||
# across the orchestrator and sub-workflow runs every cycle, so its link
|
||||
# points straight at that report.
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
ci-timings:
|
||||
name: CI timing report
|
||||
|
|
@ -364,7 +328,7 @@ jobs:
|
|||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ci-timings-review-status
|
||||
name: review-status-ci-timings
|
||||
path: review-status.json
|
||||
retention-days: 14
|
||||
|
||||
|
|
|
|||
15
.github/workflows/contributor-check.yml
vendored
15
.github/workflows/contributor-check.yml
vendored
|
|
@ -33,6 +33,7 @@ jobs:
|
|||
if [ -z "$NEW_EMAILS" ]; then
|
||||
echo "No new commits to check."
|
||||
echo "review_status=[]" >> "$GITHUB_OUTPUT"
|
||||
echo "review_status=[]" > review-status.json
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
|
@ -84,8 +85,22 @@ jobs:
|
|||
--arg how_to_fix "$HOW_TO_FIX" \
|
||||
'[{"source":"contributor attribution","results":[{"kind":"action_required","title":"Unmapped contributor email(s)","summary":"New contributor email(s) are not in AUTHOR_MAP.","detail":$detail,"how_to_fix":$how_to_fix}]}]')
|
||||
echo "review_status=$REVIEW_STATUS" >> "$GITHUB_OUTPUT"
|
||||
echo "review_status=$REVIEW_STATUS" > review-status.json
|
||||
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All contributor emails are mapped."
|
||||
echo "review_status=[]" >> "$GITHUB_OUTPUT"
|
||||
echo "review_status=[]" > review-status.json
|
||||
fi
|
||||
|
||||
- name: Upload review status artifact
|
||||
if: always() && steps.check-emails.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: review-status-contributor-check
|
||||
path: review-status.json
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
|
|
|
|||
12
.github/workflows/e2e-desktop.yml
vendored
12
.github/workflows/e2e-desktop.yml
vendored
|
|
@ -170,6 +170,18 @@ jobs:
|
|||
cat /tmp/e2e-review-status.json
|
||||
echo '__E2E_REVIEW_STATUS__'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
cp /tmp/e2e-review-status.json review-status.json
|
||||
|
||||
- name: Upload review status artifact
|
||||
if: always() && steps.review-status.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: review-status-e2e-desktop
|
||||
path: apps/desktop/review-status.json
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
|
||||
# The trusted workflow_run publisher consumes only this flat, bounded
|
||||
# artifact. It turns selected images into GitHub attachment URLs; it
|
||||
|
|
|
|||
13
.github/workflows/history-check.yml
vendored
13
.github/workflows/history-check.yml
vendored
|
|
@ -44,6 +44,7 @@ jobs:
|
|||
if ! BASE=$(git merge-base origin/main HEAD 2>/dev/null) || [ -z "$BASE" ]; then
|
||||
STATUS='[{"source":"unrelated histories","results":[{"kind":"action_required","title":"Unrelated histories","summary":"This PR has no common ancestor with main.","detail":"","how_to_fix":"Rebase your changes onto current main:\n```\ngit fetch origin main\ngit checkout -b fix-branch origin/main\n# re-apply your changes (cherry-pick, copy files, etc.)\ngit push -f origin fix-branch\n```\n"}]}]'
|
||||
echo "review_status=${STATUS}" >> "$GITHUB_OUTPUT"
|
||||
echo "review_status=${STATUS}" > review-status.json
|
||||
echo ""
|
||||
echo "::error::This PR has no common ancestor with main."
|
||||
echo ""
|
||||
|
|
@ -66,3 +67,15 @@ jobs:
|
|||
fi
|
||||
echo "::notice::Common ancestor with main: $BASE"
|
||||
echo "review_status=[]" >> "$GITHUB_OUTPUT"
|
||||
echo "review_status=[]" > review-status.json
|
||||
|
||||
- name: Upload review status artifact
|
||||
if: always() && steps.merge-base-check.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: review-status-history-check
|
||||
path: review-status.json
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
|
|
|
|||
14
.github/workflows/lockfile-diff.yml
vendored
14
.github/workflows/lockfile-diff.yml
vendored
|
|
@ -79,12 +79,24 @@ jobs:
|
|||
|
||||
if [ "$CHANGED" = "true" ]; then
|
||||
CONTENT=$(cat /tmp/lockfile-diff.md | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
||||
STATUS="[{\"source\":\"lockfile-diff\",\"results\":[{\"kind\":\"action_required\",\"title\":\"package-lock.json\",\"summary\":\"Locked npm dependency versions changed.\",\"detail\":${CONTENT},\"how_to_fix\":\"Add the \`ci-reviewed\` label after verifying the version changes are expected.\"}]}"
|
||||
STATUS="[{\"source\":\"lockfile-diff\",\"results\":[{\"kind\":\"action_required\",\"title\":\"package-lock.json\",\"summary\":\"Locked npm dependency versions changed.\",\"detail\":${CONTENT},\"how_to_fix\":\"Add the \`ci-reviewed\` label after verifying the version changes are expected.\"}]}]"
|
||||
else
|
||||
STATUS="[]"
|
||||
fi
|
||||
|
||||
echo "review_status=${STATUS}" >> "$GITHUB_OUTPUT"
|
||||
echo "review_status=${STATUS}" > review-status.json
|
||||
|
||||
- name: Upload review status artifact
|
||||
if: always() && steps.emit-status.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: review-status-lockfile-diff
|
||||
path: review-status.json
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload diff artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
|
|
|
|||
12
.github/workflows/osv-scanner.yml
vendored
12
.github/workflows/osv-scanner.yml
vendored
|
|
@ -125,3 +125,15 @@ jobs:
|
|||
fi
|
||||
|
||||
echo "review_status=${STATUS}" >> "$GITHUB_OUTPUT"
|
||||
echo "review_status=${STATUS}" > review-status.json
|
||||
|
||||
- name: Upload review status artifact
|
||||
if: always() && steps.emit.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: review-status-osv-scanner
|
||||
path: review-status.json
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
|
|
|
|||
14
.github/workflows/review-labels.yml
vendored
14
.github/workflows/review-labels.yml
vendored
|
|
@ -98,9 +98,23 @@ jobs:
|
|||
if [ "$SUPPLY_CHAIN" = "true" ]; then args+=(--supply-chain); fi
|
||||
if [ "$LABEL_PRESENT" = "true" ]; then args+=(--label-present); fi
|
||||
|
||||
# Write to both $GITHUB_OUTPUT and review-status.json for the
|
||||
# live comment poller to pick up as an artifact.
|
||||
python3 scripts/ci/emit_review_status.py "${args[@]}" \
|
||||
--repo-url "$REPO_URL" --base-sha "$BASE_SHA" --head-sha "$HEAD_SHA" \
|
||||
--output "$GITHUB_OUTPUT"
|
||||
grep '^review_status=' "$GITHUB_OUTPUT" > review-status.json
|
||||
|
||||
- name: Upload review status artifact
|
||||
if: always() && steps.build-status.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: review-status-review-labels
|
||||
path: review-status.json
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Fail on missing label
|
||||
if: steps.label-check.outputs.ci_reviewed != 'true'
|
||||
|
|
|
|||
15
.github/workflows/supply-chain-audit.yml
vendored
15
.github/workflows/supply-chain-audit.yml
vendored
|
|
@ -290,4 +290,19 @@ jobs:
|
|||
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
|
||||
f.write(f"review_status={json.dumps(merged)}\n")
|
||||
f.write("critical_findings=" + os.environ.get("CRITICAL_FINDINGS", "false") + "\n")
|
||||
|
||||
# Write review-status.json for the live comment poller artifact.
|
||||
with open("review-status.json", "w", encoding="utf-8") as f:
|
||||
f.write(f"review_status={json.dumps(merged)}\n")
|
||||
PYEOF
|
||||
|
||||
- name: Upload review status artifact
|
||||
if: always() && steps.merge.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: review-status-supply-chain
|
||||
path: review-status.json
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
|
|
|
|||
13
.github/workflows/uv-lockfile-check.yml
vendored
13
.github/workflows/uv-lockfile-check.yml
vendored
|
|
@ -126,7 +126,20 @@ jobs:
|
|||
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"
|
||||
echo "review_status=${review_status}" > review-status.json
|
||||
exit 1
|
||||
fi
|
||||
review_status='[]'
|
||||
echo "review_status=${review_status}" >> "$GITHUB_OUTPUT"
|
||||
echo "review_status=${review_status}" > review-status.json
|
||||
|
||||
- name: Upload review status artifact
|
||||
if: always() && steps.verify.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: review-status-uv-lockfile
|
||||
path: review-status.json
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue