mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
* ci: surface E2E screenshots in review comment * ci: mark completed review commits in past tense * ci: surface approved sensitive-file reviews * ci: link sensitive files to reviewed changes * ci: stage desktop E2E visual evidence Track screenshots newly introduced against main and package visual diffs for a trusted publisher. * fix(ci): pass E2E evidence output paths Supply the manifest and staging-directory arguments required by the screenshot status helper. * fix(ci): download the OSV SARIF artifact Match the artifact name and result filename emitted by the pinned upstream reusable workflow.
214 lines
8 KiB
Python
214 lines
8 KiB
Python
#!/usr/bin/env python3
|
|
"""Emit review_status JSON for the review-labels workflow.
|
|
|
|
Builds a JSON array with one entry::
|
|
|
|
[
|
|
{
|
|
"source": "review-label-gate",
|
|
"results": [
|
|
{"kind": "action_required", "title": "...", "summary": "...",
|
|
"how_to_fix": "..."},
|
|
{"kind": "info", "title": "...", "summary": "..."}
|
|
]
|
|
}
|
|
]
|
|
|
|
The ``source`` field is the workflow name that declared the status; the
|
|
assembler uses it to exclude the corresponding job from the synthesized
|
|
❌ Error list (the job already has its own status section).
|
|
|
|
The array can contain 0 to 3 results — one per lane that ran
|
|
(``ci_review``, ``mcp_catalog``, ``supply_chain``). When the ``ci-reviewed`` label is
|
|
present, the kind is ``info``; when missing, it's ``action_required``
|
|
with the verification checklist.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from urllib.parse import quote
|
|
|
|
# The source identifier used for error-synthesis exclusion. This must
|
|
# match (as a normalized substring) the job name as it appears in the
|
|
# GitHub Actions API. The ci.yml job key is ``review-labels`` with
|
|
# ``name: Review label gate``, and the reusable workflow's job is also
|
|
# ``name: Review label gate``, so the API shows the job as
|
|
# "Review label gate / Review label gate". Normalizing "review-label-gate"
|
|
# (lowercase, hyphens→spaces) gives "review label gate", which is a
|
|
# substring of "review label gate / review label gate".
|
|
SOURCE = "review-label-gate"
|
|
|
|
|
|
def _ci_review_detail(
|
|
files_json: str, repo_url: str, base_sha: str, head_sha: str,
|
|
) -> str:
|
|
"""Render links to the changed CI-sensitive files that triggered review."""
|
|
try:
|
|
files = json.loads(files_json)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return ""
|
|
if not isinstance(files, list) or not repo_url or not base_sha or not head_sha:
|
|
return ""
|
|
|
|
links = []
|
|
for path in files:
|
|
if not isinstance(path, str) or not path:
|
|
continue
|
|
label = path.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]")
|
|
path_hash = hashlib.sha256(path.encode()).hexdigest()
|
|
url = (
|
|
f"{repo_url}/compare/{quote(base_sha, safe='')}...{quote(head_sha, safe='')}"
|
|
f"#diff-{path_hash}"
|
|
)
|
|
links.append(f"- [`{label}`]({url})")
|
|
return "**Sensitive files changed:**\n" + "\n".join(links) if links else ""
|
|
|
|
|
|
def build_results(
|
|
ci_review: bool,
|
|
mcp_catalog: bool,
|
|
supply_chain: bool,
|
|
label_present: bool,
|
|
ci_review_files: str = "[]",
|
|
repo_url: str = "",
|
|
base_sha: str = "",
|
|
head_sha: str = "",
|
|
) -> list[dict]:
|
|
"""Build the list of result objects for this source."""
|
|
results: list[dict] = []
|
|
|
|
if ci_review:
|
|
detail = _ci_review_detail(ci_review_files, repo_url, base_sha, head_sha)
|
|
if label_present:
|
|
result = {
|
|
"kind": "info",
|
|
"title": "CI-sensitive file review",
|
|
"summary": (
|
|
"PR touches sensitive files, but the `ci-reviewed` label has been "
|
|
"added, approving them."
|
|
),
|
|
}
|
|
else:
|
|
result = {
|
|
"kind": "action_required",
|
|
"title": "CI-sensitive file review",
|
|
"summary": (
|
|
"This PR changes CI-sensitive files (eslint config, "
|
|
"workflow YAMLs, or composite actions). These influence "
|
|
"what the js-autofix job executes and pushes to main."
|
|
),
|
|
"how_to_fix": (
|
|
"Add the `ci-reviewed` label after verifying:\n"
|
|
"- no new eslint rules with custom `fix` functions that write outside linted paths,\n"
|
|
"- no workflow changes that widen permissions or remove guards,\n"
|
|
"- no composite action changes that alter what gets executed."
|
|
),
|
|
}
|
|
if detail:
|
|
result["detail"] = detail
|
|
results.append(result)
|
|
|
|
if mcp_catalog:
|
|
if label_present:
|
|
results.append({
|
|
"kind": "debug",
|
|
"title": "MCP catalog security review",
|
|
"summary": "`ci-reviewed` label is present.",
|
|
})
|
|
else:
|
|
results.append({
|
|
"kind": "action_required",
|
|
"title": "MCP catalog security review",
|
|
"summary": (
|
|
"This PR changes the bundled MCP catalog or MCP catalog "
|
|
"installer code. MCP entries can define local commands "
|
|
"that users later install into `mcp_servers`, so this "
|
|
"needs explicit maintainer review before merge."
|
|
),
|
|
"how_to_fix": (
|
|
"Add the `ci-reviewed` label after verifying:\n"
|
|
"- any new/changed `optional-mcps/**/manifest.yaml` command and args are expected,\n"
|
|
"- stdio transports do not use shell+egress/exfiltration payloads,\n"
|
|
"- git install refs are pinned and bootstrap commands are minimal,\n"
|
|
"- requested env vars/secrets match the upstream MCP's documented needs."
|
|
),
|
|
})
|
|
|
|
if supply_chain and not label_present:
|
|
results.append({
|
|
"kind": "action_required",
|
|
"title": "Critical supply chain risk",
|
|
"summary": "Critical supply chain risk patterns were detected in this PR.",
|
|
"how_to_fix": (
|
|
"Review the flagged code carefully. If it is intentional, add the "
|
|
"`ci-reviewed` label to confirm maintainer review."
|
|
),
|
|
})
|
|
|
|
return results
|
|
|
|
|
|
def build_statuses(
|
|
ci_review: bool,
|
|
mcp_catalog: bool,
|
|
supply_chain: bool,
|
|
label_present: bool,
|
|
ci_review_files: str = "[]",
|
|
repo_url: str = "",
|
|
base_sha: str = "",
|
|
head_sha: str = "",
|
|
) -> list[dict]:
|
|
"""Build the full review_status array (one entry with a results list)."""
|
|
results = build_results(
|
|
ci_review, mcp_catalog, supply_chain, label_present,
|
|
ci_review_files, repo_url, base_sha, head_sha,
|
|
)
|
|
if not results:
|
|
return []
|
|
return [{"source": SOURCE, "results": results}]
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--ci-review", action="store_true",
|
|
help="Whether CI-sensitive files changed.")
|
|
parser.add_argument("--ci-review-files", default="[]",
|
|
help="JSON list of CI-sensitive files changed.")
|
|
parser.add_argument("--mcp-catalog", action="store_true",
|
|
help="Whether the MCP catalog / installer changed.")
|
|
parser.add_argument("--supply-chain", action="store_true",
|
|
help="Whether the critical supply-chain scanner found a risk.")
|
|
parser.add_argument("--label-present", action="store_true",
|
|
help="Whether the ci-reviewed label is present.")
|
|
parser.add_argument("--repo-url", default="",
|
|
help="Repository URL used for changed-file links.")
|
|
parser.add_argument("--base-sha", default="",
|
|
help="Pull request base SHA used for changed-file links.")
|
|
parser.add_argument("--head-sha", default="",
|
|
help="Pull request head SHA used for changed-file links.")
|
|
parser.add_argument("--output", default="-",
|
|
help="Output file ('-' for stdout, or a GITHUB_OUTPUT path).")
|
|
args = parser.parse_args()
|
|
|
|
statuses = build_statuses(
|
|
args.ci_review, args.mcp_catalog, args.supply_chain, args.label_present,
|
|
args.ci_review_files, args.repo_url, args.base_sha, args.head_sha,
|
|
)
|
|
json_str = json.dumps(statuses)
|
|
|
|
if args.output == "-":
|
|
print(json_str)
|
|
else:
|
|
# GITHUB_OUTPUT format: key=value\n
|
|
with open(args.output, "a", encoding="utf-8") as f:
|
|
f.write(f"review_status={json_str}\n")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|