Skip to content

qa: manifest-check tool for #2225 (failed to download manifest) - #354

Closed
DTTerastar wants to merge 2 commits into
mainfrom
manifest-check-2225
Closed

qa: manifest-check tool for #2225 (failed to download manifest)#354
DTTerastar wants to merge 2 commits into
mainfrom
manifest-check-2225

Conversation

@DTTerastar

@DTTerastar DTTerastar commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the manifest endpoint probe from ESPresense/ESPresense#2320 into this repo — the one that actually serves the /releases/{version}.json?flavor={flavor} endpoints via functions/releases/[[path]].ts.

Why here, not in the firmware repo: the tool tests endpoints served by this repo, the bug it detects lives in functions/releases/[[path]].ts, and CI signal belongs next to the code under test. In the firmware repo the check fires on firmware PRs that cannot touch the website.

What this PR adds

  • tools/manifest_check/check_manifest.py — stdlib-only probe. GETs each manifest, validates ESP Web Tools schema, HEADs each binary URL.
  • tools/manifest_check/test_check_manifest.py — offline unit tests (6 tests).
  • tools/manifest_check/targets.txt — 12 (version, flavor) pairs covering latest + v4.0.6 + v4.0.5.
  • tools/manifest_check/README.md, .gitignore.
  • .github/workflows/manifest-check.yml — unit tests + production probe on PR / push / hourly cron / dispatch.

Current status: RED by design

The production probe intentionally fails in this first commit. Live confirmation:

$ curl -sS -o /dev/null -w "%{http_code}" "https://espresense.com/releases/latest.json?flavor=esp32"
404

Root cause: the manifest handler (functions/releases/[[path]].ts:127) fetches GitHub's /releases/tags/${tag} endpoint. When tag === 'latest', GitHub returns 404 because latest is not a real Git tag. The sibling /latest/download/:filename route (line 204) already uses the correct /releases/latest endpoint — the manifest handler was never updated to match.

The fix (resolve latest via /releases/latest + use rel.tag_name for binary paths) follows in the next commit on this branch once GHA confirms the red.

Test plan

  • python3 tools/manifest_check/test_check_manifest.py — 6/6 pass
  • GHA probe-manifest job fails against production (proves the repro)
  • Fix applied → re-probe goes green

Refs: #2225, ESPresense/ESPresense#2320.

Summary by CodeRabbit

  • New Features

    • Added automated checks for public release manifests and their downloadable build components.
    • Added support for scheduled and manually triggered manifest validation.
    • Added configurable target coverage for current and selected historical releases.
  • Documentation

    • Added instructions for running manifest checks locally, configuring targets, and interpreting validation results.
  • Tests

    • Added offline tests covering manifest structure validation and target parsing.

Ports the manifest endpoint probe from ESPresense/ESPresense#2320 into the
repo that actually serves the endpoints. The tool validates the ESP Web Tools
manifest schema and HEADs each binary URL for every (version, flavor) in
targets.txt.

CI workflow runs unit tests + a live production probe on PR, push, hourly
cron, and dispatch. The probe currently FAILS on production:
latest.json?flavor=* returns 404 (tag 'latest' is not a real Git tag; the
manifest handler hits /releases/tags/latest instead of /releases/latest).

The fix for the handler follows in a separate commit on this branch.

Refs: #2225, ESPresense/ESPresense#2320.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@DTTerastar, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f5c0330-2fa9-47bc-8e40-983d96ecf9fc

📥 Commits

Reviewing files that changed from the base of the PR and between cfd3692 and 7b0a961.

📒 Files selected for processing (3)
  • functions/releases/[[path]].ts
  • tools/manifest_check/check_manifest.py
  • wrangler.toml

Walkthrough

Changes

Manifest validation

Layer / File(s) Summary
Manifest contracts and offline validation
tools/manifest_check/check_manifest.py, tools/manifest_check/targets.txt, tools/manifest_check/test_check_manifest.py
Defines target and report types, parses target configuration, validates manifest structure, and adds offline unit tests.
Manifest and binary probing
tools/manifest_check/check_manifest.py
Fetches manifests with retries, validates responses and schemas, probes referenced binaries, and reports CLI results.
CI execution and operator documentation
.github/workflows/manifest-check.yml, tools/manifest_check/README.md, tools/manifest_check/.gitignore
Documents local usage, ignores Python cache files, and runs unit tests and manifest probes through scheduled, manual, push, and pull request workflow triggers.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a manifest-check QA tool for the manifest download issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch manifest-check-2225

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 22, 2026

Copy link
Copy Markdown

Deploying espresense with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7b0a961
Status: ✅  Deploy successful!
Preview URL: https://0d398af6.espresense.pages.dev
Branch Preview URL: https://manifest-check-2225.espresense.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/manifest-check.yml:
- Around line 46-47: Remove or defer the “Probe manifest URLs” workflow step
that runs tools/manifest_check/check_manifest.py until the endpoint and binary
asset fixes provide a green baseline; do not enable it as a required production
gate while it reports known failures.

