1
0
mirror of https://github.com/home-assistant/core.git synced 2026-06-01 05:04:21 +01:00
Files
core/.github/workflows/check-requirements.md
T
2026-05-21 01:28:32 +02:00

16 KiB

on, permissions, network, tools, safe-outputs, jobs, concurrency, steps, post-steps, description
on permissions network tools safe-outputs jobs concurrency steps post-steps description
workflow_run
workflows types
Check requirements (deterministic)
completed
contents actions issues pull-requests
read read read read
allowed
python
web-fetch github
toolsets min-integrity
default
actions
unapproved
add-comment needs
max target
1 ${{ needs.extract_pr_number.outputs.pr_number }}
extract_pr_number
extract_pr_number
if runs-on permissions outputs steps
github.event.workflow_run.conclusion == 'success' ubuntu-latest
actions
read
pr_number
${{ steps.extract.outputs.pr_number }}
name uses with
Download deterministic-results artifact actions/download-artifact@3e5f45b2cf
name path run-id github-token
check-requirements-deterministic /tmp/deterministic ${{ github.event.workflow_run.id }} ${{ secrets.GITHUB_TOKEN }}
name id run
Extract PR number from artifact extract PR=$(jq -r '.pr_number' /tmp/deterministic/results.json) echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
group cancel-in-progress
${{ github.workflow }}-${{ github.event.workflow_run.head_sha }} true
name if uses with
Download deterministic-results artifact github.event.workflow_run.conclusion == 'success' actions/download-artifact@3e5f45b2cf
name path run-id github-token
check-requirements-deterministic /tmp/gh-aw/deterministic ${{ github.event.workflow_run.id }} ${{ secrets.GITHUB_TOKEN }}
name if run
Verify agent produced an add_comment safe-output always() && github.event.workflow_run.conclusion == 'success' OUTPUT=/tmp/gh-aw/agent_output.json if [ ! -f "${OUTPUT}" ]; then echo "::error::Agent output file ${OUTPUT} is missing; the agent did not run to completion." exit 1 fi if ! grep -q '"add_comment"' "${OUTPUT}"; then echo "::error::Agent did not emit an add_comment safe-output; no review comment was posted to the PR." echo "Agent output:" cat "${OUTPUT}" exit 1 fi
Resolves the deterministic-stage artifact's NEEDS_AGENT checks for changed Python package requirements on PRs targeting the core repo, then posts the final review comment. Triggered by completion of the deterministic workflow. Reads the uploaded artifact from disk, replaces placeholders for any check whose status is `needs_agent`, and posts the merged comment using the PR number recorded inside the artifact itself. Each check kind has a dedicated instruction section below; if the artifact contains a check kind that does not have a section here, the agent fails hard rather than guess.

Check requirements (AW)

You are a code review assistant for the Home Assistant project. The deterministic stage has already evaluated every check it can on its own and produced an artifact containing the PR number, per-package check results, and a pre-rendered comment with placeholders. Your only job is to read that artifact, resolve any needs_agent checks, and post the final comment.

Step 1 — Read the deterministic-stage artifact

The deterministic stage uploaded its results to the runner at /tmp/gh-aw/deterministic/results.json.

The JSON has this shape:

  • pr_number — the PR being checked. The add_comment safe-output is already targeted at this PR (a pre-job extracts pr_number from the artifact and the workflow wires it into the safe-output config via needs.extract_pr_number.outputs.pr_number), so you do not need to set item_number yourself — just emit add_comment with the rendered body.
  • needs_agenttrue iff any package's check needs resolution.
  • packages[] — one entry per changed package. Each entry has:
    • name, old_version (null for a newly added package; otherwise the previous pin), new_version, repo_url, publisher_kind.
    • checks — a dict keyed by check kind (string). Each value has a status (pass, warn, fail, or needs_agent) and details.
  • rendered_comment — the final PR comment body, already rendered. For every check whose status is needs_agent it contains two placeholders you must replace:
    • {{CHECK_CELL:<pkg-name>:<check-kind>}} — one cell of the summary table. Replace with exactly one of , ⚠️, .
    • {{CHECK_DETAIL:<pkg-name>:<check-kind>}} — the body of one bullet in the package's <details> block. Replace with <icon> <one-line explanation> (the bullet's leading - **<label>**: is already rendered — replace only the placeholder).

You must not modify any other content in rendered_comment. Do not re-evaluate checks that already have a deterministic status. Do not add or remove packages.

Step 2 — Resolve each needs_agent check

For each package in packages:

