Skip to content

Commit 958f986

Browse files
author
Jonas Greifenhain
committed
feat(ci): Add workflow for package score (#6755)
1 parent 2476af3 commit 958f986

File tree

4 files changed

+165
-0
lines changed

4 files changed

+165
-0
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Package Score
2+
description: |
3+
Run pana (pub.dev package analyzer) to compute the package score,
4+
print a readable report to the workflow log, and upload the raw
5+
markdown report as a workflow artifact (score_report.md).
6+
The score is compared against the currently published version on
7+
pub.dev — the build fails if the score regresses. If the package
8+
has never been published, no threshold is enforced.
9+
10+
inputs:
11+
working-directory:
12+
description: The working directory of the package (relative to repo root)
13+
required: true
14+
package-name:
15+
description: The name of the package being scored
16+
required: true
17+
18+
runs:
19+
using: "composite"
20+
steps:
21+
- name: Install pana
22+
shell: bash
23+
run: dart pub global activate pana
24+
25+
- name: Fetch published score from pub.dev
26+
id: published
27+
shell: bash
28+
run: |
29+
PACKAGE_NAME="${{ inputs.package-name }}"
30+
HTTP_CODE=$(curl -s -o "$RUNNER_TEMP/pubdev_score.json" -w "%{http_code}" \
31+
"https://pub.dev/api/packages/${PACKAGE_NAME}/score")
32+
33+
if [ "$HTTP_CODE" = "200" ]; then
34+
GRANTED=$(python3 -c "import json; d=json.load(open('$RUNNER_TEMP/pubdev_score.json')); print(d.get('grantedPoints', 0))")
35+
MAX=$(python3 -c "import json; d=json.load(open('$RUNNER_TEMP/pubdev_score.json')); print(d.get('maxPoints', 160))")
36+
THRESHOLD=$((MAX - GRANTED))
37+
echo "published=true" >> "$GITHUB_OUTPUT"
38+
echo "granted=$GRANTED" >> "$GITHUB_OUTPUT"
39+
echo "max=$MAX" >> "$GITHUB_OUTPUT"
40+
echo "threshold=$THRESHOLD" >> "$GITHUB_OUTPUT"
41+
echo "Published score for ${PACKAGE_NAME}: ${GRANTED}/${MAX} (threshold: ${THRESHOLD})"
42+
else
43+
echo "published=false" >> "$GITHUB_OUTPUT"
44+
echo "Package ${PACKAGE_NAME} not found on pub.dev (HTTP ${HTTP_CODE}) — skipping threshold enforcement"
45+
fi
46+
47+
- name: Run pana
48+
id: pana
49+
shell: bash
50+
working-directory: ${{ inputs.working-directory }}
51+
run: |
52+
set +e
53+
if [ "${{ steps.published.outputs.published }}" = "true" ]; then
54+
echo "Enforcing threshold: score must be at least ${{ steps.published.outputs.granted }}/${{ steps.published.outputs.max }} (published score)"
55+
dart pub global run pana \
56+
--exit-code-threshold ${{ steps.published.outputs.threshold }} \
57+
--no-warning . > "$RUNNER_TEMP/score_report.md" 2>&1
58+
else
59+
echo "No published score found — running pana without threshold"
60+
dart pub global run pana \
61+
--no-warning . > "$RUNNER_TEMP/score_report.md" 2>&1
62+
fi
63+
PANA_EXIT=$?
64+
set -e
65+
echo "pana_exit=$PANA_EXIT" >> "$GITHUB_OUTPUT"
66+
67+
- name: Print score report
68+
shell: bash
69+
run: python3 "$GITHUB_ACTION_PATH/render_score.py" "$RUNNER_TEMP/score_report.md"
70+
71+
- name: Upload score report
72+
if: always()
73+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
74+
with:
75+
name: score_report-${{ inputs.package-name }}-${{ github.run_id }}
76+
path: ${{ runner.temp }}/score_report.md
77+
78+
- name: Enforce score threshold
79+
if: steps.pana.outputs.pana_exit != '0'
80+
shell: bash
81+
run: |
82+
echo "::error::Pana score regressed below the published score (${{ steps.published.outputs.granted }}/${{ steps.published.outputs.max }}) on pub.dev"
83+
exit ${{ steps.pana.outputs.pana_exit }}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env python3
2+
"""Render pana score markdown as a readable ASCII terminal report."""
3+
import re
4+
import sys
5+
6+
7+
def render(text):
8+
W = 72
9+
lines = []
10+
11+
def bar(label, got, total):
12+
pct = got / total if total else 0
13+
filled = int(pct * 20)
14+
return f" {label:<45} [{'█' * filled + '░' * (20 - filled)}] {got}/{total}"
15+
16+
lines.append("=" * W)
17+
lines.append(" PACKAGE SCORE REPORT".center(W))
18+
lines.append("=" * W)
19+
20+
m = re.search(r'Points:\s*(\d+)\s*/\s*(\d+)', text)
21+
if m:
22+
got, total = int(m.group(1)), int(m.group(2))
23+
lines.append(f"\n Total: {got} / {total} points")
24+
lines.append(bar("Overall", got, total))
25+
lines.append("")
26+
27+
for sm in re.finditer(r'^## (✓|✗)\s+(.+?)\s*\((\d+)\s*/\s*(\d+)\)', text, re.M):
28+
icon = "✓" if sm.group(1) == "✓" else "✗"
29+
name, got, total = sm.group(2), int(sm.group(3)), int(sm.group(4))
30+
lines.append("-" * W)
31+
lines.append(f" {icon} {name}")
32+
lines.append(bar(name, got, total))
33+
34+
sec_start = sm.end()
35+
sec_end = text.find("\n## ", sec_start)
36+
if sec_end == -1:
37+
sec_end = len(text)
38+
section = text[sec_start:sec_end]
39+
40+
for sub in re.finditer(r'^### \[(.)\]\s+(\d+)/(\d+) points:\s+(.+)', section, re.M):
41+
mark = sub.group(1)
42+
sg, st, label = int(sub.group(2)), int(sub.group(3)), re.sub(r'\*\*([^*]+)\*\*', r'\1', sub.group(4))
43+
sym = {"*": "✓", "x": "✗", "~": "~"}.get(mark, mark)
44+
lines.append(f" {sym} {sg:>2}/{st:<2} {label}")
45+
46+
if "platform" in name.lower():
47+
if re.search(r'\*\*WASM-ready:\*\*', section):
48+
lines.append(" ✓ WASM compatible")
49+
elif re.search(r'not WASM-compatible', section):
50+
lines.append(" ✗ WASM not compatible (partial Web score)")
51+
52+
lines.append("-" * W)
53+
54+
issues = re.findall(
55+
r'The constraint `([^`]+)` on (\w+) does not support the stable version `([^`]+)`', text
56+
)
57+
if issues:
58+
lines.append(f"\n ⚠ Dependency issues ({len(issues)}):")
59+
for constraint, pkg, ver in issues:
60+
lines.append(f" • {pkg}: {constraint} doesn't support {ver}")
61+
62+
lines.append("")
63+
print("\n".join(lines))
64+
65+
66+
if __name__ == "__main__":
67+
path = sys.argv[1] if len(sys.argv) > 1 else "score.md"
68+
render(open(path).read())

.github/workflows/dart_vm.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ jobs:
106106
run: dart analyze --fatal-infos --fatal-warnings .
107107
working-directory: ${{ inputs.working-directory }}
108108

109+
- name: Package Score
110+
if: "always() && steps.bootstrap.conclusion == 'success' && matrix.sdk == 'stable'"
111+
uses: ./.github/composite_actions/package_score
112+
with:
113+
working-directory: ${{ inputs.working-directory }}
114+
package-name: ${{ inputs.package-name }}
115+
109116
- name: Run Tests (stable)
110117
if: "always() && steps.bootstrap.conclusion == 'success' && steps.testCheck.outputs.hasTests == 'true' && matrix.sdk == 'stable'"
111118
run: dart test

.github/workflows/flutter_vm.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ jobs:
8888
run: flutter analyze --fatal-infos --fatal-warnings .
8989
working-directory: ${{ inputs.working-directory }}
9090

91+
- name: Package Score
92+
if: "always() && steps.bootstrap.conclusion == 'success' && matrix.channel == 'stable' && matrix.flutter-version == 'any'"
93+
uses: ./.github/composite_actions/package_score
94+
with:
95+
working-directory: ${{ inputs.working-directory }}
96+
package-name: ${{ inputs.package-name }}
97+
9198
- name: Run Tests
9299
id: testJob
93100
if: "always() && steps.bootstrap.conclusion == 'success' && steps.testCheck.outputs.hasTests == 'true'"

0 commit comments

Comments
 (0)