Skip to content

Build and Release

Build and Release #63

Workflow file for this run

name: Build and Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
env:
GO_VERSION: '1.21'
NODE_VERSION: '18'
jobs:
# ============================================
# STAGE 1: Checking (extract version from tag)
# ============================================
checking:
name: 🔍 Checking
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.VERSION }}
steps:
- name: Get version from tag
id: version
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "=========================================="
echo "✅ Release version: v$VERSION"
echo "=========================================="
# ============================================
# STAGE 2: Compiling (parallel builds)
# ============================================
compiling:
name: 🔨 Compile ${{ matrix.target }}
runs-on: ubuntu-latest
needs: [checking, compiling-firmware]
strategy:
matrix:
include:
- target: Windows
goos: windows
goarch: amd64
suffix: windows-amd64.exe
- target: Linux ARM64
goos: linux
goarch: arm64
suffix: linux-arm64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: server-go/web/package-lock.json
- name: Inject version from tag
run: |
VERSION="${{ needs.checking.outputs.version }}"
echo "Injecting version $VERSION into config files..."
# Inject into config.json (server)
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" server-go/config.json
echo " config.json: $(grep '"version"' server-go/config.json)"
# Inject into internal config.json copy
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" server-go/internal/server/config.json
echo " internal config.json: $(grep '"version"' server-go/internal/server/config.json)"
# Inject into package.json (frontend)
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" server-go/web/package.json
echo " package.json: $(grep '"version"' server-go/web/package.json | head -1)"
echo "✅ All versions set to $VERSION"
- name: Install frontend dependencies
working-directory: server-go/web
run: npm ci
- name: Build frontend
working-directory: server-go/web
run: npm run build
- name: Copy dist for embedding
run: |
rm -rf server-go/cmd/server/dist
cp -r server-go/web/dist server-go/cmd/server/dist
- name: Download firmware artifact for embedding
uses: actions/download-artifact@v4
with:
name: firmware-buzzclick
path: firmware-artifact
- name: Embed firmware into server assets
run: |
VERSION="${{ needs.checking.outputs.version }}"
# The merged binary is the primary artifact: supports both USB flash and OTA.
# The server auto-detects the format and extracts the app portion for OTA.
cp firmware-artifact/buzzclick-v$VERSION-merged.bin server-go/assets/firmware/buzzclick-latest.bin
echo -n "$VERSION" > server-go/assets/firmware/version.txt
# Also embed merged binary for USB flash of new/dead buzzers
if [ -f "firmware-artifact/buzzclick-v$VERSION-merged.bin" ]; then
cp firmware-artifact/buzzclick-v$VERSION-merged.bin server-go/assets/firmware/buzzclick-merged.bin
echo "✅ Merged firmware v$VERSION embedded (USB flash support)"
else
echo "⚠️ No merged firmware found, USB flash will be unavailable"
fi
echo "✅ Firmware v$VERSION embedded into server assets"
- name: Install goversioninfo (Windows only)
if: matrix.goos == 'windows'
run: go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest
- name: Generate Windows PE metadata (Windows only)
if: matrix.goos == 'windows'
env:
VERSION: ${{ needs.checking.outputs.version }}
run: |
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
echo "Generating versioninfo.json for v$VERSION..."
jq -n \
--argjson major "$MAJOR" \
--argjson minor "$MINOR" \
--argjson patch "$PATCH" \
--arg version "$VERSION" \
'{
FixedFileInfo: {
FileVersion: {Major: $major, Minor: $minor, Patch: $patch, Build: 0},
ProductVersion: {Major: $major, Minor: $minor, Patch: $patch, Build: 0},
FileFlagsMask: "3f", FileFlags: "00",
FileOS: "040004", FileType: "01", FileSubType: "00"
},
StringFileInfo: {
Comments: "", CompanyName: "CCoupel",
FileDescription: "Wireless Buzzer System for Quiz Games",
FileVersion: "\($major).\($minor).\($patch).0",
InternalName: "buzzcontrol",
LegalCopyright: "2026 CCoupel", LegalTrademarks: "",
OriginalFilename: "buzzcontrol.exe", PrivateBuild: "",
ProductName: "BuzzControl", ProductVersion: $version,
SpecialBuild: ""
},
VarFileInfo: {Translation: {LangID: "0409", CharsetID: "04B0"}},
IconPath: "../../assets/icon.ico",
ManifestPath: ""
}' > server-go/cmd/server/versioninfo.json
cd server-go/cmd/server && goversioninfo -o resource_windows_amd64.syso
echo "Windows PE metadata generated (resource_windows_amd64.syso)"
- name: Build ${{ matrix.target }}
working-directory: server-go
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
run: |
VERSION="${{ needs.checking.outputs.version }}"
echo "Building ${{ matrix.target }} with embedded version $VERSION..."
go build -ldflags="-s -w -X main.Version=$VERSION" -o ../buzzcontrol-v$VERSION-${{ matrix.suffix }} ./cmd/server
echo "✅ ${{ matrix.target }} build complete"
- name: Validate binary
run: |
FILE="buzzcontrol-v${{ needs.checking.outputs.version }}-${{ matrix.suffix }}"
SIZE=$(stat -c%s "$FILE")
echo "${{ matrix.target }} binary: $SIZE bytes ($(numfmt --to=iec $SIZE))"
MIN_SIZE=5242880
if [ "$SIZE" -lt "$MIN_SIZE" ]; then
echo "❌ ERROR: Binary too small ($SIZE < $MIN_SIZE)"
exit 1
fi
echo "✅ ${{ matrix.target }} binary validation passed"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.goos }}-${{ matrix.goarch }}
path: buzzcontrol-v${{ needs.checking.outputs.version }}-${{ matrix.suffix }}
retention-days: 1
compiling-firmware:
name: 🔨 Compile BuzzClick Firmware
runs-on: ubuntu-latest
needs: checking
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PlatformIO
run: |
pip install --upgrade platformio
pio --version
- name: Inject version into platformio.ini
run: |
VERSION="${{ needs.checking.outputs.version }}"
echo "Injecting version $VERSION into platformio.ini..."
sed -i "s/-D VERSION='\"[^\"]*\"'/-D VERSION='\"$VERSION\"'/" platformio.ini
sed -i "s/-D FIRMWARE_VERSION='\"[^\"]*\"'/-D FIRMWARE_VERSION='\"$VERSION\"'/" platformio.ini
echo "✅ Version injected (VERSION and FIRMWARE_VERSION)"
- name: Build BuzzClick firmware
run: |
echo "Building BuzzClick firmware..."
pio run -e buzzclick
echo "✅ Firmware build complete"
- name: Generate merged firmware binary
run: |
VERSION="${{ needs.checking.outputs.version }}"
echo "Generating merged firmware binary (bootloader + partitions + boot_app0 + app)..."
# Use esptool from PlatformIO packages
ESPTOOL=$(find $HOME/.platformio/packages/tool-esptoolpy -name "esptool.py" 2>/dev/null | head -1)
if [ -z "$ESPTOOL" ]; then
ESPTOOL="python -m esptool"
else
ESPTOOL="python $ESPTOOL"
fi
# boot_app0.bin: standard Arduino-ESP32 file that initializes OTA data to boot from ota_0
BOOT_APP0=$(find $HOME/.platformio/packages/framework-arduinoespressif32*/tools/partitions/boot_app0.bin 2>/dev/null | head -1)
if [ -z "$BOOT_APP0" ]; then
echo "❌ ERROR: boot_app0.bin not found in PlatformIO packages"
exit 1
fi
$ESPTOOL --chip esp32c3 merge_bin \
-o buzzclick-v$VERSION-merged.bin \
0x0 .pio/build/buzzclick/bootloader.bin \
0x8000 .pio/build/buzzclick/partitions.bin \
0xe000 $BOOT_APP0 \
0x10000 .pio/build/buzzclick/firmware.bin
echo "✅ Merged binary: buzzclick-v$VERSION-merged.bin ($(numfmt --to=iec $(stat -c%s buzzclick-v$VERSION-merged.bin)))"
- name: Validate firmware binary
run: |
VERSION="${{ needs.checking.outputs.version }}"
# Validate the merged binary (contains bootloader + app, so larger than app-only)
FILE="buzzclick-v$VERSION-merged.bin"
SIZE=$(stat -c%s "$FILE")
echo "Merged firmware binary: $SIZE bytes ($(numfmt --to=iec $SIZE))"
MIN_SIZE=204800 # 200 KB
MAX_SIZE=3145728 # 3 MB
if [ "$SIZE" -lt "$MIN_SIZE" ]; then
echo "❌ ERROR: Firmware too small ($SIZE < $MIN_SIZE)"
exit 1
fi
if [ "$SIZE" -gt "$MAX_SIZE" ]; then
echo "❌ ERROR: Firmware too large ($SIZE > $MAX_SIZE)"
exit 1
fi
echo "✅ Firmware validation passed"
- name: Upload firmware artifact
uses: actions/upload-artifact@v4
with:
name: firmware-buzzclick
# The merged binary is the primary artifact: supports both USB flash and OTA.
# The server auto-detects the format and extracts the app portion for OTA.
path: buzzclick-v${{ needs.checking.outputs.version }}-merged.bin
retention-days: 1
# ============================================
# STAGE 3: Releasing
# ============================================
releasing:
name: 🚀 Releasing
runs-on: ubuntu-latest
needs: [checking, compiling, compiling-firmware]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download server binaries
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: binary-*
merge-multiple: true
- name: Download firmware
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: firmware-*
merge-multiple: true
- name: List artifacts
run: |
echo "Downloaded artifacts:"
ls -lh artifacts/
- name: Generate release notes
id: release_notes
run: |
VERSION="${{ needs.checking.outputs.version }}"
echo "Extracting release notes for v$VERSION from CHANGELOG.md..."
# Extract release notes from CHANGELOG.md
NOTES=$(awk "/## \[$VERSION\]/{flag=1; next} /## \[/{flag=0} flag" CHANGELOG.md)
if [ -z "$NOTES" ]; then
NOTES="Release v$VERSION"
echo "⚠️ No release notes found in CHANGELOG.md, using default"
else
echo "✅ Release notes extracted"
fi
# Save to file for multiline support
echo "$NOTES" > release_notes.md
echo "--- Release Notes ---"
cat release_notes.md
echo "---------------------"
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
name: BuzzControl v${{ needs.checking.outputs.version }}
body_path: release_notes.md
files: artifacts/*
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release summary
run: |
echo "=========================================="
echo "🎉 Release v${{ needs.checking.outputs.version }} published!"
echo ""
echo "📦 Assets:"
ls -1 artifacts/
echo ""
echo "🔗 https://github.com/${{ github.repository }}/releases/tag/v${{ needs.checking.outputs.version }}"
echo "=========================================="