Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
- [ ] Any new runtime file/dir is added to the `cp` list in `.github/workflows/pages.yml`
- [ ] No new build step, framework, or runtime CDN dependency
- [ ] If `devices.json` changed: `python scripts/validate_registry.py` passes
- [ ] If a firmware or config URL changed: it is the build the product repo's classic installer flashes, `config` points at the YAML that builds it, and the first (default) variant is the one most people need
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Keep the SHA-pinned workflow actions current, like the product repos.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
19 changes: 15 additions & 4 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ permissions:
pages: write
id-token: write

# Let an in-flight production deploy finish; a newer push queues behind it.
concurrency:
group: pages
cancel-in-progress: true
cancel-in-progress: false

jobs:
deploy:
Expand All @@ -21,13 +22,23 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
# Network-free gate: a malformed registry (duplicate id, missing image,
# manifest outside its repo's Pages site) never reaches production, even
# when a PR merged with red or stale checks.
- name: Check registry (offline)
run: |
python scripts/test_validate_registry.py
python scripts/validate_registry.py --offline
- name: Stage site files
run: |
mkdir _site
cp -r index.html css js images assets vendor devices.json _site/
- uses: actions/upload-pages-artifact@v3
- uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
with:
path: _site
- id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
52 changes: 45 additions & 7 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,76 @@ name: Validate

on:
pull_request:
push:
branches: [main] # test the merged result, not just the PR head
workflow_dispatch:
schedule:
- cron: "17 6 * * *" # nightly registry check

permissions:
contents: read

jobs:
registry:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write # the nightly opens an issue when upstream firmware breaks
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Validator unit tests (offline)
run: python scripts/test_validate_registry.py
- name: Image script unit tests (offline)
run: python scripts/test_fetch_images.py
- run: python scripts/validate_registry.py
- name: Registry checks (offline)
run: python scripts/validate_registry.py --offline
- name: Registry checks (live manifests, configs and installers)
shell: bash
run: python scripts/validate_registry.py 2>&1 | tee validate.log
# Nightly failures otherwise only email whoever last edited the cron line.
- name: Report nightly failure
if: failure() && github.event_name == 'schedule'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
title="Nightly registry check failed"
log=$(tail -n 40 validate.log 2>/dev/null)
body=$(printf 'The nightly registry check failed: %s\n\n```\n%s\n```\n' "$RUN_URL" "${log:-No validator output; an earlier step failed. See the run.}")
num=$(gh issue list --state open --search "\"$title\" in:title" --json number --jq '.[0].number // empty')
if [ -n "$num" ]; then
gh issue comment "$num" --body "$body"
else
gh issue create --title "$title" --body "$body"
fi

ui-tests:
if: github.event_name == 'pull_request'
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
defaults:
run:
working-directory: tests
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
cache: npm
cache-dependency-path: tests/package-lock.json
- run: npm ci
- run: npx playwright install chromium --with-deps
- run: npx playwright test
- name: Keep traces of failed tests
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-results
path: tests/test-results/
retention-days: 14
12 changes: 7 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
Static site, **no build step** — do not add bundlers, frameworks, or npm deps outside `tests/`.

