Skip to content

Commit f2a9737

Browse files
fix: data race in auto-update and add versions.json CI job
Use atomic.Pointer for updateInfo and a mutex for releaseChannel to eliminate a data race between the background check goroutine and the UI thread. Add update-versions job to the release workflow that hashes build artifacts, verifies uploaded assets match, and commits the versions.json with all distribution zip metadata.
1 parent daa864c commit f2a9737

2 files changed

Lines changed: 148 additions & 6 deletions

File tree

.github/workflows/release.yml

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,136 @@ jobs:
285285
build32/grout-arm32
286286
draft: false
287287
prerelease: ${{ inputs.beta }}
288+
289+
update-versions:
290+
needs: [prepare, release]
291+
runs-on: ubuntu-22.04
292+
permissions:
293+
contents: write
294+
steps:
295+
- uses: actions/checkout@v4
296+
with:
297+
ref: main
298+
token: ${{ secrets.GITHUB_TOKEN }}
299+
300+
- name: Download build artifacts
301+
uses: actions/download-artifact@v4
302+
with:
303+
merge-multiple: true
304+
305+
- name: Update versions.json
306+
env:
307+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
308+
VERSION: ${{ needs.prepare.outputs.version }}
309+
IS_BETA: ${{ inputs.beta }}
310+
CHANGELOG: ${{ inputs.changelog }}
311+
run: |
312+
REPO="rommapp/grout"
313+
DOWNLOAD_BASE="https://github.com/${REPO}/releases/download/${VERSION}"
314+
315+
# Hash local build artifacts and get their sizes
316+
get_asset_info() {
317+
local path="$1"
318+
if [ -f "$path" ]; then
319+
local size=$(stat --format=%s "$path")
320+
local sha256=$(sha256sum "$path" | cut -d' ' -f1)
321+
echo "${size} ${sha256}"
322+
else
323+
echo "0 "
324+
fi
325+
}
326+
327+
# Verify uploaded release assets match local build artifacts
328+
verify_upload() {
329+
local name="$1"
330+
local expected_sha256="$2"
331+
if [ -z "$expected_sha256" ]; then return; fi
332+
333+
local url="${DOWNLOAD_BASE}/${name}"
334+
if curl -sfL -o "/tmp/${name}" "$url"; then
335+
local actual_sha256=$(sha256sum "/tmp/${name}" | cut -d' ' -f1)
336+
rm -f "/tmp/${name}"
337+
if [ "$actual_sha256" != "$expected_sha256" ]; then
338+
echo "::error::Upload verification failed for ${name}: expected ${expected_sha256}, got ${actual_sha256}"
339+
exit 1
340+
fi
341+
echo "Verified ${name}: ${actual_sha256}"
342+
else
343+
echo "::warning::Could not download ${name} for verification"
344+
fi
345+
}
346+
347+
# Distribution zip assets
348+
DIST_ASSETS=(
349+
"Grout.pak.zip:dist/Grout.pak.zip"
350+
"Grout.muxapp:dist/Grout.muxapp"
351+
"Grout-Knulli.zip:dist/Grout-Knulli.zip"
352+
"Grout.spruce.zip:dist/Grout.spruce.zip"
353+
"Grout-ROCKNIX.zip:dist/Grout-ROCKNIX.zip"
354+
"Grout-Trimui.zip:dist/Grout-Trimui.zip"
355+
"Grout-Allium.zip:dist/Grout-Allium.zip"
356+
"Grout-Onion.zip:dist/Grout-Onion.zip"
357+
"Grout-MinUI-arm64.zip:dist/Grout-MinUI-arm64.zip"
358+
"Grout-MinUI-arm32.zip:dist/Grout-MinUI-arm32.zip"
359+
"Grout-Batocera-arm64.zip:dist/Grout-Batocera-arm64.zip"
360+
"Grout-Batocera-amd64.zip:dist/Grout-Batocera-amd64.zip"
361+
"Grout-Batocera-x86.zip:dist/Grout-Batocera-x86.zip"
362+
)
363+
364+
# Build the assets JSON object
365+
ASSETS_JSON="{}"
366+
for entry in "${DIST_ASSETS[@]}"; do
367+
name="${entry%%:*}"
368+
path="${entry##*:}"
369+
370+
read size sha256 <<< $(get_asset_info "$path")
371+
if [ "$size" = "0" ]; then
372+
echo "::warning::Asset not found: ${path}"
373+
continue
374+
fi
375+
376+
verify_upload "$name" "$sha256"
377+
378+
ASSETS_JSON=$(echo "$ASSETS_JSON" | jq \
379+
--arg name "$name" \
380+
--arg url "${DOWNLOAD_BASE}/${name}" \
381+
--argjson size "${size}" \
382+
--arg sha256 "${sha256}" \
383+
'.[$name] = { url: $url, size: $size, sha256: $sha256 }')
384+
done
385+
386+
# Build the release entry
387+
RELEASE_ENTRY=$(jq -n \
388+
--arg version "$VERSION" \
389+
--arg notes "$CHANGELOG" \
390+
--argjson assets "$ASSETS_JSON" \
391+
'{ version: $version, notes: $notes, assets: $assets }')
392+
393+
# Parse major.minor.patch for the romm key
394+
ROMM_KEY=$(echo "$VERSION" | sed 's/^v//' | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+')
395+
396+
# Update versions.json
397+
if [ "$IS_BETA" = "true" ]; then
398+
jq --argjson entry "$RELEASE_ENTRY" --arg key "$ROMM_KEY" \
399+
'.beta = $entry | .romm[$key] = $entry' \
400+
docs/versions.json > docs/versions.json.tmp
401+
else
402+
jq --argjson entry "$RELEASE_ENTRY" --arg key "$ROMM_KEY" \
403+
'.stable = $entry | .romm[$key] = $entry' \
404+
docs/versions.json > docs/versions.json.tmp
405+
fi
406+
407+
mv docs/versions.json.tmp docs/versions.json
408+
409+
- name: Commit and push versions.json
410+
run: |
411+
git config user.name "github-actions[bot]"
412+
git config user.email "github-actions[bot]@users.noreply.github.com"
413+
414+
if git diff --quiet docs/versions.json; then
415+
echo "No changes to versions.json"
416+
else
417+
git add docs/versions.json
418+
git commit -m "Update versions.json for ${{ needs.prepare.outputs.version }}"
419+
git push
420+
fi

update/auto_update.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"grout/cfw"
55
"grout/internal"
66
"grout/romm"
7+
"sync"
78
"sync/atomic"
89

910
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
@@ -13,13 +14,14 @@ const updateIcon = "\U000F06B0"
1314

1415
type AutoUpdate struct {
1516
cfwType cfw.CFW
16-
releaseChannel internal.ReleaseChannel
1717
host *romm.Host
1818
icon *gaba.DynamicStatusBarIcon
1919
running atomic.Bool
2020
updateAvailable atomic.Bool
2121
done chan struct{}
22-
updateInfo *Info
22+
mu sync.Mutex
23+
releaseChannel internal.ReleaseChannel
24+
updateInfo atomic.Pointer[Info]
2325
}
2426

2527
func NewAutoUpdate(c cfw.CFW, r internal.ReleaseChannel, host *romm.Host) *AutoUpdate {
@@ -53,7 +55,7 @@ func (a *AutoUpdate) UpdateAvailable() bool {
5355
}
5456

5557
func (a *AutoUpdate) UpdateInfo() *Info {
56-
return a.updateInfo
58+
return a.updateInfo.Load()
5759
}
5860

5961
// Recheck updates the release channel and re-runs the update check.
@@ -63,9 +65,12 @@ func (a *AutoUpdate) Recheck(releaseChannel internal.ReleaseChannel) {
6365
return // Already running, skip
6466
}
6567

68+
a.mu.Lock()
6669
a.releaseChannel = releaseChannel
70+
a.mu.Unlock()
71+
6772
a.updateAvailable.Store(false)
68-
a.updateInfo = nil
73+
a.updateInfo.Store(nil)
6974
a.icon.SetText("") // Clear the icon
7075

7176
a.Start()
@@ -80,13 +85,17 @@ func (a *AutoUpdate) run() {
8085

8186
logger.Debug("AutoUpdate: Checking for updates in background")
8287

83-
info, err := CheckForUpdate(a.cfwType, a.releaseChannel, a.host)
88+
a.mu.Lock()
89+
channel := a.releaseChannel
90+
a.mu.Unlock()
91+
92+
info, err := CheckForUpdate(a.cfwType, channel, a.host)
8493
if err != nil {
8594
logger.Debug("AutoUpdate: Failed to check for updates", "error", err)
8695
return
8796
}
8897

89-
a.updateInfo = info
98+
a.updateInfo.Store(info)
9099

91100
if info.UpdateAvailable {
92101
logger.Debug("AutoUpdate: Update available", "current", info.CurrentVersion, "latest", info.LatestVersion)

0 commit comments

Comments
 (0)