feat(tools): report progress changes on pull requests
progress.py stores the names of implemented and pending items, can measure another source tree with --root and prints a Markdown diff of two runs with --compare. The Progress report workflow compares every pull request with its base, and Progress comment posts the result as a single comment, updated on every push and removed when progress no longer changes. Commenting runs as a separate workflow_run so pull requests from forks never run with write permissions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3c6b0af6f7
commit
e73cd7dec3
|
|
@ -0,0 +1,42 @@
|
|||
name: Progress comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Progress report]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: progress-report
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
- env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
RUN_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
pr=$(cat pr)
|
||||
[[ $pr =~ ^[0-9]+$ ]] || exit 1
|
||||
[ "$(gh pr view "$pr" --json headRefOid --jq .headRefOid)" = "$RUN_SHA" ] || exit 0
|
||||
marker='<!-- progress-report -->'
|
||||
id=$(gh api "repos/$GH_REPO/issues/$pr/comments" --paginate \
|
||||
--jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | .id" | head -n1)
|
||||
if [ -f report.md ]; then
|
||||
printf '%s\n%s' "$marker" "$(cat report.md)" > body.md
|
||||
if [ -n "$id" ]; then
|
||||
gh api -X PATCH "repos/$GH_REPO/issues/comments/$id" -F body=@body.md > /dev/null
|
||||
else
|
||||
gh pr comment "$pr" --body-file body.md
|
||||
fi
|
||||
elif [ -n "$id" ]; then
|
||||
gh api -X DELETE "repos/$GH_REPO/issues/comments/$id"
|
||||
fi
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
name: Progress report
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
report:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- env:
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
out=$RUNNER_TEMP/out report=$RUNNER_TEMP/report
|
||||
git worktree add --detach "$RUNNER_TEMP/base" HEAD^1
|
||||
python3 tools/progress.py --root "$RUNNER_TEMP/base" "$out/base"
|
||||
python3 tools/progress.py "$out/head"
|
||||
mkdir "$report"
|
||||
echo "$PR" > "$report/pr"
|
||||
body=$(python3 tools/progress.py --compare "$out/base/progress.json" "$out/head/progress.json")
|
||||
if [ -n "$body" ]; then
|
||||
printf '### Progress report (%s → %s)\n\n%s\n' "$(git rev-parse --short HEAD^1)" "${HEAD_SHA::7}" "$body" > "$report/report.md"
|
||||
cat "$report/report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: progress-report
|
||||
path: ${{ runner.temp }}/report
|
||||
|
|
@ -13,10 +13,20 @@ Requires only Python 3.9+ with the standard library. It writes:
|
|||
- `progress.svg`: treemaps for system libraries and GPU shader instructions
|
||||
- `badge-libraries.svg`, `badge-shaders.svg`: percentage badges
|
||||
- `index.html`: the map with per-library and per-encoding tables
|
||||
- `progress.json`: raw numbers
|
||||
- `progress.json`: raw numbers and the names of implemented and pending items
|
||||
|
||||
The [Progress workflow](../.github/workflows/progress.yml) runs it on every push to `main` and deploys the output to GitHub Pages, so nothing generated is committed to the repository.
|
||||
|
||||
To compare two runs, for example `main` and a pull request:
|
||||
|
||||
```sh
|
||||
python3 tools/progress.py --root <base-checkout> base
|
||||
python3 tools/progress.py head
|
||||
python3 tools/progress.py --compare base/progress.json head/progress.json
|
||||
```
|
||||
|
||||
`--root` measures another source tree with the current script. `--compare` prints a Markdown report of implemented, newly declared, reverted and removed items, and prints nothing when progress did not change. The [Progress report workflow](../.github/workflows/progress-report.yml) runs it for every pull request, and [Progress comment](../.github/workflows/progress-comment.yml) posts the report as a single comment on the pull request, updated on every push. It runs as a separate `workflow_run` so pull requests from forks never run with write permissions.
|
||||
|
||||
### What is measured
|
||||
|
||||
- **System libraries**: functions defined in [core/libs/prx](../core/libs/prx). A function is implemented when it no longer calls `NotImplemented_nid_no_patch`. The total only includes functions already declared in the project, not every function of the PS5 firmware.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ OPCODE_ALIASES = {
|
|||
"VMadMixloF16": "V_FMA_MIXLO_F16",
|
||||
"VMadMixhiF16": "V_FMA_MIXHI_F16",
|
||||
}
|
||||
REPORT_ROWS = 100
|
||||
PANEL_WIDTH, GAP, MAP_HEIGHT, HEADER = 495, 10, 280, 30
|
||||
DONE_COLOR, TODO_COLOR, BORDER, TEXT = "#2ea043", "#1f6feb", "#0d1117", "#ffffff"
|
||||
|
||||
|
|
@ -51,7 +52,8 @@ def scan_library(path):
|
|||
body = text[match.end() - 1:body_end(text, match.end() - 1)]
|
||||
(todo if STUB in body else done).add(name)
|
||||
todo -= done
|
||||
return {"name": path.name, "label": path.name.removeprefix("libSce"), "done": len(done), "todo": len(todo)}
|
||||
return {"name": path.name, "label": path.name.removeprefix("libSce"), "done": len(done), "todo": len(todo),
|
||||
"done_names": sorted(done), "todo_names": sorted(todo)}
|
||||
|
||||
|
||||
def summarize(groups):
|
||||
|
|
@ -88,8 +90,11 @@ def collect_shaders():
|
|||
extra.append(opcode)
|
||||
groups = {}
|
||||
for name, encoding in isa.items():
|
||||
group = groups.setdefault(encoding, {"name": encoding, "label": encoding, "done": 0, "todo": 0})
|
||||
group["done" if name in supported else "todo"] += 1
|
||||
group = groups.setdefault(encoding, {"name": encoding, "label": encoding, "done": 0, "todo": 0,
|
||||
"done_names": [], "todo_names": []})
|
||||
state = "done" if name in supported else "todo"
|
||||
group[state] += 1
|
||||
group[f"{state}_names"].append(name)
|
||||
result = summarize(sorted(groups.values(), key=lambda g: g["name"]))
|
||||
result["extra"] = extra
|
||||
return result
|
||||
|
|
@ -226,10 +231,62 @@ def summary(libraries, shaders):
|
|||
]) + "\n"
|
||||
|
||||
|
||||
def names(data, state):
|
||||
return {(g["name"], n) for g in data["groups"] for n in g.get(f"{state}_names", [])}
|
||||
|
||||
|
||||
def details(icon, title, column, items):
|
||||
if not items:
|
||||
return []
|
||||
rows = [f"<details>\n<summary>{icon} {len(items)} {title}</summary>\n", f"| {column} | Name |", "| - | - |"]
|
||||
rows += [f"| {group} | `{name}` |" for group, name in sorted(items)[:REPORT_ROWS]]
|
||||
if len(items) > REPORT_ROWS:
|
||||
rows.append(f"| ... | {len(items) - REPORT_ROWS} more |")
|
||||
return rows + ["\n</details>"]
|
||||
|
||||
|
||||
def compare(title, column, unit, base, head):
|
||||
base_done, head_done = names(base, "done"), names(head, "done")
|
||||
base_all, head_all = base_done | names(base, "todo"), head_done | names(head, "todo")
|
||||
implemented, declared = head_done - base_done, head_all - base_all - head_done
|
||||
regressed, removed = base_done & (head_all - head_done), base_all - head_all
|
||||
if not (implemented or declared or regressed or removed):
|
||||
return []
|
||||
delta = round(head["percent"] - base["percent"], 2)
|
||||
icon = "📈" if delta > 0 else "📉" if delta < 0 else "➖"
|
||||
counts = [f"{n:+} {label}" for n, label in ((len(implemented), "implemented"), (len(declared), "declared"),
|
||||
(-len(removed), "removed")) if n]
|
||||
lines = [f'{icon} **{title}**: {head["percent"]}% ({delta:+}%, {", ".join(counts)} {unit})', ""]
|
||||
lines += details("✅", "implemented", column, implemented)
|
||||
lines += details("🆕", "declared as stubs", column, declared)
|
||||
lines += details("⚠️", "went back to stubs", column, regressed)
|
||||
lines += details("🗑️", "removed", column, removed)
|
||||
return lines + [""]
|
||||
|
||||
|
||||
def report(base, head):
|
||||
lines = compare("System libraries", "Library", "functions", base["libraries"], head["libraries"])
|
||||
lines += compare("GPU shader instructions", "Encoding", "instructions", base["shaders"], head["shaders"])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("output", type=Path)
|
||||
output = parser.parse_args().output
|
||||
parser.add_argument("output", type=Path, nargs="?")
|
||||
parser.add_argument("--root", type=Path, help="source tree to measure instead of the one containing this script")
|
||||
parser.add_argument("--compare", type=Path, nargs=2, metavar=("BASE", "HEAD"),
|
||||
help="print a markdown report of the changes between two progress.json files")
|
||||
args = parser.parse_args()
|
||||
if args.compare:
|
||||
base, head = (json.loads(path.read_text()) for path in args.compare)
|
||||
print(report(base, head), end="")
|
||||
raise SystemExit
|
||||
if not args.output:
|
||||
parser.error("the output directory is required")
|
||||
if args.root:
|
||||
PRX = args.root / "core" / "libs" / "prx"
|
||||
OPCODES = args.root / OPCODES.relative_to(ROOT)
|
||||
output = args.output
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
libraries, shaders = collect_libraries(), collect_shaders()
|
||||
(output / "progress.json").write_text(json.dumps({"libraries": libraries, "shaders": shaders}, indent=2))
|
||||
|
|
|
|||
Loading…
Reference in New Issue