Skip to content

Commit 58dd883

Browse files
authored
Merge pull request #45 from kaitj/enh/badge
Automate simple README badge
2 parents 87d9d3d + dbe8045 commit 58dd883

File tree

2 files changed

+173
-3
lines changed

2 files changed

+173
-3
lines changed

.github/scripts/create_badge.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env python
2+
3+
import base64
4+
import json
5+
import sys
6+
from enum import Enum
7+
from pathlib import Path
8+
from typing import Any
9+
from urllib.parse import quote_plus
10+
11+
CategoryCounts = dict[str, dict[str, tuple[int, int]]]
12+
13+
14+
class ComplianceColor(str, Enum):
15+
GREY = "lightgrey"
16+
RED = "red"
17+
ORANGE = "orange"
18+
YELLOW = "yellow"
19+
GREEN = "green"
20+
21+
22+
class ShieldColor(str, Enum):
23+
BRONZE = "#CD7F32"
24+
SILVER = "#1C274C"
25+
GOLD = "#FFD700"
26+
27+
28+
def count_category_tier_bools(obj: Any) -> CategoryCounts:
29+
"""Count number of trues and falses by tier and category."""
30+
return {
31+
category: {
32+
tier: (
33+
sum(
34+
val is True for val in obj.get(category, {}).get(tier, {}).values()
35+
),
36+
sum(
37+
val is False for val in obj.get(category, {}).get(tier, {}).values()
38+
),
39+
)
40+
for tier in ("bronze", "silver", "gold")
41+
}
42+
for category in ("documentation", "infrastructure", "testing")
43+
}
44+
45+
46+
def count_total_bools(counts: CategoryCounts) -> tuple[int, int]:
47+
"""Count total number of trues and falses."""
48+
return (
49+
sum(true_count for cat in counts.values() for true_count, _ in cat.values()),
50+
sum(false_count for cat in counts.values() for _, false_count in cat.values()),
51+
)
52+
53+
54+
def get_highest_tier(counts: CategoryCounts) -> str | None:
55+
"""Return highest completed tier."""
56+
return next(
57+
(
58+
tier
59+
for tier in ("gold", "silver", "bronze")
60+
if any(
61+
false_count == 0
62+
for false_count in (counts[cat][tier][1] for cat in counts)
63+
)
64+
),
65+
None,
66+
)
67+
68+
69+
def get_compliance_color(ratio: float) -> ComplianceColor:
70+
"""Return compliance color."""
71+
if ratio <= 0.25:
72+
return ComplianceColor.RED
73+
elif ratio <= 0.5:
74+
return ComplianceColor.ORANGE
75+
elif ratio <= 0.75:
76+
return ComplianceColor.YELLOW
77+
else:
78+
return ComplianceColor.GREEN
79+
80+
81+
def get_shield_color(tier: str) -> ShieldColor:
82+
"""Return shield color."""
83+
if tier == "bronze":
84+
return ShieldColor.BRONZE
85+
elif tier == "silver":
86+
return ShieldColor.SILVER
87+
elif tier == "gold":
88+
return ShieldColor.GOLD
89+
else:
90+
raise ValueError(f"Unknown tier provided: {tier}")
91+
92+
93+
def generate_shield(tier: str) -> ...:
94+
"""Generate base64 encoded svg shield"""
95+
shield_color = get_shield_color(tier)
96+
shield_xml = f"""<svg viewBox="0 0 24 24" fill="{shield_color}" xmlns="http://www.w3.org/2000/svg">
97+
<path d="M3.37752 5.08241C3 5.62028 3 7.21907 3 10.4167V11.9914C3 17.6294 7.23896 20.3655 9.89856 21.5273C10.62 21.8424 10.9807 22 12 22C13.0193 22 13.38 21.8424 14.1014 21.5273C16.761 20.3655 21 17.6294 21 11.9914V10.4167C21 7.21907 21 5.62028 20.6225 5.08241C20.245 4.54454 18.7417 4.02996 15.7351 3.00079L15.1623 2.80472C13.595 2.26824 12.8114 2 12 2C11.1886 2 10.405 2.26824 8.83772 2.80472L8.26491 3.00079C5.25832 4.02996 3.75503 4.54454 3.37752 5.08241Z"/>
98+
</svg>
99+
"""
100+
return base64.b64encode(shield_xml.encode()).decode()
101+
102+
103+
def generate_badge(true_count: int, false_count: int, highest_tier: str | None) -> str:
104+
"""Generate a shields.io badge based on checklist counts."""
105+
total = true_count + false_count
106+
if total == 0:
107+
return f"https://img.shields.io/badge/NMIND-{quote_plus('0/0')}-{ComplianceColor.GREY}"
108+
109+
compliance_color = get_compliance_color(true_count / total)
110+
msg = f"{true_count}/{total}"
111+
badge_content = f"NMIND-{quote_plus(msg)}-{compliance_color}"
112+
if highest_tier:
113+
badge_content += (
114+
f"?logo=data:image/svg+xml;base64,{generate_shield(highest_tier)}"
115+
)
116+
117+
return f"https://img.shields.io/badge/{badge_content}"
118+
119+
120+
def process_checklist(checklist_path: Path) -> str:
121+
"""Process a checklist, generating a badge."""
122+
checklist = json.loads(checklist_path.read_text())
123+
category_counts = count_category_tier_bools(obj=checklist)
124+
true_count, false_count = count_total_bools(counts=category_counts)
125+
highest_tier = get_highest_tier(counts=category_counts)
126+
badge_url = generate_badge(
127+
true_count=true_count, false_count=false_count, highest_tier=highest_tier
128+
)
129+
130+
return badge_url
131+
132+
133+
if __name__ == "__main__":
134+
if len(sys.argv) != 2:
135+
print("Usage: create_badge.py <path_to_checklist_json>")
136+
sys.exit(1)
137+
checklist_fp = Path(sys.argv[1])
138+
print(process_checklist(checklist_fp))

