Skip to content

Commit 6882c7b

Browse files
authored
Merge pull request #1179 from PolicyEngine/hua7450/issue1178
Add Weekly Update Household API feature
2 parents c4397ff + c6babf9 commit 6882c7b

5 files changed

Lines changed: 542 additions & 0 deletions

File tree

.github/scripts/check_updates.py

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Check for PolicyEngine package updates and generate PR summary.
4+
5+
This script checks PyPI for newer versions of PolicyEngine packages,
6+
updates setup.py if needed, and generates changelog summaries.
7+
"""
8+
9+
import os
10+
import re
11+
import sys
12+
13+
import requests
14+
import yaml
15+
16+
# Packages to track (US only - UK is updated separately)
17+
PACKAGES = ["policyengine_us"]
18+
19+
# Map package names to GitHub repos
20+
REPO_MAP = {"policyengine_us": "PolicyEngine/policyengine-us"}
21+
22+
23+
def parse_version(version_str):
24+
"""Parse a version string into a tuple of integers."""
25+
return tuple(map(int, version_str.split(".")))
26+
27+
28+
def get_current_versions(setup_content):
29+
"""Extract current package versions from setup.py content."""
30+
current_versions = {}
31+
for pkg in PACKAGES:
32+
pattern = rf'{pkg.replace("_", "[-_]")}==([0-9]+\.[0-9]+\.[0-9]+)'
33+
match = re.search(pattern, setup_content)
34+
if match:
35+
current_versions[pkg] = match.group(1)
36+
return current_versions
37+
38+
39+
def get_latest_versions():
40+
"""Fetch latest versions from PyPI for all tracked packages."""
41+
latest_versions = {}
42+
for pkg in PACKAGES:
43+
pypi_name = pkg.replace("_", "-")
44+
resp = requests.get(f"https://pypi.org/pypi/{pypi_name}/json")
45+
if resp.status_code == 200:
46+
latest_versions[pkg] = resp.json()["info"]["version"]
47+
return latest_versions
48+
49+
50+
def find_updates(current_versions, latest_versions):
51+
"""Compare current and latest versions to find updates."""
52+
updates = {}
53+
for pkg in PACKAGES:
54+
if pkg in current_versions and pkg in latest_versions:
55+
if current_versions[pkg] != latest_versions[pkg]:
56+
updates[pkg] = {
57+
"old": current_versions[pkg],
58+
"new": latest_versions[pkg],
59+
}
60+
return updates
61+
62+
63+
def update_setup_content(setup_content, updates):
64+
"""Update setup.py content with new versions."""
65+
new_content = setup_content
66+
for pkg, versions in updates.items():
67+
pattern = rf'({pkg.replace("_", "[-_]")}==)[0-9]+\.[0-9]+\.[0-9]+'
68+
new_content = re.sub(pattern, rf'\g<1>{versions["new"]}', new_content)
69+
return new_content
70+
71+
72+
def fetch_changelog(pkg):
73+
"""Fetch changelog from GitHub for a package."""
74+
repo = REPO_MAP.get(pkg)
75+
if not repo:
76+
return None
77+
url = f"https://raw.githubusercontent.com/{repo}/main/changelog.yaml"
78+
resp = requests.get(url)
79+
if resp.status_code == 200:
80+
return yaml.safe_load(resp.text)
81+
return None
82+
83+
84+
def get_changes_between_versions(changelog, old_version, new_version):
85+
"""Extract changelog entries between old and new versions."""
86+
if not changelog:
87+
return []
88+
89+
old_v = parse_version(old_version)
90+
new_v = parse_version(new_version)
91+
92+
entries_with_versions = []
93+
current_version = None
94+
95+
for entry in changelog:
96+
if "version" in entry:
97+
current_version = parse_version(entry["version"])
98+
elif current_version and "bump" in entry:
99+
bump = entry["bump"]
100+
major, minor, patch = current_version
101+
if bump == "major":
102+
current_version = (major + 1, 0, 0)
103+
elif bump == "minor":
104+
current_version = (major, minor + 1, 0)
105+
elif bump == "patch":
106+
current_version = (major, minor, patch + 1)
107+
108+
if current_version:
109+
entries_with_versions.append((current_version, entry))
110+
111+
relevant_entries = []
112+
for version, entry in entries_with_versions:
113+
if old_v < version <= new_v:
114+
relevant_entries.append(entry)
115+
116+
return relevant_entries
117+
118+
119+
def format_changes(entries):
120+
"""Format changelog entries as markdown."""
121+
added = []
122+
changed = []
123+
fixed = []
124+
removed = []
125+
126+
for entry in entries:
127+
changes = entry.get("changes", {})
128+
added.extend(changes.get("added", []))
129+
changed.extend(changes.get("changed", []))
130+
fixed.extend(changes.get("fixed", []))
131+
removed.extend(changes.get("removed", []))
132+
133+
sections = []
134+
if added:
135+
sections.append(
136+
"### Added\n" + "\n".join(f"- {item}" for item in added)
137+
)
138+
if changed:
139+
sections.append(
140+
"### Changed\n" + "\n".join(f"- {item}" for item in changed)
141+
)
142+
if fixed:
143+
sections.append(
144+
"### Fixed\n" + "\n".join(f"- {item}" for item in fixed)
145+
)
146+
if removed:
147+
sections.append(
148+
"### Removed\n" + "\n".join(f"- {item}" for item in removed)
149+
)
150+
151+
return (
152+
"\n\n".join(sections) if sections else "No detailed changes available."
153+
)
154+
155+
156+
def generate_summary(updates):
157+
"""Generate PR summary with version table and changelogs."""
158+
summary_parts = []
159+
160+
# Version table
161+
version_table = "| Package | Old Version | New Version |\n|---------|-------------|-------------|\n"
162+
for pkg, versions in updates.items():
163+
version_table += f"| {pkg} | {versions['old']} | {versions['new']} |\n"
164+
summary_parts.append(version_table)
165+
166+
# Changelog for each package
167+
for pkg, versions in updates.items():
168+
changelog = fetch_changelog(pkg)
169+
if changelog:
170+
entries = get_changes_between_versions(
171+
changelog, versions["old"], versions["new"]
172+
)
173+
if entries:
174+
formatted = format_changes(entries)
175+
summary_parts.append(
176+
f"## What Changed ({pkg} {versions['old']}{versions['new']})\n\n{formatted}"
177+
)
178+
else:
179+
summary_parts.append(
180+
f"## What Changed ({pkg} {versions['old']}{versions['new']})\n\nNo changelog entries found between these versions."
181+
)
182+
183+
return "\n\n".join(summary_parts)
184+
185+
186+
def generate_changelog_entry(updates):
187+
"""Generate changelog entry for this repo."""
188+
new_version = updates["policyengine_us"]["new"]
189+
return f"""- bump: patch
190+
changes:
191+
changed:
192+
- Update PolicyEngine US to {new_version}
193+
"""
194+
195+
196+
def write_github_output(key, value):
197+
"""Write output to GitHub Actions output file."""
198+
github_output = os.environ.get("GITHUB_OUTPUT")
199+
if github_output:
200+
with open(github_output, "a") as f:
201+
f.write(f"{key}={value}\n")
202+
203+
204+
def main():
205+
"""Main entry point for the script."""
206+
# Read current versions from setup.py
207+
with open("setup.py", "r") as f:
208+
setup_content = f.read()
209+
210+
current_versions = get_current_versions(setup_content)
211+
print(f"Current versions: {current_versions}")
212+
213+
# Get latest versions from PyPI
214+
latest_versions = get_latest_versions()
215+
print(f"Latest versions: {latest_versions}")
216+
217+
# Check for updates
218+
updates = find_updates(current_versions, latest_versions)
219+
220+
if not updates:
221+
print("No updates available.")
222+
write_github_output("has_updates", "false")
223+
return 0
224+
225+
print(f"Updates available: {updates}")
226+
227+
# Update setup.py
228+
new_setup_content = update_setup_content(setup_content, updates)
229+
with open("setup.py", "w") as f:
230+
f.write(new_setup_content)
231+
232+
# Generate and save PR summary
233+
full_summary = generate_summary(updates)
234+
with open("pr_summary.md", "w") as f:
235+
f.write(full_summary)
236+
237+
# Create changelog entry
238+
changelog_entry = generate_changelog_entry(updates)
239+
with open("changelog_entry.yaml", "w") as f:
240+
f.write(changelog_entry)
241+
242+
# Set outputs
243+
write_github_output("has_updates", "true")
244+
updates_str = ", ".join(
245+
f"{pkg} to {v['new']}" for pkg, v in updates.items()
246+
)
247+
write_github_output("updates_summary", updates_str)
248+
249+
print("Updates prepared successfully!")
250+
return 0
251+
252+
253+
if __name__ == "__main__":
254+
sys.exit(main())

.github/scripts/create_pr.sh

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/bin/bash
2+
#
3+
# Create or update a PR for the weekly policyengine-us update.
4+
#
5+
# This script reads pr_summary.md and creates/updates a PR with the
6+
# formatted body.
7+
#
8+
# Usage: ./create_pr.sh
9+
#
10+
# Environment variables:
11+
# GH_TOKEN - GitHub token for authentication (required)
12+
#
13+
set -e
14+
15+
BRANCH_NAME="bot/weekly-us-update"
16+
PR_TITLE="Weekly policyengine-us update"
17+
18+
# Build PR body with summary
19+
if [ ! -f "pr_summary.md" ]; then
20+
echo "Error: pr_summary.md not found"
21+
exit 1
22+
fi
23+
24+
PR_SUMMARY=$(cat pr_summary.md)
25+
26+
PR_BODY="## Summary
27+
28+
Automated weekly update of policyengine-us.
29+
30+
Related to #1178
31+
32+
## Version Updates
33+
34+
${PR_SUMMARY}
35+
36+
---
37+
Generated automatically by GitHub Actions"
38+
39+
# Check if PR already exists
40+
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number' 2>/dev/null || echo "")
41+
42+
if [ -n "$EXISTING_PR" ]; then
43+
echo "PR #$EXISTING_PR already exists, updating it"
44+
gh pr edit "$EXISTING_PR" --body "$PR_BODY"
45+
else
46+
echo "Creating new PR"
47+
gh pr create \
48+
--title "$PR_TITLE" \
49+
--body "$PR_BODY"
50+
fi

0 commit comments

Comments
 (0)