- Serve: `python -m http.server 8123` from repo root.
- Test: `cd tests && npx playwright test` (fails confusingly if run from repo root). Registry-driven suite.
- Validate registry: `python scripts/validate_registry.py` (network, ~60s).
- Test: `cd tests && npx playwright test` (fails confusingly if run from repo root). Registry-driven suite; it stubs GitHub, manifests and configs, so it runs offline.
- Python unit tests: `python scripts/test_validate_registry.py` and `python scripts/test_fetch_images.py` (offline).
- Validate registry: `python scripts/validate_registry.py` (network, ~15s); `--offline` for the shape checks alone.
- `devices.json` is the single source of truth; views, scripts, and tests all read it. Device ids are lowercase-kebab and become URL hashes (`#/air-1`) and image filenames.
- Per-variant overrides in `devices.json` all follow one shape: `channel -> variant -> value`, listing only the variants that differ from the device-level field. `repos` overrides `repo`, `installers` overrides `githubPagesInstaller` (`null` hides the link), `platforms` overrides `platform` (`esphome` default, or `wled`, which selects the step-3 wording). `variantNotes` is the same shape but adds rather than overrides: a sentence or two under the variant toggle saying when to pick that variant. The device-level field stays the fallback, so a device that ships one ecosystem today needs no map.
- Every device needs `id`, `name`, `category` (hub filter pill), `description`, `image`, `wiki`, `repo`, and `firmware` (`channel -> variant -> manifest URL`), and normally `imageSource` (input to `fetch_images.py`) and `githubPagesInstaller`. `stable` is required and preferred; the first variant key is the default selection. Manifests must live on the variant's own repo's GitHub Pages site and be one of the builds that repo's classic installer page offers (the validator checks both, and that no two variants share a manifest; it can't catch two of the page's builds swapped between variants, so check the mapping by hand). Optional `config` has the same shape as `firmware` and points at the raw YAML that builds each manifest.
- Per-variant overrides in `devices.json` all follow one shape: `channel -> variant -> value`, listing only the variants that differ from the device-level field. `repos` overrides `repo`, `installers` overrides `githubPagesInstaller` (`null` hides the link), `platforms` overrides `platform`, which selects the step-3 wording: `esphome` (default), `wled`, or `standalone` (no network at all, e.g. the ornaments' Basic firmware: step 3 says there is nothing to add). `variantNotes` is the same shape but adds rather than overrides: a sentence or two under the variant toggle saying when to pick that variant. The device-level field stays the fallback, so a device that ships one ecosystem today needs no map.
- **Deploy gotcha:** `.github/workflows/pages.yml` copies an explicit file list into `_site/`. Any new runtime file or directory must be added there or it 404s in production while working locally.
- `vendor/esp-web-tools/` is a pinned upstream copy — never hand-edit.
- Security stance: registry fields are trusted repo content; runtime-fetched content (GitHub releases, manifest-derived filenames) must go through the existing escaping (`esc()`, `encodeURI`) in `js/views/device.js`.
- Async renders in `js/views/device.js` guard against stale writes (variant/device changed mid-fetch). If you add an async fetch there, follow the same pattern — a wrong-firmware link is this site's worst failure mode.
- Security stance: registry fields are trusted repo content (`description` and `variantNotes` are rendered as HTML); runtime-fetched content (GitHub releases, manifest-derived URLs and filenames, the URL hash) must be escaped with `esc()` or set via `textContent`, and manifest part URLs are parsed with `URL` and limited to https (see `js/views/device.js`).
- Async renders in `js/views/device.js` guard against stale writes (variant/device changed mid-fetch). If you add an async fetch there, follow the same pattern — a wrong-firmware link is this site's worst failure mode. Release notes are shown only when the release matches the manifest's version, for the same reason.
22 changes: 18 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
python -m http.server 8123
```

from the repo root, then open http://localhost:8123. No build step — there deliberately isn't one. localhost is a secure context, so real USB flashing works in Chrome/Edge.
from the repo root, then open http://localhost:8123. No build step — there deliberately isn't one. localhost is a secure context, so real USB flashing works in Chrome, Edge, or Firefox.

## Tests

Expand All @@ -16,11 +16,25 @@ npm ci
npx playwright test
```

Run from `tests/`, not the repo root. PRs run this suite plus the registry validator (`python scripts/validate_registry.py`) in CI.
Run from `tests/`, not the repo root. The suite stubs GitHub, manifests and configs, so it runs offline and never spends GitHub API quota.

From the repo root:

```
python scripts/test_validate_registry.py # validator unit tests (offline)
python scripts/test_fetch_images.py # image script unit tests (offline)
python scripts/validate_registry.py --offline # registry shape only, no network
python scripts/validate_registry.py # plus live manifests, configs, wiki and classic installers (~15s)
```

CI runs all of these on every PR and again on `main` after merge. The deploy re-runs the offline checks, so a malformed registry never reaches production.

## Adding a device

One entry in `devices.json` (id must be lowercase-kebab; `firmware.stable` pointing at the product repo's GitHub Pages manifest) + one image in `images/` (`python scripts/fetch_images.py` downloads it from the `imageSource` URL). The tests are registry-driven and cover new devices automatically.
1. **Pick the firmware.** Use the manifest(s) the product repo's own classic installer page flashes. When a repo publishes several builds, that is usually the improv-enabled `firmware-factory/` build, not `firmware/`: without improv the installer can't offer Wi-Fi setup after flashing. The validator fails if the registry points at a build the classic page doesn't offer, but it can't tell which of the page's builds belongs to which variant, so check that by hand.
2. **Add one entry to `devices.json`.** The id is lowercase-kebab; it becomes the URL hash (`#/air-1`) and the image filename. Set `config` to the YAML that builds that same manifest. The first variant listed is the one selected on load, so put support-only or legacy builds after it. For the optional per-variant fields (`variantNotes`, `platform`/`platforms`, `repos`, `installers`) see `AGENTS.md`.
3. **Add the image.** Run `python scripts/fetch_images.py <id>`. Shopify sources are downscaled automatically; for any other source, save a ~600px JPEG as `images/<id>.jpg` yourself and the script keeps it.
4. **Check it.** Run `python scripts/validate_registry.py` and the Playwright suite. The tests are registry-driven and visit every device and variant automatically.

## Rules that will save you a red X

Expand All @@ -29,4 +43,4 @@ One entry in `devices.json` (id must be lowercase-kebab; `firmware.stable` point
- Registry content (`devices.json`) is trusted; anything fetched at runtime (GitHub API, manifests) must be escaped/sanitized before touching the DOM — see `esc()` in `js/views/device.js`.
- This site never builds or hosts firmware. Firmware changes belong in the product repos.

PRs target `main`. Merging deploys to https://apolloautomation.github.io/installer/.
PRs target `main`. Merging deploys to https://install.apolloautomation.com.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
Central browser installer for Apollo devices — https://install.apolloautomation.com

- Hub of every flashable Apollo device → guided 3-step install wizard.
- Static site, no build step. Serve locally: `python -m http.server 8123` then open http://localhost:8123 (localhost is a secure context, so real flashing works in Chrome/Edge).
- Static site, no build step. Serve locally: `python -m http.server 8123` then open http://localhost:8123 (localhost is a secure context, so real flashing works in Chrome, Edge, or Firefox).
- All device knowledge lives in `devices.json`. Adding a device = one JSON entry + one image.
- Firmware comes from each product repo's existing GitHub Pages manifests — this repo never builds or hosts firmware.
- Existing per-repo installer pages stay standalone; this site is the promoted front door, they are the fallback.

Design spec: `docs/superpowers/specs/2026-07-08-apollo-web-installer-design.md`
How to add a device and run the checks: `CONTRIBUTING.md`. Original design (historical): `docs/superpowers/specs/2026-07-08-apollo-web-installer-design.md`
Loading
Loading