release.yml 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. name: Release
  2. on:
  3. push:
  4. branches:
  5. - main
  6. tags:
  7. - '[0-9]*.[0-9]*.[0-9]*'
  8. release:
  9. types:
  10. - published
  11. permissions:
  12. contents: write
  13. jobs:
  14. bump-version-tag:
  15. if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  16. runs-on: ubuntu-latest
  17. steps:
  18. - name: Check out repository
  19. uses: actions/checkout@v4
  20. with:
  21. fetch-depth: 0
  22. - name: Tag next patch version
  23. run: |
  24. set -eu
  25. git fetch --force --tags origin
  26. SEMVER_RE='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
  27. if git tag --points-at "$GITHUB_SHA" | grep -Eq "$SEMVER_RE"; then
  28. echo "semver tag already exists on this commit; skipping bump"
  29. echo "Tag already present on ${GITHUB_SHA}; release build runs from the tag event." >> "$GITHUB_STEP_SUMMARY"
  30. exit 0
  31. fi
  32. LAST="$(
  33. git tag --list \
  34. | grep -E "$SEMVER_RE" \
  35. | sort -V \
  36. | tail -n1 || true
  37. )"
  38. if [ -z "$LAST" ]; then
  39. # Seed semver history at the requested public baseline.
  40. NEXT="0.9.1"
  41. else
  42. MAJOR="${LAST%%.*}"
  43. REST="${LAST#*.}"
  44. MINOR="${REST%%.*}"
  45. PATCH="${LAST##*.}"
  46. NEXT="${MAJOR}.${MINOR}.$((PATCH + 1))"
  47. fi
  48. git tag "$NEXT" "$GITHUB_SHA"
  49. git push origin "$NEXT"
  50. build-release:
  51. if: github.event_name == 'release' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
  52. runs-on: ubuntu-latest
  53. env:
  54. SEMVER_RE: '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
  55. steps:
  56. - name: Check out repository
  57. uses: actions/checkout@v4
  58. with:
  59. fetch-depth: 0
  60. - name: Validate pushed tag is semver
  61. if: github.event_name == 'push'
  62. run: |
  63. echo "${GITHUB_REF_NAME}" | grep -Eq "${SEMVER_RE}"
  64. - name: Resolve release version
  65. id: release_version
  66. run: |
  67. if [ "${{ github.event_name }}" = "release" ]; then
  68. VERSION="${{ github.event.release.tag_name }}"
  69. else
  70. VERSION="${GITHUB_REF_NAME}"
  71. fi
  72. echo "version=$VERSION" >> "$GITHUB_OUTPUT"
  73. - name: Set up Arduino CLI
  74. uses: arduino/setup-arduino-cli@v2
  75. - name: Set up Python
  76. uses: actions/setup-python@v5
  77. with:
  78. python-version: '3.x'
  79. - name: Install adafruit-nrfutil
  80. run: pip install adafruit-nrfutil
  81. - name: Install Adafruit nRF52 core
  82. run: |
  83. arduino-cli config init --overwrite
  84. arduino-cli config set board_manager.additional_urls https://adafruit.github.io/arduino-board-index/package_adafruit_index.json
  85. arduino-cli core update-index
  86. arduino-cli core install adafruit:nrf52
  87. - name: Generate version header
  88. env:
  89. OPK_BUILD_VERSION: ${{ steps.release_version.outputs.version }}
  90. run: ./gen_version.sh
  91. - name: Compile OpenPuck
  92. run: |
  93. mkdir -p build/openpuck build/cache/openpuck
  94. arduino-cli compile \
  95. -b adafruit:nrf52:feather52840 \
  96. --clean \
  97. --build-path build/cache/openpuck \
  98. --output-dir build/openpuck \
  99. --build-property "build.extra_flags=-DNRF52840_XXAA {build.flags.usb} -DCFG_TUD_HID=4 -DCFG_TUD_TASK_QUEUE_SZ=64 -DCFG_TUD_VENDOR_TX_BUFSIZE=256" \
  100. OpenPuck
  101. - name: Generate UF2 (standard)
  102. run: ./gen_uf2.sh build/openpuck/OpenPuck.ino.hex build/openpuck/OpenPuck.ino.uf2
  103. - name: Compile ReversePuckFirmware
  104. run: |
  105. mkdir -p build/reversepuck build/cache/reversepuck
  106. arduino-cli compile \
  107. -b adafruit:nrf52:feather52840 \
  108. --clean \
  109. --build-path build/cache/reversepuck \
  110. --output-dir build/reversepuck \
  111. --build-property "build.extra_flags=-DNRF52840_XXAA {build.flags.usb}" \
  112. ReversePuckFirmware
  113. - name: Generate UF2 (ReversePuckFirmware)
  114. run: ./gen_uf2.sh build/reversepuck/ReversePuckFirmware.ino.hex build/reversepuck/ReversePuckFirmware.ino.uf2
  115. - name: Compile recovery build
  116. run: |
  117. mkdir -p build/openpuck-recovery build/cache/openpuck-recovery
  118. arduino-cli compile \
  119. -b adafruit:nrf52:feather52840 \
  120. --clean \
  121. --build-path build/cache/openpuck-recovery \
  122. --output-dir build/openpuck-recovery \
  123. --build-property "build.extra_flags=-DNRF52840_XXAA {build.flags.usb} -DCFG_TUD_HID=4 -DCFG_TUD_TASK_QUEUE_SZ=64 -DCFG_TUD_VENDOR_TX_BUFSIZE=256 -DOPK_FACTORY_RESET=1" \
  124. OpenPuck
  125. - name: Generate UF2 (recovery)
  126. run: ./gen_uf2.sh build/openpuck-recovery/OpenPuck.ino.hex build/openpuck-recovery/OpenPuck.ino.uf2
  127. - name: Collect artifacts
  128. env:
  129. VERSION: ${{ steps.release_version.outputs.version }}
  130. run: |
  131. mkdir -p release_assets
  132. find build/openpuck build/openpuck-recovery \
  133. -maxdepth 1 -type f \
  134. \( -name '*.uf2' -o -name '*.hex' \) \
  135. -print | while read -r f; do
  136. # Keep only the extension; the arduino base name (OpenPuck.ino.*)
  137. # is redundant once we prefix with OpenPuck-<version>-<flavor>.
  138. ext="${f##*.}"
  139. dir="$(basename "$(dirname "$f")")"
  140. if [ "$dir" = "openpuck-recovery" ]; then
  141. flavor="factory-reset"
  142. else
  143. flavor="standard"
  144. fi
  145. cp "$f" "release_assets/OpenPuck-${VERSION}-${flavor}.${ext}"
  146. done
  147. find build/reversepuck \
  148. -maxdepth 1 -type f \
  149. \( -name '*.uf2' -o -name '*.hex' \) \
  150. -print | while read -r f; do
  151. ext="${f##*.}"
  152. cp "$f" "release_assets/ReversePuckFirmware-${VERSION}.${ext}"
  153. done
  154. test -n "$(find release_assets -maxdepth 1 -type f -print -quit)"
  155. ls -la release_assets
  156. - name: Create GitHub Release
  157. uses: softprops/action-gh-release@v2
  158. with:
  159. tag_name: ${{ steps.release_version.outputs.version }}
  160. name: ${{ format('OpenPuck {0}', steps.release_version.outputs.version) }}
  161. prerelease: false
  162. files: release_assets/*
  163. body: |
  164. OpenPuck Firmware Release
  165. Version: ${{ steps.release_version.outputs.version }}
  166. Commit: ${{ github.sha }}
  167. ### Files included:
  168. - `OpenPuck-*-standard.*` — puck firmware (standard)
  169. - `OpenPuck-*-factory-reset.*` — puck firmware (factory reset)
  170. - `ReversePuckFirmware-*.*` — reverse-puck firmware (controller emulator)
  171. - `.uf2` files for drag-and-drop flashing (BOOT/UF2 drive)
  172. - `.hex` files for direct flashing workflows
  173. env:
  174. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  175. - name: Mirror OpenPuck UF2s to the firmware branch
  176. # The WebUSB panel's "update from a release" flow downloads these in-browser. GitHub's release-asset
  177. # CDN (release-assets.githubusercontent.com) sends no CORS headers, so browsers cannot fetch release
  178. # assets directly; raw.githubusercontent.com IS CORS-clean. Mirror the puck .uf2s onto the orphan
  179. # `firmware` branch, where the panel fetches raw.githubusercontent.com/<repo>/firmware/<asset-name>.
  180. # Isolated clone in a temp dir so the build tree (untracked artifacts, regenerated version header)
  181. # can't leak into the mirror commit.
  182. env:
  183. VERSION: ${{ steps.release_version.outputs.version }}
  184. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  185. run: |
  186. set -eu
  187. MIRROR="$(mktemp -d)/mirror"
  188. REPO="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
  189. if git ls-remote --exit-code "$REPO" refs/heads/firmware >/dev/null 2>&1; then
  190. git clone --depth 1 --branch firmware "$REPO" "$MIRROR"
  191. else
  192. git init "$MIRROR"
  193. git -C "$MIRROR" checkout --orphan firmware
  194. git -C "$MIRROR" remote add origin "$REPO"
  195. printf '%s\n' \
  196. '# firmware mirror' '' \
  197. 'OpenPuck release `.uf2` files, mirrored from the GitHub release assets because' \
  198. 'raw.githubusercontent.com serves files WITH CORS headers while the release-asset CDN does' \
  199. 'not — this is what lets the WebUSB config panel download and flash releases in-browser.' \
  200. 'Maintained automatically by `.github/workflows/release.yml`; do not edit by hand.' \
  201. > "$MIRROR/README.md"
  202. fi
  203. cp release_assets/OpenPuck-"${VERSION}"-*.uf2 "$MIRROR"/
  204. # manifest.json: name -> {panelUpdate}. panelUpdate = the image embeds the FWUP_TAG capability
  205. # string (fw_update.cpp), i.e. a puck running it can be updated FROM the panel. The panel badges
  206. # releases whose images can't ("no panel updates") before the user flashes one.
  207. ( cd "$MIRROR" && python3 - <<'PY'
  208. import json, os, glob
  209. m = {}
  210. if os.path.exists("manifest.json"):
  211. m = json.load(open("manifest.json"))
  212. for f in sorted(glob.glob("OpenPuck-*.uf2")):
  213. m[f] = {"panelUpdate": b"OPK-FWUP-v1" in open(f, "rb").read()}
  214. json.dump(m, open("manifest.json", "w"), indent=1, sort_keys=True)
  215. PY
  216. )
  217. git -C "$MIRROR" config user.name "github-actions[bot]"
  218. git -C "$MIRROR" config user.email "github-actions[bot]@users.noreply.github.com"
  219. git -C "$MIRROR" add -A
  220. git -C "$MIRROR" commit -m "mirror OpenPuck ${VERSION} uf2s for the panel updater"
  221. git -C "$MIRROR" push origin firmware