name: Release on: push: branches: - main tags: - '[0-9]*.[0-9]*.[0-9]*' release: types: - published permissions: contents: write jobs: bump-version-tag: if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v7 with: fetch-depth: 0 - name: Tag next patch version run: | set -eu git fetch --force --tags origin SEMVER_RE='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' if git tag --points-at "$GITHUB_SHA" | grep -Eq "$SEMVER_RE"; then echo "semver tag already exists on this commit; skipping bump" echo "Tag already present on ${GITHUB_SHA}; release build runs from the tag event." >> "$GITHUB_STEP_SUMMARY" exit 0 fi LAST="$( git tag --list \ | grep -E "$SEMVER_RE" \ | sort -V \ | tail -n1 || true )" if [ -z "$LAST" ]; then # Seed semver history at the requested public baseline. NEXT="0.9.1" else MAJOR="${LAST%%.*}" REST="${LAST#*.}" MINOR="${REST%%.*}" PATCH="${LAST##*.}" NEXT="${MAJOR}.${MINOR}.$((PATCH + 1))" fi git tag "$NEXT" "$GITHUB_SHA" git push origin "$NEXT" build-release: if: github.event_name == 'release' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) runs-on: ubuntu-latest env: SEMVER_RE: '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' steps: - name: Check out repository uses: actions/checkout@v7 with: fetch-depth: 0 - name: Validate pushed tag is semver if: github.event_name == 'push' run: | echo "${GITHUB_REF_NAME}" | grep -Eq "${SEMVER_RE}" - name: Resolve release version id: release_version run: | if [ "${{ github.event_name }}" = "release" ]; then VERSION="${{ github.event.release.tag_name }}" else VERSION="${GITHUB_REF_NAME}" fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Set up Arduino CLI uses: arduino/setup-arduino-cli@v2 - name: Set up Python uses: actions/setup-python@v7 with: python-version: '3.x' - name: Install adafruit-nrfutil run: pip install adafruit-nrfutil - name: Install Adafruit nRF52 core run: | arduino-cli config init --overwrite arduino-cli config set board_manager.additional_urls https://adafruit.github.io/arduino-board-index/package_adafruit_index.json arduino-cli core update-index arduino-cli core install adafruit:nrf52 - name: Generate version header env: OPK_BUILD_VERSION: ${{ steps.release_version.outputs.version }} run: ./gen_version.sh - name: Compile OpenPuck run: | mkdir -p build/openpuck build/cache/openpuck make build BUILD_PATH=build/cache/openpuck OUTPUT_DIR=build/openpuck - name: Generate UF2 (standard) run: ./gen_uf2.sh build/openpuck/OpenPuck.ino.hex build/openpuck/OpenPuck.ino.uf2 - name: Compile ReversePuckFirmware run: | mkdir -p build/reversepuck build/cache/reversepuck make reversepuck BUILD_PATH=build/cache/reversepuck OUTPUT_DIR=build/reversepuck - name: Generate UF2 (ReversePuckFirmware) run: ./gen_uf2.sh build/reversepuck/ReversePuckFirmware.ino.hex build/reversepuck/ReversePuckFirmware.ino.uf2 - name: Compile recovery build run: | mkdir -p build/openpuck-recovery build/cache/openpuck-recovery make build-recovery BUILD_PATH=build/cache/openpuck-recovery OUTPUT_DIR=build/openpuck-recovery - name: Generate UF2 (recovery) run: ./gen_uf2.sh build/openpuck-recovery/OpenPuck.ino.hex build/openpuck-recovery/OpenPuck.ino.uf2 - name: Collect artifacts env: VERSION: ${{ steps.release_version.outputs.version }} run: | mkdir -p release_assets find build/openpuck build/openpuck-recovery \ -maxdepth 1 -type f \ \( -name '*.uf2' -o -name '*.hex' \) \ -print | while read -r f; do # Keep only the extension; the arduino base name (OpenPuck.ino.*) # is redundant once we prefix with OpenPuck--. ext="${f##*.}" dir="$(basename "$(dirname "$f")")" if [ "$dir" = "openpuck-recovery" ]; then flavor="factory-reset" else flavor="standard" fi cp "$f" "release_assets/OpenPuck-${VERSION}-${flavor}.${ext}" done find build/reversepuck \ -maxdepth 1 -type f \ \( -name '*.uf2' -o -name '*.hex' \) \ -print | while read -r f; do ext="${f##*.}" cp "$f" "release_assets/ReversePuckFirmware-${VERSION}.${ext}" done test -n "$(find release_assets -maxdepth 1 -type f -print -quit)" ls -la release_assets - name: Publish artifacts to the GitHub Release # NEVER rewrite an existing release's notes, title or pre-release flag. This job also runs on # `release: published`, i.e. right after a human wrote release notes in the UI (or marked the # release as a pre-release) -- rewriting the release here would throw that away. So: upload the # assets onto an existing release, and only author a default title/body when we are the ones # creating the release (a bare semver tag push, no release object yet). env: VERSION: ${{ steps.release_version.outputs.version }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu if gh release view "$VERSION" >/dev/null 2>&1; then echo "release $VERSION already exists; uploading assets only (notes/title/pre-release preserved)" gh release upload "$VERSION" release_assets/* --clobber else gh release create "$VERSION" release_assets/* \ --title "OpenPuck $VERSION" \ --notes "OpenPuck Firmware Release Version: ${VERSION} Commit: ${GITHUB_SHA} ### Files included: - \`OpenPuck-*-standard.*\` — puck firmware (standard) - \`OpenPuck-*-factory-reset.*\` — puck firmware (factory reset) - \`ReversePuckFirmware-*.*\` — reverse-puck firmware (controller emulator) - \`.uf2\` files for drag-and-drop flashing (BOOT/UF2 drive) - \`.hex\` files for direct flashing workflows" fi - name: Mirror OpenPuck UF2s to the firmware branch # The WebUSB panel's "update from a release" flow downloads these in-browser. GitHub's release-asset # CDN (release-assets.githubusercontent.com) sends no CORS headers, so browsers cannot fetch release # assets directly; raw.githubusercontent.com IS CORS-clean. Mirror the puck .uf2s onto the orphan # `firmware` branch, where the panel fetches raw.githubusercontent.com//firmware/. # Isolated clone in a temp dir so the build tree (untracked artifacts, regenerated version header) # can't leak into the mirror commit. env: VERSION: ${{ steps.release_version.outputs.version }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu MIRROR="$(mktemp -d)/mirror" REPO="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" if git ls-remote --exit-code "$REPO" refs/heads/firmware >/dev/null 2>&1; then git clone --depth 1 --branch firmware "$REPO" "$MIRROR" else git init "$MIRROR" git -C "$MIRROR" checkout --orphan firmware git -C "$MIRROR" remote add origin "$REPO" printf '%s\n' \ '# firmware mirror' '' \ 'OpenPuck release `.uf2` files, mirrored from the GitHub release assets because' \ 'raw.githubusercontent.com serves files WITH CORS headers while the release-asset CDN does' \ 'not — this is what lets the WebUSB config panel download and flash releases in-browser.' \ 'Maintained automatically by `.github/workflows/release.yml`; do not edit by hand.' \ > "$MIRROR/README.md" fi cp release_assets/OpenPuck-"${VERSION}"-*.uf2 "$MIRROR"/ # manifest.json: name -> {panelUpdate}. panelUpdate = the image embeds the FWUP_TAG capability # string (fw_update.cpp), i.e. a puck running it can be updated FROM the panel. The panel badges # releases whose images can't ("no panel updates") before the user flashes one. ( cd "$MIRROR" && python3 - <<'PY' import json, os, glob m = {} if os.path.exists("manifest.json"): m = json.load(open("manifest.json")) for f in sorted(glob.glob("OpenPuck-*.uf2")): m[f] = {"panelUpdate": b"OPK-FWUP-v1" in open(f, "rb").read()} json.dump(m, open("manifest.json", "w"), indent=1, sort_keys=True) PY ) git -C "$MIRROR" config user.name "github-actions[bot]" git -C "$MIRROR" config user.email "github-actions[bot]@users.noreply.github.com" git -C "$MIRROR" add -A git -C "$MIRROR" commit -m "mirror OpenPuck ${VERSION} uf2s for the panel updater" git -C "$MIRROR" push origin firmware