In `@tools/manifest_check/check_manifest.py`:
- Around line 90-99: Update the HTTPError handling in the retry loop to treat
HTTP 429 as transient instead of returning immediately, while preserving
immediate handling for other non-5xx responses. When retrying 429 responses,
read and honor the Retry-After header when present, falling back to the existing
backoff schedule otherwise, and keep the final-attempt behavior intact.
- Around line 166-180: Constrain URL handling in the manifest-check loop around
`parts[].path` and `http_request`: preserve local paths resolved against the
documented `--base`, but allow only explicitly permitted schemes and origins for
the initial URL. Configure or extend `http_request` so redirects are validated
against the same allowlist rather than followed to arbitrary destinations,
rejecting disallowed URLs before any request is made.
- Around line 105-125: Update validate_schema to first reject payloads that are
not dictionaries before checking REQUIRED_TOP_KEYS, and validate each part’s
path field as a string so null or other non-string values return a schema error
before startswith() is reached. Add regression cases covering a scalar JSON
payload and a part with a non-string path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 017eb28a-4738-424d-a956-e07574bcd8da

📥 Commits

Reviewing files that changed from the base of the PR and between bc835cd and cfd3692.

📒 Files selected for processing (6)
  • .github/workflows/manifest-check.yml
  • tools/manifest_check/.gitignore
  • tools/manifest_check/README.md
  • tools/manifest_check/check_manifest.py
  • tools/manifest_check/targets.txt
  • tools/manifest_check/test_check_manifest.py

Comment on lines +46 to +47
- name: Probe manifest URLs
run: python3 tools/manifest_check/check_manifest.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not enable a known-red production gate.

This command currently reports 31 failures: latest manifests return 404/429 and multiple versioned binary URLs return 404. The planned latest handler follow-up alone will not clear the stale-binary failures. Land the endpoint/asset fixes before enabling this required probe, or defer the workflow until it has a green baseline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/manifest-check.yml around lines 46 - 47, Remove or defer
the “Probe manifest URLs” workflow step that runs
tools/manifest_check/check_manifest.py until the endpoint and binary asset fixes
provide a green baseline; do not enable it as a required production gate while
it reports known failures.

Source: Pipeline failures

Comment thread tools/manifest_check/check_manifest.py
Comment on lines +105 to +125
def validate_schema(payload: dict) -> str | None:
for key in REQUIRED_TOP_KEYS:
if key not in payload:
return f"missing top-level key: {key}"
builds = payload.get("builds")
if not isinstance(builds, list) or not builds:
return "builds[] must be a non-empty list"
for idx, build in enumerate(builds):
if not isinstance(build, dict):
return f"builds[{idx}] is not an object"
if "chipFamily" not in build:
return f"builds[{idx}] missing chipFamily"
parts = build.get("parts")
if not isinstance(parts, list) or not parts:
return f"builds[{idx}].parts must be a non-empty list"
for pidx, part in enumerate(parts):
if not isinstance(part, dict):
return f"builds[{idx}].parts[{pidx}] is not an object"
for key in REQUIRED_PART_KEYS:
if key not in part:
return f"builds[{idx}].parts[{pidx}] missing {key}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject non-object JSON and non-string part paths.

A scalar JSON payload can raise at key not in payload, and "path": null passes validation then crashes at Line 173 on startswith(). Return a schema failure instead, and add regression cases for both inputs.

Proposed fix
-def validate_schema(payload: dict) -> str | None:
+def validate_schema(payload: object) -> str | None:
+    if not isinstance(payload, dict):
+        return "manifest must be a JSON object"
     for key in REQUIRED_TOP_KEYS:
         if key not in payload:
             return f"missing top-level key: {key}"
...
             for key in REQUIRED_PART_KEYS:
                 if key not in part:
                     return f"builds[{idx}].parts[{pidx}] missing {key}"
