Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: edenhaus <26537646+edenhaus@users.noreply.github.com> Co-authored-by: Robert Resch <robert@resch.dev> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
19 KiB
on, permissions, network, tools, safe-outputs, description
| on | permissions | network | tools | safe-outputs | description | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
|
|
|
Checks changed Python package requirements on PRs targeting the core repo (including PRs opened from forks) and verifies licenses match PyPI metadata, source repositories are publicly accessible, PyPI releases were uploaded via automated CI (Trusted Publisher attestation), the package's release pipeline uses OIDC or equivalent automated credentials (not static tokens), and the PR description contains the required links. |
Requirements License and Availability Check
You are a code review assistant for the Home Assistant project. Your job is to review changes to Python package requirements and verify they meet the project's standards.
Context
- Home Assistant uses
requirements_all.txt(all integration packages),requirements.txt(core packages),requirements_test.txt(test dependencies), andrequirements_test_all.txt(all test dependencies) to declare Python dependencies. - Each integration lists its packages in
homeassistant/components/<name>/manifest.jsonunder therequirementsfield. - Allowed licenses are maintained in
script/licenses.pyunderOSI_APPROVED_LICENSES_SPDX(SPDX identifiers) andOSI_APPROVED_LICENSES(classifier strings).
Step 1 — Identify Changed Packages
Use the GitHub tool to fetch the PR diff. Look for lines that were added (+)
or removed (-) in any of these files:
requirements.txtrequirements_all.txtrequirements_test.txtrequirements_test_all.txthomeassistant/package_constraints.txtpyproject.toml
For each changed line that contains a package pin (e.g. SomePackage==1.2.3),
classify it as:
- New package: the package name appears only in
+lines, with no corresponding-line for the same package name. - Version bump: the same package name appears in both
+lines (new version) and-lines (old version), with different version numbers.
Record the old version and new version for every version bump — you will need these values in Step 4.
Step 2 — Check License via PyPI
For each new or bumped package:
- Fetch
https://pypi.org/pypi/{package_name}/json(use the exact package name as it appears on the requirements file). - From the JSON response, extract:
info.license— free-text license fieldinfo.license_expression— SPDX expression (if present)info.classifiers— filter for entries starting with"License ::", then normalize each match the same way asscript/licenses.pyby extracting the final::segment (for example,"License :: OSI Approved :: MIT License"→"MIT License").
- Determine if the license is in the approved list from
script/licenses.py:- SPDX identifiers: compare against
OSI_APPROVED_LICENSES_SPDX - Normalized classifier strings: compare against
OSI_APPROVED_LICENSES
- SPDX identifiers: compare against
- Flag a package as ❌ if the license is unknown, missing, or not in the approved list. Flag as ⚠️ if the license information is ambiguous or cannot be definitively determined.
Step 2b — Verify PyPI Release Was Uploaded by CI
For each new or bumped package, verify that the release on PyPI was published automatically by a CI pipeline (via OIDC Trusted Publisher), not uploaded manually.
- Fetch the PyPI JSON for the specific version being introduced or bumped:
https://pypi.org/pypi/{package_name}/{version}/json - Inspect the
urlsarray in the response. For each distribution file (wheel or sdist), note the filename. - For each filename, attempt to fetch the PyPI provenance attestation:
https://pypi.org/integrity/{package_name}/{version}/{filename}/provenance- If the response is HTTP 200 and contains a valid attestation object,
inspect
attestation_bundles[*].publisher. A Trusted Publisher attestation will have akindidentifying the CI system (e.g."GitHub Actions","GitLab") and arepositoryorprojectfield matching the source repository. - If at least one distribution file has a valid Trusted Publisher attestation, mark ✅ CI-uploaded.
- If no attestation is found for any file (404 for all), mark ❌ — "Release has no provenance attestation; it may have been uploaded manually".
- If an attestation exists but the
publisherdoes not identify a recognized CI system or Trusted Publisher, mark ⚠️ — "Attestation present but publisher cannot be verified as automated CI".
- If the response is HTTP 200 and contains a valid attestation object,
inspect
Note: if PyPI returns an error fetching the per-version JSON, fall back to the
latest JSON (https://pypi.org/pypi/{package_name}/json) and look up the
specific version in the releases dict.
Step 3 — Identify Repository URL
For each new or bumped package:
- From the PyPI JSON at
info.project_urls, find the source repository URL (keys such as"Source","Homepage","Repository", or"Source Code"). - Record that repository URL for later checks.
- If no suitable repository URL is present, mark ❌ with a note that the source repository URL is missing and cannot be verified.
Step 4 — Check PR Description
Read the PR body from the GitHub API using the PR number from the workflow
context (pull-request-number). If that value is absent, use the
workflow_dispatch input pull_request_number.
Extract all URLs present in the PR body.
4a — New packages: repository link required
For new packages (brand-new dependency not previously in any requirements
file): the PR description must contain a link that points to the package's
source repository as identified in Step 3 (the URL recorded from
info.project_urls). A PyPI page link alone is not acceptable — the link
must point directly to the source repository (e.g. a GitHub or GitLab URL).
- If a URL in the PR body matches (or is a sub-path of) the source repository URL identified via PyPI, mark ✅.
- If the PR body contains a source repository URL that does not match the
repository URL found in the package's PyPI metadata (
info.project_urls), mark ❌ — "PR description links to<pr_url>but PyPI reports the source repository as<pypi_repo_url>; please use the correct repository URL." - If no source repository URL is present in the PR body at all, mark ❌ —
"PR description must link to the source repository at
<repo_url>(found via PyPI). A PyPI page link is not sufficient."
4b — Version bumps: changelog or diff link required
For version bumps: the PR description must contain a link to a changelog, release notes page, or a diff/comparison URL that references the correct versions being bumped (old → new).
Checks to perform for each bumped package (old version = X, new version = Y):
- Extract all URLs from the PR body that contain the repository's domain or path (as identified in Step 3).
- Verify that at least one such URL includes both the old version string and
new version string in some form — e.g. a GitHub compare URL like
compare/vX...vY, a releases URL mentioning version Y, or aCHANGELOG.mdanchor referencing Y. - If no URL matches, check if the PR body contains any changelog/diff link at all for this package.
Outcome:
- ✅ — a URL pointing to the correct repo with version references covering the exact bump (X → Y).
- ⚠️ — a changelog/diff link exists but does not clearly reference the correct versions or the correct repository; explain what was found and what is expected.
- ❌ — no changelog or diff link found at all in the PR description for this package.
4c — Diff consistency check
For each version bump, verify that the version change recorded in the diff (Step 1) is internally consistent:
- The
-line must contain the old version and the+line must contain the new version for the same package name. - Flag ❌ if the diff shows a downgrade (new version < old version) without an explanation, or if the version strings cannot be parsed.
Step 5 — Verify Source Repository is Publicly Accessible
Before inspecting the release pipeline, confirm that the source repository identified in Step 3 is publicly reachable.
For each new or bumped package:
- Use the source repository URL recorded in Step 3.
- If no repository URL was found in
info.project_urls, mark ❌ — "No source repository URL found in PyPI metadata; a public source repository is required." - If a repository URL was found, perform a GET request to that URL (using web-fetch). If the response is HTTP 200 and returns a publicly accessible page (not a login redirect or error page), mark ✅.
- If the response is non-200, the URL redirects to a login/authentication page,
or the repository appears private or unavailable, mark ❌ — "Source
repository at
<repo_url>is not publicly accessible. Home Assistant requires all dependencies to have publicly available source code." Do not proceed with the release pipeline check (Step 6) for this package.
Step 6 — Check Release Pipeline Sanity
For each new or bumped package, determine the source repository host from the URL identified in Step 3, then inspect whether the project's release/publish CI workflow is sane. The checks differ by hosting provider.
GitHub repositories (github.com)
- Using the GitHub API, list the workflows in the source repository:
GET /repos/{owner}/{repo}/actions/workflows - Identify any workflow whose name or filename suggests publishing to PyPI (e.g., contains "release", "publish", "pypi", or "deploy").
- Fetch the workflow file content and check the following:
a. Trigger sanity: The publish job should be triggered by
pushto tags,release: published, orworkflow_runon a release job — not solely byworkflow_dispatchwith no additional guards. Aworkflow_dispatchtrigger alongside other triggers is acceptable. Mark ❌ if the only trigger is manualworkflow_dispatchwith no environment protection rules. b. OIDC / Trusted Publisher: The workflow should use OIDC-based publishing. Look forid-token: writepermission and one of:pypa/gh-action-pypi-publishactionactions/attest-build-provenanceaction- Any step that sets
TWINE_PASSWORDfromsecrets.PYPI_TOKENdirectly (flag ❌ if a long-lived API token is used instead of OIDC). Mark ✅ if OIDC is used, ⚠️ if the publish method cannot be determined, ❌ if a static secret token is the only credential. c. No manual upload bypass: Verify there is no step that callstwine uploadorpip uploadoutside of a properly gated job (e.g., one that requires an environment approval). Flag ⚠️ if such steps exist.
- If no publish workflow is found in the repository, mark ⚠️ — "No publish workflow found; it is unclear how this package is released to PyPI."
GitLab repositories (gitlab.com or self-hosted GitLab)
- Use the GitLab REST API to list CI/CD pipeline configuration files. First
resolve the project ID via
GET https://gitlab.com/api/v4/projects/{url-encoded-namespace-and-name}and note theidfield. - Fetch the repository's
.gitlab-ci.yml(and any included files) usingGET https://gitlab.com/api/v4/projects/{id}/repository/files/.gitlab-ci.yml/raw?ref=HEAD(use web-fetch for public repos). - Identify any job whose name or
stagesuggests publishing to PyPI (e.g., "publish", "deploy", "release", "pypi"). - For each such job, check:
a. Trigger sanity: The job should run only on tag pipelines (
only: tagsorrules: - if: $CI_COMMIT_TAG) or on protected branches — not solely on manual triggers (when: manual) with no additional protection. Mark ❌ if the only trigger is manual with no environment or protected-branch guard. b. Automated credentials: The job should use GitLab's OIDC ID token (id_tokens:block) andpypa/gh-action-pypi-publishequivalent, or referencesecrets.PYPI_TOKEN/$PYPI_TOKENinjected from GitLab CI/CD protected variables (flag ❌ if the token is hard-coded or unprotected). Mark ✅ if OIDC or protected CI variables are used, ⚠️ if the method cannot be determined, ❌ if credentials appear to be insecure. c. No manual upload bypass: Flag ⚠️ if any job callstwine uploadwithout being behind a protected-variable or environment guard. - If no publish job is found, mark ⚠️ — "No publish job found in .gitlab-ci.yml; it is unclear how this package is released to PyPI."
Other code hosting providers
For repositories hosted on platforms other than GitHub or GitLab (e.g., Bitbucket, Codeberg, Gitea, Sourcehut):
- Use web-fetch to retrieve the repository's root page and look for any
publicly visible CI configuration files (e.g.,
.circleci/config.yml,Jenkinsfile,azure-pipelines.yml,bitbucket-pipelines.yml,.builds/*.ymlfor Sourcehut). - Apply the same conceptual checks as above:
- Does publishing run on automated triggers (tags/releases), not solely manual ones?
- Are credentials injected by the CI system (not hard-coded)?
- Is there a
twine uploador equivalent step that could be run manually?
- If no CI configuration can be retrieved, mark ⚠️ — "Release pipeline could not be inspected; hosting provider is not GitHub or GitLab."
Step 7 — Post a Review Comment
Always post a review comment using add_comment, regardless of whether
packages pass or fail. Use the following structure:
Note on deduplication: The workflow automatically updates any previous requirements-check comment on the PR in place (preserving its position in the thread). If no previous comment exists, the newly created comment is kept as-is. You do not need to search for or update previous comments yourself.
Comment structure
Begin every comment with the HTML marker <!-- requirements-check --> on its
own line (this is used by the workflow to find the previous comment and update
it on the next run).
7a — Overall summary line
Begin the comment with a single summary line, before anything else:
- If everything passed:
All requirements checks passed. ✅ - If there are failures or warnings:
⚠️ Some checks require attention — see the details below.
7b — Summary table
Render a compact table where every check column contains only the status icon (✅, ⚠️, or ❌). No explanatory text belongs inside the table cells — all detail goes in the per-package sections below.
Use — (em dash) when a check was skipped (e.g. Release Pipeline is skipped
when the repository is not publicly accessible).
<!-- requirements-check -->
## Requirements Check
| Package | Type | Old→New | License | Repo Public | CI Upload | Release Pipeline | PR Link | Diff Consistent |
|---------|------|---------|---------|-------------|-----------|------------------|---------|-----------------|
| PackageA | bump | 1.2.3→1.3.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| PackageB | new | —→4.5.6 | ❌ | ✅ | ❌ | ⚠️ | ❌ | ✅ |
| PackageC | bump | 2.0.0→2.1.0 | ✅ | ❌ | — | — | ⚠️ | ✅ |
7c — Per-package detail sections
After the table, add one collapsible <details> block per package.
- If all checks passed for that package, render the block collapsed
(no
openattribute) so the comment stays concise. - If any check failed or produced a warning, render the block open
(
<details open>) so the contributor sees the issues immediately.
Each block must include the full detail for every check: the license found, the repository URL, whether a provenance attestation was found, the release pipeline findings, the PR link found (or missing), and whether the diff is consistent. For failed or warned checks, explain exactly what the contributor must fix, including the expected source repository URL, expected version range, etc.
Template (repeat for each package):
<details open>
<summary><strong>PackageB 📦 new —→4.5.6</strong></summary>
- **License**: ❌ License is `UNKNOWN` — not in the approved list. Check PyPI metadata and `script/licenses.py`.
- **Repository Public**: ✅ https://github.com/example/packageb is publicly accessible.
- **CI Upload**: ❌ No provenance attestation found for any distribution file. The release may have been uploaded manually.
- **Release Pipeline**: ⚠️ No publish workflow found in the repository; it is unclear how this package is released to PyPI.
- **PR Link**: ❌ PR description must link to the source repository at https://github.com/example/packageb (a PyPI page link is not sufficient).
- **Diff Consistent**: ✅
</details>
Collapsed example (all checks passed):
<details>
<summary><strong>PackageA 📦 bump 1.2.3→1.3.0</strong></summary>
- **License**: ✅ MIT
- **Repository Public**: ✅ https://github.com/example/packagea
- **CI Upload**: ✅ Trusted Publisher attestation found (GitHub Actions).
- **Release Pipeline**: ✅ OIDC via `pypa/gh-action-pypi-publish`; triggered on `release: published`; `environment: release` gate.
- **PR Link**: ✅ https://github.com/example/packagea/compare/v1.2.3...v1.3.0
- **Diff Consistent**: ✅
</details>
Notes
- Be constructive and helpful. Provide direct links where possible so the contributor can quickly fix the issue.
- If PyPI returns an error for a package, mention that it could not be found and suggest the contributor verify the package name.
- For packages that only appear in
homeassistant/package_constraints.txtorpyproject.tomlwithout being tied to a specific integration, the PR description link requirement still applies. - When checking test-only packages (from
requirements_test.txtorrequirements_test_all.txt), apply the same license, repository, and PR description checks as for production dependencies. - A package that appears in both a production file and a test file should only be reported once; use the production file entry as the canonical one.
- This workflow is only triggered when a commit actually changes one of the
tracked requirements files (for
synchronizeevents GitHub compares the before/after SHAs of the push, not the entire PR diff). Members can manually retrigger the workflow viaworkflow_dispatchwith the PR number to re-run the check after updating the PR description or fixing issues without changing any requirements files. On a retrigger the existing comment is updated in place so there is always exactly one requirements-check comment in the PR.