mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-29 01:09:02 +00:00
ci: stage observer+ flasher channel workflow (disabled)
Second-channel pipeline for mqtt-observer-plus, mirroring build-observer-firmwares.yml with full channel isolation: - rolling tag observer-mqtt-plus-latest (never shared: per-tag asset pruning would cross-delete on a shared tag) - FIRMWARE_VERSION v1.16.0-plus, separate build counter - OTA_MANIFEST_BASE overridden to /v-plus at build time so device pull-OTA can never hop a plus node back onto the stable channel (env build flags append after ini flags, so the override wins — verified in the binary) Not enabled: workflow_dispatch only (push trigger staged in comments), and the flasher-repo sync is gated behind SYNC_FLASHER=false with a fail-closed guard so a premature enable cannot clobber the stable config.json. The header documents the flasher-side prerequisites (channel-aware update-firmware.py, SPA channel picker, v-plus manifests, plus changelog). Manual dispatch already works for publishing hand-flashable test builds to the release page.
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
name: Build MQTT Observer+ Firmwares (keymind channel)
|
||||
|
||||
# ============================================================================
|
||||
# STATUS: STAGED BUT NOT ENABLED — manual dispatch only.
|
||||
#
|
||||
# This is the experimental "plus" channel (mqtt-observer-plus = flex +
|
||||
# mcarper/keymindCascade). It mirrors build-observer-firmwares.yml with the
|
||||
# channel isolated end-to-end:
|
||||
# - rolling release tag: observer-mqtt-plus-latest (NEVER share the
|
||||
# stable tag: each workflow prunes old assets within its tag by build
|
||||
# hash, so a shared tag would have the channels deleting each other's
|
||||
# binaries)
|
||||
# - firmware version: v1.16.0-plus (filenames + embedded `ver`)
|
||||
# - device pull-OTA: OTA_MANIFEST_BASE overridden to .../v-plus so
|
||||
# `ota update` on a plus node can never "update" it back onto the stable
|
||||
# channel (the per-env platformio.ini bakes .../v; the env-injected flag
|
||||
# below wins because PLATFORMIO_BUILD_FLAGS append after ini flags —
|
||||
# verified by building and checking the embedded URL string)
|
||||
#
|
||||
# TO ENABLE THE CHANNEL:
|
||||
# 1. Uncomment the `push:` trigger below.
|
||||
# 2. Flip SYNC_FLASHER to "true" — but FIRST the flasher repo
|
||||
# (agessaman/flasher.meshcore.io) needs channel support; until it does,
|
||||
# the sync step below fails closed on purpose so the stable config.json
|
||||
# cannot be clobbered. Flasher-side prerequisites:
|
||||
# - update-firmware.py: a --channel/--config argument writing a
|
||||
# plus-specific config (e.g. config-plus.json), not config.json
|
||||
# - SPA: a channel picker (Observer / Observer+ experimental)
|
||||
# - gen-slim-manifests.py output dir flasher/v-plus (arg already
|
||||
# parameterized; wired below)
|
||||
# - a plus changelog target (CHANGELOG-plus.md) — the stable
|
||||
# CHANGELOG.md tracks the flex branch only
|
||||
#
|
||||
# Until SYNC_FLASHER is enabled, the per-base build counter has no writer, so
|
||||
# every manual dispatch publishes as build .1 — fine for hand-flashed testing
|
||||
# off the release page.
|
||||
# ============================================================================
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# ENABLE: uncomment to build+publish on every push to the plus branch.
|
||||
# push:
|
||||
# branches:
|
||||
# - mqtt-observer-plus
|
||||
# # Keep in step with build-observer-firmwares.yml's paths-ignore.
|
||||
# paths-ignore:
|
||||
# - '**.md'
|
||||
# - 'docs/**'
|
||||
# - 'scripts/gen_changelog.py'
|
||||
# - '.github/**'
|
||||
# - '.gitignore'
|
||||
# - '.gitattributes'
|
||||
# - '.editorconfig'
|
||||
# - 'LICENSE'
|
||||
# - '.vscode/**'
|
||||
# - '.claude/**'
|
||||
# - 'mesh-america/**'
|
||||
|
||||
# Shared with build-observer-firmwares.yml and sync-flasher-content.yml so no
|
||||
# two workflows ever push to the flasher repo at the same time.
|
||||
concurrency:
|
||||
group: flasher-publish
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
# Channel-tagged version: embedded in firmware filenames and (with the
|
||||
# -observer tag + build number appended by build.sh) in `ver`/MQTT/SNMP.
|
||||
FIRMWARE_VERSION: v1.16.0-plus
|
||||
# Rolling release tag for this channel — distinct from observer-mqtt-latest.
|
||||
RELEASE_TAG: observer-mqtt-plus-latest
|
||||
# Channel-isolated pull-OTA manifest base baked into these binaries
|
||||
# (overrides the .../v default from the per-env platformio.ini flags).
|
||||
OTA_MANIFEST_BASE_URL: https://observer.gessaman.com/v-plus
|
||||
# Fail-closed flasher-repo sync gate; see "TO ENABLE" above.
|
||||
SYNC_FLASHER: "false"
|
||||
|
||||
jobs:
|
||||
|
||||
# Discover the *_observer_mqtt envs and shard them across runners — same
|
||||
# env set as the stable channel; only version/flags/tag differ.
|
||||
enumerate:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.split.outputs.matrix }}
|
||||
build_number: ${{ steps.buildnum.outputs.n }}
|
||||
steps:
|
||||
- name: Clone Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Split observer envs into shards
|
||||
id: split
|
||||
shell: bash
|
||||
run: |
|
||||
SHARDS=14
|
||||
ENVS=$(grep -rhoE '^\[env:[^]]*observer_mqtt\]' platformio.ini variants/*/platformio.ini \
|
||||
| sed -E 's/^\[env:(.*)\]$/\1/' | sort -u)
|
||||
echo "Discovered envs:"; echo "$ENVS"
|
||||
MATRIX=$(echo "$ENVS" | awk -v n="$SHARDS" '
|
||||
{ shard[NR % n] = shard[NR % n] " " $0 }
|
||||
END { for (i = 0; i < n; i++) { sub(/^ /, "", shard[i]);
|
||||
printf "{\"idx\":%d,\"envs\":\"%s\"}\n", i, shard[i] } }' \
|
||||
| jq -cs .)
|
||||
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Compute plus-channel build number
|
||||
id: buildnum
|
||||
shell: bash
|
||||
run: |
|
||||
# Plus-channel counter, separate from the stable channel's. The
|
||||
# flasher-sync step is the sole writer; while SYNC_FLASHER is off
|
||||
# this 404s -> N=1 every run (documented above).
|
||||
COUNTER_URL="https://observer.gessaman.com/observer-plus-build-counter.json"
|
||||
CUR=$(curl -fsSL "$COUNTER_URL" 2>/dev/null || echo '{}')
|
||||
PREV_BASE=$(echo "$CUR" | jq -r '.baseVersion // ""')
|
||||
PREV_BUILD=$(echo "$CUR" | jq -r '.build // 0')
|
||||
if [ "$PREV_BASE" = "$FIRMWARE_VERSION" ]; then
|
||||
N=$((PREV_BUILD + 1))
|
||||
else
|
||||
N=1
|
||||
fi
|
||||
echo "Base $FIRMWARE_VERSION; previous build $PREV_BUILD (base $PREV_BASE) -> N=$N"
|
||||
echo "n=$N" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
needs: enumerate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: ${{ fromJSON(needs.enumerate.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Clone Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache PlatformIO Toolchains
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.platformio/packages
|
||||
~/.platformio/platforms
|
||||
key: pio-toolchains-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
||||
restore-keys: |
|
||||
pio-toolchains-${{ runner.os }}-
|
||||
|
||||
- name: Setup Build Environment
|
||||
uses: ./.github/actions/setup-build-environment
|
||||
|
||||
- name: Build Shard ${{ matrix.shard.idx }}
|
||||
env:
|
||||
FIRMWARE_BUILD_NUMBER: ${{ needs.enumerate.outputs.build_number }}
|
||||
# Channel isolation for device pull-OTA: appended after the per-env
|
||||
# ini flags, so this definition of OTA_MANIFEST_BASE wins. build.sh
|
||||
# appends its own version/OTA_VARIANT flags to this variable.
|
||||
PLATFORMIO_BUILD_FLAGS: -D OTA_MANIFEST_BASE='"${{ env.OTA_MANIFEST_BASE_URL }}"'
|
||||
run: /usr/bin/env bash build.sh build-firmware ${{ matrix.shard.envs }}
|
||||
|
||||
- name: Upload Shard Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fw-plus-${{ matrix.shard.idx }}
|
||||
path: out
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
needs: [enumerate, build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Repo
|
||||
uses: actions/checkout@v4
|
||||
# Shallow on purpose — see the stable workflow's note: build.sh named
|
||||
# the assets from a shallow clone's 7-char short hash.
|
||||
|
||||
- name: Download All Shard Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Flatten into out/
|
||||
run: |
|
||||
mkdir -p out
|
||||
find artifacts -type f -name '*.bin' -exec cp -f {} out/ \;
|
||||
find artifacts -type f -name '*.partsig' -exec cp -f {} out/ \;
|
||||
echo "Collected binaries:"; ls -1 out
|
||||
|
||||
- name: Compute Short SHA
|
||||
id: sha
|
||||
run: echo "short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish to Rolling Release (tag pinned; assets replaced in place)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
|
||||
gh release create "$RELEASE_TAG" --prerelease \
|
||||
--title "MQTT Observer+ Firmwares (experimental keymind channel)" \
|
||||
--notes "Rolling build of the EXPERIMENTAL observer+ channel (mqtt-observer-plus = observer + mcarper keymind transmission-reliability work). Flashing between this and the stable observer channel preserves MQTT and core settings; downgrading to stable resets a handful of radio toggles (cad, fem rxgain, rx boost) and retry settings to defaults. The git short hash is embedded in each asset filename."
|
||||
fi
|
||||
|
||||
gh release upload "$RELEASE_TAG" $(find out -maxdepth 1 -type f ! -name '*.partsig') --clobber
|
||||
|
||||
# Prune old assets within THIS tag only, retaining the most recent
|
||||
# KEEP_BUILDS build hashes (same logic + rationale as the stable
|
||||
# workflow: a flasher tab opened mid-cycle must not 404).
|
||||
KEEP_BUILDS=2
|
||||
keep_hashes=$(gh release view "$RELEASE_TAG" --json assets \
|
||||
-q '.assets[] | "\(.createdAt) \(.name)"' \
|
||||
| sort -r \
|
||||
| while read -r _ts name; do
|
||||
printf '%s' "$name" | grep -oiE '[0-9a-f]{7,40}(-merged)?\.bin$' | grep -oiE '^[0-9a-f]{7,40}'
|
||||
done \
|
||||
| awk '!seen[$0]++' | head -n "$KEEP_BUILDS")
|
||||
echo "Retaining build hashes:"; echo "$keep_hashes"
|
||||
gh release view "$RELEASE_TAG" --json assets -q '.assets[].name' \
|
||||
| while read -r asset; do
|
||||
ah=$(printf '%s' "$asset" | grep -oiE '[0-9a-f]{7,40}(-merged)?\.bin$' | grep -oiE '^[0-9a-f]{7,40}' || true)
|
||||
if [ -n "$ah" ] && grep -qxF "$ah" <<<"$keep_hashes"; then
|
||||
continue
|
||||
fi
|
||||
gh release delete-asset "$RELEASE_TAG" "$asset" --yes || true
|
||||
done
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Flasher-repo sync: OFF until the flasher supports channels (see header).
|
||||
# Fails closed if enabled prematurely so the stable config.json can never
|
||||
# be clobbered by this channel.
|
||||
# ----------------------------------------------------------------------
|
||||
- name: Checkout Flasher Repo
|
||||
if: env.SYNC_FLASHER == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: agessaman/flasher.meshcore.io
|
||||
token: ${{ secrets.FLASHER_DISPATCH_TOKEN }}
|
||||
path: flasher
|
||||
|
||||
- name: Update Flasher Config (plus channel)
|
||||
if: env.SYNC_FLASHER == 'true'
|
||||
run: |
|
||||
# FAIL-CLOSED GUARD: running update-firmware.py as-is would rewrite
|
||||
# the STABLE channel's config.json. Replace this block with the
|
||||
# channel-aware invocation once the flasher repo supports it, e.g.:
|
||||
# python3 flasher/scripts/update-firmware.py \
|
||||
# --config flasher/config-plus.json \
|
||||
# "${{ steps.sha.outputs.short }}" "$GITHUB_WORKSPACE/firmware-notes.html"
|
||||
echo "ERROR: flasher repo has no plus-channel support yet (see workflow header)." >&2
|
||||
exit 1
|
||||
|
||||
- name: Generate Slim Per-Variant Manifests + Persist Build Counter (plus channel)
|
||||
if: env.SYNC_FLASHER == 'true'
|
||||
env:
|
||||
BUILD_NUMBER: ${{ needs.enumerate.outputs.build_number }}
|
||||
run: |
|
||||
# Channel-isolated manifest namespace: these are what OTA_MANIFEST_BASE_URL
|
||||
# points plus binaries at, so `ota update` stays within the channel.
|
||||
python3 flasher/scripts/gen-slim-manifests.py \
|
||||
--config flasher/config-plus.json \
|
||||
--out-dir flasher/v-plus \
|
||||
--base-version "$FIRMWARE_VERSION" \
|
||||
--build "$BUILD_NUMBER" \
|
||||
--partsig-dir out
|
||||
printf '{\n "baseVersion": "%s",\n "build": %s\n}\n' \
|
||||
"$FIRMWARE_VERSION" "$BUILD_NUMBER" > flasher/observer-plus-build-counter.json
|
||||
echo "Build $FIRMWARE_VERSION.$BUILD_NUMBER"
|
||||
|
||||
- name: Generate Changelog (plus channel)
|
||||
if: env.SYNC_FLASHER == 'true'
|
||||
run: |
|
||||
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
|
||||
git fetch --unshallow --quiet
|
||||
fi
|
||||
# Plus channel keeps its own changelog; the stable CHANGELOG.md
|
||||
# tracks the flex branch only.
|
||||
python3 scripts/gen_changelog.py flasher/CHANGELOG-plus.md
|
||||
|
||||
# (No docs sync: the stable channel owns the flasher's docs pages.)
|
||||
|
||||
- name: Commit & Push Flasher Config
|
||||
if: env.SYNC_FLASHER == 'true'
|
||||
working-directory: flasher
|
||||
run: |
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "No flasher changes to commit."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "meshcore-bot"
|
||||
git config user.email "noreply@gessaman.com"
|
||||
git commit -m "Update observer+ firmware to ${{ steps.sha.outputs.short }} (build ${FIRMWARE_VERSION}.${{ needs.enumerate.outputs.build_number }})"
|
||||
git push
|
||||
Reference in New Issue
Block a user