Skip to content

Commit cde8bb7

Browse files
authored
Merge pull request #21 from ApolloAutomation/feat/per-variant-repo
Make release notes follow the selected firmware variant's repo
2 parents 456dddb + 6cf74d1 commit cde8bb7

5 files changed

Lines changed: 93 additions & 4 deletions

File tree

‎devices.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@
234234
"WLED-MM 14.5.1 (Rev4)": "https://apolloautomation.github.io/WLED-MM-M1/14.5.1/manifest.json",
235235
"WLED 16.0.1 (Rev6 only)": "https://apolloautomation.github.io/WLED-M1/manifest.json"
236236
}
237+
},
238+
"repos": {
239+
"stable": {
240+
"WLED 16.0.1 (Rev6 only)": "ApolloAutomation/WLED-M1"
241+
}
237242
}
238243
}
239244
]

‎js/views/device.js‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ function selectedManifest(device, channel, variant) {
2626
return device.firmware[channel][variant];
2727
}
2828

29+
function repoFor(device, channel, variant) {
30+
return (device.repos && device.repos[channel] && device.repos[channel][variant]) || device.repo;
31+
}
32+
2933
function segHtml(id, label, keys, active, dataAttr) {
3034
if (keys.length < 2) return '';
3135
return `
@@ -112,12 +116,14 @@ export function renderDevice(el, device) {
112116
});
113117
renderInstall();
114118
renderConfig();
119+
renderReleaseNotes();
115120
});
116121
}
117122

118123
async function renderInstall() {
119124
const myEpoch = epoch;
120125
const manifest = selectedManifest(device, channel, variant);
126+
const repo = repoFor(device, channel, variant);
121127
if (hasSerial) {
122128
installSlot.innerHTML = `
123129
<esp-web-install-button manifest="${manifest}">
@@ -153,7 +159,7 @@ export function renderDevice(el, device) {
153159
if (epoch !== myEpoch) return; // selection changed mid-fetch
154160
filesEl.innerHTML =
155161
`<li>Couldn't load the file list — download firmware from the
156-
<a href="https://github.com/${device.repo}/releases">latest release</a>.</li>`;
162+
<a href="https://github.com/${repo}/releases">latest release</a>.</li>`;
157163
}
158164
}
159165
}
@@ -162,10 +168,11 @@ export function renderDevice(el, device) {
162168
const slot = el.querySelector('#release-slot');
163169
slot.innerHTML = '';
164170
const myEpoch = epoch;
171+
const repo = repoFor(device, channel, variant);
165172
try {
166-
const rel = await fetchReleaseNotes(device.repo, channel);
173+
const rel = await fetchReleaseNotes(repo, channel);
167174
if (epoch !== myEpoch) return; // selection changed mid-fetch
168-
const url = /^https:\/\/github\.com\//.test(rel.url) ? rel.url : `https://github.com/${device.repo}/releases`;
175+
const url = /^https:\/\/github\.com\//.test(rel.url) ? rel.url : `https://github.com/${repo}/releases`;
169176
slot.innerHTML = `
170177
<div class="release-notes">
171178
<details>
@@ -178,7 +185,7 @@ export function renderDevice(el, device) {
178185
if (epoch !== myEpoch) return; // selection changed mid-fetch
179186
slot.innerHTML = `
180187
<div class="release-notes">
181-
See <a class="fail-link" href="https://github.com/${device.repo}/releases">recent releases</a>
188+
See <a class="fail-link" href="https://github.com/${repo}/releases">recent releases</a>
182189
for what's new.
183190
</div>`;
184191
}

‎scripts/test_validate_registry.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,33 @@ def test_non_dict_config_errors_without_crashing(self):
9292
self.assertTrue(any("config" in e for e in errs), (bad, errs))
9393

9494

95+
class ReposShapeChecks(unittest.TestCase):
96+
FW = {"stable": {"v16": "https://x/m.json", "v14": "https://y/m.json"}}
97+
98+
def test_absent_repos_ok(self):
99+
self.assertEqual(vr.check_repos_shape(None, self.FW, "dev"), [])
100+
101+
def test_valid_override_ok(self):
102+
repos = {"stable": {"v16": "Owner/Repo"}}
103+
self.assertEqual(vr.check_repos_shape(repos, self.FW, "dev"), [])
104+
105+
def test_repos_not_dict_errors(self):
106+
errs = vr.check_repos_shape([], self.FW, "dev")
107+
self.assertTrue(any("repos" in e for e in errs), errs)
108+
109+
def test_channel_not_dict_errors(self):
110+
errs = vr.check_repos_shape({"stable": "x"}, self.FW, "dev")
111+
self.assertTrue(any("stable" in e for e in errs), errs)
112+
113+
def test_bad_owner_name_errors(self):
114+
for bad in ("OwnerRepo", "a/b/c", "own er/repo", "", "/repo", "owner/"):
115+
errs = vr.check_repos_shape({"stable": {"v16": bad}}, self.FW, "dev")
116+
self.assertTrue(any("owner/name" in e for e in errs), (bad, errs))
117+
118+
def test_variant_not_in_firmware_errors(self):
119+
errs = vr.check_repos_shape({"stable": {"ghost": "Owner/Repo"}}, self.FW, "dev")
120+
self.assertTrue(any("no such firmware variant" in e for e in errs), errs)
121+
122+
95123
if __name__ == "__main__":
96124
unittest.main()

‎scripts/validate_registry.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,31 @@ def check_config_shape(config, dev_id):
6767
errs.append(f"{dev_id} config {channel}/{variant}: not an https URL")
6868
return errs
6969

70+
def check_repos_shape(repos, firmware, dev_id):
71+
"""Validate the optional `repos` map (channel -> variant -> "owner/name").
72+
73+
Network-free. `repos` absent (None) is valid. Every variant key must exist
74+
in `firmware[channel]`, so a mistyped key that would silently fall back to
75+
the device repo is caught instead. Returns a list of error strings.
76+
"""
77+
errs = []
78+
if repos is None:
79+
return errs
80+
if not isinstance(repos, dict):
81+
errs.append(f"{dev_id} repos: not an object")
82+
return errs
83+
for channel, variants in repos.items():
84+
if not isinstance(variants, dict):
85+
errs.append(f"{dev_id} repos {channel}: not an object")
86+
continue
87+
for variant, repo in variants.items():
88+
if (not isinstance(repo, str) or repo.count("/") != 1
89+
or " " in repo or not all(repo.split("/"))):
90+
errs.append(f"{dev_id} repos {channel}/{variant}: not an 'owner/name' string")
91+
if variant not in firmware.get(channel, {}):
92+
errs.append(f"{dev_id} repos {channel}/{variant}: no such firmware variant")
93+
return errs
94+
7095
def check_manifest(dev_id, channel, variant, murl):
7196
where = f"{dev_id} {channel}/{variant}"
7297
try:
@@ -97,6 +122,7 @@ def main():
97122
check_manifest(dev["id"], channel, variant, murl)
98123
config = dev.get("config", {})
99124
errors.extend(check_config_shape(config, dev["id"]))
125+
errors.extend(check_repos_shape(dev.get("repos"), dev.get("firmware", {}), dev["id"]))
100126
if not isinstance(config, dict):
101127
continue
102128
for channel, variants in config.items():

‎tests/installer.spec.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,29 @@ test('release notes ignore an off-allowlist API url and use the safe releases hr
183183
await expect(full).toHaveAttribute('href', `https://github.com/${d.repo}/releases`);
184184
});
185185