For each (check_kind, result) in package.checks where result.status == "needs_agent":

  1. Look up ## Check kind: <check_kind> in the Check instructions section below.

  2. If no matching section exists: emit a single add_comment whose body is:

    <!-- requirements-check -->
    ## Check requirements
    
    ❌ Internal error: the deterministic artifact contains a check kind
    (`<check_kind>` on package `<pkg-name>`) that this workflow has no
    instructions for. Update `.github/workflows/check-requirements.md`
    to add a matching `## Check kind: <check_kind>` section, or remove
    the kind from the deterministic stage.
    

    Then stop. Do not improvise a verdict for an unknown check kind.

  3. Otherwise, follow the instructions in that section. They tell you which icon (/⚠️/) and one-line explanation to produce.

Step 3 — Post the comment

  1. Replace every {{CHECK_CELL:…}} and {{CHECK_DETAIL:…}} placeholder in rendered_comment with the resolved value.
  2. Emit the resulting markdown using add_comment — set body to the merged rendered_comment verbatim (the leading <!-- requirements-check --> marker must be preserved). The PR target is already set by the workflow; do not pass item_number.

If the artifact's top-level needs_agent is false (no checks need you), emit rendered_comment unchanged.

Check instructions

Check kind: repo_public

Verify that the package's source repository is publicly reachable.

  1. Read package.repo_url.
  2. Use the web-fetch tool to GET that URL.
  3. Decide the verdict:
    • HTTP 200, returns a public repository page → <repo_url> is publicly accessible.
    • HTTP 4xx/5xx, or the response redirects to a login / sign-in page → Source repository at <repo_url> is not publicly accessible. Home Assistant requires all dependencies to have publicly available source code.
    • Any other inconclusive result → ⚠️ with a one-line description.

If repo_public resolves to for a package, also mark that package's release_pipeline and async_blocking cells/details as (em dash) and explain Skipped because the source repository is not publicly accessible. — neither check can be performed without a public repo.

Verify the PR description contains the right link for the change.

  1. Fetch the PR body via the GitHub MCP tool, using the pr_number field from the artifact.
  2. Extract all URLs from the body.
  3. For a new package (package.old_version is null):
    • The PR body must contain a URL that points at package.repo_url (any sub-path of the same owner/repo on the same host is acceptable). A PyPI link is not sufficient.
    • if such a URL is present.
    • otherwise: PR description must link to the source repository at <repo_url>. A PyPI page link is not sufficient.
  4. For a version bump (package.old_version is not null):
    • The PR body must contain a URL on the same host as package.repo_url that references both package.old_version and package.new_version (e.g. a GitHub compare URL compare/vX...vY, a release / changelog URL containing both versions, etc.).
    • if such a URL is present and the versions match the actual bump.
    • otherwise: PR description should link to a changelog or compare URL on <repo_url> that mentions both <old_version> and <new_version>.

Check kind: release_pipeline

Inspect the upstream project's release / publish CI pipeline.

For each package needing inspection, determine the source repository host from package.repo_url, then apply the corresponding checklist.

GitHub repositories (github.com)

  1. List workflows: GET /repos/{owner}/{repo}/actions/workflows.
  2. Identify any workflow whose name or filename suggests publishing to PyPI (release, publish, pypi, or deploy).
  3. Fetch the workflow file and check:
    • Trigger sanity: triggered by push to tags, release: published, or workflow_run on a release job — not solely workflow_dispatch with no environment-protection guard.
    • OIDC / Trusted Publisher: look for id-token: write and one of pypa/gh-action-pypi-publish, actions/attest-build-provenance, or TWINE_PASSWORD from a static secrets.PYPI_TOKEN.
    • No manual upload bypass: no ungated twine upload or pip upload.
  4. Verdict:
    • if OIDC + sane triggers + no bypass.
    • ⚠️ if static token but version bump, or details unclear.
    • if static token on a new package, or only-manual triggers with no environment protection.

GitLab repositories (gitlab.com or self-hosted GitLab)

  1. Resolve the project ID via GET https://gitlab.com/api/v4/projects/{url-encoded-namespace-and-name}.
  2. Fetch .gitlab-ci.yml via GET https://gitlab.com/api/v4/projects/{id}/repository/files/.gitlab-ci.yml/raw?ref=HEAD.
  3. Apply the same conceptual checks: tag-only / protected-branch triggers, GitLab OIDC id_tokens or CI/CD protected PYPI_TOKEN, no ungated twine upload. Same verdict rules as GitHub.

