diff --git a/.github/workflows/flasher-link-comment.yml b/.github/workflows/flasher-link-comment.yml
index 09d13e386..b906ab492 100644
--- a/.github/workflows/flasher-link-comment.yml
+++ b/.github/workflows/flasher-link-comment.yml
@@ -18,6 +18,18 @@ jobs:
continue-on-error: true
runs-on: ubuntu-latest
steps:
+ # Per-board manifests carry the firmware's own metadata (activelySupported,
+ # displayName, ...) generated from each target's custom_meshtastic_* config.
+ - name: Download board manifests
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ run-id: ${{ github.event.workflow_run.id }}
+ pattern: manifest-*
+ path: ./manifests
+ merge-multiple: true
+
- name: Post or update web flasher link comment
uses: actions/github-script@v8
with:
@@ -74,34 +86,52 @@ jobs:
? new Date(archArtifacts[0].expires_at).toISOString().slice(0, 10)
: null;
- // Per-board deep links from manifest-{platform}-{board}-{version} artifacts
- const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- const boardRe = new RegExp(`^manifest-([a-z0-9]+)-(.+)-${escapedVersion}$`);
- let boards = artifacts
- .map((a) => boardRe.exec(a.name))
- .filter(Boolean)
- .map((m) => ({ platform: m[1], board: m[2] }))
- .sort((a, b) => a.board.localeCompare(b.board));
-
- // Limit the list to devices the web flasher actively supports — the same
- // activelySupported / non-portduino filter the flasher applies to its own
- // device list — matching variant envs (-tft/-inkhud) to their base target.
- // On a fetch failure, fall back to listing all built boards.
+ // Read each built board's manifest (.mt.json). activelySupported,
+ // displayName and architecture come straight from the board's
+ // custom_meshtastic_* platformio config, so the list is in sync with
+ // the firmware itself — no external device database needed.
+ const fs = require('fs');
+ let boards = [];
try {
- const hw = await (await fetch('https://api.meshtastic.org/resource/deviceHardware')).json();
- const supported = new Set(
- hw.filter((d) => d.activelySupported && !String(d.architecture || '').startsWith('portduino'))
- .map((d) => d.platformioTarget),
- );
- const baseTarget = (b) => b.replace(/-(tft|inkhud)$/, '');
- boards = boards.filter((b) => supported.has(b.board) || supported.has(baseTarget(b.board)));
+ boards = fs.readdirSync('./manifests')
+ .filter((f) => f.endsWith('.mt.json'))
+ .map((f) => {
+ try { return JSON.parse(fs.readFileSync(`./manifests/${f}`, 'utf8')); }
+ catch { return null; }
+ })
+ .filter((m) => m && m.activelySupported === true && m.platformioTarget)
+ .map((m) => ({
+ board: m.platformioTarget,
+ platform: m.architecture || '',
+ // displayName is maintainer-authored text; escape table-breaking pipes
+ displayName: String(m.displayName || m.platformioTarget).replace(/\|/g, '\\|'),
+ image: Array.isArray(m.images) && m.images[0] ? String(m.images[0]) : '',
+ }))
+ .sort((a, b) => a.board.localeCompare(b.board));
} catch (e) {
- core.warning(`Could not fetch device hardware list; listing all built boards. ${e.message}`);
+ core.warning(`Could not read board manifests: ${e.message}`);
}
const flasherUrl = `https://flasher.meshtastic.org/?pr=${prNumber}`;
+ // Device illustrations are served by the flasher from the same image
+ // names the manifest declares (custom_meshtastic_images). The flasher
+ // serves its SPA shell (HTML, 200) for unknown paths, so confirm each
+ // image really resolves to an image before linking it.
+ const imageBase = 'https://flasher.meshtastic.org/img/devices/';
+ await Promise.all(boards.map(async (b) => {
+ if (!b.image) return;
+ try {
+ const res = await fetch(`${imageBase}${encodeURIComponent(b.image)}`);
+ const type = res.headers.get('content-type') || '';
+ if (!res.ok || !type.startsWith('image/')) b.image = '';
+ } catch { b.image = ''; }
+ }));
+
const boardLines = boards
- .map((b) => `| [\`${b.board}\`](${flasherUrl}&device=${encodeURIComponent(b.board)}) | ${b.platform} |`)
+ .map((b) => {
+ const img = b.image ? `
` : '';
+ return `| ${img} | ${b.displayName} | [\`${b.board}\`](${flasherUrl}&device=${encodeURIComponent(b.board)}) | ${b.platform} |`;
+ })
.join('\n');
// Shields.io badges. Only non-user-controlled, charset-constrained values
@@ -124,8 +154,8 @@ jobs:
const boardTable = boards.length > 0 ? [
`Supported boards built by this PR (${boards.length})
`,
'',
- '| Board | Platform |',
- '| --- | --- |',
+ '| | Device | Board | Platform |',
+ '| --- | --- | --- | --- |',
boardLines,
'',
' ',
@@ -134,7 +164,7 @@ jobs:
const body = [
marker,
- '## 🔦 Try this PR in the Web Flasher',
+ '## ⚡ Try this PR in the Web Flasher',
'',
`[](${flasherUrl})`,
'',
diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml
index 374cbbe0a..12b2f0e55 100644
--- a/.github/workflows/main_matrix.yml
+++ b/.github/workflows/main_matrix.yml
@@ -286,11 +286,11 @@ jobs:
--limit 1 --json databaseId --jq '.[0].databaseId // empty')
if [ -n "$RUN_ID" ]; then
ARTIFACT_NAME=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \
- --jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | .name' | head -1)
+ --jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | select(.expired == false) | .name' | head -1)
if [ -n "$ARTIFACT_NAME" ]; then
gh run download "$RUN_ID" -R "${{ github.repository }}" \
--name "$ARTIFACT_NAME" --dir ./baseline-develop/
- cp "./baseline-develop/${ARTIFACT_NAME}/current-sizes.json" ./develop-sizes.json
+ cp "./baseline-develop/current-sizes.json" ./develop-sizes.json
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
@@ -311,11 +311,11 @@ jobs:
--limit 1 --json databaseId --jq '.[0].databaseId // empty')
if [ -n "$RUN_ID" ]; then
ARTIFACT_NAME=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \
- --jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | .name' | head -1)
+ --jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | select(.expired == false) | .name' | head -1)
if [ -n "$ARTIFACT_NAME" ]; then
gh run download "$RUN_ID" -R "${{ github.repository }}" \
--name "$ARTIFACT_NAME" --dir ./baseline-master/
- cp "./baseline-master/${ARTIFACT_NAME}/current-sizes.json" ./master-sizes.json
+ cp "./baseline-master/current-sizes.json" ./master-sizes.json
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"