Fetch existing HACS data once before 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.
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")
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
A snapshot file with valid JSON but the wrong shape (e.g. a list where a dict is expected) would previously be returned as-is and crash later. _read_snapshot now takes the expected type and falls back to fetching when the parsed content does not match. Add regression tests for both helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4
- summarize: iterate only the expected category directories (needs generate-matrix + CATEGORIES), so the new stored-data artifact (and any non-category dir) can't crash the JSON.parse(summary.json) loop. - Rename the existing-data snapshot to stored-data across the workflow, the HACS_STORED_DATA_DIR env var, and the script constant, for consistency with fetch-stored-data / get_stored_data. - Comment the fetch-stored-data failure-isolation trade-off (one failed fetch fails the whole run, by design). - _read_snapshot: generic return type so callers keep their dict/list types. - Add an end-to-end test proving the stored-snapshot path reproduces the fetch-path output byte-for-byte; add timeout-minutes to the fetch job. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4
Summary
The generate workflow fetched the existing published data from R2 inside every
category-dataleg: each leg calledHacsDataClient.get_data(<category>)for the category'sdata.json, andget_repositories("removed")for the removed list. Since the 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.This PR fetches that data exactly once in a new lightweight job and shares it with every category leg via an artifact, so the whole run uses one consistent snapshot. It is a standalone change against
main; the sharding work in #5419 can rebase on top and reuse the same mechanism.Key Changes
New helpers in
generate_category_data.py(_read_snapshot,get_stored_data,get_removed_repositories): read each category'sdata.jsonand the removed list from$HACS_STORED_DATA_DIRwhen set, and fall back to the data client when the dir is unset or when a file is missing, is invalid JSON, or has the wrong type (a warning is logged in each fallback case). The two existing fetches now go through these helpers.GitHub Actions workflow (
generate-hacs-data.yml):fetch-stored-datajob that fetches every category'sdata.jsonplus the removed list once withcurl(retry 5× then hard-fail the run), intooutputdata/stored, and uploads it as thestored-dataartifact. A comment documents the accepted trade-off: any single fetch failing fails the whole run (one consistent snapshot), rather than being isolated to that category's leg.category-datadepends onfetch-stored-data, downloads that artifact, and runs withHACS_STORED_DATA_DIR=outputdata/stored.generate-matrixnow emits the categories list as valid JSON (viajq) so bothfromJSONand the fetch job'sjq -rparse it robustly.summarizenow iterates only the expected category directories (fromgenerate-matrix) that produced asummary.json, so the new artifact (and any non-category directory) can't break it.notify_on_failuredepends onfetch-stored-data.Tests (
test_generate_category_data.py):_StubDataClient/_StubHacsreturn the real data-client shapes; tests cover the snapshot-read path, every fallback (unset dir, missing file, invalid JSON, wrong shape), and an end-to-end test proving the stored-snapshot path reproduces the fetch-path output.Implementation Details
HACS_STORED_DATA_DIRis optional. When unset (tests, local dev, single-repovalidate.yml) the original data-client fetch behavior is preserved, so output is unchanged.stored.jsonpublished to R2 is untouched — only the source of the baseline changed.category-dataruns serially over hours, a repo removed mid-run is not dropped until the next scheduled run (the removed list is part of the run-start snapshot). Honoring mid-run removals uniformly is a possible follow-up.https://claude.ai/code/session_01L22jYdNGF4hB4fvTYPTzP4