1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-10 00:05:13 +01:00

Add bump-dependency development skill and PyPI resolver (#172221)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
This commit is contained in:
Allen Porter
2026-06-29 08:43:55 -07:00
committed by GitHub
parent bc9c270117
commit 1186f153cb
2 changed files with 303 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
---
name: bump-dependency
description: Bumps a Python package dependency across Home Assistant Core integrations, regenerates core requirement files, runs verification tests and prek lint, and prepares a pull request with proper release/compare links.
---
# Bump Python Package Dependency in Home Assistant Core
Follow these systematic steps to successfully bump a python package requirement in the repository, regenerate necessary derivative files, verify the integration, and raise a pull request.
## Gotchas & Non-Obvious Constraints
- **PR Template Integrity**: Follow Home Assistant's Pull Request template (`.github/PULL_REQUEST_TEMPLATE.md`) exactly as written, including any instructions inside the template itself. Preserve all sections, comments, and unchecked checkboxes unless the template explicitly says otherwise; the only allowed removal is the **Breaking change** section when the template instructs you to remove it if not applicable.
- **GitHub Tag Volatility**: Release tags on GitHub are highly inconsistent (e.g., `v1.2.3` vs `1.2.3` vs `release-1.2.3`). Always use the automated resolver `resolve_dependency.py` to check HEAD status for correct tags before hardcoding comparison URLs.
## Step-by-Step Workflow Checklist
### Phase A: Research and Plan
- [ ] **1. Identify Targets**: Note the requested target package and target version to bump.
- [ ] **2. Discover Codebase References**: Search the codebase to find all `manifest.json` and requirements files referencing the package.
- [ ] **3. Resolve Version/Tag Details**: Run the integrated validation helper script to resolve version details, GitHub repo, release tag format, and formatted PR links:
```bash
uv run python3 ./.claude/skills/bump-dependency/scripts/resolve_dependency.py <package> <old_version> [--new-version <new_version>]
```
- [ ] **4. Plan-Validate-Execute (Draft Plan)**: Before modifying any files, write a brief, structured plan outlining the integrations to change, old version, new version, and the resolved comparison link. Show this draft plan to the user.
### Phase B: Execute and Validate (Local Changes)
- [ ] **5. Check Uncommitted Changes**: Check for any uncommitted changes in the repository. If they exist, ask the user whether to stash, commit, or discard them before proceeding.
- [ ] **6. Git Branch Setup**: Create a clean branch starting from the latest `upstream/dev`:
```bash
git fetch upstream dev
git checkout -b bump-<package>-to-<version> upstream/dev
```
- [ ] **7. Apply Bump to manifests**: Update the version constraint string in all identified `manifest.json` files (e.g., change `"package==1.0.0"` to `"package==1.1.0"`).
- [ ] **8. Regenerate Core Requirements**: Run the requirements generator to update all derivative requirements and constraint files:
```bash
uv run python3 -m script.gen_requirements_all
```
- [ ] **9. Validate Requirements**: Check `git diff` to ensure that only the targeted `manifest.json` files and `requirements_all.txt` (and potentially standard constraints) were modified. No unrelated files must be affected.
- [ ] **10. Local Venv Verification**: Install the exact targeted package version directly inside the virtual environment:
```bash
uv pip install "<package>==<version>"
```
### Phase C: Validation Loop (Tests & Lint)
- [ ] **11. Run Integration Tests**: Execute the pytest suite for all integrations that consume the bumped package:
```bash
uv run pytest tests/components/<integration_name>
```
- *Validation Loop*: If tests fail, analyze the error, apply appropriate fixes, and re-run pytest until all tests pass cleanly.
- [ ] **12. Run prek Lint Checks**: Run the local prek hooks on modified files:
```bash
uv run prek run
```
- *Validation Loop*: If prek checks report any formatting or linting violations, fix them and repeat `uv run prek run` until it passes completely without errors.
### Phase D: User Confirmation & PR Creation
- [ ] **13. Commit Changes**: Commit the clean changes:
```bash
git add <modified_files>
git commit -m "Bump <package> to <version>"
```
- [ ] **14. Push Branch**: Push the local branch to your origin remote:
```bash
git push origin bump-<package>-to-<version>
```
- [ ] **15. PR Description Preparation**: Generate the pull request body from `.github/PULL_REQUEST_TEMPLATE.md`:
- **Proposed change**: Describe the package, old version, new version, target/source branches, and insert the resolved PyPI, changelog, and comparison diff links.
- **Type of change**: Check only 1 box in this section, and mark the `Dependency upgrade` checkbox as checked: `[x] Dependency upgrade`.
- **Breaking change**: You may remove the "Breaking change" section entirely from the template.
- **Validation checklists**: Mark `The code change is tested` checkbox as checked: `[x] The code change is tested`.
- **Keep remaining template intact**: Do NOT remove any other commented-out blocks, headers, or unchecked checkboxes in the template.
- [ ] **16. Mandatory Review Presentation**: Format the PR proposal using the **PR Presentation Template** below and display it to the user. **Stop and wait for the user to review and explicitly confirm/approve the PR template and draft details before creating the PR.**
- [ ] **17. Raise Pull Request**: Once the user approves, create the Pull Request using the GitHub CLI:
```bash
gh pr create --repo home-assistant/core --base dev --head <username>:bump-<package>-to-<version> --title "Bump <package> to <version>" --body-file <pr_body_file>
```
## PR Presentation Template
```markdown
### 🚀 Dependency Bump Pull Request Draft Review
- **Package**: `<package_name>` (`<old_version>` → `<new_version>`)
- **PR Title**: `Bump <package_name> to <new_version>`
- **Target Branch**: `dev`
- **Head Branch**: `<fork_username>:bump-<package_name>-to-<new_version>`
#### 🔗 PyPI & GitHub Links
- **PyPI Release**: https://pypi.org/project/<package_name>/<new_version>/
- **Changelog Link**: `<changelog_url>`
- **Comparison Diff**: `<compare_url>`
#### 📁 Modified Files
- `<list_of_modified_files>`
#### 📝 Proposed PR Body
<render the complete filled PR template body here, showing all checks and modifications for user approval>
```
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
# ruff: noqa: T201, D103, BLE001
"""Helper script to resolve package details, GitHub repo, release tags, and diff links from PyPI."""
import argparse
import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from packaging.version import Version
_VERSION_MATCH = r"^[a-zA-Z0-9_\-\.\+\!\*]+$"
_REPO_MATCH = r"https?://(?:www\.)?github\.com/([^/]+)/([^/]+)"
# Project owner or repo name
_NAME_MATCH = r"^[a-zA-Z0-9_\-\.]+$"
def get_pypi_data(package_name):
# Sanitize and URL-quote the package name to prevent URL path injection
safe_package_name = urllib.parse.quote(package_name)
url = f"https://pypi.org/pypi/{safe_package_name}/json"
req = urllib.request.Request(
url,
headers={
"User-Agent": "HomeAssistant-Skill-Resolver/1.0",
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=10) as response:
return json.loads(response.read().decode())
except urllib.error.HTTPError as e:
print(f"Error fetching PyPI data (HTTP {e.code}): {e.reason}", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError as e:
print(f"Network error fetching PyPI data: {e.reason}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Unexpected error fetching PyPI data: {e}", file=sys.stderr)
sys.exit(1)
def find_github_repo(info):
urls = []
if info.get("home_page"):
urls.append(info["home_page"])
if info.get("project_urls"):
urls.extend(info["project_urls"].values())
for u in urls:
if not u:
continue
cleaned_url = u.replace("git+", "")
m = re.search(_REPO_MATCH, cleaned_url, re.IGNORECASE)
if m:
owner, repo = m.groups()
# Strip query parameters, hashes, trailing slashes, and .git extension
repo = repo.split("?")[0].split("#")[0].split("/")[0]
repo = repo.removesuffix(".git")
# Validate that the owner and repo name conform to valid formats,
# preventing prompt injection payloads embedded in repository URLs.
if re.match(_NAME_MATCH, owner) and re.match(_NAME_MATCH, repo):
return f"https://github.com/{owner}/{repo}"
return None
def check_github_tag(repo_url, version):
# Try with 'v' prefix, without prefix, and with 'release-' prefix
tag_options = [f"v{version}", version, f"release-{version}"]
for tag in tag_options:
# Check both tree and releases paths on GitHub
for path_template in [f"tree/{tag}", f"releases/tag/{tag}"]:
url = f"{repo_url}/{path_template}"
try:
req = urllib.request.Request(
url,
method="HEAD",
headers={"User-Agent": "HomeAssistant-Skill-Resolver/1.0"},
)
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status == 200:
return tag
except urllib.error.HTTPError as e:
# If it's a 404, we continue checking other tag/path options
if e.code == 404:
continue
# For non-404 HTTP errors (like 403 Forbidden/429 rate limit), exit with error to avoid false reports
print(
f"\n[ERROR] HTTP error contacting GitHub ({e.code} {e.reason}) for tag '{tag}'",
file=sys.stderr,
)
sys.exit(1)
except urllib.error.URLError as e:
# Connection or timeout error
print(
f"\n[ERROR] Network error contacting GitHub: {e.reason}",
file=sys.stderr,
)
sys.exit(1)
except Exception as e:
# Unexpected exceptions
print(
f"\n[ERROR] Unexpected error verifying tag '{tag}': {e}",
file=sys.stderr,
)
sys.exit(1)
return None
def main():
parser = argparse.ArgumentParser(
description="Resolve PyPI package info and GitHub release diffs."
)
parser.add_argument("package", help="Name of the PyPI package")
parser.add_argument(
"old_version", help="Current version installed in Home Assistant"
)
parser.add_argument(
"--new-version", help="Target version to bump to (defaults to latest on PyPI)"
)
args = parser.parse_args()
if not re.match(_NAME_MATCH, args.package):
print(f"[ERROR] Invalid package name format: '{args.package}'", file=sys.stderr)
sys.exit(1)
if not re.match(_VERSION_MATCH, args.old_version):
print(
f"[ERROR] Invalid old version format: '{args.old_version}'", file=sys.stderr
)
sys.exit(1)
if args.new_version and not re.match(_VERSION_MATCH, args.new_version):
print(
f"[ERROR] Invalid target version format: '{args.new_version}'",
file=sys.stderr,
)
sys.exit(1)
data = get_pypi_data(args.package)
info = data.get("info", {})
# Filter out pre-releases from the releases list to identify the latest stable version
stable_versions = []
for v in data.get("releases", {}):
try:
ver = Version(v)
if not ver.is_prerelease:
stable_versions.append(ver)
except Exception:
continue
latest = str(max(stable_versions)) if stable_versions else info.get("version")
if latest and not re.match(_VERSION_MATCH, latest):
print("[ERROR] Invalid latest version resolved from PyPI.", file=sys.stderr)
sys.exit(1)
target_version = args.new_version or latest
if not target_version:
print("[ERROR] Could not resolve a target version from PyPI.", file=sys.stderr)
sys.exit(1)
github_repo = find_github_repo(info)
print("--- RESOLVED DEPENDENCY DETAILS ---")
print(f"Package: {args.package}")
print(f"Current Version: {args.old_version}")
print(f"Latest (PyPI): {latest}")
print(f"Target Version: {target_version}")
if github_repo:
print(f"GitHub Repository: {github_repo}")
# Verify tag formats on GitHub
old_tag = check_github_tag(github_repo, args.old_version)
new_tag = check_github_tag(github_repo, target_version)
if not old_tag or not new_tag:
missing_tags = []
if not old_tag:
missing_tags.append(args.old_version)
if not new_tag:
missing_tags.append(target_version)
print(
f"\n[ERROR] Could not resolve GitHub release tag for version(s): {', '.join(missing_tags)}",
file=sys.stderr,
)
sys.exit(1)
changelog_url = f"{github_repo}/releases/tag/{new_tag}"
compare_url = f"{github_repo}/compare/{old_tag}...{new_tag}"
print("\n--- PR DOCUMENTATION LINKS ---")
print(f"Changelog Link: {changelog_url}")
print(f"Comparison Diff Link: {compare_url}")
else:
print("\n[WARNING] GitHub repository could not be resolved from PyPI metadata.")
print("Please resolve the release notes and diff links manually.")
if __name__ == "__main__":
main()