diff --git a/.github/workflows/build-observer-firmwares-beta.yml b/.github/workflows/build-observer-firmwares-beta.yml index 73c6fd40..ddb89647 100644 --- a/.github/workflows/build-observer-firmwares-beta.yml +++ b/.github/workflows/build-observer-firmwares-beta.yml @@ -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 diff --git a/.github/workflows/build-observer-firmwares.yml b/.github/workflows/build-observer-firmwares.yml index 3c13bf0c..35514591 100644 --- a/.github/workflows/build-observer-firmwares.yml +++ b/.github/workflows/build-observer-firmwares.yml @@ -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/.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 diff --git a/.github/workflows/sync-flasher-content.yml b/.github/workflows/sync-flasher-content.yml index e2bd5033..93ef1b34 100644 --- a/.github/workflows/sync-flasher-content.yml +++ b/.github/workflows/sync-flasher-content.yml @@ -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