Explorar el Código

feat: add semver-driven release tagging

copilot-swe-agent[bot] hace 3 meses
padre
commit
2a41f589a9

+ 2 - 0
.github/workflows/build.yml

@@ -4,6 +4,8 @@ on:
   push:
     branches:
       - main
+    tags:
+      - '*.*.*'
   # PRs get the same gate -- without this the checks only ran after merge.
   pull_request:
 

+ 68 - 10
.github/workflows/release.yml

@@ -4,17 +4,76 @@ on:
   push:
     branches:
       - main
+    tags:
+      - '*.*.*'
+  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@v4
+        with:
+          fetch-depth: 0
+
+      - name: Tag next patch version
+        run: |
+          set -eu
+          git fetch --force --tags origin
+
+          if git tag --points-at "$GITHUB_SHA" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
+            echo "semver tag already exists on this commit; skipping"
+            exit 0
+          fi
+
+          LAST="$(
+            git tag --list \
+              | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \
+              | sort -V \
+              | tail -n1 || true
+          )"
+
+          if [ -z "$LAST" ]; then
+            # Bootstrap the first semver release tag at 0.9.1.
+            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
 
     steps:
       - name: Check out repository
         uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - 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
@@ -35,6 +94,8 @@ jobs:
           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
@@ -80,12 +141,10 @@ jobs:
         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
-          REF="${GITHUB_REF_NAME}"
-          SAFE_REF="$(printf '%s' "$REF" | tr -c 'A-Za-z0-9._-' '-')"
-          SHA_SHORT="$(git rev-parse --short=8 HEAD)"
-          VERSION="${SAFE_REF}-${SHA_SHORT}"
 
           find build/openpuck build/openpuck-recovery \
             -maxdepth 1 -type f \
@@ -116,16 +175,15 @@ jobs:
       - name: Create GitHub Release
         uses: softprops/action-gh-release@v2
         with:
