@xiaotianxt/skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EXCLUDED.md +42 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/SECURITY.md +23 -0
- package/SOURCES.md +45 -0
- package/bin/skills.mjs +241 -0
- package/package.json +38 -0
- package/skills/1password/SKILL.md +94 -0
- package/skills/1password/agents/openai.yaml +4 -0
- package/skills/1password/references/item-management.md +80 -0
- package/skills/1password/references/op-cli.md +107 -0
- package/skills/apple-calendar-event/SKILL.md +81 -0
- package/skills/apple-calendar-event/agents/openai.yaml +4 -0
- package/skills/apple-calendar-event/scripts/calendar_audit.py +201 -0
- package/skills/apple-calendar-event/scripts/calendar_event.py +164 -0
- package/skills/bro-browser/SKILL.md +118 -0
- package/skills/bro-browser/agents/openai.yaml +4 -0
- package/skills/bro-browser/references/tool-map.md +102 -0
- package/skills/bro-browser/references/workflows.md +146 -0
- package/skills/bro-browser/scripts/bro-call.mjs +189 -0
- package/skills/calendar/SKILL.md +182 -0
- package/skills/calendar/agents/openai.yaml +4 -0
- package/skills/calendar/references/operations.md +255 -0
- package/skills/calendar/scripts/calendar_list_review.py +157 -0
- package/skills/calendar/scripts/event_dedupe_preview.py +155 -0
- package/skills/canvas/SKILL.md +70 -0
- package/skills/canvas/agents/openai.yaml +4 -0
- package/skills/canvas/references/canvas-api.md +76 -0
- package/skills/course-exam-review-planner/SKILL.md +127 -0
- package/skills/cx/SKILL.md +25 -0
- package/skills/gh-fix-ci/LICENSE.txt +201 -0
- package/skills/gh-fix-ci/SKILL.md +81 -0
- package/skills/gh-fix-ci/agents/openai.yaml +6 -0
- package/skills/gh-fix-ci/assets/github-small.svg +3 -0
- package/skills/gh-fix-ci/assets/github.png +0 -0
- package/skills/gh-fix-ci/scripts/inspect_pr_checks.py +509 -0
- package/skills/gh-review-workflow/SKILL.md +61 -0
- package/skills/gh-review-workflow/agents/openai.yaml +4 -0
- package/skills/gh-review-workflow/references/workflow.md +48 -0
- package/skills/gh-review-workflow/scripts/fetch_review_state.py +222 -0
- package/skills/gh-review-workflow/scripts/resolve_review_threads.py +83 -0
- package/skills/github/SKILL.md +74 -0
- package/skills/github/agents/openai.yaml +6 -0
- package/skills/github/assets/github-small.svg +3 -0
- package/skills/github/assets/github.png +0 -0
- package/skills/gws-calendar/SKILL.md +126 -0
- package/skills/gws-calendar-agenda/SKILL.md +52 -0
- package/skills/gws-calendar-insert/SKILL.md +66 -0
- package/skills/gws-docs/SKILL.md +48 -0
- package/skills/gws-docs-write/SKILL.md +49 -0
- package/skills/gws-drive/SKILL.md +137 -0
- package/skills/gws-drive-upload/SKILL.md +52 -0
- package/skills/gws-gmail/SKILL.md +62 -0
- package/skills/gws-gmail-forward/SKILL.md +55 -0
- package/skills/gws-gmail-reply/SKILL.md +58 -0
- package/skills/gws-gmail-reply-all/SKILL.md +62 -0
- package/skills/gws-gmail-send/SKILL.md +57 -0
- package/skills/gws-gmail-triage/SKILL.md +50 -0
- package/skills/gws-gmail-watch/SKILL.md +58 -0
- package/skills/gws-shared/SKILL.md +27 -0
- package/skills/helium-browser-mcp/SKILL.md +137 -0
- package/skills/helium-browser-mcp/agents/openai.yaml +4 -0
- package/skills/helium-browser-mcp/scripts/obmcp.mjs +92 -0
- package/skills/helium-browser-mcp/scripts/openbrowsermcp-stdio-proxy.mjs +170 -0
- package/skills/learn/SKILL.md +122 -0
- package/skills/learn/agents/openai.yaml +7 -0
- package/skills/learn/assets/AGENTS.template.md +33 -0
- package/skills/learn/assets/errorlog.template.typ +61 -0
- package/skills/learn/assets/reading-sequence.template.md +23 -0
- package/skills/learn/assets/source-index.template.md +17 -0
- package/skills/learn/assets/tasklog.template.typ +57 -0
- package/skills/learn/assets/workbook.template.typ +60 -0
- package/skills/learn/references/learning-science.md +103 -0
- package/skills/learn/scripts/init_learning_workspace.py +70 -0
- package/skills/macos-messages/SKILL.md +258 -0
- package/skills/memory/SKILL.md +33 -0
- package/skills/memory/codex.md +186 -0
- package/skills/memory/opencode.md +164 -0
- package/skills/mimestreamctl/SKILL.md +170 -0
- package/skills/mimestreamctl/agents/openai.yaml +4 -0
- package/skills/mimestreamctl/scripts/mimestreamctl +33 -0
- package/skills/mon/SKILL.md +51 -0
- package/skills/mon/scripts/mon_spend_review.py +458 -0
- package/skills/ocr/SKILL.md +136 -0
- package/skills/ocr/agents/openai.yaml +4 -0
- package/skills/ocr/references/local-ocr-best-practices.md +297 -0
- package/skills/ocr/references/mineru-api.md +159 -0
- package/skills/ocr/scripts/ocr-router +22 -0
- package/skills/ocr/scripts/ocr_router.py +741 -0
- package/skills/panopto-mp4-bulk-download/SKILL.md +57 -0
- package/skills/panopto-mp4-bulk-download/agents/openai.yaml +4 -0
- package/skills/panopto-mp4-bulk-download/references/url-patterns.md +26 -0
- package/skills/panopto-mp4-bulk-download/scripts/panopto_bulk_mp4.sh +213 -0
- package/skills/rust-systems-style/SKILL.md +109 -0
- package/skills/rust-systems-style/agents/openai.yaml +4 -0
- package/skills/rust-systems-style/references/rust-review-checklist.md +77 -0
- package/skills/rust-systems-style/references/style-sources.md +68 -0
- package/skills/ship-ai-native-cli/SKILL.md +76 -0
- package/skills/ship-ai-native-cli/agents/openai.yaml +4 -0
- package/skills/ship-ai-native-cli/references/case-notes.md +83 -0
- package/skills/ship-ai-native-cli/references/product-method.md +82 -0
- package/skills/ship-ai-native-cli/references/release-checklist.md +147 -0
- package/skills/ship-ai-native-cli/references/rust-cli-shape.md +111 -0
- package/skills/telegram-mtproto-session/SKILL.md +125 -0
- package/skills/telegram-mtproto-session/agents/openai.yaml +4 -0
- package/skills/telegram-mtproto-session/scripts/telegram_session.py +687 -0
- package/skills/tg/SKILL.md +173 -0
- package/skills/things3-manager/SKILL.md +116 -0
- package/skills/things3-manager/scripts/things +42 -0
- package/skills/things3-manager/scripts/things_cli.py +514 -0
- package/skills/web-artifacts-builder/LICENSE.txt +202 -0
- package/skills/web-artifacts-builder/SKILL.md +74 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +379 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/yeet/LICENSE.txt +201 -0
- package/skills/yeet/SKILL.md +71 -0
- package/skills/yeet/agents/openai.yaml +6 -0
- package/skills/yeet/assets/yeet-small.svg +3 -0
- package/skills/yeet/assets/yeet.png +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "gh-fix-ci"
|
|
3
|
+
description: "Use when a user asks to debug or fix failing GitHub PR checks that run in GitHub Actions. Use GitHub MCP tools for PR metadata and patch context, and use `gh` for Actions check and log inspection before implementing any approved fix."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# GitHub Actions CI Fix
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Use this skill when the task is specifically about failing GitHub Actions checks on a pull request. This workflow is hybrid by design:
|
|
12
|
+
|
|
13
|
+
- Use GitHub MCP tools for PR metadata, changed files, and review context.
|
|
14
|
+
- Use `gh` for GitHub Actions checks and logs because the connector does not expose that workflow end to end.
|
|
15
|
+
- Summarize the root cause first, propose a focused fix plan, and implement only after explicit approval.
|
|
16
|
+
|
|
17
|
+
Prereq: authenticate with GitHub CLI once, then confirm with `gh auth status`. Repo and workflow scopes are typically required for Actions inspection.
|
|
18
|
+
|
|
19
|
+
## Inputs
|
|
20
|
+
|
|
21
|
+
- `repo`: path inside the repo (default `.`)
|
|
22
|
+
- `pr`: PR number or URL (optional; defaults to current branch PR)
|
|
23
|
+
- `gh` authentication for the repo host
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
- `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "<number-or-url>"`
|
|
28
|
+
- Add `--json` if you want machine-friendly output for summarization.
|
|
29
|
+
|
|
30
|
+
## Workflow
|
|
31
|
+
|
|
32
|
+
1. Verify gh authentication.
|
|
33
|
+
- Run `gh auth status` in the repo.
|
|
34
|
+
- If unauthenticated, ask the user to run `gh auth login` (ensuring repo + workflow scopes) before proceeding.
|
|
35
|
+
2. Resolve the PR.
|
|
36
|
+
- If the user provides a PR number or URL, use that directly.
|
|
37
|
+
- Otherwise prefer the current branch PR with `gh pr view --json number,url`.
|
|
38
|
+
- When repo and PR are known, fetch PR metadata and patch context through GitHub MCP tools.
|
|
39
|
+
3. Inspect failing checks (GitHub Actions only).
|
|
40
|
+
- Preferred: run the bundled script (handles gh field drift and job-log fallbacks):
|
|
41
|
+
- `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "<number-or-url>"`
|
|
42
|
+
- Add `--json` for machine-friendly output.
|
|
43
|
+
- Manual fallback:
|
|
44
|
+
- `gh pr checks <pr> --json name,state,bucket,link,startedAt,completedAt,workflow`
|
|
45
|
+
- If a field is rejected, rerun with the available fields reported by `gh`.
|
|
46
|
+
- For each failing check, extract the run id from `detailsUrl` and run:
|
|
47
|
+
- `gh run view <run_id> --json name,workflowName,conclusion,status,url,event,headBranch,headSha`
|
|
48
|
+
- `gh run view <run_id> --log`
|
|
49
|
+
- If the run log says it is still in progress, fetch job logs directly:
|
|
50
|
+
- `gh api "/repos/<owner>/<repo>/actions/jobs/<job_id>/logs" > "<path>"`
|
|
51
|
+
4. Scope non-GitHub Actions checks.
|
|
52
|
+
- If `detailsUrl` is not a GitHub Actions run, label it as external and only report the URL.
|
|
53
|
+
- Do not attempt Buildkite or other providers; keep the workflow lean.
|
|
54
|
+
5. Summarize failures for the user.
|
|
55
|
+
- Provide the failing check name, run URL (if any), and a concise log snippet.
|
|
56
|
+
- Call out missing logs explicitly and do not over-claim certainty.
|
|
57
|
+
6. Propose a focused fix plan and wait for approval.
|
|
58
|
+
- Keep the plan tied directly to the failing checks and the observed root cause.
|
|
59
|
+
7. Implement after approval.
|
|
60
|
+
- Apply the approved fix locally.
|
|
61
|
+
- Run the most relevant local verification available.
|
|
62
|
+
8. Recheck status and summarize residual risk.
|
|
63
|
+
- Suggest re-running the relevant tests and `gh pr checks`.
|
|
64
|
+
- Report what is still unverified, what may still be flaky, and whether any failing checks were external and therefore not actionable here.
|
|
65
|
+
|
|
66
|
+
## Bundled Resources
|
|
67
|
+
|
|
68
|
+
### scripts/inspect_pr_checks.py
|
|
69
|
+
|
|
70
|
+
Fetch failing PR checks, pull GitHub Actions logs, and extract a failure snippet. Exits non-zero when failures remain so it can be used in automation.
|
|
71
|
+
|
|
72
|
+
Usage examples:
|
|
73
|
+
- `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "123"`
|
|
74
|
+
- `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "https://github.com/org/repo/pull/123" --json`
|
|
75
|
+
- `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --max-lines 200 --context 40`
|
|
76
|
+
|
|
77
|
+
## Guardrails
|
|
78
|
+
|
|
79
|
+
- Do not imply that GitHub MCP can replace `gh` for Actions log retrieval.
|
|
80
|
+
- Treat non-GitHub Actions providers as report-only unless the user explicitly wants a separate investigation path.
|
|
81
|
+
- If the failure is clearly unrelated to the local diff, say so before proposing code changes.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "CI Debug"
|
|
3
|
+
short_description: "Debug failing GitHub Actions checks"
|
|
4
|
+
icon_small: "./assets/github-small.svg"
|
|
5
|
+
icon_large: "./assets/github.png"
|
|
6
|
+
default_prompt: "Use $gh-fix-ci to inspect the failing GitHub Actions checks on this PR, summarize the root cause, and propose the smallest viable fix."
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
2
|
+
<path fill="currentColor" d="M8 1.3a6.665 6.665 0 0 1 5.413 10.56 6.677 6.677 0 0 1-3.288 2.432c-.333.067-.458-.142-.458-.316 0-.226.008-.942.008-1.834 0-.625-.208-1.025-.45-1.233 1.483-.167 3.042-.734 3.042-3.292a2.58 2.58 0 0 0-.684-1.792c.067-.166.3-.85-.066-1.766 0 0-.559-.184-1.834.683a6.186 6.186 0 0 0-1.666-.225c-.567 0-1.134.075-1.667.225-1.275-.858-1.833-.683-1.833-.683-.367.916-.134 1.6-.067 1.766a2.594 2.594 0 0 0-.683 1.792c0 2.55 1.55 3.125 3.033 3.292-.192.166-.367.458-.425.891-.383.175-1.342.459-1.942-.55-.125-.2-.5-.691-1.025-.683-.558.008-.225.317.009.442.283.158.608.75.683.941.133.376.567 1.092 2.242.784 0 .558.008 1.083.008 1.242 0 .174-.125.374-.458.316a6.662 6.662 0 0 1-4.559-6.325A6.665 6.665 0 0 1 8 1.3Z"/>
|
|
3
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from shutil import which
|
|
11
|
+
from typing import Any, Iterable, Sequence
|
|
12
|
+
|
|
13
|
+
FAILURE_CONCLUSIONS = {
|
|
14
|
+
"failure",
|
|
15
|
+
"cancelled",
|
|
16
|
+
"timed_out",
|
|
17
|
+
"action_required",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
FAILURE_STATES = {
|
|
21
|
+
"failure",
|
|
22
|
+
"error",
|
|
23
|
+
"cancelled",
|
|
24
|
+
"timed_out",
|
|
25
|
+
"action_required",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
FAILURE_BUCKETS = {"fail"}
|
|
29
|
+
|
|
30
|
+
FAILURE_MARKERS = (
|
|
31
|
+
"error",
|
|
32
|
+
"fail",
|
|
33
|
+
"failed",
|
|
34
|
+
"traceback",
|
|
35
|
+
"exception",
|
|
36
|
+
"assert",
|
|
37
|
+
"panic",
|
|
38
|
+
"fatal",
|
|
39
|
+
"timeout",
|
|
40
|
+
"segmentation fault",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
DEFAULT_MAX_LINES = 160
|
|
44
|
+
DEFAULT_CONTEXT_LINES = 30
|
|
45
|
+
PENDING_LOG_MARKERS = (
|
|
46
|
+
"still in progress",
|
|
47
|
+
"log will be available when it is complete",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GhResult:
|
|
52
|
+
def __init__(self, returncode: int, stdout: str, stderr: str):
|
|
53
|
+
self.returncode = returncode
|
|
54
|
+
self.stdout = stdout
|
|
55
|
+
self.stderr = stderr
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_gh_command(args: Sequence[str], cwd: Path) -> GhResult:
|
|
59
|
+
process = subprocess.run(
|
|
60
|
+
["gh", *args],
|
|
61
|
+
cwd=cwd,
|
|
62
|
+
text=True,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
)
|
|
65
|
+
return GhResult(process.returncode, process.stdout, process.stderr)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_gh_command_raw(args: Sequence[str], cwd: Path) -> tuple[int, bytes, str]:
|
|
69
|
+
process = subprocess.run(
|
|
70
|
+
["gh", *args],
|
|
71
|
+
cwd=cwd,
|
|
72
|
+
capture_output=True,
|
|
73
|
+
)
|
|
74
|
+
stderr = process.stderr.decode(errors="replace")
|
|
75
|
+
return process.returncode, process.stdout, stderr
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse_args() -> argparse.Namespace:
|
|
79
|
+
parser = argparse.ArgumentParser(
|
|
80
|
+
description=(
|
|
81
|
+
"Inspect failing GitHub PR checks, fetch GitHub Actions logs, and extract a "
|
|
82
|
+
"failure snippet."
|
|
83
|
+
),
|
|
84
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument("--repo", default=".", help="Path inside the target Git repository.")
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--pr", default=None, help="PR number or URL (defaults to current branch PR)."
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument("--max-lines", type=int, default=DEFAULT_MAX_LINES)
|
|
91
|
+
parser.add_argument("--context", type=int, default=DEFAULT_CONTEXT_LINES)
|
|
92
|
+
parser.add_argument("--json", action="store_true", help="Emit JSON instead of text output.")
|
|
93
|
+
return parser.parse_args()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main() -> int:
|
|
97
|
+
args = parse_args()
|
|
98
|
+
repo_root = find_git_root(Path(args.repo))
|
|
99
|
+
if repo_root is None:
|
|
100
|
+
print("Error: not inside a Git repository.", file=sys.stderr)
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
if not ensure_gh_available(repo_root):
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
pr_value = resolve_pr(args.pr, repo_root)
|
|
107
|
+
if pr_value is None:
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
checks = fetch_checks(pr_value, repo_root)
|
|
111
|
+
if checks is None:
|
|
112
|
+
return 1
|
|
113
|
+
|
|
114
|
+
failing = [c for c in checks if is_failing(c)]
|
|
115
|
+
if not failing:
|
|
116
|
+
print(f"PR #{pr_value}: no failing checks detected.")
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
results = []
|
|
120
|
+
for check in failing:
|
|
121
|
+
results.append(
|
|
122
|
+
analyze_check(
|
|
123
|
+
check,
|
|
124
|
+
repo_root=repo_root,
|
|
125
|
+
max_lines=max(1, args.max_lines),
|
|
126
|
+
context=max(1, args.context),
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if args.json:
|
|
131
|
+
print(json.dumps({"pr": pr_value, "results": results}, indent=2))
|
|
132
|
+
else:
|
|
133
|
+
render_results(pr_value, results)
|
|
134
|
+
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def find_git_root(start: Path) -> Path | None:
|
|
139
|
+
result = subprocess.run(
|
|
140
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
141
|
+
cwd=start,
|
|
142
|
+
text=True,
|
|
143
|
+
capture_output=True,
|
|
144
|
+
)
|
|
145
|
+
if result.returncode != 0:
|
|
146
|
+
return None
|
|
147
|
+
return Path(result.stdout.strip())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def ensure_gh_available(repo_root: Path) -> bool:
|
|
151
|
+
if which("gh") is None:
|
|
152
|
+
print("Error: gh is not installed or not on PATH.", file=sys.stderr)
|
|
153
|
+
return False
|
|
154
|
+
result = run_gh_command(["auth", "status"], cwd=repo_root)
|
|
155
|
+
if result.returncode == 0:
|
|
156
|
+
return True
|
|
157
|
+
message = (result.stderr or result.stdout or "").strip()
|
|
158
|
+
print(message or "Error: gh not authenticated.", file=sys.stderr)
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def resolve_pr(pr_value: str | None, repo_root: Path) -> str | None:
|
|
163
|
+
if pr_value:
|
|
164
|
+
return pr_value
|
|
165
|
+
result = run_gh_command(["pr", "view", "--json", "number"], cwd=repo_root)
|
|
166
|
+
if result.returncode != 0:
|
|
167
|
+
message = (result.stderr or result.stdout or "").strip()
|
|
168
|
+
print(message or "Error: unable to resolve PR.", file=sys.stderr)
|
|
169
|
+
return None
|
|
170
|
+
try:
|
|
171
|
+
data = json.loads(result.stdout or "{}")
|
|
172
|
+
except json.JSONDecodeError:
|
|
173
|
+
print("Error: unable to parse PR JSON.", file=sys.stderr)
|
|
174
|
+
return None
|
|
175
|
+
number = data.get("number")
|
|
176
|
+
if not number:
|
|
177
|
+
print("Error: no PR number found.", file=sys.stderr)
|
|
178
|
+
return None
|
|
179
|
+
return str(number)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def fetch_checks(pr_value: str, repo_root: Path) -> list[dict[str, Any]] | None:
|
|
183
|
+
primary_fields = ["name", "state", "conclusion", "detailsUrl", "startedAt", "completedAt"]
|
|
184
|
+
result = run_gh_command(
|
|
185
|
+
["pr", "checks", pr_value, "--json", ",".join(primary_fields)],
|
|
186
|
+
cwd=repo_root,
|
|
187
|
+
)
|
|
188
|
+
if result.returncode != 0:
|
|
189
|
+
message = "\n".join(filter(None, [result.stderr, result.stdout])).strip()
|
|
190
|
+
available_fields = parse_available_fields(message)
|
|
191
|
+
if available_fields:
|
|
192
|
+
fallback_fields = [
|
|
193
|
+
"name",
|
|
194
|
+
"state",
|
|
195
|
+
"bucket",
|
|
196
|
+
"link",
|
|
197
|
+
"startedAt",
|
|
198
|
+
"completedAt",
|
|
199
|
+
"workflow",
|
|
200
|
+
]
|
|
201
|
+
selected_fields = [field for field in fallback_fields if field in available_fields]
|
|
202
|
+
if not selected_fields:
|
|
203
|
+
print("Error: no usable fields available for gh pr checks.", file=sys.stderr)
|
|
204
|
+
return None
|
|
205
|
+
result = run_gh_command(
|
|
206
|
+
["pr", "checks", pr_value, "--json", ",".join(selected_fields)],
|
|
207
|
+
cwd=repo_root,
|
|
208
|
+
)
|
|
209
|
+
if result.returncode != 0:
|
|
210
|
+
message = (result.stderr or result.stdout or "").strip()
|
|
211
|
+
print(message or "Error: gh pr checks failed.", file=sys.stderr)
|
|
212
|
+
return None
|
|
213
|
+
else:
|
|
214
|
+
print(message or "Error: gh pr checks failed.", file=sys.stderr)
|
|
215
|
+
return None
|
|
216
|
+
try:
|
|
217
|
+
data = json.loads(result.stdout or "[]")
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
print("Error: unable to parse checks JSON.", file=sys.stderr)
|
|
220
|
+
return None
|
|
221
|
+
if not isinstance(data, list):
|
|
222
|
+
print("Error: unexpected checks JSON shape.", file=sys.stderr)
|
|
223
|
+
return None
|
|
224
|
+
return data
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def is_failing(check: dict[str, Any]) -> bool:
|
|
228
|
+
conclusion = normalize_field(check.get("conclusion"))
|
|
229
|
+
if conclusion in FAILURE_CONCLUSIONS:
|
|
230
|
+
return True
|
|
231
|
+
state = normalize_field(check.get("state") or check.get("status"))
|
|
232
|
+
if state in FAILURE_STATES:
|
|
233
|
+
return True
|
|
234
|
+
bucket = normalize_field(check.get("bucket"))
|
|
235
|
+
return bucket in FAILURE_BUCKETS
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def analyze_check(
|
|
239
|
+
check: dict[str, Any],
|
|
240
|
+
repo_root: Path,
|
|
241
|
+
max_lines: int,
|
|
242
|
+
context: int,
|
|
243
|
+
) -> dict[str, Any]:
|
|
244
|
+
url = check.get("detailsUrl") or check.get("link") or ""
|
|
245
|
+
run_id = extract_run_id(url)
|
|
246
|
+
job_id = extract_job_id(url)
|
|
247
|
+
base: dict[str, Any] = {
|
|
248
|
+
"name": check.get("name", ""),
|
|
249
|
+
"detailsUrl": url,
|
|
250
|
+
"runId": run_id,
|
|
251
|
+
"jobId": job_id,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if run_id is None:
|
|
255
|
+
base["status"] = "external"
|
|
256
|
+
base["note"] = "No GitHub Actions run id detected in detailsUrl."
|
|
257
|
+
return base
|
|
258
|
+
|
|
259
|
+
metadata = fetch_run_metadata(run_id, repo_root)
|
|
260
|
+
log_text, log_error, log_status = fetch_check_log(
|
|
261
|
+
run_id=run_id,
|
|
262
|
+
job_id=job_id,
|
|
263
|
+
repo_root=repo_root,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if log_status == "pending":
|
|
267
|
+
base["status"] = "log_pending"
|
|
268
|
+
base["note"] = log_error or "Logs are not available yet."
|
|
269
|
+
if metadata:
|
|
270
|
+
base["run"] = metadata
|
|
271
|
+
return base
|
|
272
|
+
|
|
273
|
+
if log_error:
|
|
274
|
+
base["status"] = "log_unavailable"
|
|
275
|
+
base["error"] = log_error
|
|
276
|
+
if metadata:
|
|
277
|
+
base["run"] = metadata
|
|
278
|
+
return base
|
|
279
|
+
|
|
280
|
+
snippet = extract_failure_snippet(log_text, max_lines=max_lines, context=context)
|
|
281
|
+
base["status"] = "ok"
|
|
282
|
+
base["run"] = metadata or {}
|
|
283
|
+
base["logSnippet"] = snippet
|
|
284
|
+
base["logTail"] = tail_lines(log_text, max_lines)
|
|
285
|
+
return base
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def extract_run_id(url: str) -> str | None:
|
|
289
|
+
if not url:
|
|
290
|
+
return None
|
|
291
|
+
for pattern in (r"/actions/runs/(\d+)", r"/runs/(\d+)"):
|
|
292
|
+
match = re.search(pattern, url)
|
|
293
|
+
if match:
|
|
294
|
+
return match.group(1)
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def extract_job_id(url: str) -> str | None:
|
|
299
|
+
if not url:
|
|
300
|
+
return None
|
|
301
|
+
match = re.search(r"/actions/runs/\d+/job/(\d+)", url)
|
|
302
|
+
if match:
|
|
303
|
+
return match.group(1)
|
|
304
|
+
match = re.search(r"/job/(\d+)", url)
|
|
305
|
+
if match:
|
|
306
|
+
return match.group(1)
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def fetch_run_metadata(run_id: str, repo_root: Path) -> dict[str, Any] | None:
|
|
311
|
+
fields = [
|
|
312
|
+
"conclusion",
|
|
313
|
+
"status",
|
|
314
|
+
"workflowName",
|
|
315
|
+
"name",
|
|
316
|
+
"event",
|
|
317
|
+
"headBranch",
|
|
318
|
+
"headSha",
|
|
319
|
+
"url",
|
|
320
|
+
]
|
|
321
|
+
result = run_gh_command(["run", "view", run_id, "--json", ",".join(fields)], cwd=repo_root)
|
|
322
|
+
if result.returncode != 0:
|
|
323
|
+
return None
|
|
324
|
+
try:
|
|
325
|
+
data = json.loads(result.stdout or "{}")
|
|
326
|
+
except json.JSONDecodeError:
|
|
327
|
+
return None
|
|
328
|
+
if not isinstance(data, dict):
|
|
329
|
+
return None
|
|
330
|
+
return data
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def fetch_check_log(
|
|
334
|
+
run_id: str,
|
|
335
|
+
job_id: str | None,
|
|
336
|
+
repo_root: Path,
|
|
337
|
+
) -> tuple[str, str, str]:
|
|
338
|
+
log_text, log_error = fetch_run_log(run_id, repo_root)
|
|
339
|
+
if not log_error:
|
|
340
|
+
return log_text, "", "ok"
|
|
341
|
+
|
|
342
|
+
if is_log_pending_message(log_error) and job_id:
|
|
343
|
+
job_log, job_error = fetch_job_log(job_id, repo_root)
|
|
344
|
+
if job_log:
|
|
345
|
+
return job_log, "", "ok"
|
|
346
|
+
if job_error and is_log_pending_message(job_error):
|
|
347
|
+
return "", job_error, "pending"
|
|
348
|
+
if job_error:
|
|
349
|
+
return "", job_error, "error"
|
|
350
|
+
return "", log_error, "pending"
|
|
351
|
+
|
|
352
|
+
if is_log_pending_message(log_error):
|
|
353
|
+
return "", log_error, "pending"
|
|
354
|
+
|
|
355
|
+
return "", log_error, "error"
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def fetch_run_log(run_id: str, repo_root: Path) -> tuple[str, str]:
|
|
359
|
+
result = run_gh_command(["run", "view", run_id, "--log"], cwd=repo_root)
|
|
360
|
+
if result.returncode != 0:
|
|
361
|
+
error = (result.stderr or result.stdout or "").strip()
|
|
362
|
+
return "", error or "gh run view failed"
|
|
363
|
+
return result.stdout, ""
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def fetch_job_log(job_id: str, repo_root: Path) -> tuple[str, str]:
|
|
367
|
+
repo_slug = fetch_repo_slug(repo_root)
|
|
368
|
+
if not repo_slug:
|
|
369
|
+
return "", "Error: unable to resolve repository name for job logs."
|
|
370
|
+
endpoint = f"/repos/{repo_slug}/actions/jobs/{job_id}/logs"
|
|
371
|
+
returncode, stdout_bytes, stderr = run_gh_command_raw(["api", endpoint], cwd=repo_root)
|
|
372
|
+
if returncode != 0:
|
|
373
|
+
message = (stderr or stdout_bytes.decode(errors="replace")).strip()
|
|
374
|
+
return "", message or "gh api job logs failed"
|
|
375
|
+
if is_zip_payload(stdout_bytes):
|
|
376
|
+
return "", "Job logs returned a zip archive; unable to parse."
|
|
377
|
+
return stdout_bytes.decode(errors="replace"), ""
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def fetch_repo_slug(repo_root: Path) -> str | None:
|
|
381
|
+
result = run_gh_command(["repo", "view", "--json", "nameWithOwner"], cwd=repo_root)
|
|
382
|
+
if result.returncode != 0:
|
|
383
|
+
return None
|
|
384
|
+
try:
|
|
385
|
+
data = json.loads(result.stdout or "{}")
|
|
386
|
+
except json.JSONDecodeError:
|
|
387
|
+
return None
|
|
388
|
+
name_with_owner = data.get("nameWithOwner")
|
|
389
|
+
if not name_with_owner:
|
|
390
|
+
return None
|
|
391
|
+
return str(name_with_owner)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def normalize_field(value: Any) -> str:
|
|
395
|
+
if value is None:
|
|
396
|
+
return ""
|
|
397
|
+
return str(value).strip().lower()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def parse_available_fields(message: str) -> list[str]:
|
|
401
|
+
if "Available fields:" not in message:
|
|
402
|
+
return []
|
|
403
|
+
fields: list[str] = []
|
|
404
|
+
collecting = False
|
|
405
|
+
for line in message.splitlines():
|
|
406
|
+
if "Available fields:" in line:
|
|
407
|
+
collecting = True
|
|
408
|
+
continue
|
|
409
|
+
if not collecting:
|
|
410
|
+
continue
|
|
411
|
+
field = line.strip()
|
|
412
|
+
if not field:
|
|
413
|
+
continue
|
|
414
|
+
fields.append(field)
|
|
415
|
+
return fields
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def is_log_pending_message(message: str) -> bool:
|
|
419
|
+
lowered = message.lower()
|
|
420
|
+
return any(marker in lowered for marker in PENDING_LOG_MARKERS)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def is_zip_payload(payload: bytes) -> bool:
|
|
424
|
+
return payload.startswith(b"PK")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def extract_failure_snippet(log_text: str, max_lines: int, context: int) -> str:
|
|
428
|
+
lines = log_text.splitlines()
|
|
429
|
+
if not lines:
|
|
430
|
+
return ""
|
|
431
|
+
|
|
432
|
+
marker_index = find_failure_index(lines)
|
|
433
|
+
if marker_index is None:
|
|
434
|
+
return "\n".join(lines[-max_lines:])
|
|
435
|
+
|
|
436
|
+
start = max(0, marker_index - context)
|
|
437
|
+
end = min(len(lines), marker_index + context)
|
|
438
|
+
window = lines[start:end]
|
|
439
|
+
if len(window) > max_lines:
|
|
440
|
+
window = window[-max_lines:]
|
|
441
|
+
return "\n".join(window)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def find_failure_index(lines: Sequence[str]) -> int | None:
|
|
445
|
+
for idx in range(len(lines) - 1, -1, -1):
|
|
446
|
+
lowered = lines[idx].lower()
|
|
447
|
+
if any(marker in lowered for marker in FAILURE_MARKERS):
|
|
448
|
+
return idx
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def tail_lines(text: str, max_lines: int) -> str:
|
|
453
|
+
if max_lines <= 0:
|
|
454
|
+
return ""
|
|
455
|
+
lines = text.splitlines()
|
|
456
|
+
return "\n".join(lines[-max_lines:])
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def render_results(pr_number: str, results: Iterable[dict[str, Any]]) -> None:
|
|
460
|
+
results_list = list(results)
|
|
461
|
+
print(f"PR #{pr_number}: {len(results_list)} failing checks analyzed.")
|
|
462
|
+
for result in results_list:
|
|
463
|
+
print("-" * 60)
|
|
464
|
+
print(f"Check: {result.get('name', '')}")
|
|
465
|
+
if result.get("detailsUrl"):
|
|
466
|
+
print(f"Details: {result['detailsUrl']}")
|
|
467
|
+
run_id = result.get("runId")
|
|
468
|
+
if run_id:
|
|
469
|
+
print(f"Run ID: {run_id}")
|
|
470
|
+
job_id = result.get("jobId")
|
|
471
|
+
if job_id:
|
|
472
|
+
print(f"Job ID: {job_id}")
|
|
473
|
+
status = result.get("status", "unknown")
|
|
474
|
+
print(f"Status: {status}")
|
|
475
|
+
|
|
476
|
+
run_meta = result.get("run", {})
|
|
477
|
+
if run_meta:
|
|
478
|
+
branch = run_meta.get("headBranch", "")
|
|
479
|
+
sha = (run_meta.get("headSha") or "")[:12]
|
|
480
|
+
workflow = run_meta.get("workflowName") or run_meta.get("name") or ""
|
|
481
|
+
conclusion = run_meta.get("conclusion") or run_meta.get("status") or ""
|
|
482
|
+
print(f"Workflow: {workflow} ({conclusion})")
|
|
483
|
+
if branch or sha:
|
|
484
|
+
print(f"Branch/SHA: {branch} {sha}")
|
|
485
|
+
if run_meta.get("url"):
|
|
486
|
+
print(f"Run URL: {run_meta['url']}")
|
|
487
|
+
|
|
488
|
+
if result.get("note"):
|
|
489
|
+
print(f"Note: {result['note']}")
|
|
490
|
+
|
|
491
|
+
if result.get("error"):
|
|
492
|
+
print(f"Error fetching logs: {result['error']}")
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
snippet = result.get("logSnippet") or ""
|
|
496
|
+
if snippet:
|
|
497
|
+
print("Failure snippet:")
|
|
498
|
+
print(indent_block(snippet, prefix=" "))
|
|
499
|
+
else:
|
|
500
|
+
print("No snippet available.")
|
|
501
|
+
print("-" * 60)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def indent_block(text: str, prefix: str = " ") -> str:
|
|
505
|
+
return "\n".join(f"{prefix}{line}" for line in text.splitlines())
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
if __name__ == "__main__":
|
|
509
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gh-review-workflow
|
|
3
|
+
description: Inspect and address GitHub pull request review state, including latest PR comments, review submissions, unresolved inline threads, actionable review feedback, and post-fix thread resolution. Use when Codex needs to check "latest PR comments", summarize or implement requested PR changes, inspect the current branch PR, or resolve review threads after fixes are pushed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GH Review Workflow
|
|
7
|
+
|
|
8
|
+
Use this skill when the user wants the real review state of a GitHub PR, not just flat issue comments. Prefer GitHub connector/MCP tools for repository and PR metadata. Use the bundled scripts or `gh api graphql` when you need inline review threads, `isResolved`, `isOutdated`, or file and line anchors.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Resolve the target PR.
|
|
13
|
+
- If the user provides a PR URL or repo plus PR number, use that directly.
|
|
14
|
+
- If the task is about the current branch PR, run `gh auth status` and then use the bundled `scripts/fetch_review_state.py` with no arguments.
|
|
15
|
+
- If the task is about the latest PR in a repo, first use GitHub metadata tools to list PRs sorted by create time.
|
|
16
|
+
|
|
17
|
+
2. Inspect metadata and flat comments.
|
|
18
|
+
- Use GitHub connector data for PR title, head/base branches, and top-level PR comments.
|
|
19
|
+
- Do not treat flat PR comments as complete review state.
|
|
20
|
+
|
|
21
|
+
3. Inspect review threads.
|
|
22
|
+
- Run `python3 ~/.codex/skills/gh-review-workflow/scripts/fetch_review_state.py` for the current branch PR.
|
|
23
|
+
- Or run `python3 ~/.codex/skills/gh-review-workflow/scripts/fetch_review_state.py --owner OWNER --repo REPO --number 123`.
|
|
24
|
+
- Read `review_threads` first. That is the source of truth for inline review comments and resolution state.
|
|
25
|
+
|
|
26
|
+
4. Summarize actionability.
|
|
27
|
+
- Group duplicate comments by behavior or file instead of answering each line independently.
|
|
28
|
+
- Separate real blockers from style suggestions and "reply only" items.
|
|
29
|
+
- Call out whether a thread is still current or already `isOutdated`.
|
|
30
|
+
|
|
31
|
+
5. If the user asks to address review feedback.
|
|
32
|
+
- Confirm the intended scope unless the user explicitly says to fix all unresolved actionable threads.
|
|
33
|
+
- Keep each code change traceable to a feedback cluster.
|
|
34
|
+
- If a comment needs explanation rather than code, draft the response instead of forcing a code change.
|
|
35
|
+
- Verify locally before pushing or resolving any thread.
|
|
36
|
+
|
|
37
|
+
6. After fixes.
|
|
38
|
+
- Verify code locally before pushing.
|
|
39
|
+
- Push the branch.
|
|
40
|
+
- If the user asked to resolve addressed threads, run `python3 ~/.codex/skills/gh-review-workflow/scripts/resolve_review_threads.py THREAD_ID...`.
|
|
41
|
+
- If the user wants to resolve all unresolved threads captured in a saved JSON dump, run `python3 ~/.codex/skills/gh-review-workflow/scripts/resolve_review_threads.py --from-json review_state.json`.
|
|
42
|
+
|
|
43
|
+
## Interpretation Rules
|
|
44
|
+
|
|
45
|
+
- `conversation_comments` are top-level PR comments.
|
|
46
|
+
- `reviews` are submitted or draft review objects. `PENDING` means there is a draft review even if no formal review event was submitted yet.
|
|
47
|
+
- `review_threads` hold inline review comments, anchors, and resolution state.
|
|
48
|
+
- `isOutdated: true` means the original diff context moved or changed; decide whether the underlying concern is already addressed before ignoring it.
|
|
49
|
+
- Repeated comments like "can we reuse X?" often map to one design decision. Cluster them.
|
|
50
|
+
|
|
51
|
+
## Write Safety
|
|
52
|
+
|
|
53
|
+
- Do not comment on GitHub or resolve review threads unless the user explicitly asked for a write.
|
|
54
|
+
- Do not reject a suggestion vaguely. If keeping the current design, explain the ABI, spec, or ownership reason concretely.
|
|
55
|
+
- For review requests, prioritize behavioral bugs, regressions, and missing validation over cosmetic style changes.
|
|
56
|
+
|
|
57
|
+
## Resources
|
|
58
|
+
|
|
59
|
+
- Read [workflow.md](references/workflow.md) for the tool split and response template.
|
|
60
|
+
- Use `scripts/fetch_review_state.py` for thread-aware reads.
|
|
61
|
+
- Use `scripts/resolve_review_threads.py` for post-fix cleanup.
|