feedBack-desktop/.github/workflows/build.yml
OmikronApex 83b8b66fe7 fix(ci): rename entitlements file — vpk requires .entitlements extension
Nightly run 29093119570 failed:
  --signEntitlements does not have an .entitlements extension
vpk validates the file extension. Rename
resources/entitlements.mac.plist -> entitlements.mac.entitlements
and update all references (electron-builder config, sign-macos-binaries.sh,
both workflows). Content unchanged; codesign/electron-builder accept any
extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:04:31 +02:00

600 lines
30 KiB
YAML

name: Build
on:
# Release builds only: plain pushes to main are covered by ship-ci.yml
# (checks) and nightly.yml (daily main build) — no need for a full
# 3-platform matrix on every merge.
push:
tags: ['v*']
# PRs do not build automatically — every commit during review would
# otherwise re-run the full 3-platform matrix. Trigger the matrix build
# manually (Actions tab → Build → "Run workflow", pick the PR branch)
# once the review round is finished.
# Note: workflow_dispatch only allows selecting branches that exist in
# this repository; it cannot target branches from contributor forks.
# For fork PRs, the maintainer must merge to a local branch first.
#
# The slopsmith_ref input selects which slopsmith *core* ref to bundle.
# It defaults to `main` (matching the historical behaviour), but for a
# coherent release build the desktop release/vX.Y.Z branch should be
# built against the matching core branch — e.g. run this workflow on
# release/v0.3.0 with slopsmith_ref=release/v0.3.0.
workflow_dispatch:
inputs:
slopsmith_ref:
description: "Slopsmith core ref (branch or tag) to bundle, e.g. release/v0.3.0. Defaults to main."
required: false
default: main
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
platform: linux
arch: x64
- os: macos-14
platform: mac
arch: arm64
- os: windows-latest
platform: win
arch: x64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
# Read build configuration to get version numbers for setup tools
- name: Read build configuration
id: config
shell: bash
run: |
NODE_VERSION=$(node -p "require('./.build-config.json').versions.node")
PYTHON_VERSION=$(node -p "require('./.build-config.json').versions.python")
DOTNET_VERSION=$(node -p "require('./.build-config.json').versions.dotnet")
echo "node=$NODE_VERSION" >> $GITHUB_OUTPUT
echo "python=$PYTHON_VERSION" >> $GITHUB_OUTPUT
echo "dotnet=$DOTNET_VERSION" >> $GITHUB_OUTPUT
echo "Node ${NODE_VERSION}, Python ${PYTHON_VERSION}, .NET ${DOTNET_VERSION}"
- uses: actions/setup-node@v4
with:
node-version: ${{ steps.config.outputs.node }}
- uses: actions/setup-python@v5
with:
python-version: ${{ steps.config.outputs.python }}
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ steps.config.outputs.dotnet }}.x
# macOS only: import the Developer ID Application certificate into a
# temporary keychain so codesign + electron-builder + notarytool can
# find it. The keychain is wiped at the end of the job (see "Clean
# up signing keychain" below) so a re-run of the same job doesn't
# see stale state. The whole step is a no-op when the secret is
# absent — supports forks and PRs from contributors who can't
# access repository secrets.
# Step is unconditional on macOS so we always enter the shell, then
# bail out at runtime if the cert secret isn't present (forks /
# contributor PRs without secret access, etc.). step-level env: is
# NOT available in step-level if: expressions, and `secrets.*` is
# also not available there — runtime check is the only robust way
# to gate this on secret presence.
- name: Import Apple signing certificate
if: matrix.platform == 'mac'
env:
APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
set -euo pipefail
if [[ -z "${APPLE_CERTIFICATE_P12_BASE64:-}" ]]; then
echo "APPLE_CERTIFICATE_P12_BASE64 not set — skipping (unsigned build)"
exit 0
fi
# printf '%s' rather than echo — echo would append a trailing
# newline that some toolchains accept and some reject when
# piped into base64 --decode, producing a subtly corrupted
# .p12 that fails security import with the unhelpful error
# "SecKeychainItemImport: One or more parameters passed to a
# function were not valid."
printf '%s' "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -lut 21600 build.keychain
# Add build.keychain to the user search list — codesign and
# electron-builder look up identities via the search list, not
# the default-keychain pointer. Without this, identity-by-name
# lookups fall back to the unmodified login keychain and the
# imported cert is invisible. Keep login.keychain-db in the
# list so trust anchors etc. still resolve.
security list-keychains -d user -s build.keychain login.keychain-db
security import "$RUNNER_TEMP/cert.p12" -k build.keychain \
-P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
rm -f "$RUNNER_TEMP/cert.p12"
# Sanity check — verify the identity is actually present.
security find-identity -v -p codesigning build.keychain | grep -q "Developer ID Application"
# macOS only: inline the team ID into package.json so electron-builder's
# `notarize.teamId` field has a real value (it doesn't expand env vars).
# Build using the unified platform script
# All bundling and dependency installation is handled by the script.
# macOS-only env vars below are consumed by:
# APPLE_SIGNING_IDENTITY → sign-macos-binaries.sh (codesign --sign)
# APPLE_ID,
# APPLE_APP_SPECIFIC_PASSWORD,
# APPLE_TEAM_ID → electron-builder's notarytool path
# build-macos.sh derives CSC_NAME (which electron-builder uses to
# pick the signing identity) from APPLE_SIGNING_IDENTITY by
# stripping the "Developer ID Application:" prefix electron-
# builder rejects. On non-mac platforms these are empty / unused.
- name: Build
run: ./scripts/build-release.sh
shell: bash
env:
# Which slopsmith core ref to clone + bundle. Empty on push/tag
# events (the inputs context only carries values on
# workflow_dispatch), so fall back to main — preserving the
# historical default-branch behaviour. clone_slopsmith() in
# build-common.sh also defaults to main as a second safety net.
SLOPSMITH_REF: ${{ inputs.slopsmith_ref || 'main' }}
# PAT with read access to the private got-feedback repos so
# build-common.sh can clone core + the bundled plugins.
GH_CLONE_TOKEN: ${{ secrets.FEEDBACK_CLONE_TOKEN }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# electron-builder defensively skips code signing when the
# triggering event is `pull_request` ("forks usually don't have
# signing secrets"). This workflow no longer runs on the
# pull_request event — it runs on push and workflow_dispatch —
# so this env var is currently a no-op. It is kept here as a
# safety net in case a pull_request trigger is ever re-added:
# without it the notary service would reject the submission with
# "signature of the binary is invalid" + "no secure timestamp"
# + "no hardened runtime" on every nested Electron Helper app.
CSC_FOR_PULL_REQUEST: "true"
# Start a debugging session — runs BEFORE keychain cleanup so a
# signing-related failure can be inspected with the keychain still
# populated.
- name: Setup upterm session
uses: owenthereal/action-upterm@v1
if: ${{ failure() }}
with:
wait-timeout-minutes: 5
# Tester builds (non-tag only): macOS is packaged for distribution by the
# Velopack steps below, which run ONLY on tag pushes. On a manual
# workflow_dispatch, electron-builder's "dir" target
# leaves an unpackaged release/mac-*/<productName>.app that the artifact
# upload globs (AppImage/deb/exe/velopack) don't match — so testers got
# no macOS build. Zip the .app with ditto (preserves the bundle's
# internal symlinks + any signature/xattrs) so a tester-ready macOS
# archive ships from ordinary CI runs without cutting a release tag.
# Skipped on tags so it never races or duplicates the Velopack output.
# The archive is unsigned/un-notarized — testers clear Gatekeeper with
# xattr -dr com.apple.quarantine "fee[dB]ack.app"
- name: Zip macOS app for tester distribution (non-tag)
if: matrix.platform == 'mac' && !startsWith(github.ref, 'refs/tags/v')
shell: bash
run: |
set -euo pipefail
# The .app bundle is named after productName (e.g. fee[dB]ack.app),
# not the old "Slopsmith" name — find it dynamically so a rebrand
# can't break the tester zip.
app=$(ls -d release/mac*/*.app 2>/dev/null | head -n1 || true)
if [[ -z "${app:-}" || ! -d "$app" ]]; then
echo "::error::No .app bundle found under release/mac*/ — macOS build produced no bundle."
exit 1
fi
name=$(basename "$app" .app)
ditto -c -k --sequesterRsrc --keepParent "$app" "release/${name}-macos-arm64.zip"
echo "Zipped $app -> release/${name}-macos-arm64.zip"
ls -lh "release/${name}-macos-arm64.zip"
# Velopack: derive update channel from tag name. Only runs for
# tag pushes (refs/tags/v*) on win + mac — Linux still ships via
# electron-builder AppImage/deb with no auto-update.
#
# Channel mapping (plan gotcha #4 — fail on ambiguous tags rather
# than silently defaulting to stable, which would publish a
# prerelease into the stable feed):
# v1.2.3 → stable
# v1.2.3-alpha.<n> → alpha
# v1.2.3-beta.<n> → beta
# v1.2.3-rc.<n> → rc
# anything else → job fails
- name: Derive Velopack channel
id: velopack_channel
# Tag builds: derive the release channel for win + mac (linux is
# electron-builder AppImage/deb, no Velopack). Manual dispatch: also
# pack a Velopack package for WINDOWS only, on a throwaway `dev`
# channel. Without this, dispatch (the documented tester-build path)
# produced no usable Windows artifact — win-unpacked/ isn't uploaded
# and Velopack only ran on tags, so testers got just .pdb symbols.
# macOS dispatch stays Velopack-free (it already uploads a tester .zip
# from the "Zip macOS app" step), so we don't add an unsigned-update
# mac pack here.
if: >-
(startsWith(github.ref, 'refs/tags/v') && matrix.platform != 'linux')
|| (github.event_name == 'workflow_dispatch' && matrix.platform == 'win')
shell: bash
run: |
set -euo pipefail
# Read tag / event / run number from runtime env vars rather than
# interpolating GitHub Actions workflow expressions into the script
# text — a tag containing shell syntax could otherwise execute
# (template injection).
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
# Strip any prerelease suffix from the app version (0.2.9-beta.1 ->
# 0.2.9), then tag a unique dev build so successive dispatch builds
# produce distinct Velopack versions. The `dev` channel keeps these
# out of the alpha/beta/rc/stable auto-update feeds.
base="$(node -p "require('./package.json').version")"
base="${base%%-*}"
channel=dev
version="${base}-dev.${GITHUB_RUN_NUMBER}"
echo "channel=$channel" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Manual dispatch -> channel=$channel version=$version"
exit 0
fi
tag="${GITHUB_REF_NAME}"
if [[ "$tag" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)-alpha\.[0-9]+$ ]]; then
channel=alpha
version="${BASH_REMATCH[1]}-alpha.${tag##*-alpha.}"
elif [[ "$tag" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)-beta\.[0-9]+$ ]]; then
channel=beta
version="${BASH_REMATCH[1]}-beta.${tag##*-beta.}"
elif [[ "$tag" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)-rc\.[0-9]+$ ]]; then
channel=rc
version="${BASH_REMATCH[1]}-rc.${tag##*-rc.}"
elif [[ "$tag" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
channel=stable
version="${BASH_REMATCH[1]}"
else
echo "::error::Tag '$tag' does not match a known Velopack channel pattern (vX.Y.Z[-{alpha,beta,rc}.N]). Refusing to default to stable."
exit 1
fi
echo "channel=$channel" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Derived channel=$channel version=$version from tag $tag"
- name: Install Velopack CLI (vpk)
if: steps.velopack_channel.outputs.channel != ''
shell: bash
run: |
set -euo pipefail
# Pin vpk to the exact version of the velopack npm SDK used by the
# app (package.json) — Velopack ships the CLI and SDK in lockstep,
# and an unpinned install would pull a different latest on each run.
dotnet tool install -g vpk --version 0.0.1589-ga2c5a97
echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
# Windows: pack the electron-builder unpacked dir into Velopack
# release assets (per-machine MSI, *-full.nupkg, *-delta.nupkg,
# releases.<ch>.json). Unsigned for now — see plan gotcha #3.
#
# The channel is rid-scoped (win-x64-<track>): Velopack requires a
# unique channel per os/rid so the win + macOS releases.<ch>.json
# manifests don't collide when published to one GitHub release.
# The client's update-manager.ts builds the same name to match.
#
# --msi emits a machine-wide MSI bootstrap (validated decision #2 in
# the integration plan: install to %ProgramFiles%\Slopsmith). The flag
# is a bool — verified against vpk source at our pinned commit
# a2c5a97 (src/vpk/Velopack.Vpk/Commands/Packaging/WindowsPackCommand.cs):
# AddOption<bool>((v) => BuildMsi = v, "--msi")
#
# --instLocation defaults to "Either" (a dual-mode MSI where the user
# picks at install time). We force PerMachine so the MSI is always
# machine-wide, matching the plan.
#
# vpk also writes a per-user Setup.exe by default — we delete it
# right after packing so only ONE install method ships. Reasons:
# - Two install methods for the same app split the user base in
# half for every support question ("where is my app installed?").
# - The MSI is what the in-app NSIS migration banner downloads, so
# it has to ship anyway.
# The Velopack feed assets (.nupkg, releases.<channel>.json) drive
# auto-update for both install methods, so dropping Setup.exe does
# NOT break auto-update for users who installed via the MSI.
#
# Velopack's WindowsVelopackLocator falls back to %LOCALAPPDATA% for
# the packages staging dir when Program Files isn't writable, so the
# auto-updater works without elevation. Apply-step elevation is
# handled by Update.exe's manifest.
- name: Velopack pack (Windows)
if: matrix.platform == 'win' && steps.velopack_channel.outputs.channel != ''
shell: bash
run: |
set -euo pipefail
# The launcher exe is the electron-builder 'dir' output, whose name is
# the SANITIZED productName (e.g. fee[dB]ack -> feedback.exe), not the
# raw productName or the packId. Find it dynamically — there's a
# single .exe at the win-unpacked root.
mainexe=$(basename "$(ls release/win-unpacked/*.exe 2>/dev/null | head -n1)")
if [[ -z "${mainexe:-}" ]]; then
echo "::error::No .exe launcher in release/win-unpacked/"; ls -1 release/win-unpacked/ | head -20; exit 1
fi
vpk pack \
--packId feedback \
--packVersion "${{ steps.velopack_channel.outputs.version }}" \
--channel "win-x64-${{ steps.velopack_channel.outputs.channel }}" \
--packDir release/win-unpacked \
--mainExe "$mainexe" \
--msi \
--instLocation PerMachine \
-o release/velopack
# Drop the per-user Setup.exe (default vpk output) so only the
# per-machine MSI ships. Match permissively — the exact name
# varies between Velopack versions (Slopsmith-win-Setup.exe vs
# Slopsmith-win-x64-Setup.exe etc.). nocaseglob so a casing
# change in vpk's output can't slip an installer past the glob.
shopt -s nullglob nocaseglob
removed=0
for f in release/velopack/*setup.exe; do
echo "Removing per-user installer: $f"
rm -f "$f"
removed=$((removed+1))
done
# Enforce the MSI-only policy: hard-fail if any setup installer
# survived cleanup (e.g. an rm failure) rather than publishing
# two installer types.
if compgen -G "release/velopack/*setup.exe" > /dev/null; then
echo "::error::Setup.exe artifact(s) remain after cleanup; refusing to publish dual installer types."
ls -1 release/velopack/*setup.exe
exit 1
fi
if [[ "$removed" -eq 0 ]]; then
# MSI-only is the goal — if vpk emits no Setup.exe at all that
# is fine, not a failure. Note it in case the naming drifted.
echo "::notice::No Setup.exe output from vpk; shipping MSI-only artifacts."
fi
# Sanity check: the MSI must exist or the release is meaningless.
# compgen -G, not `ls *.msi` — with nullglob on, an unmatched glob
# expands to nothing and `ls` would list the cwd and pass silently.
if ! compgen -G "release/velopack/*.msi" > /dev/null; then
echo "::error::vpk pack --msi x64 did not produce a .msi file in release/velopack/"
exit 1
fi
# macOS: same, but pass Apple signing identity + notarization
# creds so Velopack codesigns + notarizes the bundle it generates
# (otherwise Gatekeeper blocks auto-applied updates — plan
# gotcha #2). vpk codesigns via --signAppIdentity and notarizes via
# --notaryProfile, which names a `xcrun notarytool` credential
# profile — vpk has no raw --apple-id/--password/--team-id flags,
# so the profile must be created with `notarytool store-credentials`
# before vpk pack runs.
# --packDir points directly at the .app bundle: Velopack uses a
# path ending in .app as a prebuilt bundle. A non-.app folder
# would instead trigger auto-bundle mode (which also needs
# --icon). electron-builder's "dir" target writes the bundle to
# release/mac-arm64/<productName>.app on the arm64 runner.
- name: Velopack pack (macOS)
if: matrix.platform == 'mac' && steps.velopack_channel.outputs.channel != ''
shell: bash
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
# The signed/notarized branch needs ALL four Apple secrets — vpk
# would fail outright if handed an empty --notary* value. If any
# is missing (forks, partial secret configs) fall back to an
# unsigned pack, matching the all-four check in build-macos.sh.
# The .app bundle + its executable are named after productName
# (e.g. fee[dB]ack.app), not the packId — resolve them dynamically
# so a rebrand can't break the pack (Velopack otherwise looks for
# <packId>.app/Contents/MacOS/<packId> and fails).
app=$(ls -d release/mac-arm64/*.app 2>/dev/null | head -n1 || true)
if [[ -z "${app:-}" || ! -d "$app" ]]; then
echo "::error::No .app bundle in release/mac-arm64/"
exit 1
fi
mainexe=$(basename "$app" .app)
if [[ -z "${APPLE_SIGNING_IDENTITY:-}" || -z "${APPLE_ID:-}" \
|| -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" \
|| -z "${APPLE_TEAM_ID:-}" ]]; then
echo "::warning::Apple signing/notarization secrets incomplete — packing macOS Velopack release UNSIGNED. Gatekeeper will block auto-updates on user machines."
vpk pack \
--packId feedback \
--packVersion "${{ steps.velopack_channel.outputs.version }}" \
--channel "osx-arm64-${{ steps.velopack_channel.outputs.channel }}" \
--packDir "$app" \
--mainExe "$mainexe" \
-o release/velopack
else
# vpk notarizes through a notarytool *credential profile*, not
# raw Apple-ID flags (it shells out to `xcrun notarytool`, which
# takes --keychain-profile). Create the profile first, then hand
# vpk --notaryProfile. Do NOT pass --keychain: notarytool's
# --keychain wants a file *path* (not a name like
# "build.keychain"), and notarytool stores/reads credential
# profiles in the login keychain by default. vpk's own internal
# notarytool call also defaults to the login keychain, so
# leaving --keychain off keeps both sides pointing at the same
# store. (build.keychain only holds the signing cert, which
# codesign finds via the keychain search list.)
# --signEntitlements is mandatory: vpk re-signs the bundle, and a
# codesign without an entitlements file STRIPS the ones
# electron-builder applied (audio-input, allow-jit, ...) — the
# hardened-runtime app then gets mic access auto-denied with no
# TCC prompt.
xcrun notarytool store-credentials "velopack-notary" \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--team-id "$APPLE_TEAM_ID"
vpk pack \
--packId feedback \
--packVersion "${{ steps.velopack_channel.outputs.version }}" \
--channel "osx-arm64-${{ steps.velopack_channel.outputs.channel }}" \
--packDir "$app" \
--mainExe "$mainexe" \
-o release/velopack \
--signAppIdentity "$APPLE_SIGNING_IDENTITY" \
--signEntitlements "$PWD/resources/entitlements.mac.entitlements" \
--notaryProfile "velopack-notary"
fi
# macOS only: drop the temporary keychain so a re-run doesn't see
# leftover state. Placed AFTER the Velopack pack steps (which call
# codesign + notarytool and need the keychain present on tag builds).
# always() ensures cleanup even on Velopack pack failures.
- name: Clean up signing keychain
if: always() && matrix.platform == 'mac'
run: |
security delete-keychain build.keychain 2>/dev/null || true
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: slopsmith-${{ matrix.platform }}-${{ matrix.arch }}
# release/win-unpacked/ is intentionally NOT uploaded: it is the
# uncompressed intermediate build tree (~1 GB on Windows) that
# Velopack packs into release/velopack/. Uploading it duplicated
# the whole app in the artifact and ballooned the Windows artifact
# ~6x (0.27 -> 1.58 GiB). The packed velopack/ output is all that
# downstream needs. See issue #185.
# build/Release/*.pdb: Windows debug symbols for the native addon and
# the sandbox host. Archived as a workflow artifact (it is NOT
# attached to the public Release) so a tester's crash minidump can be
# symbolised. Empty on Linux/macOS — if-no-files-found: warn covers it.
path: |
release/*.AppImage
release/*.deb
release/*.exe
release/velopack/**/*
build/Release/*.pdb
# Tester-only macOS archive from the "Zip macOS app" step above.
# Only produced on non-tag runs; the `release` job's globs below
# do NOT include *.zip, so it never leaks into a tagged Release.
release/*.zip
if-no-files-found: warn
# Default retention (90 days) snowballed the org Actions storage cap
# — a single matrix run uploads ~2.5 GB across the three platforms,
# and old builds piled up for months. Tag builds don't need long
# retention either: the `release` job downloads and attaches these
# to the GitHub Release within the same workflow run.
retention-days: 7
release:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
# Every release is published with prerelease=false, but only
# stable (bare vX.Y.Z) tags get make_latest=true. Reason:
#
# The Velopack JS SDK at our pinned version 0.0.1589-ga2c5a97
# routes any github.com URL through AutoSource → GithubSource,
# and AutoSource hardcodes the third constructor arg
# (`prerelease: false`). UpdateOptions has no Prerelease field,
# so there is no SDK-side override. If we tagged alpha/beta/rc as
# prerelease=true on GitHub, the in-app updater on those channels
# would silently see zero updates (verified live during the
# v0.2.9-alpha.102 validation: toggling the prerelease flag off
# made the release visible to the running app).
#
# Using make_latest=false on alpha/beta/rc tags keeps the GitHub
# UI's "Latest release" badge pinned to the most recent stable
# tag — humans browsing the repo still see stable as latest, even
# though the underlying release rows are all prerelease=false.
# Per the action-gh-release@v2 docs:
# "Drafts and prereleases cannot be set as latest"
# so we can't use prerelease=true + make_latest=true together.
#
# Channel scoping for the auto-updater is independent of either
# flag — Velopack picks the right release by the channel-named
# manifest asset (releases.<channel>.json), iterating recent
# releases until it finds the matching channel.
#
# draft is intentionally NOT set — Velopack clients can't see
# draft releases either way.
- name: Create Release
uses: softprops/action-gh-release@v2
with:
# Explicitly select only real distributables:
# - Linux ships electron-builder .AppImage / .deb
# - Windows + macOS ship via Velopack (release/velopack/**):
# MSI, *-osx.zip, *.nupkg, releases.<channel>.json
# win-unpacked/** and a bare *.exe glob are deliberately omitted —
# win-unpacked/ is for CI inspection only.
# No electron-builder *.dmg / *.zip globs: macOS is Velopack-only,
# so they would match nothing or duplicate the Velopack *-osx.zip.
#
# The velopack glob is `artifacts/**/velopack/**/*`, NOT
# `artifacts/**/release/velopack/**/*`. actions/upload-artifact@v4
# strips the longest common path prefix from upload inputs before
# archiving, so when the build job uploads both
# `release/win-unpacked/**` and `release/velopack/**`, the common
# prefix `release/` is stripped and files inside the artifact are
# rooted at `velopack/...` / `win-unpacked/...`. The old
# `**/release/velopack/**` glob never matched anything, which is
# why prior tagged releases shipped only Linux assets and the
# Windows + macOS Velopack feeds were silently empty (the in-app
# updater would 404 on releases.<channel>.json and report "no
# updates available").
files: |
artifacts/**/*.AppImage
artifacts/**/*.deb
artifacts/**/velopack/**/*
generate_release_notes: true
# All tags non-prerelease (Velopack SDK limitation, see the
# long comment above). Only stable tags become the GitHub
# "Latest release" — alpha/beta/rc are still visible to the
# in-app updater but won't be promoted in the repo UI.
prerelease: false
make_latest: ${{ (contains(github.ref_name, '-alpha.') || contains(github.ref_name, '-beta.') || contains(github.ref_name, '-rc.')) && 'false' || 'true' }}
# Mirror release version to slopsmith core
# Requires FEEDBACK_CLONE_TOKEN secret
notify-slopsmith:
needs: release
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Dispatch VERSION sync to slopsmith
env:
GH_TOKEN: ${{ secrets.FEEDBACK_CLONE_TOKEN }}
REF_NAME: ${{ github.ref_name }}
run: |
version="${REF_NAME#v}"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Tag $REF_NAME is not vX.Y.Z — skipping slopsmith VERSION sync."
exit 0
fi
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::FEEDBACK_CLONE_TOKEN secret is missing; cannot dispatch."
exit 1
fi
gh api -X POST repos/got-feedback/feedback/dispatches \
-f event_type=desktop-released \
-f "client_payload[version]=$version"
echo "Dispatched desktop-released event (version=$version) to slopsmith."