186+
test('release notes follow the selected variant repo (per-variant repos override)', async ({ page }) => {
187+
const d = registry.devices.find((x) => x.repos && x.repos.stable);
188+
test.skip(!d, 'no device with a per-variant repos override');
189+
const overrideVariant = Object.keys(d.repos.stable)[0];
190+
const overrideRepo = d.repos.stable[overrideVariant];
191+
const defaultVariant = Object.keys(d.firmware.stable)[0];
192+
test.skip(overrideVariant === defaultVariant, 'override is on the default variant');
193+
194+
// Force the API-failure path so the deterministic .fail-link (built from the
195+
// resolved repo) is what we assert on.
196+
await page.route('https://api.github.com/**', (route) => route.fulfill({ status: 403 }));
197+
await page.goto(`/#/${d.id}`);
198+
199+
// Default variant resolves to the device-level repo.
200+
await expect(page.locator('.release-notes .fail-link'))
201+
.toHaveAttribute('href', `https://github.com/${d.repo}/releases`);
202+
203+
// Selecting the override variant must re-render release notes against the override repo.
204+
await page.locator(`#variant-seg button[data-variant="${overrideVariant}"]`).click();
205+
await expect(page.locator('.release-notes .fail-link'))
206+
.toHaveAttribute('href', `https://github.com/${overrideRepo}/releases`);
207+
});
208+
186209
test('step 3 shows the Home Assistant hand-off', async ({ page }) => {
187210
const d = registry.devices[0];
188211
await page.goto(`/#/${d.id}`);

0 commit comments

Comments
 (0)