Build and Release #63
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 "==========================================" |