-
Notifications
You must be signed in to change notification settings - Fork 619
ci(pr): inline annotations and changed-specs review #15755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dmcilvaney
merged 3 commits into
microsoft:tomls/base/main
from
dmcilvaney:damcilva/spec_agent_workflow
Feb 9, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| # Spec Review: Local Developer Guide | ||
|
|
||
| Run the spec-review workflow locally via VS Code, shell scripts, or the Copilot CLI. | ||
|
|
||
| There are three major paths: | ||
|
|
||
| 1. **[Use VS Code / Copilot Chat](#use-vs-code--copilot-chat)** (interactive, most flexible) | ||
| 2. **[Run the shell scripts](#run-single-model-review-spec_reviewsh)** (single or multi-model, automated) | ||
| 3. **[Use Copilot CLI manually](#use-copilot-cli-manually)** (interactive, most flexible, scriptable) | ||
|
|
||
| --- | ||
|
|
||
| ## Use VS Code / Copilot Chat | ||
|
|
||
| The easiest way to get quick, interactive feedback is with [GitHub Copilot](https://github.com/features/copilot) in VS Code (or via [Copilot CLI](#use-copilot-cli-manually)). The `spec-review` agent is designed to review spec files and provide feedback in a structured format the same way as the shell scripts, but with the added benefit of a conversational interface where you can ask follow-up questions, request clarifications, or dive deeper into specific issues. | ||
|
|
||
| 1. Open this repo in VS Code with **GitHub Copilot Chat**. | ||
| 2. In Copilot Chat, select the **`spec-review`** agent (bottom left corner of the chat window, "Agent", "Ask", "Edit", "Plan", etc.) | ||
| 3. Prompt example: | ||
|
|
||
| ```md | ||
| Review the azl release spec please. | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Run single-model review (spec_review.sh) | ||
|
|
||
| `spec_review.sh` orchestrates the Copilot agent, writes report/log/kb, and validates JSON. This is a subset of the multi-model script (`spec_review_multi.sh`) which the pipeline uses, but is faster to run. | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| - **Python**: 3.11+ (CI uses 3.12) | ||
| - **Node.js + npm** (for `@github/copilot` CLI) | ||
| - **GitHub Copilot token**: export `GH_TOKEN` (scope: `copilot:org_member`) or run `gh auth login` | ||
| - Optional: `jq` (for JSON pretty-print) | ||
|
|
||
| ```bash | ||
| cd ~/repos/azurelinux | ||
| python -m venv .venv && source .venv/bin/activate | ||
| pip install -r .github/workflows/scripts/requirements.txt | ||
| gh copilot | ||
| # OR | ||
| npm install -g @github/copilot | ||
| copilot --version # verify CLI | ||
| ``` | ||
|
|
||
| ### Usage | ||
|
|
||
| ```bash | ||
| # Defaults: reviews all *.spec in repo, writes to repo root | ||
| # spec_review_report.json, copilot_log.md, spec_review_kb.md | ||
|
|
||
| ./.github/workflows/scripts/spec_review.sh \ | ||
| --spec base/comps/azurelinux-release/azurelinux-release.spec \ | ||
| --spec base/comps/azurelinux-repos/azurelinux-repos.spec | ||
|
|
||
| # Validate / inspect | ||
| python .github/workflows/scripts/spec_review_schema.py /tmp/spec_review_report.json --all | ||
| python .github/workflows/scripts/spec_review_schema.py /tmp/spec_review_report.json --json | jq | ||
| ``` | ||
|
|
||
| The script supports additional options (like selecting a model, or using different URLs); run with `--help` for details. | ||
|
|
||
| ```bash | ||
| ./.github/workflows/scripts/spec_review.sh --help | ||
| ``` | ||
|
|
||
| ## Run multi-model review (spec_review_multi.sh) | ||
|
|
||
| `spec_review_multi.sh` runs two different LLMs sequentially as independent reviewers, then | ||
| uses a third model pass to synthesize their findings into a single high-quality report. This is the flow used in the CI pipeline. | ||
|
|
||
| **Why multi-model?** | ||
|
|
||
| - Model diversity catches different issues (reduces false negatives) | ||
| - Synthesis pass deduplicates and resolves conflicts intelligently | ||
| - Higher confidence in findings that both models agree on | ||
|
|
||
| ```bash | ||
| # Defaults: claude-opus-4.6 + gpt-5.2-codex reviewers, gpt-5.2-codex synthesizer | ||
| ./.github/workflows/scripts/spec_review_multi.sh \ | ||
| --spec base/comps/azurelinux-release/azurelinux-release.spec | ||
|
|
||
| # Custom models | ||
| ./.github/workflows/scripts/spec_review_multi.sh \ | ||
| --spec foo.spec \ | ||
| --model1 gpt-5.2-codex \ | ||
| --model2 claude-opus-4.6 \ | ||
| --synth-model gpt-5.2-codex \ | ||
| --output final_report.json | ||
|
|
||
| # View intermediate files for debugging | ||
| ls /tmp/spec_review_workdir/ | ||
| # report_a.json, report_b.json - individual reviewer reports | ||
| # kb_a.md, kb_b.md - knowledge bases | ||
| # kb_synth.md - synthesis notes | ||
| # log_a.md, log_b.md, log_synth.md - copilot session logs | ||
| ``` | ||
|
|
||
| Run with `--help` for all options: | ||
|
|
||
| ```bash | ||
| ./.github/workflows/scripts/spec_review_multi.sh --help | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Use Copilot CLI manually (interactive, most flexible, scriptable) | ||
|
|
||
| ```bash | ||
| # Expects the agent file to be in .github/agents/spec-review.agent.md. Can also be | ||
| # copied to the host global agents dir (~/.copilot/agents/). | ||
|
|
||
| # Interactive run | ||
| copilot --agent spec-review | ||
| ``` | ||
|
|
||
| ```bash | ||
| # For semi-automated or fully automated runs, use -i or -p flags: | ||
| prompt="Review: base/comps/azurelinux-release/azurelinux-release.spec against packaging guidelines.\n"\ | ||
| "Write JSON to spec_review_report.json and validate with: python .github/workflows/scripts/spec_review_schema.py spec_review_report.json" | ||
|
|
||
| # Semi-interactive (runs prompt, then waits for the user) | ||
| copilot --agent spec-review \ | ||
| --allow-all-tools \ | ||
| --add-dir "$PWD" \ | ||
| --allow-url https://docs.fedoraproject.org \ | ||
| --allow-url https://rpm-packaging-guide.github.io \ | ||
| --allow-url http://rpm.org \ | ||
| -i "$prompt" | ||
|
|
||
| # NOTE: | ||
| # --allow-all-urls can be used instead of individual --allow-url entries, | ||
| # but is less reliable since it may allow unwanted URLs. | ||
|
|
||
| # Fully automated (runs prompt directly, exits when done) | ||
| copilot --agent spec-review \ | ||
| --allow-all-tools \ | ||
| --add-dir "$PWD" \ | ||
| --allow-url https://docs.fedoraproject.org \ | ||
| --allow-url https://rpm-packaging-guide.github.io \ | ||
| --allow-url http://rpm.org \ | ||
| -p "$prompt" | ||
|
|
||
| # Review output | ||
| python .github/workflows/scripts/spec_review_schema.py spec_review_report.json --all | ||
| ``` | ||
|
|
||
| ## Future work | ||
|
|
||
| - Support generated specfiles from overlays | ||
| - Add explicit instruction files for different files (e.g., packaging guidelines, best practices) | ||
| - Cache the knowledge base between runs to speed up subsequent reviews and reduce web requests | ||
| - De-prioritize upstream issues in the spec file, focus on local changes | ||
| - Clearly indicate changes in the current PR | ||
| - More tools for the agent (e.g., diffing specs, rpmlint, etc.) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| """Shared utilities for spec review scripts.""" | ||
|
|
||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
|
|
||
| def get_repo_relative_path(spec_file: str, repo_root: Optional[Path] = None) -> str: | ||
| """Convert absolute path to repo-relative path. | ||
|
|
||
| Uses repo_root if available; falls back to filename only so that | ||
| GitHub annotations/links still have a chance of matching. | ||
| """ | ||
| spec_path = Path(spec_file) | ||
|
|
||
| if not spec_path.is_absolute(): | ||
| return spec_file | ||
|
|
||
| if repo_root: | ||
| try: | ||
| return str(spec_path.resolve().relative_to(repo_root.resolve())) | ||
| except ValueError: | ||
| pass | ||
|
|
||
| return spec_path.name |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Generate GitHub Check annotations from spec review report. | ||
|
|
||
| Usage: | ||
| python create_check_annotations.py report.json --workflow-commands | ||
| python create_check_annotations.py report.json --json | ||
| python create_check_annotations.py report.json --repo-root /path/to/repo | ||
| """ | ||
|
|
||
| import argparse | ||
| import json | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
| from _common import get_repo_relative_path | ||
|
|
||
| # Mapping from finding category to (workflow command level, checks API level, checks API title) | ||
| _SEVERITY_MAP = { | ||
| "errors": ("error", "failure", "Spec Error"), | ||
| "warnings": ("warning", "warning", "Spec Warning"), | ||
| "suggestions": ("notice", "notice", "Suggestion"), | ||
| } | ||
|
|
||
|
|
||
| def _iter_findings(report: dict, repo_root: Optional[Path] = None): | ||
| """Yield (spec_file, category, finding) for every finding in the report.""" | ||
| for review in report.get("spec_reviews", []): | ||
| spec_file = get_repo_relative_path(review.get("spec_file", ""), repo_root) | ||
| for category in _SEVERITY_MAP: | ||
| for finding in review.get(category, []): | ||
| yield spec_file, category, finding | ||
|
|
||
|
|
||
| def _format_message(finding: dict, escape_fn=None) -> str: | ||
| """Build a message string from a finding, optionally escaping it.""" | ||
| desc = finding.get("description", "") | ||
| citation = finding.get("citation") | ||
| if escape_fn: | ||
| desc = escape_fn(desc) | ||
| msg = desc | ||
| if citation and citation not in ("N/A", "n/a", ""): | ||
| cite = escape_fn(citation) if escape_fn else citation | ||
| msg += f" (Ref: {cite})" if escape_fn else f"\n\nRef: {citation}" | ||
| return msg | ||
|
|
||
|
|
||
| def escape_workflow_command(s: str) -> str: | ||
| """Escape special characters for GitHub Actions workflow commands. | ||
|
|
||
| See https://github.com/actions/toolkit/issues/193 | ||
| Order matters: % must be escaped first to avoid double-escaping. | ||
| """ | ||
| return ( | ||
| s.replace("%", "%25") | ||
| .replace("\r", "%0D") | ||
| .replace("\n", "%0A") | ||
| .replace(":", "%3A") | ||
| .replace(",", "%2C") | ||
| ) | ||
|
|
||
|
|
||
| def generate_workflow_commands(report: dict, repo_root: Optional[Path] = None) -> list[str]: | ||
| """Generate GitHub Actions workflow commands for annotations.""" | ||
| commands = [] | ||
| for spec_file, category, finding in _iter_findings(report, repo_root): | ||
| level = _SEVERITY_MAP[category][0] | ||
| line = finding.get("line") or 1 | ||
| msg = _format_message(finding, escape_fn=escape_workflow_command) | ||
| escaped_file = escape_workflow_command(spec_file) | ||
| commands.append(f"::{level} file={escaped_file},line={line}::{msg}") | ||
| return commands | ||
|
|
||
|
|
||
| def generate_check_annotations(report: dict, repo_root: Optional[Path] = None) -> list[dict]: | ||
| """Generate annotations for GitHub Checks API.""" | ||
| annotations = [] | ||
| for spec_file, category, finding in _iter_findings(report, repo_root): | ||
| _, api_level, title = _SEVERITY_MAP[category] | ||
| line = finding.get("line") or 1 | ||
| msg = _format_message(finding) | ||
| annotations.append({ | ||
| "path": spec_file, | ||
| "start_line": line, | ||
| "end_line": line, | ||
| "annotation_level": api_level, | ||
| "message": msg, | ||
| "title": title, | ||
| }) | ||
|
dmcilvaney marked this conversation as resolved.
|
||
| return annotations | ||
|
|
||
|
|
||
| def main() -> int: | ||
| parser = argparse.ArgumentParser(description="Generate check annotations from spec review") | ||
| parser.add_argument("file", type=Path, help="Path to report JSON") | ||
| parser.add_argument("--workflow-commands", action="store_true", | ||
| help="Output GitHub Actions workflow commands") | ||
| parser.add_argument("--json", action="store_true", | ||
| help="Output annotations as JSON for Checks API") | ||
| parser.add_argument("--repo-root", type=Path, default=None, | ||
| help="Repository root for converting absolute paths to relative (default: auto-detect via git)") | ||
| args = parser.parse_args() | ||
|
|
||
| try: | ||
| with open(args.file, encoding="utf-8") as f: | ||
| report = json.load(f) | ||
| except (FileNotFoundError, json.JSONDecodeError) as e: | ||
| print(f"Error: {e}", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| # Use provided repo root, or fall back to cwd | ||
| repo_root = args.repo_root or Path.cwd() | ||
|
|
||
| if args.workflow_commands: | ||
| commands = generate_workflow_commands(report, repo_root) | ||
| for cmd in commands: | ||
| print(cmd) | ||
| elif args.json: | ||
| annotations = generate_check_annotations(report, repo_root) | ||
| print(json.dumps(annotations, indent=2)) | ||
| else: | ||
| # Default: workflow commands | ||
| commands = generate_workflow_commands(report, repo_root) | ||
| for cmd in commands: | ||
| print(cmd) | ||
|
|
||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.