Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/manifest-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: manifest-check

# Catch issue #2225 ("Failed to download manifest") regressions. Probes the
# public ESP Web Tools manifest endpoints served by functions/releases/[[path]].ts
# and asserts they return well-formed JSON with reachable binaries.

on:
push:
branches: [main]
paths:
- "tools/manifest_check/**"
- "functions/releases/**"
- ".github/workflows/manifest-check.yml"
pull_request:
paths:
- "tools/manifest_check/**"
- "functions/releases/**"
- ".github/workflows/manifest-check.yml"
schedule:
- cron: "17 * * * *"
workflow_dispatch: {}

permissions:
contents: read

jobs:
unit-tests:
name: Schema unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python3 tools/manifest_check/test_check_manifest.py

probe-manifest:
name: Probe public manifest endpoints
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Probe manifest URLs
run: python3 tools/manifest_check/check_manifest.py
Comment on lines +46 to +47

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

23 changes: 15 additions & 8 deletions functions/releases/[[path]].ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ interface Asset {

interface Release {
name: string
tag_name: string
assets: Asset[]
}

Expand All @@ -124,7 +125,10 @@ app.get('/:tag{[^/]+\\.json}',
// latest changes frequently, specific releases are immutable
const maxAge = tag === 'latest' ? 300 : 86400

const response = await fetch(`https://api.github.com/repos/ESPresense/ESPresense/releases/tags/${tag}`, {
// 'latest' is not a real Git tag; GitHub's /releases/latest resolves it
// to the newest non-prerelease. Specific tags use /releases/tags/{tag}.
const releasePath = tag === 'latest' ? 'releases/latest' : `releases/tags/${tag}`
const response = await fetch(`https://api.github.com/repos/ESPresense/ESPresense/${releasePath}`, {
headers: { "User-Agent": "espresense-release-proxy" },
cf: {
cacheTtlByStatus: { '200-299': 300, '400-499': 60, '500-599': 0 }
Expand All @@ -139,6 +143,9 @@ app.get('/:tag{[^/]+\\.json}',
}

const rel: Release = await response.json()
// Binary paths must point at the resolved release's real tag (e.g. v4.0.6),
// not the literal 'latest' token which has no GitHub download URL.
const realTag = rel.tag_name || tag

const manifest = {
"name": "ESPresense " + rel.name + (flavor && flavor !== "" ? ` (${flavor})` : ""),
Expand All @@ -148,25 +155,25 @@ app.get('/:tag{[^/]+\\.json}',
}

const a32 = findAsset(rel, `esp32-${flavor}.bin`) || findAsset(rel, `${flavor}.bin`) || findAsset(rel, `esp32.bin`)
if (a32) manifest.builds.push(esp32(`download/${tag}/${a32.name}`))
if (a32) manifest.builds.push(esp32(`download/${realTag}/${a32.name}`))

const c3 = findAsset(rel, `esp32c3-${flavor}.bin`) || findAsset(rel, `esp32c3.bin`)
if (c3) manifest.builds.push(esp32c3(`download/${tag}/${c3.name}`, "uart"))
if (c3) manifest.builds.push(esp32c3(`download/${realTag}/${c3.name}`, "uart"))

const c3_cdc = findAsset(rel, `esp32c3-${flavor}-cdc.bin`) || findAsset(rel, `esp32c3-cdc.bin`)
if (c3_cdc) manifest.builds.push(esp32c3(`download/${tag}/${c3_cdc.name}`, "cdc"))
if (c3_cdc) manifest.builds.push(esp32c3(`download/${realTag}/${c3_cdc.name}`, "cdc"))

const s3 = findAsset(rel, `esp32s3-${flavor}.bin`) || findAsset(rel, `esp32s3.bin`)
if (s3) manifest.builds.push(esp32s3(`download/${tag}/${s3.name}`, "uart"))
if (s3) manifest.builds.push(esp32s3(`download/${realTag}/${s3.name}`, "uart"))

const s3_cdc = findAsset(rel, `esp32s3-${flavor}-cdc.bin`) || findAsset(rel, `esp32s3-cdc.bin`)
if (s3_cdc) manifest.builds.push(esp32s3(`download/${tag}/${s3_cdc.name}`, "cdc"))
if (s3_cdc) manifest.builds.push(esp32s3(`download/${realTag}/${s3_cdc.name}`, "cdc"))

const c6 = findAsset(rel, `esp32c6-${flavor}.bin`) || findAsset(rel, `esp32c6.bin`)
if (c6) manifest.builds.push(esp32c6(`download/${tag}/${c6.name}`, "uart"))
if (c6) manifest.builds.push(esp32c6(`download/${realTag}/${c6.name}`, "uart"))

const c6_cdc = findAsset(rel, `esp32c6-${flavor}-cdc.bin`) || findAsset(rel, `esp32c6-cdc.bin`)
if (c6_cdc) manifest.builds.push(esp32c6(`download/${tag}/${c6_cdc.name}`, "cdc"))
if (c6_cdc) manifest.builds.push(esp32c6(`download/${realTag}/${c6_cdc.name}`, "cdc"))

c.header('Cache-Control', `public, max-age=${maxAge}`)
return c.json(manifest)
Expand Down
2 changes: 2 additions & 0 deletions tools/manifest_check/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
67 changes: 67 additions & 0 deletions tools/manifest_check/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# manifest-check

Headless probe of the public ESP Web Tools manifest endpoints served by this
repo's `functions/releases/[[path]].ts`. Reproduces issue
[#2225](https://github.com/ESPresense/ESPresense/issues/2225) ("Failed to
download manifest"), where `https://espresense.com/releases/{version}.json?flavor={flavor}`
returns 5xx/404 and the web installer surfaces it as a generic "Failed to
download manifest" toast with no diagnostic.

## Run locally

```sh
python3 tools/manifest_check/check_manifest.py
```

Skip the per-binary HEAD validation (faster, useful when iterating on the
script itself):

```sh
python3 tools/manifest_check/check_manifest.py --no-check-parts
```

Probe a different host (for local dev via `wrangler pages dev`):

```sh
python3 tools/manifest_check/check_manifest.py --base http://localhost:8788
```

## What the probe checks

For each `version,flavor` listed in [`targets.txt`](./targets.txt):

1. `GET /releases/{version}.json?flavor={flavor}` returns HTTP 200 with
`application/json`.
2. The body parses as JSON.
3. Required top-level keys are present (`name`, `version`, `builds`).
4. Each `builds[]` entry has a non-empty `parts[]` array with `path` and
`offset`.
5. Every `parts[].path` HEAD-resolves to 200 / 3xx (catches stale binaries
removed from the CDN).

Single transient 5xx responses are retried up to 3 times with exponential
backoff before failing the run; this keeps CI from flapping on transient
upstream hiccups while still catching persistent failures like the one
reported on [#2225](https://github.com/ESPresense/ESPresense/issues/2225).

## Updating the target list

[`targets.txt`](./targets.txt) is the source of truth for which (version,
flavor) pairs the installer is expected to serve. Edit it when:

- A new release ships and is added to the installer dropdown.
- A new flavor (board / variant) enters the installer dropdown.
- A release is removed from the public installer (e.g., yanked due to a
bricking bug).

Lines starting with `#` are comments. Format is `version,flavor` per line.

## CI

[`.github/workflows/manifest-check.yml`](../../.github/workflows/manifest-check.yml)
runs the probe on:

- pull requests that touch this directory, the release handler, or the workflow,
- pushes to `main` that touch the same,
- an hourly cron (`17 * * * *`),
- manual `workflow_dispatch`.
Loading
Loading