+            if not isinstance(part["path"], str) or not part["path"]:
+                return f"builds[{idx}].parts[{pidx}].path must be a non-empty string"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def validate_schema(payload: dict) -> str | None:
for key in REQUIRED_TOP_KEYS:
if key not in payload:
return f"missing top-level key: {key}"
builds = payload.get("builds")
if not isinstance(builds, list) or not builds:
return "builds[] must be a non-empty list"
for idx, build in enumerate(builds):
if not isinstance(build, dict):
return f"builds[{idx}] is not an object"
if "chipFamily" not in build:
return f"builds[{idx}] missing chipFamily"
parts = build.get("parts")
if not isinstance(parts, list) or not parts:
return f"builds[{idx}].parts must be a non-empty list"
for pidx, part in enumerate(parts):
if not isinstance(part, dict):
return f"builds[{idx}].parts[{pidx}] is not an object"
for key in REQUIRED_PART_KEYS:
if key not in part:
return f"builds[{idx}].parts[{pidx}] missing {key}"
def validate_schema(payload: object) -> str | None:
if not isinstance(payload, dict):
return "manifest must be a JSON object"
for key in REQUIRED_TOP_KEYS:
if key not in payload:
return f"missing top-level key: {key}"
builds = payload.get("builds")
if not isinstance(builds, list) or not builds:
return "builds[] must be a non-empty list"
for idx, build in enumerate(builds):
if not isinstance(build, dict):
return f"builds[{idx}] is not an object"
if "chipFamily" not in build:
return f"builds[{idx}] missing chipFamily"
parts = build.get("parts")
if not isinstance(parts, list) or not parts:
return f"builds[{idx}].parts must be a non-empty list"
for pidx, part in enumerate(parts):
if not isinstance(part, dict):
return f"builds[{idx}].parts[{pidx}] is not an object"
for key in REQUIRED_PART_KEYS:
if key not in part:
return f"builds[{idx}].parts[{pidx}] missing {key}"
if not isinstance(part["path"], str) or not part["path"]:
return f"builds[{idx}].parts[{pidx}].path must be a non-empty string"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/manifest_check/check_manifest.py` around lines 105 - 125, Update
validate_schema to first reject payloads that are not dictionaries before
checking REQUIRED_TOP_KEYS, and validate each part’s path field as a string so
null or other non-string values return a schema error before startswith() is
reached. Add regression cases covering a scalar JSON payload and a part with a
non-string path.

Comment on lines +166 to +180
seen_paths: set[str] = set()
for build in payload["builds"]:
for part in build["parts"]:
path = part["path"]
if path in seen_paths:
continue
seen_paths.add(path)
if path.startswith(("http://", "https://")):
part_url = path
else:
part_url = urllib.parse.urljoin(base + "/", path.lstrip("/"))
try:
pstatus, _, _ = http_request(
part_url, method="HEAD", timeout=timeout, retries=retries, backoff=backoff
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Constrain manifest-provided binary URLs.

parts[].path is remote input. Absolute URLs are fetched directly, and other schemes can survive urljoin; redirects are also followed automatically. A compromised manifest can make the CI runner probe arbitrary internal or external endpoints. Keep the documented local --base flow, but enforce an allowlist of permitted origins/schemes for initial URLs and redirects.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 172-172: Do not make http calls without encryption
Context: "http://"
Note: [CWE-319] Cleartext Transmission of Sensitive Information.

(requests-http)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/manifest_check/check_manifest.py` around lines 166 - 180, Constrain URL
handling in the manifest-check loop around `parts[].path` and `http_request`:
preserve local paths resolved against the documented `--base`, but allow only
explicitly permitted schemes and origins for the initial URL. Configure or
extend `http_request` so redirects are validated against the same allowlist
rather than followed to arbitrary destinations, rejecting disallowed URLs before
any request is made.

Source: Linters/SAST tools

The manifest handler fetched GitHub's /releases/tags/${tag}, but 'latest'
is not a real Git tag — GitHub returns 404, propagated as
{"error":"Release not found"} to the web installer, surfacing as
"Failed to download manifest" (#2225).

Fix: when tag === 'latest', use /releases/latest (the endpoint GitHub
provides for resolving newest non-prerelease), matching what the sibling
/latest/download/:filename route already does. Binary part paths now use
rel.tag_name (the resolved real tag, e.g. v4.0.6) instead of the literal
${tag}, so 'latest' manifests produce working download/v4.0.6/*.bin URLs
rather than nonexistent download/latest/*.bin paths.

Tool fixes surfaced by GHA run #29879346118 (31 failures / 12 targets):
- check_manifest.py: resolve part paths relative to the manifest URL
  (not the base URL), matching esp-web-tools client behavior. Previously
  produced /download/... instead of /releases/download/... → false 404s.
- check_manifest.py: retry on HTTP 429 (GitHub rate-limit) in addition
  to 5xx, preventing false negatives on unauthenticated CI runs.

wrangler.toml: add compatibility_date (was missing; local dev defaulted
to today's date which exceeds the bundled workerd).

Verified locally: wrangler pages dev + check_manifest.py --base
http://localhost:8788 → 12/12 targets pass (was 31 failures).

Refs: #2225, ESPresense/ESPresense#2320.
@DTTerastar

Copy link
Copy Markdown
Contributor Author

Split into two focused PRs per request:

Merge #356 first, then #355.

@DTTerastar DTTerastar closed this Jul 22, 2026
@DTTerastar
DTTerastar deleted the manifest-check-2225 branch July 22, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant