Files
core/.github/workflows/check-requirements.md
T
Copilot 2e45ce36a7 Create agentic workflow to validate dependencies (#168855)
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>
2026-05-18 16:59:49 +02:00

19 KiB

on, permissions, network, tools, safe-outputs, description
on permissions network tools safe-outputs description
pull_request workflow_dispatch
types paths
opened
synchronize
reopened
requirements*.txt
homeassistant/package_constraints.txt
pyproject.toml
inputs
pull_request_number
description required type
Pull request number to (re-)check true number
contents pull-requests issues
read read read
allowed
python
web-fetch github
toolsets
default
add-comment
max
1
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), and requirements_test_all.txt (all test dependencies) to declare Python dependencies.
  • Each integration lists its packages in homeassistant/components/<name>/manifest.json under the requirements field.
  • Allowed licenses are maintained in script/licenses.py under OSI_APPROVED_LICENSES_SPDX (SPDX identifiers) and OSI_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.txt
  • requirements_all.txt
  • requirements_test.txt
  • requirements_test_all.txt
  • homeassistant/package_constraints.txt
  • pyproject.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:

  1. Fetch https://pypi.org/pypi/{package_name}/json (use the exact package name as it appears on the requirements file).
  2. From the JSON response, extract:
    • info.license — free-text license field
    • info.license_expression — SPDX expression (if present)
    • info.classifiers — filter for entries starting with "License ::", then normalize each match the same way as script/licenses.py by extracting the final :: segment (for example, "License :: OSI Approved :: MIT License""MIT License").
  3. 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
  4. 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.

  1. Fetch the PyPI JSON for the specific version being introduced or bumped: https://pypi.org/pypi/{package_name}/{version}/json
  2. Inspect the urls array in the response. For each distribution file (wheel or sdist), note the filename.
  3. 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 a kind identifying the CI system (e.g. "GitHub Actions", "GitLab") and a repository or project field 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 publisher does not identify a recognized CI system or Trusted Publisher, mark ⚠️ — "Attestation present but publisher cannot be verified as automated CI".

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:

  1. From the PyPI JSON at info.project_urls, find the source repository URL (keys such as "Source", "Homepage", "Repository", or "Source Code").
  2. Record that repository URL for later checks.
  3. 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.

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."

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):

  1. Extract all URLs from the PR body that contain the repository's domain or path (as identified in Step 3).
  2. 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 a CHANGELOG.md anchor referencing Y.
  3. 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:

  1. Use the source repository URL recorded in Step 3.
  2. 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."
  3. 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 .
  4. 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)

  1. Using the GitHub API, list the workflows in the source repository: GET /repos/{owner}/{repo}/actions/workflows
  2. Identify any workflow whose name or filename suggests publishing to PyPI (e.g., contains "release", "publish", "pypi", or "deploy").
  3. Fetch the workflow file content and check the following: a. Trigger sanity: The publish job should be triggered by push to tags, release: published, or workflow_run on a release job — not solely by workflow_dispatch with no additional guards. A workflow_dispatch trigger alongside other triggers is acceptable. Mark if the only trigger is manual workflow_dispatch with no environment protection rules. b. OIDC / Trusted Publisher: The workflow should use OIDC-based publishing. Look for id-token: write permission and one of:
    • pypa/gh-action-pypi-publish action
    • actions/attest-build-provenance action
    • Any step that sets TWINE_PASSWORD from secrets.PYPI_TOKEN directly (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 calls twine upload or pip upload outside of a properly gated job (e.g., one that requires an environment approval). Flag ⚠️ if such steps exist.
  4. 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)

  1. 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 the id field.
  2. Fetch the repository's .gitlab-ci.yml (and any included files) using GET https://gitlab.com/api/v4/projects/{id}/repository/files/.gitlab-ci.yml/raw?ref=HEAD (use web-fetch for public repos).
  3. Identify any job whose name or stage suggests publishing to PyPI (e.g., "publish", "deploy", "release", "pypi").
  4. For each such job, check: a. Trigger sanity: The job should run only on tag pipelines (only: tags or rules: - 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) and pypa/gh-action-pypi-publish equivalent, or reference secrets.PYPI_TOKEN / $PYPI_TOKEN injected 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 calls twine upload without being behind a protected-variable or environment guard.
  5. 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):

  1. 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/*.yml for Sourcehut).
  2. 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 upload or equivalent step that could be run manually?
  3. 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 open attribute) 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.txt or pyproject.toml without being tied to a specific integration, the PR description link requirement still applies.
  • When checking test-only packages (from requirements_test.txt or requirements_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 synchronize events GitHub compares the before/after SHAs of the push, not the entire PR diff). Members can manually retrigger the workflow via workflow_dispatch with 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.