chore(release): bump version to 2.5.1 #64
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: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| # false, not true (#178): a release tag doesn't move, so a second trigger on | |
| # the same ref is a re-run of the same release, not new work superseding it. | |
| # Cancelling mid-flight can leave a release with some platforms' assets | |
| # uploaded and others missing/corrupt, with no clear signal that happened. | |
| # A duplicate run queues behind the first and completing it re-uploads the | |
| # same tagged version's assets with --clobber, which is a harmless no-op. | |
| cancel-in-progress: false | |
| jobs: | |
| create-release: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| fetch-depth: 0 # full tag history — needed for the "Full Changelog" compare link below | |
| - name: Create GitHub release (if not exists) | |
| # Release notes come from CHANGELOG.md, not --generate-notes: this repo | |
| # pushes straight to main instead of merging PRs, so GitHub's PR-based | |
| # auto-generated notes come back empty (see git history around | |
| # v2.0.0/v2.0.3 for what that looked like). A CHANGELOG.md entry is | |
| # mandatory for every release — this step fails loudly instead of | |
| # publishing a release with no notes if one is missing. | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| if gh release view "v${VERSION}" --repo FreeOpenSourcePOS/FloCafe >/dev/null 2>&1; then | |
| echo "Release v${VERSION} already exists, skipping creation." | |
| exit 0 | |
| fi | |
| if ! ./scripts/changelog-notes.sh "${VERSION}" > /tmp/release-notes.md; then | |
| echo "::error::CHANGELOG.md has no '## [${VERSION}]' entry — add one before tagging a release." | |
| exit 1 | |
| fi | |
| PREV_TAG=$(git tag --list 'v*' --sort=-v:refname | grep -v "^v${VERSION}\$" | head -1) | |
| if [ -n "$PREV_TAG" ]; then | |
| echo "" >> /tmp/release-notes.md | |
| echo "**Full Changelog**: https://github.com/FreeOpenSourcePOS/FloCafe/compare/${PREV_TAG}...v${VERSION}" >> /tmp/release-notes.md | |
| fi | |
| gh release create "v${VERSION}" \ | |
| --repo FreeOpenSourcePOS/FloCafe \ | |
| --title "v${VERSION}" \ | |
| --notes-file /tmp/release-notes.md | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Upload standalone uninstaller scripts | |
| # Independent utility scripts, not part of the app bundle — for | |
| # users who need to fully remove/reinstall outside the packaged | |
| # per-platform uninstaller (e.g. a broken install). | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| gh release upload "v${VERSION}" \ | |
| scripts/uninstallers/uninstall-macos.sh \ | |
| scripts/uninstallers/uninstall-windows.ps1 \ | |
| --repo FreeOpenSourcePOS/FloCafe --clobber | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| release-linux: | |
| runs-on: ${{ matrix.runner }} | |
| needs: create-release | |
| timeout-minutes: 60 | |
| # Each matrix entry builds AppImage + deb + rpm + snap for its host | |
| # architecture.electron-builder doesn't cross-compile, so x64 hosts | |
| # produce x86_64 / amd64 artifacts and arm64 hosts produce arm64 ones. | |
| # Both upload to the same GitHub release (filename suffixes uniquely | |
| # identify arch, no collision) and both publish their snap to Snap | |
| # Store under one snap name with multi-arch revisions. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: x64 | |
| runner: ubuntu-24.04 | |
| - arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| concurrency: | |
| # Scoped per-arch so x64 and arm64 (distinct group keys) never contend with | |
| # each other — each only queues behind a previous run of that *same* arch on | |
| # this ref. cancel-in-progress is false for the same reason as the | |
| # top-level group above (#178): don't kill a mid-upload job on re-trigger. | |
| group: release-linux-${{ github.ref }}-${{ matrix.arch }} | |
| cancel-in-progress: false | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 | |
| with: | |
| node-version: '22' | |
| - name: Install system dependencies | |
| # rpm packaging tools are x86-only via apt — arm64 runner ships its | |
| # own rpm toolchain. | |
| if: matrix.arch == 'x64' | |
| run: sudo apt-get update && sudo apt-get install -y build-essential libssl-dev rpm | |
| - name: Install snapcraft | |
| # Required for both matrix entries: x64 and arm64 each produce a snap. | |
| run: sudo snap install snapcraft --classic | |
| - name: Install npm dependencies | |
| run: npm ci | |
| env: | |
| npm_config_build_from_source: true | |
| - name: Prepend AppStream release entry | |
| # Idempotent. x64 entry modifies the file; arm64 entry sees the | |
| # already-fresh entry and skips. Safe to run on both. | |
| run: node scripts/update-metainfo.js | |
| - name: Build Linux artifacts | |
| # Builds all four targets for the host arch only. electron-builder's | |
| # per-target arch filter handles which arch each target produces; | |
| # we declare arm64 on every target so the arm64 matrix entry builds | |
| # all four as arm64. | |
| run: npm run build:frontend && npm run build && npx electron-builder --linux AppImage deb rpm snap --publish never | |
| env: | |
| SNAPCRAFT_BUILD_ENVIRONMENT: host | |
| - name: Upload Linux assets to GitHub release | |
| # Each matrix entry uploads its own arch-tagged artifacts. Filename | |
| # suffixes uniquely identify architecture (x86_64 / arm64 / amd64), | |
| # so x64 and arm64 entries never collide on the same GitHub release. | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| shopt -s nullglob | |
| FILES=(release/*.AppImage release/*.deb release/*.rpm release/*.snap) | |
| [ ${#FILES[@]} -gt 0 ] || { echo "::error::no artifacts produced for ${{ matrix.arch }}"; exit 1; } | |
| gh release upload "v${VERSION}" "${FILES[@]}" --repo FreeOpenSourcePOS/FloCafe --clobber | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Publish snap to Snap Store (stable) | |
| # Both matrix entries publish their snap, each tagged with its arch. | |
| # Snap Store keeps multiple per-arch revisions under one snap name. | |
| # The credentials check is at runtime, not in `if:` — GitHub Actions | |
| # rejects `secrets` in `if:` expressions (unrecognized named-value). | |
| env: | |
| SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} | |
| run: | | |
| if [ -z "$SNAPCRAFT_STORE_CREDENTIALS" ]; then | |
| echo "::warning::SNAPCRAFT_STORE_CREDENTIALS not set — skipping snap publish for ${{ matrix.arch }}" | |
| exit 0 | |
| fi | |
| SNAP_NAME="flocafe" | |
| SNAP=$(ls release/*.snap | head -1) | |
| [ -n "$SNAP" ] || { echo "::error::No .snap produced by electron-builder for ${{ matrix.arch }}"; exit 1; } | |
| snapcraft register "$SNAP_NAME" || echo "::warning::register skipped/failed; continuing" | |
| snapcraft upload "$SNAP" --release=stable | |
| release-mac: | |
| runs-on: macos-latest | |
| needs: create-release | |
| # Was two parallel 60-minute matrix jobs (one per arch); now one job builds, | |
| # signs, and notarizes both — packaging + notarizing arch #2 no longer | |
| # overlaps arch #1's wall-clock time, so this needs real headroom over the | |
| # old single-arch budget. | |
| timeout-minutes: 100 | |
| concurrency: | |
| group: release-mac-${{ github.ref }} | |
| cancel-in-progress: false | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 | |
| with: | |
| node-version: '22' | |
| - name: Install npm dependencies | |
| run: npm ci | |
| - name: Verify Electron dev runtime | |
| # npm ci already runs this via package.json's postinstall — repeated | |
| # here as an explicit, visible gate before the build below relies on it. | |
| run: npm run verify:electron | |
| - name: Verify macOS signing credentials | |
| # Fail loudly rather than let electron-builder silently emit an | |
| # unsigned/unnotarized build — that shipped for every release before | |
| # this check existed and is exactly what gets the app Gatekeeper-blocked | |
| # ("...should move it to the Bin") for anyone who downloads it. | |
| # | |
| # Notarization uses an App Store Connect API key rather than | |
| # Apple ID + app-specific password (#168): the password-based flow | |
| # works fine run interactively from a trusted Mac but was reliably | |
| # rejected by Apple with a misleading "Your Apple ID has been locked" | |
| # 401 when run from GitHub Actions — a known limitation of | |
| # password-based auth from automated/CI contexts. API keys don't | |
| # expire and sidestep this entirely; see electron.build/docs/notarization. | |
| run: | | |
| missing=() | |
| [ -z "$CSC_LINK" ] && missing+=("MAC_CERTS") | |
| [ -z "$CSC_KEY_PASSWORD" ] && missing+=("MAC_CERTS_PASSWORD") | |
| [ -z "$APPLE_API_KEY" ] && missing+=("APPLE_API_KEY") | |
| [ -z "$APPLE_API_KEY_ID" ] && missing+=("APPLE_API_KEY_ID") | |
| [ -z "$APPLE_API_ISSUER" ] && missing+=("APPLE_API_ISSUER") | |
| if [ ${#missing[@]} -gt 0 ]; then | |
| echo "::error::Missing required secret(s) for signed/notarized macOS build: ${missing[*]}" | |
| exit 1 | |
| fi | |
| env: | |
| CSC_LINK: ${{ secrets.MAC_CERTS }} | |
| CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }} | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| - name: Write App Store Connect API key to disk | |
| # @electron/notarize's notarytool strategy expects APPLE_API_KEY to be | |
| # a *file path* to the .p8, not its contents — unlike CSC_LINK, which | |
| # electron-builder accepts as base64. The secret holds the base64 of | |
| # the .p8 (see CONTRIBUTING.md); decode it to a temp file per run. | |
| run: | | |
| KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" | |
| echo "$APPLE_API_KEY" | base64 --decode > "$KEY_PATH" | |
| echo "APPLE_API_KEY_PATH=$KEY_PATH" >> "$GITHUB_ENV" | |
| env: | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| - name: Build macOS | |
| # Both archs in one electron-builder invocation (#199, #178): package.json's | |
| # build.mac.target already declares arch: [x64, arm64] for dmg/zip, and | |
| # npmRebuild defaults to true, so electron-builder rebuilds native deps | |
| # (better-sqlite3) per arch on its own — the same machinery the existing | |
| # `build:mas --universal` script already relies on. This is what actually | |
| # fixes the latest-mac.yml race: electron-builder's own updateInfoBuilder | |
| # only omits an arch suffix from the mac update-manifest filename (unlike | |
| # Linux), and merges same-named update-info tasks by concatenating their | |
| # `files` entries — so one process building both archs produces a single | |
| # correctly-merged latest-mac.yml instead of two per-arch ones stepping on | |
| # each other via --clobber. | |
| run: npm run build:frontend && npm run build && npx electron-builder --mac --x64 --arm64 --publish never | |
| env: | |
| CSC_LINK: ${{ secrets.MAC_CERTS }} | |
| CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }} | |
| APPLE_API_KEY: ${{ env.APPLE_API_KEY_PATH }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| APPLE_TEAM_ID: BKDY677XJA | |
| - name: Verify macOS release assets | |
| run: | | |
| test -f release/latest-mac.yml || { echo "::error::release/latest-mac.yml is missing — macOS auto-update would be broken for this release"; exit 1; } | |
| ls release/*.zip >/dev/null 2>&1 || { echo "::error::no .zip in release/ — macOS auto-update would be broken for this release"; exit 1; } | |
| ls release/*.zip.blockmap >/dev/null 2>&1 || { echo "::error::no .zip.blockmap in release/ — macOS auto-update would be broken for this release"; exit 1; } | |
| # Guards against a regression back to #199/#178: this should list one | |
| # `files:` entry per .zip built (both archs), not just exist. | |
| # | |
| # mac.target configures both dmg and zip (each x64+arm64), and | |
| # electron-builder's update-info writer lists both under `files:` in | |
| # latest-mac.yml, not just zip — confirmed directly against a local | |
| # v26.15.3 build (v2.5.0's release-mac run failed here with a false | |
| # positive: 4 total `- url:` lines against 2 zips, because dmg | |
| # entries were being counted as if they were missing zip entries). | |
| # electron-updater's MacUpdater only ever selects a `.zip` entry, so | |
| # only those need to be counted here. | |
| ZIP_COUNT=$(ls release/*.zip | wc -l | tr -d ' ') | |
| MANIFEST_COUNT=$(grep -cE '^\s*- url: .*\.zip$' release/latest-mac.yml || true) | |
| if [ "$MANIFEST_COUNT" -ne "$ZIP_COUNT" ]; then | |
| echo "::error::latest-mac.yml lists $MANIFEST_COUNT .zip file(s) but $ZIP_COUNT .zip(s) were built — auto-update would be broken for whichever architecture's entry is missing" | |
| exit 1 | |
| fi | |
| VERIFY_DIR=$(mktemp -d) | |
| trap 'rm -rf "$VERIFY_DIR"' EXIT | |
| for ZIP in release/*.zip; do | |
| APP_DIR="$VERIFY_DIR/$(basename "$ZIP" .zip)" | |
| mkdir -p "$APP_DIR" | |
| ditto -x -k "$ZIP" "$APP_DIR" | |
| APP=$(find "$APP_DIR" -maxdepth 2 -type d -name '*.app' -print -quit) | |
| [ -n "$APP" ] || { echo "::error::no app bundle in $ZIP"; exit 1; } | |
| codesign --verify --deep --strict --verbose=2 "$APP" | |
| spctl --assess --type execute --verbose=4 "$APP" | |
| xcrun stapler validate "$APP" | |
| done | |
| - name: Upload macOS assets to GitHub release | |
| # latest-mac.yml (+ the .zip/.blockmap it points at) is what | |
| # electron-updater's MacUpdater fetches to check for/apply updates — | |
| # DMG alone can't be used for silent background updates. Every | |
| # release before this one shipped without it, so auto-update on Mac | |
| # has 404'd since v1.6.7 (see main.log: "Cannot find latest-mac.yml"). | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| gh release upload "v${VERSION}" \ | |
| release/*.dmg \ | |
| release/*.zip \ | |
| release/*.zip.blockmap \ | |
| release/latest-mac.yml \ | |
| --repo FreeOpenSourcePOS/FloCafe --clobber | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Verify freshly downloaded macOS release artifact | |
| # The "Verify macOS release assets" step above only checks the local | |
| # electron-builder output directory — it can't catch anything that | |
| # goes wrong between build and publish (upload corruption, the | |
| # `--clobber` overwrite, GitHub's own storage). Re-download what was | |
| # just published and run the same signature/Gatekeeper/notarization | |
| # checks against that, so a "clean" local build can't mask a broken | |
| # published release. | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| DOWNLOAD_DIR=$(mktemp -d) | |
| VERIFY_DIR=$(mktemp -d) | |
| trap 'rm -rf "$DOWNLOAD_DIR" "$VERIFY_DIR"' EXIT | |
| gh release download "v${VERSION}" --repo FreeOpenSourcePOS/FloCafe --pattern '*.zip' --dir "$DOWNLOAD_DIR" | |
| ls "$DOWNLOAD_DIR"/*.zip >/dev/null 2>&1 || { echo "::error::no .zip downloaded from the published release"; exit 1; } | |
| for ZIP in "$DOWNLOAD_DIR"/*.zip; do | |
| APP_DIR="$VERIFY_DIR/$(basename "$ZIP" .zip)" | |
| mkdir -p "$APP_DIR" | |
| ditto -x -k "$ZIP" "$APP_DIR" | |
| APP=$(find "$APP_DIR" -maxdepth 2 -type d -name '*.app' -print -quit) | |
| [ -n "$APP" ] || { echo "::error::no app bundle in downloaded $ZIP"; exit 1; } | |
| codesign --verify --deep --strict --verbose=2 "$APP" | |
| spctl --assess --type execute --verbose=4 "$APP" | |
| xcrun stapler validate "$APP" | |
| done | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| release-windows: | |
| runs-on: windows-latest | |
| needs: create-release | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 | |
| with: | |
| node-version: '22' | |
| - name: Install npm dependencies | |
| run: npm ci | |
| - name: Check Windows signing credentials | |
| # Shipping unsigned for now: a new WINDOWS_CERTS cert isn't a simple | |
| # portable .pfx anymore (post-2023 CA/Browser Forum rules put new | |
| # code-signing keys on a hardware token or cloud HSM), so this needs a | |
| # real signing-service integration (e.g. SignPath Foundation's free | |
| # OSS program), not just two secrets. Log which case we're in but | |
| # never fail the build — remove this whole step once that's wired up. | |
| shell: pwsh | |
| env: | |
| CSC_LINK: ${{ secrets.WINDOWS_CERTS }} | |
| CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERTS_PASSWORD }} | |
| run: | | |
| if ([string]::IsNullOrWhiteSpace($env:CSC_LINK) -or [string]::IsNullOrWhiteSpace($env:CSC_KEY_PASSWORD)) { | |
| Write-Warning "WINDOWS_CERTS/WINDOWS_CERTS_PASSWORD not set — building an UNSIGNED Windows installer. Users will see a SmartScreen 'Windows protected your PC' prompt until this is wired up." | |
| } else { | |
| Write-Host "Windows signing credentials present — building a signed installer." | |
| } | |
| - name: Build Windows | |
| run: npm run build:frontend && npm run build && npx electron-builder --win --publish never | |
| env: | |
| CSC_LINK: ${{ secrets.WINDOWS_CERTS }} | |
| CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERTS_PASSWORD }} | |
| # Empty when the secrets above aren't set — electron-builder just | |
| # builds unsigned in that case. CSC_IDENTITY_AUTO_DISCOVERY stays | |
| # disabled either way so it never picks up an unrelated identity | |
| # (mirrors release-mac; see #168, electron-userland/electron-builder#7515). | |
| CSC_IDENTITY_AUTO_DISCOVERY: false | |
| - name: Verify Windows release assets | |
| # Same class of bug as macOS's missing latest-mac.yml: electron-updater's | |
| # NSIS updater needs latest.yml + the installer's .blockmap to check for | |
| # and apply updates. Fail loudly here instead of silently shipping a | |
| # release nothing can auto-update from. | |
| run: | | |
| if (-not (Test-Path release\latest.yml)) { Write-Error "release\latest.yml is missing — Windows auto-update would be broken for this release"; exit 1 } | |
| if (-not (Get-ChildItem release\*.exe.blockmap -ErrorAction SilentlyContinue)) { Write-Error "no .exe.blockmap in release\ — Windows auto-update would be broken for this release"; exit 1 } | |
| - name: Upload Windows assets to GitHub release | |
| run: | | |
| $VERSION = node -p "require('./package.json').version" | |
| gh release upload "v$VERSION" (Get-ChildItem release\*.exe, release\*.exe.blockmap, release\latest.yml | Select-Object -ExpandProperty FullName) --repo FreeOpenSourcePOS/FloCafe --clobber | |
| env: | |
| GH_TOKEN: ${{ github.token }} |