.github/workflows/checklist.yaml

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
run: |
3535
issue_body='${{ github.event.issue.body }}'
3636
checklist=$(echo "$issue_body" | sed -n '/```json/I,/```/p' | sed '1d;$d')
37-
name=$(echo "$checklist" | jq -r '.name' | sed 's/ /_/g')
37+
name=$(echo "$checklist" | jq -r '.name' | sed -e 's/ /_/g' -e 's/-/_/g')
3838
echo "checklist=$(echo $checklist)" >> $GITHUB_ENV
3939
echo "name=$(echo $name)" >> $GITHUB_ENV
4040
@@ -52,7 +52,7 @@ jobs:
5252
base_dir: src/lib/data/entries
5353
json_schema: .github/static/checklist_schema.json
5454
ajv_strict_mode: false
55-
json_exclude_regex: ''
55+
files: ${{ env.file_name }}
5656

5757
- name: Configure git
5858
if: steps.check_labels.outputs.result == 'true'
@@ -66,4 +66,36 @@ jobs:
6666
git add ${{ env.file_name }}
6767
git commit -m 'Closes #${{ github.event.issue.number }}'
6868
git push
69-
69+
70+
# Badge generation steps
71+
- name: Setup Python
72+
if: steps.check_labels.outputs.result == 'true'
73+
uses: actions/setup-python@v5
74+
with:
75+
python-version: '3.13'
76+
77+
- name: Generate badge
78+
if: steps.check_labels.outputs.result == 'true'
79+
run: |
80+
badge_url=$(python .github/scripts/create_badge.py "${{ env.file_name }}")
81+
echo "badge=$(echo $badge_url)" >> $GITHUB_ENV
82+
83+
- name: Comment on issue
84+
if: steps.check_labels.outputs.result == 'true'
85+
uses: peter-evans/create-or-update-comment@v4
86+
with:
87+
issue-number: ${{ github.event.issue.number }}
88+
body: |
89+
✅ **Your new NMIND badge has been generated!**:
90+
![${{ env.name }}](${{ env.badge }})
91+
92+
🔗 **Badge URL**:
93+
```
94+
${{ env.badge }}
95+
```
96+
97+
📄 **To embed this badge in a `README.md`, use the following Markdown:**
98+
```md
99+
[![${{ env.name }}](${{ env.badge }})](https://nmind.org/proceedings/${{ env.name }})
100+
```
101+

0 commit comments

Comments
 (0)