ci: give the beta channel its own concurrency group

Concurrency groups are repo-wide, not per-branch, so the beta build sharing
`flasher-publish` with the production workflows meant an in-flight beta build
forced a production build into the pending state — where the next run queued into
that group cancelled it outright. That is what silently dropped the production
build of the eastidahomesh preset fix: the beta build held the group from
22:29:24 to 22:37:34, production's build queued at 22:31:26 and went pending, and
its docs sync evicted it a second later.

Beta now uses its own group and, since the channels are no longer serialized
against each other, its flasher push rebase-retries the same way production's
does (with --autostash, because the scoped add here leaves the checkout dirty).

Also refresh the two workflow copies this branch never received: they still
triggered on the retired mqtt-bridge-implementation-flex branch, so they were
dead on push while still sitting in the shared group. Taking production's current
versions verbatim keeps them inert here and conflict-free on promotion.
This commit is contained in:
agessaman committed 2026-07-24 20:04:55 -07:00
1 parent cfff9d5cb7
commit c691864397
3 files changed
+108 -26

No files matched your search

@@ -39,10 +39,13 @@ on:
- '.vscode/**'
- '.claude/**'
# Shared with build-observer-firmwares.yml and sync-flasher-content.yml so the
# workflows never push to the flasher repo at the same time.
# Own group: serialize beta builds against each other only. Concurrency groups are
# repo-wide, not per-branch, so sharing one with the production workflows let an
# in-flight beta build force a production build into the pending state, where the
# next run queued into the group cancelled it outright. Concurrent flasher pushes
# are instead made safe by the rebase-retry in "Commit & Push Beta Artifacts".
concurrency:
group: flasher-publish
group: observer-firmware-beta-build
cancel-in-progress: false
env:
@@ -346,4 +349,26 @@ jobs:
git config user.name "meshcore-bot"
git config user.email "noreply@gessaman.com"
git commit -m "Update BETA observer firmware to ${{ steps.sha.outputs.short }} (build ${FIRMWARE_VERSION}.${{ needs.enumerate.outputs.build_number }})"
git push
# The production workflows can be pushing to the flasher repo right now
# (no shared concurrency group any more), so a non-fast-forward is expected
# rather than fatal: rebase onto their commit and retry. The scoped add
# above keeps this commit inside the beta paths, so a conflict with
# production's files is not possible.
br=$(git rev-parse --abbrev-ref HEAD)
for attempt in 1 2 3; do
if git push origin "HEAD:$br"; then
exit 0
fi
echo "push rejected (attempt $attempt); rebasing onto origin/$br"
git fetch origin "$br"
# --autostash: the scoped add above leaves unrelated edits in the
# checkout unstaged on purpose, and plain rebase refuses to run with a
# dirty tree. They stay unstaged, so they still never get published.
git rebase --autostash "origin/$br" || {
git rebase --abort || true
echo "::error::flasher rebase conflicted; re-run this workflow to republish"
exit 1
}
done
echo "::error::could not push beta changes after 3 attempts"
exit 1
+52 -17
View File
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
push:
branches:
- mqtt-bridge-implementation-flex
- observer-firmware
# Only rebuild firmware when something that affects the binaries changes.
# Docs, the changelog, the changelog generator, CI files, and repo-meta
# files do not change firmware output — docs/changelog are handled by
@@ -26,10 +26,13 @@ on:
- '.vscode/**'
- '.claude/**'
# Serialize with sync-flasher-content.yml so the two workflows never push to the
# flasher repo at the same time (shared group name across both workflows).
# Own group: serialize builds against each other only. Do NOT share this group
# with sync-flasher-content.yml — a mixed code+docs commit triggers both, and a
# run that is still pending when its sibling is queued gets cancelled, which
# silently skipped the firmware build. Concurrent flasher pushes are instead made
# safe by the rebase-retry in "Commit & Push Flasher Config".
concurrency:
group: flasher-publish
group: observer-firmware-build
cancel-in-progress: false
env:
@@ -38,6 +41,10 @@ env:
FIRMWARE_VERSION: v1.16.0
# Rolling release tag that hosts the latest observer binaries.
RELEASE_TAG: observer-mqtt-latest
# Download host serving RELEASE_TAG's assets (cloudflare-worker in the flasher
# repo). Baked into the slim OTA manifests' file URLs; must stay consistent
# with config.json's staticPath.
STATIC_PATH: https://observer-fw.gessaman.com
jobs:
@@ -185,6 +192,13 @@ jobs:
# not published as release assets (and they'd break the hash-based prune).
gh release upload "$RELEASE_TAG" $(find out -maxdepth 1 -type f ! -name '*.partsig') --clobber
# Keep the release body in sync with the repo's notes source: the
# /releases feed serves it as the flasher dropdown's changelog
# (config.json no longer carries per-version notes). Non-fatal —
# stale notes beat a red build whose binaries are already live.
gh release edit "$RELEASE_TAG" --notes-file firmware-notes.html \
|| echo "WARNING: release notes sync failed" >&2
# Prune old assets, but RETAIN the most recent KEEP_BUILDS build hashes
# (not just the current one). The flasher SPA reads config.json once at
# page load and the embedded git short-hash is what links it to assets;
@@ -222,22 +236,23 @@ jobs:
token: ${{ secrets.FLASHER_DISPATCH_TOKEN }}
path: flasher
- name: Update Flasher Config
run: |
python3 flasher/scripts/update-firmware.py \
"${{ steps.sha.outputs.short }}" \
"$GITHUB_WORKSPACE/firmware-notes.html"
# update-firmware.py is no longer called here: config.json's observer
# entries are github release defs (no embedded filenames to rewrite), the
# flasher's versions come from the Worker's /releases feed, and the
# changelog notes ride the release body (see the publish step).
- name: Generate Slim Per-Variant Manifests + Persist Build Counter
env:
BUILD_NUMBER: ${{ needs.enumerate.outputs.build_number }}
run: |
# Derive the slim per-variant manifests (flasher/v/<env>.json) that the
# firmware fetches for `ota check`/`ota update`, from the just-updated
# config.json (single source of truth), stamping this build's number.
# Then persist the counter so the next run increments from here.
# firmware fetches for `ota check`/`ota update`, from the build output
# in out/ — the assets actually uploaded to the release — stamping this
# build's number. Then persist the counter so the next run increments
# from here.
python3 flasher/scripts/gen-slim-manifests.py \
--config flasher/config.json \
--bin-dir out \
--static-path "$STATIC_PATH" \
--out-dir flasher/v \
--base-version "$FIRMWARE_VERSION" \
--build "$BUILD_NUMBER" \
@@ -283,9 +298,9 @@ jobs:
- name: Commit & Push Flasher Config
working-directory: flasher
run: |
# Stage everything: update-firmware.py edits config.json in place, but the
# slim manifests (v/*.json) and the build counter can be NEW files, which
# `commit -am` would miss — so add -A and check the staged diff.
# Stage everything: the slim manifests (v/*.json) and the build counter
# can be NEW files, which `commit -am` would miss — so add -A and check
# the staged diff. (config.json is no longer rewritten per build.)
git add -A
if git diff --cached --quiet; then
echo "No flasher changes to commit."
@@ -294,4 +309,24 @@ jobs:
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
# sync-flasher-content.yml can be pushing the same docs/changelog right
# now (a mixed code+docs commit triggers both), so a non-fast-forward is
# expected rather than fatal: rebase onto the sibling's commit and retry.
# If the rebase leaves nothing, git reports up-to-date and we exit clean.
br=$(git rev-parse --abbrev-ref HEAD)
for attempt in 1 2 3; do
if git push origin "HEAD:$br"; then
exit 0
fi
echo "push rejected (attempt $attempt); rebasing onto origin/$br"
git fetch origin "$br"
# --autostash so a dirty tree can never make the rebase refuse to run;
# nothing unstaged gets published either way.
git rebase --autostash "origin/$br" || {
git rebase --abort || true
echo "::error::flasher rebase conflicted; re-run this workflow to republish"
exit 1
}
done
echo "::error::could not push flasher changes after 3 attempts"
exit 1
+27 -5
View File
@@ -14,16 +14,18 @@ on:
workflow_dispatch:
push:
branches:
- mqtt-bridge-implementation-flex
- observer-firmware
paths:
- '**.md'
- 'docs/**'
- 'scripts/gen_changelog.py'
# Shared with build-observer-firmwares.yml so the two never push to the flasher
# repo concurrently (a mixed code+docs commit can trigger both).
# Own group: serialize doc syncs against each other only. Deliberately NOT shared
# with build-observer-firmwares.yml — a shared group let one workflow cancel the
# other's pending run (a mixed code+docs commit triggers both). Concurrent flasher
# pushes are handled by the rebase-retry in "Commit & Push Flasher Content".
concurrency:
group: flasher-publish
group: flasher-docs-sync
cancel-in-progress: false
jobs:
@@ -73,4 +75,24 @@ jobs:
git config user.name "meshcore-bot"
git config user.email "noreply@gessaman.com"
git commit -am "Sync docs & changelog from MeshCore ${GITHUB_SHA::7}"
git push
# build-observer-firmwares.yml can be pushing the same docs/changelog
# right now (a mixed code+docs commit triggers both), so a non-fast-forward
# is expected rather than fatal: rebase onto its commit and retry. If the
# rebase leaves nothing, git reports up-to-date and we exit clean.
br=$(git rev-parse --abbrev-ref HEAD)
for attempt in 1 2 3; do
if git push origin "HEAD:$br"; then
exit 0
fi
echo "push rejected (attempt $attempt); rebasing onto origin/$br"
git fetch origin "$br"
# --autostash so a dirty tree can never make the rebase refuse to run;
# nothing unstaged gets published either way.
git rebase --autostash "origin/$br" || {
git rebase --abort || true
echo "::error::flasher rebase conflicted; re-run this workflow to resync"
exit 1
}
done
echo "::error::could not push flasher changes after 3 attempts"
exit 1