name: Build on: push: branches: [main] 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 plain push to # main or a manual workflow_dispatch, electron-builder's "dir" target # leaves an unpackaged release/mac-*/.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. → alpha # v1.2.3-beta. → beta # v1.2.3-rc. → 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..json). Unsigned for now — see plan gotcha #3. # # The channel is rid-scoped (win-x64-): Velopack requires a # unique channel per os/rid so the win + macOS releases..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((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..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/.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 # .app/Contents/MacOS/ 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.) 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" \ --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..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..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..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."