-          tag_name: ${{ format('build-{0}', github.sha) }}
-          target_commitish: ${{ github.sha }}
-          name: ${{ format('Build {0}', github.sha) }}
-          prerelease: true
+          tag_name: ${{ steps.release_version.outputs.version }}
+          name: ${{ format('OpenPuck {0}', steps.release_version.outputs.version) }}
+          prerelease: false
           files: release_assets/*
           body: |
             OpenPuck Firmware Release
 
-            Build: ${{ github.sha }}
-            Ref: ${{ github.ref_name }}
+            Version: ${{ steps.release_version.outputs.version }}
+            Commit: ${{ github.sha }}
 
             ### Files included:
 

+ 8 - 4
OpenPuck/build_info.h

@@ -1,11 +1,12 @@
 // build_info.h -- firmware build provenance, surfaced in the WebUSB panel so you can confirm EXACTLY which
-// build is flashed on a board (the git commit it was built from, and whether that working tree was dirty).
+// build is flashed on a board (semantic version when available, otherwise git hash, plus dirty flag).
 //
 // The values come from one of two sources, in priority order:
-//   1) -D defines on the compile line, e.g. build.extra_flags += -DOPK_GIT_HASH=\"abc12345\" -DOPK_GIT_DIRTY=1
+//   1) -D defines on the compile line, e.g. build.extra_flags += -DOPK_BUILD_VERSION=\"0.9.1\"
+//      -DOPK_GIT_HASH=\"abc12345\" -DOPK_GIT_DIRTY=1
 //   2) a generated git_version.h (written by gen_version.sh before compiling), picked up via __has_include.
-// If neither is present (a bare checkout compiled by hand) the values fall back to "unknown" / not-dirty, so
-// the build always succeeds -- the panel just shows "unknown" until you generate the header.
+// If neither is present (a bare checkout compiled by hand) the values fall back to empty-version/"unknown"/
+// not-dirty, so the build always succeeds.
 #pragma once
 
 #if !defined(OPK_GIT_HASH) && defined(__has_include)
@@ -14,6 +15,9 @@
 #endif
 #endif
 
+#ifndef OPK_BUILD_VERSION
+#define OPK_BUILD_VERSION ""
+#endif
 #ifndef OPK_GIT_HASH
 #define OPK_GIT_HASH "unknown"
 #endif

+ 8 - 7
OpenPuck/webusb_config.cpp

@@ -28,7 +28,7 @@ static volatile bool g_bondExportRequest = false;
 //                [qos][persistMode][chordBtn B][chordBtn X][chordBtn Y][pollsps_lo][pollsps_hi]
 //                [loopPeriod_lo][loopPeriod_hi][loopWorstIdx][loopWorstUs_lo][loopWorstUs_hi]
 //                [pollPeriod_lo][pollPeriod_hi][logEnabled][battery%][rssi|dBm|]
-//                [gitDirty][gitHash 12B ASCII, NUL-padded][rumbleScale][swPro120][swGyroScale10][raw accel ax ay az 3x s16 LE]
+//                [gitDirty][buildId 12B ASCII, NUL-padded][rumbleScale][swPro120][swGyroScale10][raw accel ax ay az 3x s16 LE]
 //                [bondedCount][slot0_up][slot0_batt][slot0_rssi]...[slot3_up][slot3_batt][slot3_rssi]
 //                [v10: per-type cfg, 4x8B: ET_XBOX/SWITCH/DS4/DS5 each {back0..3, qam, abSwap, padHaptics, ledBright}]
 //                [v11: resetReason(RR_* code)][resetReas raw u32 LE][rxWin/10 us][hapticBlockOn][hapticBlock s]
@@ -111,14 +111,15 @@ static void webusbSendBlob()
 	p[39] = g_linkRssi[g_curSlot >= 0 && g_curSlot < NSLOT ?
 				   g_curSlot :
 				   0]; // RAW signal strength |dBm| (0=no sample)
-	// git commit this firmware was built from + dirty flag; injected at build time
-	// (build_info.h / gen_version.sh); "unknown" if the version header wasn't generated.
+	// Build ID + dirty flag from build_info.h/gen_version.sh. Prefer semver when
+	// present, otherwise fall back to the git hash.
 	p[40] = OPK_GIT_DIRTY ? 1 : 0;
-	memset(&p[41], 0, 12); // 12B ASCII git hash, NUL-padded
+	memset(&p[41], 0, 12); // 12B ASCII build ID, NUL-padded
 	{
-		const char *h = OPK_GIT_HASH;
-		for (uint8_t i = 0; i < 12 && h[i]; i++)
-			p[41 + i] = (uint8_t)h[i];
+		const char *buildId = OPK_BUILD_VERSION[0] ? OPK_BUILD_VERSION :
+						      OPK_GIT_HASH;
+		for (uint8_t i = 0; i < 12 && buildId[i]; i++)
+			p[41 + i] = (uint8_t)buildId[i];
 	}
 	p[53] = g_rumbleScale; // rumble strength % (protocol v5)
 

+ 1 - 1
docs/BUILD_AND_DEPLOY.md

@@ -156,7 +156,7 @@ Re-flashing firmware does **not** erase the board's internal LittleFS. The paire
 - **Recovery build (`-DOPK_FACTORY_RESET=1`):** a firmware that wipes all persistent storage **once, on the first boot after flashing**, then behaves like a normal build that persists settings. Use it to recover a board from a bad config/bond without a console or panel:
 
   ```bash
-  ./gen_version.sh   # recommended: gives the build a distinct git hash (see below)
+  ./gen_version.sh   # recommended: embeds version (when tagged) + git hash provenance
   make build-recovery
   ```
 

+ 5 - 5
docs/index.html

@@ -132,7 +132,7 @@
         <div class="stat logonly"><div class="k">Loop period</div><div class="v" id="stLoop">—</div></div>
         <div class="stat logonly"><div class="k">Slowest stage</div><div class="v" id="stWorst">—</div></div>
         <div class="stat logonly"><div class="k">Poll period act/want</div><div class="v" id="stPollPer">—</div></div>
-        <div class="stat"><div class="k">Build (git)</div><div class="v" id="stBuild">—</div></div>
+        <div class="stat"><div class="k">Build</div><div class="v" id="stBuild">—</div></div>
         <div class="stat"><div class="k">Last reset</div><div class="v" id="stReset">—</div></div>
       </div>
       <p class="note" style="margin-top:6px"><strong>Reset detail:</strong> <span id="resetHist" style="font-family:ui-monospace,Menlo,monospace">—</span><br>
@@ -739,10 +739,10 @@ function applyBlob(p){
     if (RR_FAULT.has(code) && !window._flightAuto){ window._flightAuto=true; window._autoFlight=true; }
   }
 
-  const gitDirty=(p.length>38?p[38]:0); let gitHash="";
-  for(let i=39;i<51 && i<p.length && p[i];i++) gitHash+=String.fromCharCode(p[i]);
-  $("#stBuild").innerHTML = gitHash
-    ? (gitHash + (gitDirty ? ' <span class="pill dn">dirty</span>' : ' <span class="pill up">clean</span>'))
+  const gitDirty=(p.length>38?p[38]:0); let buildId="";
+  for(let i=39;i<51 && i<p.length && p[i];i++) buildId+=String.fromCharCode(p[i]);
+  $("#stBuild").innerHTML = buildId
+    ? (buildId + (gitDirty ? ' <span class="pill dn">dirty</span>' : ' <span class="pill up">clean</span>'))
     : "—";
   setSlider("mDiv",mDiv); setSlider("mFric",mFric); setSlider("rumble",rumbleScale);
   $("#persistMode").textContent = persistMode? "on":"off"; $("#persistMode").classList.toggle("active",!!persistMode);

+ 17 - 4
gen_version.sh

@@ -1,11 +1,23 @@
 #!/bin/sh
-# gen_version.sh -- write OpenPuck/git_version.h with the current git short hash + dirty flag, for the build
-# provenance shown in the WebUSB panel. Run this BEFORE `arduino-cli compile` (it's a no-op to re-run). The
-# generated header is git-ignored, so generating it never itself marks the tree dirty.
+# gen_version.sh -- write OpenPuck/git_version.h with build provenance for the WebUSB panel:
+#   - semantic version (when OPK_BUILD_VERSION is set, or HEAD has a semver tag),
+#   - git short hash,
+#   - dirty flag.
+# Run this BEFORE `arduino-cli compile` (it's a no-op to re-run). The generated header is git-ignored, so
+# generating it never itself marks the tree dirty.
 set -e
 cd "$(dirname "$0")"
 
 HASH=$(git rev-parse --short=8 HEAD 2>/dev/null || echo unknown)
+VERSION="${OPK_BUILD_VERSION:-}"
+if [ -z "$VERSION" ]; then
+	VERSION="$(
+		git tag --points-at HEAD 2>/dev/null \
+			| grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \
+			| sort -V \
+			| tail -n1 || true
+	)"
+fi
 # Dirty = any tracked change OR untracked file (porcelain is empty only on a clean tree). git_version.h is
 # git-ignored so it doesn't count.
 if [ -z "$(git status --porcelain 2>/dev/null)" ]; then DIRTY=0; else DIRTY=1; fi
@@ -13,8 +25,9 @@ if [ -z "$(git status --porcelain 2>/dev/null)" ]; then DIRTY=0; else DIRTY=1; f
 cat > OpenPuck/git_version.h <<EOF
 // AUTO-GENERATED by gen_version.sh -- do not edit, do not commit (git-ignored).
 #pragma once
+#define OPK_BUILD_VERSION "$VERSION"
 #define OPK_GIT_HASH "$HASH"
 #define OPK_GIT_DIRTY $DIRTY
 EOF
 
-echo "OpenPuck/git_version.h: hash=$HASH dirty=$DIRTY"
+echo "OpenPuck/git_version.h: version=${VERSION:-none} hash=$HASH dirty=$DIRTY"