mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-18 19:31:56 -04:00
Increase the cache-control age limits for R2 uploads. Releases: Cache for 1 day in browser, 1 month on CDN Nightly: Cache for 1 hour in browser, 1 day on CDN Add metadata to R2 uploads so we can track which commit and which GitHub Actions run produced the upload.
857 lines
33 KiB
YAML
857 lines
33 KiB
YAML
name: CI
|
|
concurrency:
|
|
group: ci-${{ github.head_ref || github.run_id }}
|
|
cancel-in-progress: true
|
|
on:
|
|
# The merge queue is the pre-merge gate for the protected branches (master,
|
|
# develop): a merge_group run validates the merged result before code lands. For
|
|
# now PRs and merge_group runs build the same narrowed board subset (--level pr);
|
|
# see the setup job. push still runs the full matrix on master/develop (post-merge)
|
|
# as well as on the event/* and feature/* branches, which have no merge queue.
|
|
merge_group:
|
|
types: [checks_requested]
|
|
branches:
|
|
- master
|
|
- develop
|
|
|
|
push:
|
|
branches:
|
|
- master
|
|
- develop
|
|
- event/*
|
|
- feature/*
|
|
paths-ignore:
|
|
- "**.md"
|
|
- version.properties
|
|
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
- develop
|
|
- event/*
|
|
- feature/*
|
|
paths-ignore:
|
|
- "**.md"
|
|
#- "**.yml"
|
|
|
|
schedule:
|
|
# Nightly develop build, published to meshtastic.github.io firmware-nightly/ and to the
|
|
# meshtastic-firmware-nightly R2 bucket (no GitHub release).
|
|
# Scheduled runs execute on the default branch (develop). 07:00 UTC avoids the 00:00 tests
|
|
# and 02:00 daily_packaging crons.
|
|
- cron: 0 7 * * * # Nightly develop build/publish (default branch is develop)
|
|
|
|
workflow_dispatch:
|
|
inputs:
|
|
# trunk-ignore(checkov/CKV_GHA_7): intentional manual-test switch for the nightly publish path
|
|
nightly:
|
|
description: "Nightly mode: build + publish develop to github.io firmware-nightly/ and R2 (skips creating a GitHub release)"
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions: read-all
|
|
|
|
jobs:
|
|
setup:
|
|
runs-on: ubuntu-24.04
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
# Needed to diff against the base branch for newly added variants.
|
|
fetch-depth: 0
|
|
- uses: actions/setup-python@v6
|
|
with:
|
|
python-version: 3.x
|
|
cache: pip
|
|
- run: pip install -U platformio
|
|
- name: Generate matrix
|
|
id: jsonStep
|
|
env:
|
|
BASE_REF: ${{ github.base_ref }}
|
|
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
|
|
run: |
|
|
# A new board is 'release' and gets no CI until after merge, so force-build the
|
|
# first env of each ADDED variant config. A new env in an existing one does not count.
|
|
DIFF_BASE=""
|
|
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
|
|
# checkout above already fetched every branch (fetch-depth: 0), so the base ref
|
|
# is local. Do not re-fetch it with --depth=1: that grafts the fetched tip as
|
|
# parentless, and merge-base then finds no common commit whenever the base
|
|
# branch has moved past this merge commit, which is every re-run of an older run.
|
|
DIFF_BASE=$(git merge-base "origin/$BASE_REF" HEAD)
|
|
elif [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then
|
|
DIFF_BASE="$MERGE_GROUP_BASE_SHA"
|
|
fi
|
|
ADDED_ARGS=()
|
|
if [[ -n "$DIFF_BASE" ]]; then
|
|
# Assign rather than pipe: a failing diff must abort the step under 'set -e',
|
|
# not silently yield an empty list and drop the new board from the matrix.
|
|
ADDED_CONFIGS=$(git diff --name-only --diff-filter=A \
|
|
"$DIFF_BASE" HEAD -- 'variants/**/platformio.ini')
|
|
while IFS= read -r cfg; do
|
|
[[ -n "$cfg" ]] && ADDED_ARGS+=(--added-config "$cfg")
|
|
done <<<"$ADDED_CONFIGS"
|
|
fi
|
|
# PRs and (for now) merge_group builds use the narrowed --level pr board
|
|
# subset. Full-matrix builds run on push / schedule / workflow_dispatch.
|
|
if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "merge_group" ]]; then
|
|
TARGETS=$(./bin/generate_ci_matrix.py all --level pr "${ADDED_ARGS[@]}")
|
|
else
|
|
TARGETS=$(./bin/generate_ci_matrix.py all)
|
|
fi
|
|
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
|
|
echo "all=$TARGETS" >> $GITHUB_OUTPUT
|
|
echo "$TARGETS" >> $GITHUB_STEP_SUMMARY
|
|
outputs:
|
|
all: ${{ steps.jsonStep.outputs.all }}
|
|
|
|
version:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- name: Get release version string
|
|
run: |
|
|
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
|
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
|
|
id: version
|
|
env:
|
|
BUILD_LOCATION: local
|
|
outputs:
|
|
long: ${{ steps.version.outputs.long }}
|
|
deb: ${{ steps.version.outputs.deb }}
|
|
|
|
check:
|
|
needs: setup
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
check: ${{ fromJson(needs.setup.outputs.all) }}
|
|
# Runs on GitHub-hosted runners so checks don't compete with builds for the
|
|
# self-hosted 'arctastic' pool (which builds use).
|
|
runs-on: ubuntu-latest
|
|
# Only run checks for PRs and Merge Queue runs.
|
|
if: contains(fromJSON('["pull_request", "merge_group"]'), github.event_name)
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
submodules: recursive
|
|
- name: Check ${{ matrix.check.board }}
|
|
uses: meshtastic/gh-action-firmware@main
|
|
with:
|
|
pio_platform: ${{ matrix.check.platform }}
|
|
pio_env: ${{ matrix.check.board }}
|
|
pio_target: check
|
|
|
|
build:
|
|
needs: [setup, version]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
build: ${{ fromJson(needs.setup.outputs.all) }}
|
|
uses: ./.github/workflows/build_firmware.yml
|
|
with:
|
|
version: ${{ needs.version.outputs.long }}
|
|
pio_env: ${{ matrix.build.board }}
|
|
platform: ${{ matrix.build.platform }}
|
|
|
|
build-debian-src:
|
|
if: github.repository_owner == 'meshtastic' && github.event_name != 'schedule' && github.event.inputs.nightly != 'true'
|
|
uses: ./.github/workflows/build_debian_src.yml
|
|
with:
|
|
series: UNRELEASED
|
|
build_location: local
|
|
secrets: inherit
|
|
|
|
MacOS:
|
|
if: github.event_name != 'schedule' && github.event.inputs.nightly != 'true' && !contains(github.ref_name, 'event/')
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
macos_ver:
|
|
- "26" # ARM64
|
|
# - '26-intel' # x86_64
|
|
- "15" # ARM64
|
|
# - '15-intel' # x86_64
|
|
uses: ./.github/workflows/build_macos_bin.yml
|
|
with:
|
|
macos_ver: ${{ matrix.macos_ver }}
|
|
# secrets: inherit
|
|
|
|
Windows:
|
|
if: github.event_name != 'schedule' && github.event.inputs.nightly != 'true' && !contains(github.ref_name, 'event/')
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
windows_ver:
|
|
- "2025" # x86_64
|
|
uses: ./.github/workflows/build_windows_bin.yml
|
|
with:
|
|
windows_ver: ${{ matrix.windows_ver }}
|
|
# secrets: inherit
|
|
|
|
package-pio-deps-native-tft:
|
|
if: github.repository_owner == 'meshtastic' && github.event_name == 'workflow_dispatch'
|
|
uses: ./.github/workflows/package_pio_deps.yml
|
|
with:
|
|
pio_env: native-tft
|
|
secrets: inherit
|
|
|
|
test-native:
|
|
if: github.repository_owner == 'meshtastic' && github.event_name != 'schedule' && github.event.inputs.nightly != 'true' && !contains(github.ref_name, 'event/')
|
|
permissions: # Needed for dorny/test-reporter.
|
|
contents: read
|
|
actions: read
|
|
checks: write
|
|
uses: ./.github/workflows/test_native.yml
|
|
|
|
build-wasm:
|
|
if: github.event_name != 'schedule' && github.event.inputs.nightly != 'true' && !contains(github.ref_name, 'event/')
|
|
# Build the WebAssembly portduino node ([env:native-wasm]) as part of normal CI,
|
|
# like the other platforms. It's a dedicated job (not a row in the `build`
|
|
# matrix) because its artifact is meshnode.{mjs,wasm} - not a flashable
|
|
# .bin/.uf2/.hex - and it needs the Emscripten SDK; board_level=extra keeps
|
|
# it out of generate_ci_matrix.py.
|
|
uses: ./.github/workflows/build_portduino_wasm.yml
|
|
|
|
docker:
|
|
if: github.event_name != 'schedule' && github.event.inputs.nightly != 'true' && !contains(github.ref_name, 'event/')
|
|
permissions: # Needed for pushing to GHCR.
|
|
contents: read
|
|
packages: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
distro: [debian, alpine]
|
|
platform: [linux/arm64]
|
|
pio_env: [native-tft]
|
|
uses: ./.github/workflows/docker_build.yml
|
|
with:
|
|
distro: ${{ matrix.distro }}
|
|
platform: ${{ matrix.platform }}
|
|
runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
|
|
pio_env: ${{ matrix.pio_env }}
|
|
push: false
|
|
|
|
# Single, stable status check for branch protection.
|
|
# Always runs and passes only if every build and check that ran succeeded.
|
|
ci-gate:
|
|
# Consumed in PRs and Merge Queue
|
|
if: contains(fromJSON('["pull_request", "merge_group"]'), github.event_name) && !cancelled()
|
|
needs: [build, check]
|
|
runs-on: ubuntu-slim
|
|
steps:
|
|
- name: Verify build and check results
|
|
run: |
|
|
echo "build=${{ needs.build.result }} check=${{ needs.check.result }}"
|
|
if [[ "${{ needs.build.result }}" != "success" ]]; then
|
|
echo "::error::One or more firmware builds failed, were cancelled, or did not run."
|
|
exit 1
|
|
fi
|
|
case "${{ needs.check.result }}" in
|
|
success | skipped) ;;
|
|
*)
|
|
echo "::error::One or more checks failed or were cancelled (${{ needs.check.result }})."
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
gather-artifacts:
|
|
# Only run on Release (workflow_dispatch) and Nightly (schedule) runs, not on PRs or merge_group runs.
|
|
if: github.repository_owner == 'meshtastic' && contains(fromJSON('["workflow_dispatch", "schedule"]'), github.event_name)
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
arch:
|
|
- esp32
|
|
- esp32s3
|
|
- esp32c3
|
|
- esp32c6
|
|
- nrf52840
|
|
- rp2040
|
|
- rp2350
|
|
- stm32
|
|
runs-on: ubuntu-latest
|
|
needs: [version, build]
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7
|
|
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: ./
|
|
pattern: firmware-${{matrix.arch}}-*
|
|
merge-multiple: true
|
|
|
|
- name: Display structure of downloaded files
|
|
run: ls -R
|
|
|
|
- name: Repackage in single firmware zip
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
|
overwrite: true
|
|
path: |
|
|
./firmware-*.mt.json
|
|
./firmware-*.bin
|
|
./firmware-*.uf2
|
|
./firmware-*.hex
|
|
./firmware-*.zip
|
|
./device-*.sh
|
|
./device-*.bat
|
|
./littlefs-*.bin
|
|
./bleota*bin
|
|
./mt-*-ota.bin
|
|
./Meshtastic_nRF52_factory_erase*.uf2
|
|
retention-days: 30
|
|
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
|
merge-multiple: true
|
|
path: ./output
|
|
|
|
# For diagnostics
|
|
- name: Show artifacts
|
|
run: ls -lR
|
|
|
|
- name: Device scripts permissions
|
|
run: |
|
|
chmod +x ./output/device-install.sh || true
|
|
chmod +x ./output/device-update.sh || true
|
|
|
|
- name: Zip firmware
|
|
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
|
|
|
- name: Repackage in single elfs zip
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
|
overwrite: true
|
|
path: ./*.elf
|
|
retention-days: 30
|
|
|
|
firmware-size-report:
|
|
if: github.repository_owner == 'meshtastic' && github.event_name != 'schedule' && github.event.inputs.nightly != 'true'
|
|
continue-on-error: true
|
|
permissions:
|
|
contents: read
|
|
actions: read
|
|
runs-on: ubuntu-latest
|
|
needs: [build]
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Download current manifests
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
path: ./manifests/
|
|
pattern: manifest-*
|
|
merge-multiple: true
|
|
|
|
- name: Collect current firmware sizes
|
|
run: python3 bin/collect_sizes.py ./manifests/ ./current-sizes.json
|
|
|
|
- name: Upload size report artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: firmware-sizes-${{ github.sha }}
|
|
overwrite: true
|
|
path: ./current-sizes.json
|
|
retention-days: 90
|
|
|
|
- name: Download baseline sizes from develop
|
|
if: github.event_name == 'pull_request'
|
|
continue-on-error: true
|
|
id: baseline-develop
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
RUN_ID=$(gh run list -R "${{ github.repository }}" \
|
|
--workflow CI --branch develop --status success \
|
|
--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-")) | 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/current-sizes.json" ./develop-sizes.json
|
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
else
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Download baseline sizes from master
|
|
if: github.event_name == 'pull_request'
|
|
continue-on-error: true
|
|
id: baseline-master
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
RUN_ID=$(gh run list -R "${{ github.repository }}" \
|
|
--workflow CI --branch master --status success \
|
|
--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-")) | 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/current-sizes.json" ./master-sizes.json
|
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
else
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Generate size comparison report
|
|
if: github.event_name == 'pull_request'
|
|
id: report
|
|
run: |
|
|
ARGS="./current-sizes.json --budgets bin/ram_budgets.json"
|
|
if [ -f ./develop-sizes.json ]; then
|
|
ARGS="$ARGS --baseline develop:./develop-sizes.json"
|
|
fi
|
|
if [ -f ./master-sizes.json ]; then
|
|
ARGS="$ARGS --baseline master:./master-sizes.json"
|
|
fi
|
|
REPORT=$(python3 bin/size_report.py $ARGS)
|
|
if [ -z "$REPORT" ]; then
|
|
echo "has_report=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "has_report=true" >> "$GITHUB_OUTPUT"
|
|
{
|
|
echo '<!-- firmware-size-report -->'
|
|
echo '# Firmware Size Report'
|
|
echo ''
|
|
echo "$REPORT"
|
|
echo ''
|
|
echo '---'
|
|
echo "*Updated for ${{ github.sha }}*"
|
|
} > ./size-report.md
|
|
cat ./size-report.md >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
|
|
- name: Save PR number
|
|
if: github.event_name == 'pull_request' && steps.report.outputs.has_report == 'true'
|
|
run: echo "${{ github.event.pull_request.number }}" > ./pr-number.txt
|
|
|
|
- name: Upload size report
|
|
if: github.event_name == 'pull_request' && steps.report.outputs.has_report == 'true'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: size-report
|
|
path: |
|
|
./size-report.md
|
|
./pr-number.txt
|
|
retention-days: 5
|
|
|
|
# RAM/flash guardrails: fails CI when an env listed in bin/ram_budgets.json
|
|
# exceeds its static RAM (.data+.bss) or flash budget. Kept separate from
|
|
# firmware-size-report, which is informational and continue-on-error.
|
|
size-budget-gate:
|
|
if: github.event_name != 'schedule' && github.event.inputs.nightly != 'true'
|
|
permissions:
|
|
contents: read
|
|
actions: read
|
|
runs-on: ubuntu-latest
|
|
needs: [build]
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
# No continue-on-error / empty-dir fallback: the gate must fail closed when
|
|
# the data it enforces on cannot be fetched (size_report.py additionally
|
|
# fails on missing budgeted envs under --enforce-budgets).
|
|
- name: Download current manifests
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
path: ./manifests/
|
|
pattern: manifest-*
|
|
merge-multiple: true
|
|
|
|
- name: Collect current firmware sizes
|
|
run: python3 bin/collect_sizes.py ./manifests/ ./current-sizes.json
|
|
|
|
- name: Enforce RAM/flash budgets
|
|
run: python3 bin/size_report.py ./current-sizes.json --budgets bin/ram_budgets.json --enforce-budgets
|
|
|
|
release-artifacts:
|
|
permissions: # Needed for 'gh release upload'.
|
|
contents: write
|
|
runs-on: ubuntu-latest
|
|
if: github.repository_owner == 'meshtastic' && github.event_name == 'workflow_dispatch' && github.event.inputs.nightly != 'true'
|
|
outputs:
|
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
needs:
|
|
- setup
|
|
- version
|
|
- gather-artifacts
|
|
- build-debian-src
|
|
- package-pio-deps-native-tft
|
|
# - MacOS
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: 3.x
|
|
|
|
- name: Generate release notes
|
|
id: release_notes
|
|
run: |
|
|
chmod +x ./bin/generate_release_notes.py
|
|
NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }} --compare-ref HEAD 2>release_notes.log)
|
|
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
|
echo "$NOTES" >> $GITHUB_OUTPUT
|
|
echo "EOF" >> $GITHUB_OUTPUT
|
|
echo "### Release note range" >> $GITHUB_STEP_SUMMARY
|
|
cat release_notes.log >> $GITHUB_STEP_SUMMARY
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Create release
|
|
uses: softprops/action-gh-release@v3
|
|
id: create_release
|
|
with:
|
|
draft: true
|
|
prerelease: true
|
|
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
|
|
tag_name: v${{ needs.version.outputs.long }}
|
|
target_commitish: ${{ github.sha }}
|
|
body: ${{ steps.release_notes.outputs.notes }}
|
|
|
|
- name: Download source deb
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
|
|
merge-multiple: true
|
|
path: ./output/debian-src
|
|
|
|
- name: Download `native-tft` pio deps
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
|
merge-multiple: true
|
|
path: ./output/pio-deps-native-tft
|
|
|
|
- name: Zip Linux sources
|
|
working-directory: output
|
|
run: |
|
|
zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src
|
|
zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft
|
|
|
|
# For diagnostics
|
|
- name: Display structure of downloaded files
|
|
run: ls -lR
|
|
|
|
- name: Generate Release manifest
|
|
run: |
|
|
jq -n --arg ver "${{ needs.version.outputs.long }}" --argjson targets ${{ toJson(needs.setup.outputs.all) }} '{
|
|
"version": $ver,
|
|
"targets": $targets
|
|
}' > firmware-${{ needs.version.outputs.long }}.json
|
|
|
|
- name: Save Release manifest artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: manifest-${{ needs.version.outputs.long }}
|
|
overwrite: true
|
|
path: firmware-${{ needs.version.outputs.long }}.json
|
|
|
|
- name: Add sources to GitHub Release
|
|
# Only run when targeting master branch with workflow_dispatch
|
|
if: github.ref_name == 'master'
|
|
run: |
|
|
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{ needs.version.outputs.long }}.json
|
|
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip
|
|
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
release-firmware:
|
|
permissions: # Needed for 'gh release upload'.
|
|
contents: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
arch:
|
|
- esp32
|
|
- esp32s3
|
|
- esp32c3
|
|
- esp32c6
|
|
- nrf52840
|
|
- rp2040
|
|
- rp2350
|
|
- stm32
|
|
runs-on: ubuntu-latest
|
|
if: github.repository_owner == 'meshtastic' && github.event_name == 'workflow_dispatch' && github.event.inputs.nightly != 'true'
|
|
needs: [release-artifacts, version]
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: 3.x
|
|
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
|
merge-multiple: true
|
|
path: ./output
|
|
|
|
- name: Display structure of downloaded files
|
|
run: ls -lR
|
|
|
|
- name: Device scripts permissions
|
|
run: |
|
|
chmod +x ./output/device-install.sh || true
|
|
chmod +x ./output/device-update.sh || true
|
|
|
|
- name: Zip firmware
|
|
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
|
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
|
merge-multiple: true
|
|
path: ./elfs
|
|
|
|
- name: Zip debug elfs
|
|
run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs
|
|
|
|
# For diagnostics
|
|
- name: Display structure of downloaded files
|
|
run: ls -lR
|
|
|
|
- name: Add bins and debug elfs to GitHub Release
|
|
# Only run when targeting master branch with workflow_dispatch
|
|
if: github.ref_name == 'master'
|
|
run: |
|
|
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
|
gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
publish-firmware:
|
|
runs-on: ubuntu-24.04
|
|
if: github.repository_owner == 'meshtastic' && github.event_name == 'workflow_dispatch' && github.event.inputs.nightly != 'true'
|
|
needs: [release-firmware, version]
|
|
env:
|
|
targets: |-
|
|
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
|
|
r2_bucket: meshtastic-firmware-release
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: 3.x
|
|
|
|
- name: Get firmware artifacts
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
|
merge-multiple: true
|
|
path: ./publish
|
|
|
|
- name: Get manifest artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: manifest-${{ needs.version.outputs.long }}
|
|
path: ./publish
|
|
|
|
- name: Generate release notes
|
|
run: |
|
|
chmod +x ./bin/generate_release_notes.py
|
|
./bin/generate_release_notes.py ${{ needs.version.outputs.long }} --compare-ref HEAD > ./publish/release_notes.md
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Publish firmware to meshtastic.github.io
|
|
uses: peaceiris/actions-gh-pages@v4
|
|
env:
|
|
# On event/* branches, use the event name as the destination prefix
|
|
DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }}
|
|
with:
|
|
deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }}
|
|
external_repository: meshtastic/meshtastic.github.io
|
|
publish_branch: master
|
|
publish_dir: ./publish
|
|
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }}
|
|
keep_files: true
|
|
user_name: github-actions[bot]
|
|
user_email: github-actions[bot]@users.noreply.github.com
|
|
commit_message: ${{ needs.version.outputs.long }}
|
|
enable_jekyll: true
|
|
|
|
# Mirror the same staged directory to Cloudflare R2, under a version prefix
|
|
# at the bucket root. Additive (no --delete), matching keep_files:true
|
|
# above: release_channels.yml later publishes an updated release_notes.md
|
|
# into this same prefix, and a --delete sync on a re-run would remove it.
|
|
- name: Publish firmware to Cloudflare R2
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
# R2 is single-region; the S3 API still requires a region to be set.
|
|
AWS_DEFAULT_REGION: auto
|
|
# Cloudflare's documented aws-cli settings for R2. Left at the AWS
|
|
# default, CLI >= 2.23 attaches CRC32 request checksums that R2 can
|
|
# reject, so both of these must stay at when_required.
|
|
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
|
|
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
|
|
R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
|
# Same event/* prefixing as the github.io publish above.
|
|
DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }}
|
|
VERSION: ${{ needs.version.outputs.long }}
|
|
run: |
|
|
set -euo pipefail
|
|
aws --version
|
|
# Cache for 1 day in browser, 1 month on CDN.
|
|
aws s3 sync ./publish "s3://${r2_bucket}/${DEST_PREFIX}${VERSION}/" \
|
|
--endpoint-url "$R2_ENDPOINT" \
|
|
--no-progress \
|
|
--metadata "commit=${{ github.sha }},run=${{ github.run_id }},version=${VERSION}" \
|
|
--cache-control 'public, max-age=86400, s-maxage=2592000'
|
|
|
|
# Nightly publish: refresh the single, stable firmware-nightly/ folder on
|
|
# meshtastic.github.io, and the root of the meshtastic-firmware-nightly R2
|
|
# bucket, with the current develop build. Runs on the cron schedule (or a manual
|
|
# nightly=true dispatch) and never creates a GitHub release. The folder's
|
|
# release_notes.md is maintained by hand and deliberately left untouched.
|
|
publish-nightly:
|
|
runs-on: ubuntu-24.04
|
|
if: github.repository_owner == 'meshtastic' && (github.event_name == 'schedule' || github.event.inputs.nightly == 'true')
|
|
needs: [setup, version, gather-artifacts]
|
|
env:
|
|
targets: |-
|
|
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
|
|
r2_bucket: meshtastic-firmware-nightly
|
|
steps:
|
|
- name: Get firmware artifacts
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
|
merge-multiple: true
|
|
path: ./stage
|
|
|
|
- name: Generate Release manifest
|
|
run: |
|
|
jq -n --arg ver "${{ needs.version.outputs.long }}" --argjson targets ${{ toJson(needs.setup.outputs.all) }} '{
|
|
"version": $ver,
|
|
"targets": $targets
|
|
}' > ./stage/firmware-${{ needs.version.outputs.long }}.json
|
|
|
|
- name: Generate nightly pointer
|
|
run: |
|
|
jq -n \
|
|
--arg ver "${{ needs.version.outputs.long }}" \
|
|
--arg sha "${{ github.sha }}" \
|
|
'{version: $ver, id: ("v" + $ver), title: ("Meshtastic Firmware " + $ver + " Nightly"), commit: $sha}' \
|
|
> ./stage/index.json
|
|
|
|
- name: Preserve manually-maintained release notes
|
|
# firmware-nightly/release_notes.md is edited by hand. Carry the current
|
|
# copy into ./stage so the keep_files:false publish (which refreshes the
|
|
# folder and clears stale nightly binaries) does not drop it. Seed a
|
|
# placeholder on the first run (404); fail closed on any other error so a
|
|
# transient fetch failure never clobbers the notes.
|
|
run: |
|
|
set -euo pipefail
|
|
url=https://raw.githubusercontent.com/meshtastic/meshtastic.github.io/master/firmware-nightly/release_notes.md
|
|
code=$(curl -sSL -o ./stage/release_notes.md -w '%{http_code}' --retry 5 --retry-all-errors "$url" || echo 000)
|
|
if [ "$code" = "200" ]; then
|
|
echo "Preserved existing release_notes.md"
|
|
elif [ "$code" = "404" ]; then
|
|
echo "No existing release_notes.md; seeding placeholder"
|
|
printf '# Nightly (develop)\n\nAutomated nightly build from the `develop` branch. Edit these notes by hand.\n' > ./stage/release_notes.md
|
|
else
|
|
echo "Unexpected HTTP $code fetching release_notes.md; refusing to publish to avoid clobbering manual notes"
|
|
exit 1
|
|
fi
|
|
|
|
# For diagnostics
|
|
- name: Display structure of files to publish
|
|
run: ls -lR ./stage
|
|
|
|
- name: Verify the staged nightly is not empty
|
|
# Both publishes below refresh their destination in place (keep_files:false
|
|
# for github.io, --delete for the R2 sync), so an empty ./stage would clear
|
|
# the live nightly rather than replace it. A failed or pattern-mismatched
|
|
# artifact download is the way that happens, so fail closed here instead.
|
|
run: |
|
|
set -euo pipefail
|
|
images=$(find ./stage -type f \
|
|
\( -name 'firmware-*.bin' -o -name 'firmware-*.uf2' -o -name 'firmware-*.hex' \) | wc -l)
|
|
echo "Staged firmware images: $images"
|
|
if [ "$images" -eq 0 ]; then
|
|
echo "::error::No firmware images in ./stage; refusing to publish an empty nightly."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Publish nightly to meshtastic.github.io
|
|
uses: peaceiris/actions-gh-pages@v4
|
|
with:
|
|
deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }}
|
|
external_repository: meshtastic/meshtastic.github.io
|
|
publish_branch: master
|
|
publish_dir: ./stage
|
|
# keep_files:false is scoped to destination_dir, so this refreshes only
|
|
# firmware-nightly/ (clearing stale nightly binaries) while sibling
|
|
# release folders stay untouched; release_notes.md is carried in above.
|
|
destination_dir: firmware-nightly
|
|
keep_files: false
|
|
user_name: github-actions[bot]
|
|
user_email: github-actions[bot]@users.noreply.github.com
|
|
commit_message: Nightly ${{ needs.version.outputs.long }}
|
|
enable_jekyll: true
|
|
|
|
# Mirror the same staged directory to Cloudflare R2. --delete at the bucket
|
|
# root is the counterpart of keep_files:false above - it clears stale nightly
|
|
# binaries - and is in scope for the whole bucket because this bucket holds
|
|
# nothing but the nightly build. release_notes.md is carried into ./stage by
|
|
# the step above, so the sync preserves it here too.
|
|
- name: Publish nightly to Cloudflare R2
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
# R2 is single-region; the S3 API still requires a region to be set.
|
|
AWS_DEFAULT_REGION: auto
|
|
# Cloudflare's documented aws-cli settings for R2. Left at the AWS
|
|
# default, CLI >= 2.23 attaches CRC32 request checksums that R2 can
|
|
# reject, so both of these must stay at when_required.
|
|
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
|
|
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
|
|
R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
|
run: |
|
|
set -euo pipefail
|
|
aws --version
|
|
# Cache for 1 hour in browser, 1 day on CDN.
|
|
aws s3 sync ./stage "s3://${r2_bucket}/" \
|
|
--endpoint-url "$R2_ENDPOINT" \
|
|
--no-progress \
|
|
--delete \
|
|
--exclude 'index.json' \
|
|
--metadata "commit=${{ github.sha }},run=${{ github.run_id }}" \
|
|
--cache-control 'public, max-age=3600, s-maxage=86400'
|
|
# index.json is the pointer to the current nightly, don't cache it so hard (5 minutes).
|
|
aws s3 cp ./stage/index.json "s3://${r2_bucket}/index.json" \
|
|
--endpoint-url "$R2_ENDPOINT" \
|
|
--no-progress \
|
|
--metadata "commit=${{ github.sha }},run=${{ github.run_id }}" \
|
|
--cache-control 'public, max-age=300'
|