瀏覽代碼

Fix how releases are done

Jari 2 月之前
父節點
當前提交
6f47607eaf
共有 3 個文件被更改,包括 91 次插入 和 25 次删除
  1. 29 21
      .github/workflows/release.yml
  2. 8 1
      docs/BUILD_AND_DEPLOY.md
  3. 54 3
      docs/index.html

+ 29 - 21
.github/workflows/release.yml

@@ -163,28 +163,36 @@ jobs:
           test -n "$(find release_assets -maxdepth 1 -type f -print -quit)"
           ls -la release_assets
 
-      - name: Create GitHub Release
-        uses: softprops/action-gh-release@v3
-        with:
-          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
-
-            Version: ${{ steps.release_version.outputs.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
+      - 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:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          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

+ 8 - 1
docs/BUILD_AND_DEPLOY.md

@@ -234,7 +234,9 @@ A board already running OpenPuck (status protocol v15+) can be updated entirely
 [WebUSB configurator](https://safijari.github.io/openpuck/)'s **Firmware update** tab: drag-and-drop a
 `.uf2` (or click to browse), or pick a version from the built-in **releases list** — each release offers the
 standard build or, via its checkbox, the `-factory-reset` build (wipes settings + pairing once on first
-boot). A blocking modal shows download/transfer/verify/apply progress and reports the old → new build once
+boot). Each entry shows the release's GitHub notes under a collapsible **release notes** toggle and, when
+the release is marked as a pre-release on GitHub, a **pre-release** badge (un-marking it on GitHub removes
+the badge on the next list refresh; pre-releases are never suggested by the "update available" notice). A blocking modal shows download/transfer/verify/apply progress and reports the old → new build once
 the puck reconnects. Under the hood the panel extracts the app image from the `.uf2` and streams it over the
 normal WebUSB connection into spare flash high in the app region (~15 s; the running firmware keeps working);
 the firmware CRC32-verifies what landed and commits a one-page "apply on reboot" record; a final automatic
@@ -247,6 +249,11 @@ reboot copies staged→app from RAM (~5 s dark) and comes back up on the new fir
 > `raw.githubusercontent.com/safijari/openpuck/firmware/<asset>` (which is CORS-clean). If a release is
 > missing from the mirror, the panel falls back to opening the asset in a new tab for manual drag-and-drop.
 
+> **Release notes and the pre-release flag are owned by GitHub, not the workflow:** the release job uploads
+> its artifacts onto an existing release (`gh release upload --clobber`) and only authors a title/body when
+> it creates the release itself from a bare tag push. Notes written in the GitHub UI, a custom release
+> title, and the pre-release checkbox therefore survive the artifact build that follows publishing.
+
 Failure safety: **nothing is armed until the staged image verifies in flash**, so a disconnect, error, or
 power cut during the transfer leaves the current firmware untouched. The apply step erases the app's vector
 page first and rewrites it last (first word dead-last), so even a power cut mid-apply leaves the board

+ 54 - 3
docs/index.html

@@ -90,6 +90,21 @@
   .rel-row .grow{flex:1}
   .rel-row label{display:flex;align-items:center;gap:6px;color:var(--mut);font-size:12px;cursor:pointer}
   .rel-row input[type=checkbox]{accent-color:var(--bad)}
+  /* one release = a row plus (optionally) its GitHub release notes, collapsed */
+  .rel-item{border-top:1px solid #232a3a}
+  .rel-item:first-child{border-top:0}
+  .rel-item .rel-row{border-top:0}
+  .pill.pre{background:#3a2f14;color:var(--warn)}
+  .rel-notes{margin:0 2px 10px;padding:0}
+  .rel-notes>summary{cursor:pointer;color:var(--acc2);font-size:12px;list-style:none;padding:2px 0}
+  .rel-notes>summary::-webkit-details-marker{display:none}
+  .rel-notes>summary::before{content:"\25B8 ";display:inline-block;transition:transform .1s}
+  .rel-notes[open]>summary::before{content:"\25BE "}
+  .rel-notes .body{white-space:pre-wrap;word-break:break-word;color:var(--fg);font-size:12.5px;
+    background:#151925;border:1px solid #232a3a;border-radius:8px;padding:9px 11px;margin-top:4px;
+    max-height:280px;overflow:auto}
+  .rel-notes .body code{background:#0f131c;border-radius:4px;padding:1px 4px;font-size:12px}
+  .rel-notes .body a{color:var(--acc2)}
   /* firmware-tab gate: when the connected puck's firmware predates panel updates, both update cards go
      inert (pointer-events kills clicks, drops, and the file picker) and the banner explains why */
   .fwupcard.gated{opacity:.45;pointer-events:none;user-select:none}
@@ -2099,6 +2114,21 @@ function flashRelease(rel,factory){
     +"automatic reboot. A failed or interrupted transfer leaves the current firmware untouched.",
     ()=>downloadAsset(asset));
 }
+// Release notes come straight from GitHub (rel.body: markdown written by a human in the release UI).
+// Render a deliberately small subset -- headings, bullets, `code`, **bold**, links, and bare/markdown URLs
+// -- from ESCAPED text, so nothing in a release body can inject markup into the panel.
+function relNotesHtml(md){
+  const esc=t=>t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
+  return esc((md||"").replace(/\r\n/g,"\n").trim())
+    .replace(/`([^`\n]+)`/g,(m,c)=>"<code>"+c+"</code>")
+    .replace(/\*\*([^*\n]+)\*\*/g,(m,b)=>"<b>"+b+"</b>")
+    .replace(/^(#{1,6})\s+(.+)$/gm,(m,h,t)=>"<b>"+t+"</b>")
+    .replace(/^\s*[-*]\s+/gm,"• ")
+    .replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g,
+             (m,t,u)=>'<a href="'+u+'" target="_blank" rel="noopener">'+t+"</a>")
+    .replace(/(^|[\s(])(https?:\/\/[^\s<)]+)/g,
+             (m,pre,u)=>pre+'<a href="'+u+'" target="_blank" rel="noopener">'+u+"</a>");
+}
 function renderReleases(){
   const list=$("#relList");
   list.textContent="";
@@ -2107,10 +2137,11 @@ function renderReleases(){
     const std=relAsset(rel,false), fac=relAsset(rel,true);
     if(!std && !fac) continue;
     shown++;
+    const item=document.createElement("div"); item.className="rel-item";
     const row=document.createElement("div"); row.className="rel-row";
     const ver=document.createElement("span"); ver.className="ver"; ver.textContent=rel.tag_name;
     const date=document.createElement("span"); date.className="date";
-    date.textContent=(rel.published_at||"").slice(0,10)+(rel.prerelease?" · pre":"");
+    date.textContent=(rel.published_at||"").slice(0,10);
     const name=document.createElement("span"); name.className="grow note"; name.style.margin="0";
     name.textContent=rel.name && rel.name!==rel.tag_name ? rel.name : "";
     const lab=document.createElement("label");
@@ -2121,6 +2152,15 @@ function renderReleases(){
     const btn=document.createElement("button"); btn.textContent="Flash "+rel.tag_name;
     btn.onclick=()=>flashRelease(rel,cb.checked);
     row.append(ver,date,name);
+    // Pre-release marking is read live from the GitHub API on every refresh, so un-marking a release on
+    // GitHub makes the badge disappear here as soon as the list is reloaded.
+    if(rel.prerelease){
+      const pre=document.createElement("span"); pre.className="pill pre";
+      pre.textContent="pre-release";
+      pre.title="Marked as a pre-release on GitHub: a test build, not the recommended version. "
+        +"It flashes like any other release, and the update notice never points at it.";
+      row.appendChild(pre);
+    }
     if(relPanelUpdatable(rel)===false){
       const pill=document.createElement("span"); pill.className="pill dn";
       pill.textContent="no panel updates";
@@ -2129,7 +2169,16 @@ function renderReleases(){
       row.appendChild(pill);
     }
     row.append(lab,btn);
-    list.appendChild(row);
+    item.appendChild(row);
+    if((rel.body||"").trim()){
+      const det=document.createElement("details"); det.className="rel-notes";
+      const sum=document.createElement("summary"); sum.textContent="release notes";
+      const body=document.createElement("div"); body.className="body";
+      body.innerHTML=relNotesHtml(rel.body);
+      det.append(sum,body);
+      item.appendChild(det);
+    }
+    list.appendChild(item);
   }
   if(!shown) list.textContent="no releases with OpenPuck .uf2 assets found";
 }
@@ -2139,8 +2188,10 @@ async function loadReleases(force){
   list.textContent="loading…";
   try{
     const [r,mr]=await Promise.all([
+      // no-store: the pre-release flag and the notes are edited on GitHub after the fact, and a cached
+      // list would keep showing the stale state after a refresh.
       fetch("https://api.github.com/repos/"+REL_REPO+"/releases?per_page=15",
-            {headers:{Accept:"application/vnd.github+json"}}),
+            {cache:"no-store",headers:{Accept:"application/vnd.github+json"}}),
       fetch("https://raw.githubusercontent.com/"+REL_REPO+"/firmware/manifest.json",{cache:"no-store"})
         .catch(()=>null),
     ]);