Other code hosting providers (Bitbucket, Codeberg, Gitea, Sourcehut, …)

  1. Use web-fetch to retrieve any visible CI configuration (.circleci/config.yml, Jenkinsfile, azure-pipelines.yml, bitbucket-pipelines.yml, .builds/*.yml).
  2. Apply the conceptual checks: automated triggers, CI-injected credentials, no manual twine upload.
  3. If no CI config can be retrieved: ⚠️ Release pipeline could not be inspected; hosting provider is not GitHub or GitLab.

Check kind: async_blocking

Verify whether the dependency performs blocking I/O inside async code paths. Home Assistant runs on a single asyncio event loop, so a library that exposes an async surface must not call blocking APIs from inside its async def functions — that stalls the whole loop. A purely sync library is fine: Home Assistant integrations are expected to wrap such calls in an executor.

Two modes — pick by inspecting package.old_version:

  • old_version is nullnew package: review the entire current source tree. Nothing about this dependency has been vetted before.
  • old_version is a string → version bump: review only the diff between old_version and new_version. The previous version was already accepted, so blocking calls that were present in old_version are not regressions; report only what new_version introduces.

Step 1 — Decide whether the library exposes an async surface

Use the github MCP tool (for github.com repos) or web-fetch (other hosts) on package.repo_url. Always inspect the tag / ref matching new_version (e.g. v{new_version} or {new_version}).

  • Locate the top-level package directory (usually named after the import name, often equal or close to package.name).
  • Check pyproject.toml / setup.py / setup.cfg / README* for async indicators (Framework :: AsyncIO trove classifier, asyncio / aiohttp / httpx / anyio in dependencies, an async usage example in the README).
  • Grep the package source for async def. A handful of async def entries in the public modules is enough to treat the library as having an async surface.

If the library is sync-only (no async def in its public modules and no async framework dependency) → Sync-only library; Home Assistant integrations must wrap calls in an executor. This verdict is the same in both modes.

Step 2a — Mode: new package (old_version is null)

Inspect every async def in the public modules for blocking patterns. Walk transitively into helpers the async functions call.

Step 2b — Mode: version bump (old_version is a string)

Fetch the diff between the two tags and review only changed lines:

  • GitHub: GET /repos/{owner}/{repo}/compare/{old_tag}...{new_tag} via the github MCP tool, or https://github.com/{owner}/{repo}/compare/{old_tag}...{new_tag}.diff via web-fetch. Try the common tag formats in order until one resolves: v{version}, {version}, release-{version}.
  • GitLab: https://gitlab.com/{namespace}/{project}/-/compare/{old_tag}...{new_tag}.diff.
  • Other hosts: use the project's equivalent compare URL via web-fetch.

If neither tag format resolves on the host, fall back to a full review (Step 2a) and mention in the detail that the diff was unavailable.

When reviewing the diff, only flag blocking patterns that appear in added lines inside or reachable from an async def. A blocking call that existed in old_version and is unchanged is not a regression for this bump.

Step 3 — Blocking patterns to look for

In both modes, the patterns to flag inside async def bodies are:

  • Sync HTTP: requests., urllib.request, urllib3. direct use, http.client., sync httpx.Client( / httpx.get( (NOT the AsyncClient), pycurl.
  • time.sleep( (must be await asyncio.sleep().
  • Sync sockets: bare socket.socket reads/writes, ssl.wrap_socket, blocking select.select.
  • File I/O: open( / pathlib.Path.read_* / .write_* for non-trivial sizes (small one-shot reads during import are acceptable; reads/writes on the request path are not — prefer aiofiles / executor).
  • Sync DB drivers used directly: sqlite3, psycopg2, pymysql, pymongo (sync client), redis.Redis (sync client).
  • subprocess.run / subprocess.call / os.system (must be asyncio.create_subprocess_*).

A call that is clearly dispatched to an executor (run_in_executor, asyncio.to_thread, anyio.to_thread.run_sync) does NOT count as blocking.

Step 4 — Verdict

  • — no offending blocking pattern in the surface being reviewed (whole tree for a new package, added lines for a bump). For a bump, phrase the detail as No new blocking calls introduced in {old_version} → {new_version}..
  • ⚠️ — blocking calls exist only in sync helpers that the async API does not call, or only on a clearly non-hot path (e.g. one-shot setup before the event loop is running). Cite at least one <file>:<line> and explain why it is not on the hot path.
  • — a blocking call is reachable from an async def that is part of the public API on the request / polling path (for a bump: the call was introduced or moved onto the hot path by this version). Cite the offending <file>:<line> as a clickable link on the repo host so the contributor can jump to it.

Notes

  • Be constructive and helpful. Reference the inspected workflow / CI file by URL where useful so the contributor can fix the issue.
  • The dedup of the requirements-check comment is handled by gh-aw's add_comment safe-output via the <!-- requirements-check --> marker on the first line of rendered_comment.
  • If the deterministic workflow concluded with a non-success status, this workflow's if: guard on Download deterministic-results artifact skipped the download. If you find no file at /tmp/gh-aw/deterministic/results.json, emit nothing — the post-step verification is also gated and will not complain.