Add snapshot data support to category data generation#5424
Conversation
The generate workflow fetched the existing published data from R2 inside
every category leg: each leg called HacsDataClient.get_data(<category>) for
the category's data.json, and get_repositories("removed") for the removed
list. Since the six legs run serially, the removed list was fetched once per
category, and a republish of R2 mid-run could leave later legs working off a
different baseline than earlier ones.
Fetch that data exactly once in a new lightweight preflight job (curl only)
and share it with every category leg via an artifact, so the whole run uses
one consistent snapshot:
- generate_category_data.py: add get_stored_data()/get_removed_repositories()
helpers that read each category's data.json and the removed list from
$HACS_EXISTING_DATA_DIR when set, and fall back to fetching from the data
client when unset (tests, local dev, single-repo validate.yml). Re-point the
two existing fetches at these helpers.
- generate-hacs-data.yml: add a preflight job that fetches every category's
data.json plus the removed list into outputdata/existing (retrying 5 times
then failing the run), uploads it as the existing-data artifact, and has
category-data depend on it, download it, and run with
HACS_EXISTING_DATA_DIR=outputdata/existing. Add preflight to
notify_on_failure.needs.
- Add unit tests covering both helper paths (snapshot dir vs. fetch fallback).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4
Build the generate-matrix categories output with jq so it is valid JSON (double-quoted) instead of single-quoted, letting both the matrix (fromJSON) and the preflight loop (jq -r) parse it robustly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4
Read HACS_EXISTING_DATA_DIR once at module level instead of keeping the env var name in a constant and resolving it on every call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4
There was a problem hiding this comment.
Pull request overview
This PR adds optional “snapshot” inputs to the category data generator so it can reuse already-published data from disk (when provided) instead of always fetching from the remote data client, and updates the GitHub Actions workflow to prefetch that snapshot once per run.
Changes:
- Add
get_stored_data()andget_removed_repositories()helpers to read pre-fetched JSON fromHACS_EXISTING_DATA_DIR(with fallback to the data client). - Update the generator to use the new helpers for stored category data and removed repositories.
- Extend CI with a
preflightjob to download published data into an artifact, then pass it to category jobs viaHACS_EXISTING_DATA_DIR; add tests for the new helper behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
scripts/data/generate_category_data.py |
Adds snapshot-aware helper functions and wires them into category generation. |
tests/scripts/data/test_generate_category_data.py |
Adds tests/stubs verifying snapshot read behavior and fallback fetching. |
.github/workflows/generate-hacs-data.yml |
Adds a preflight fetch job + artifact handoff and improves matrix JSON handling. |
Comments suppressed due to low confidence (2)
scripts/data/generate_category_data.py:80
- When HACS_EXISTING_DATA_DIR is set but removed.json is missing/invalid, this will raise and fail the whole run. To match the intended "use snapshot when available" behavior, fall back to fetching removed repositories on file/JSON errors.
if EXISTING_DATA_DIR:
with open(
os.path.join(EXISTING_DATA_DIR, "removed.json"), encoding="utf-8"
) as file:
return json.load(file)
tests/scripts/data/test_generate_category_data.py:372
- This assertion should match the (repo-id -> repo-data) shape returned by the real data client and the updated test stub, otherwise the test can pass while still diverging from production expectations.
assert await get_stored_data(hacs, "plugin") == {"fetched": "plugin"}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async def get_stored_data(hacs: AdjustedHacs, category: str) -> dict[str, dict[str, Any]]: | ||
| """Return existing category data from the snapshot dir when set, else fetch.""" | ||
| if EXISTING_DATA_DIR: | ||
| with open( | ||
| os.path.join(EXISTING_DATA_DIR, f"{category}.json"), encoding="utf-8" | ||
| ) as file: | ||
| return json.load(file) | ||
| return await hacs.data_client.get_data(category, validate=False) |
| async def get_data(self, section: str, *, validate: bool) -> dict[str, Any]: | ||
| self.calls.append(("get_data", section)) | ||
| return {"fetched": section} | ||
|
|
| assert await get_removed_repositories(hacs) == removed | ||
| assert hacs.data_client.calls == [] | ||
|
|
||
|
|
Reading the existing-data snapshot now degrades gracefully: a missing file or invalid JSON logs a warning and falls back to the data client instead of aborting generation, so a partial snapshot cannot fail the run. Make the test stub return realistic data-client shapes and add regression tests for the missing/invalid-file fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
scripts/data/generate_category_data.py:92
- Similarly,
get_removed_repositorieswill return whatever JSON is inremoved.jsonif it parses, even if it isn't a list of repository names. Validating that the snapshot is a list (and falling back when it's not) avoids hard-to-debug failures later in the generation flow.
async def get_removed_repositories(hacs: AdjustedHacs) -> list[str]:
"""Return the removed-repositories list from the snapshot dir when available, else fetch."""
if (removed := _read_snapshot(hacs, "removed.json")) is not None:
return removed
return await hacs.data_client.get_repositories("removed")
| EXISTING_DATA_DIR = os.getenv("HACS_EXISTING_DATA_DIR") | ||
|
|
||
|
|
||
| def _read_snapshot(hacs: AdjustedHacs, filename: str) -> Any | None: |
| async def get_stored_data(hacs: AdjustedHacs, category: str) -> dict[str, dict[str, Any]]: | ||
| """Return existing category data from the snapshot dir when available, else fetch.""" | ||
| if (data := _read_snapshot(hacs, f"{category}.json")) is not None: | ||
| return data | ||
| return await hacs.data_client.get_data(category, validate=False) |
Summary
This PR adds support for using pre-fetched snapshot data during category data generation, enabling faster and more reliable data generation workflows. When snapshot data is available, the generator uses it instead of fetching from the data client.
Key Changes
New helper functions in
generate_category_data.py:get_stored_data(): Returns category data from snapshot directory if available, otherwise fetches from data clientget_removed_repositories(): Returns removed repositories list from snapshot directory if available, otherwise fetches from data clientHACS_EXISTING_DATA_DIRenvironment variableGitHub Actions workflow updates (
generate-hacs-data.yml):preflightjob that fetches and caches existing published data before category generationcategory-datajob to download the cached snapshot data and pass it viaHACS_EXISTING_DATA_DIRenvironment variablepreflightjobTest coverage in
test_generate_category_data.py:_StubDataClientand_StubHacstest fixtures for mockingImplementation Details
HACS_EXISTING_DATA_DIRenvironment variablehttps://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4