Skip to content

chore(release): bump version to 2.5.1 #64

chore(release): bump version to 2.5.1

chore(release): bump version to 2.5.1 #64

Workflow file for this run

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 }}