3 Commits
Author SHA1 Message Date
Zack SheppardandClaude Opus 5.5 0693668b08 Set the author to Traveler Dev Ltd <hi@traveler.dev> in every package.json (#3)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:20:32 +01:00
Zack SheppardandClaude Opus 5.5 26872c7396 Sign and notarize macOS release builds (#1)
* Sign and notarize macOS release builds

Release builds of the Mac app now run in the protected release-signing
environment (approval required, v* tags only), are signed with the
Developer ID certificate, notarized with the App Store Connect key, and
checked with codesign, spctl and stapler before upload. Push and PR
builds stay ad-hoc signed and never see the secrets.

- electron-builder config moved to electron-builder.config.cjs, which
  switches between ad-hoc and Developer ID signing based on CSC_LINK
- Hardened runtime entitlements for Electron
- Local network usage description and Bonjour services in Info.plist
  for the macOS 15+ local network prompt
- README: signing setup and current "Open Anyway" steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Only export signing variables for signed Mac builds

electron-builder treats an empty CSC_LINK as a certificate path, so PR
builds failed with "apps/desktop not a file". The variables are now
exported only by the signing step, which also checks every secret is
present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep Mac signing secrets inside the build step

They were exported through GITHUB_ENV, which makes them visible to every
later step and action in the job (artifact upload, post steps). They now
live only in the build step's environment, and the notarization key file
is deleted when the step exits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Build pull requests once, not once per push and once per PR

Push builds now run only for main; branch commits are built through their
pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:12:19 +01:00
Zack SheppardandClaude Opus 5.5 c6f1c8b203 Add the app icon and put the logo at the top of the README (#2)
build/icon.icns is the macOS version (Apple's rounded-square grid with
padding and shadow); build/icon.png is the full-bleed square electron-builder
uses for Windows and Linux.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:09:24 +01:00
11 changed files with 201 additions and 70 deletions

No files matched your search

+77 -7
View File
@@ -1,12 +1,13 @@
name: Build
# Every push and pull request: check the code, then build installers for each
# Every push to main and every pull request: check the code, then build installers for each
# platform and attach them to the run as artifacts. The release workflow calls
# this same file with a version to stamp into the app.
on:
# Pushes to main, plus pull requests. Branch pushes aren't built separately, so a
# pull request's commits run once (as pull_request), not twice.
push:
branches: ["**"]
tags-ignore: ["**"]
branches: [main]
pull_request:
workflow_call:
inputs:
@@ -18,6 +19,10 @@ on:
description: Commit to build.
type: string
default: ""
sign_mac:
description: Sign and notarize the macOS build (needs the release-signing environment).
type: boolean
default: false
concurrency:
group: build-${{ github.workflow }}-${{ inputs.ref || github.ref }}
@@ -56,9 +61,6 @@ jobs:
fail-fast: false
matrix:
include:
- name: macOS
os: macos-latest
args: --mac --arm64 --x64
- name: Windows
os: windows-latest
args: --win --x64
@@ -98,7 +100,7 @@ jobs:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
run: |
pnpm build
pnpm exec electron-builder ${{ matrix.args }} --publish never
pnpm exec electron-builder --config electron-builder.config.cjs ${{ matrix.args }} --publish never
- uses: actions/upload-artifact@v4
with:
name: frameloader-${{ matrix.os }}
@@ -109,3 +111,71 @@ jobs:
apps/desktop/release/*.AppImage
apps/desktop/release/*.deb
if-no-files-found: error
# macOS is its own job so release builds can run inside the protected
# release-signing environment (approval required, v* tags only), which holds
# the Developer ID certificate and the notarization key. Pushes and pull
# requests never touch it and produce ad-hoc signed builds.
package-mac:
name: Package (macOS${{ inputs.sign_mac && ', signed' || '' }})
needs: check
runs-on: macos-latest
environment: ${{ inputs.sign_mac && 'release-signing' || '' }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Stamp version
if: inputs.version != ''
run: node scripts/version.mjs set "${{ inputs.version }}"
# Signing secrets live only in this step's environment, never in GITHUB_ENV, so no
# later step or action (artifact upload, post steps) can read them. They're exported
# only for signed builds: electron-builder treats an empty CSC_LINK as a file path.
- name: Build installers
working-directory: apps/desktop
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
SIGN_MAC: ${{ inputs.sign_mac }}
SIGN_CSC_LINK: ${{ secrets.CSC_LINK }}
SIGN_CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
SIGN_APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
SIGN_APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
SIGN_APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: |
if [ "$SIGN_MAC" = "true" ]; then
for name in SIGN_CSC_LINK SIGN_CSC_KEY_PASSWORD SIGN_APPLE_API_KEY SIGN_APPLE_API_KEY_ID SIGN_APPLE_API_ISSUER; do
if [ -z "${!name}" ]; then
echo "::error::release-signing secret ${name#SIGN_} is missing. See README → Signing."
exit 1
fi
done
key_file="$RUNNER_TEMP/notary.p8"
printf '%s' "$SIGN_APPLE_API_KEY" > "$key_file"
trap 'rm -f "$key_file"' EXIT
export CSC_LINK="$SIGN_CSC_LINK" CSC_KEY_PASSWORD="$SIGN_CSC_KEY_PASSWORD"
export APPLE_API_KEY="$key_file" APPLE_API_KEY_ID="$SIGN_APPLE_API_KEY_ID" APPLE_API_ISSUER="$SIGN_APPLE_API_ISSUER"
fi
unset SIGN_CSC_LINK SIGN_CSC_KEY_PASSWORD SIGN_APPLE_API_KEY SIGN_APPLE_API_KEY_ID SIGN_APPLE_API_ISSUER
pnpm build
pnpm exec electron-builder --config electron-builder.config.cjs --mac --arm64 --x64 --publish never
- name: Check signature
if: inputs.sign_mac
working-directory: apps/desktop
run: |
app="release/mac-arm64/Frameloader.app"
codesign --verify --deep --strict --verbose=2 "$app"
spctl --assess --type execute --verbose=2 "$app"
xcrun stapler validate "$app"
- uses: actions/upload-artifact@v4
with:
name: frameloader-macos
path: |
apps/desktop/release/*.dmg
apps/desktop/release/*.zip
if-no-files-found: error
+2
View File
@@ -37,9 +37,11 @@ jobs:
name: Build
needs: prepare
uses: ./.github/workflows/build.yml
secrets: inherit
with:
version: ${{ needs.prepare.outputs.version }}
ref: refs/tags/${{ github.event.release.tag_name }}
sign_mac: true
attach:
name: Attach installers to the release
+6 -2
View File
@@ -1,3 +1,7 @@
<p align="center">
<img src="apps/desktop/build/icon.png" alt="Frameloader logo" width="160" />
</p>
# Frameloader
[www.frameloader.com](https://www.frameloader.com)
@@ -64,7 +68,7 @@ Frameloader has no analytics, telemetry, crash reporting or accounts. It connect
## Builds and releases
Every push and pull request runs `.github/workflows/build.yml`: typecheck, tests, the website build, then installers for macOS (arm64 and x64), Windows x64, and Linux x64 and arm64. The installers are attached to the run as artifacts.
Every push to `main` and every pull request runs `.github/workflows/build.yml`: typecheck, tests, the website build, then installers for macOS (arm64 and x64), Windows x64, and Linux x64 and arm64. The installers are attached to the run as artifacts.
To ship a release:
@@ -74,7 +78,7 @@ To ship a release:
Nothing is pushed to `main`, so this works with branch protection. The version in `apps/desktop/package.json` is only what local and CI builds report; releases take their version from the tag.
Builds are not code-signed yet. On macOS the app is ad-hoc signed, so the first launch needs right-click → Open (or `xattr -dr com.apple.quarantine /Applications/Frameloader.app`). Windows SmartScreen will warn until the installer is signed.
**Signing.** Release builds of the Mac app are signed with a Developer ID certificate and notarized, so they open normally. The certificate and the App Store Connect notarization key live only in the `release-signing` GitHub environment as `CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_API_KEY`, `APPLE_API_KEY_ID` and `APPLE_API_ISSUER`. That environment only accepts `v*` tags and needs approval, so the release run pauses until you approve it. Push and pull-request builds are ad-hoc signed; to open one on macOS 15 or later, use System Settings → Privacy & Security → Open Anyway, or run `xattr -dr com.apple.quarantine /Applications/Frameloader.app`. Windows builds are unsigned, so SmartScreen warns before the installer runs.
Installed apps notice new releases on their own: they check GitHub's releases API and show a banner with a link once a release has its installers attached. Nothing is downloaded or installed automatically, and Settings → Updates turns the check off.
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- V8's JIT needs these under the hardened runtime. -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

+85
View File
@@ -0,0 +1,85 @@
// electron-builder config. Pass with: electron-builder --config electron-builder.config.cjs
//
// macOS signing depends on the environment:
// - Release builds (the release-signing GitHub environment) set CSC_LINK, so the app is
// signed with the Developer ID certificate, uses the hardened runtime, and is notarized
// with the App Store Connect key (APPLE_API_KEY / APPLE_API_KEY_ID / APPLE_API_ISSUER).
// - Everything else (local builds, pushes, pull requests) is ad-hoc signed.
const signed = Boolean(process.env.CSC_LINK);
const base = {
"appId": "dev.traveler.frameloader",
"productName": "Frameloader",
"directories": {
"output": "release",
"buildResources": "build"
},
"files": [
"dist/**/*",
"package.json",
"!dist/test/**",
"!dist/**/*.map"
],
"extraResources": [
{
"from": "vendor/devkit-utils",
"to": "devkit-utils",
"filter": [
"**/*",
"!**/__pycache__/**"
]
}
],
"mac": {
"category": "public.app-category.utilities",
"target": [
"dmg",
"zip"
],
"artifactName": "Frameloader-mac-${arch}.${ext}",
"extendInfo": {
"NSLocalNetworkUsageDescription": "Frameloader connects to your Steam Frame on your local network to install and launch apps.",
"NSBonjourServices": [
"_steamos-devkit._tcp",
"_ssh._tcp"
]
}
},
"win": {
"target": [
"nsis",
"zip"
],
"artifactName": "Frameloader-win-${arch}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true
},
"linux": {
"target": [
"AppImage",
"deb"
],
"category": "Utility",
"artifactName": "Frameloader-linux-${arch}.${ext}",
"executableName": "frameloader"
},
"electronVersion": "44.5.1"
};
module.exports = {
...base,
mac: {
...base.mac,
...(signed
? {
hardenedRuntime: true,
entitlements: "build/entitlements.mac.plist",
entitlementsInherit: "build/entitlements.mac.plist",
notarize: true,
}
: { identity: "-", hardenedRuntime: false }),
},
};
+8 -61
View File
@@ -4,7 +4,10 @@
"version": "0.0.1",
"description": "Drop an APK, Windows app, or Linux build onto your Steam Frame and it shows up in your Steam library.",
"license": "MIT",
"author": "Zack <zack@traveler.dev>",
"author": {
"name": "Traveler Dev Ltd",
"email": "hi@traveler.dev"
},
"private": true,
"main": "dist/main.cjs",
"type": "module",
@@ -14,10 +17,10 @@
"start": "pnpm build && electron .",
"typecheck": "tsc -p tsconfig.main.json --noEmit && tsc -p tsconfig.renderer.json --noEmit",
"test": "pnpm build && node --test dist/test/*.test.cjs",
"dist": "pnpm build && electron-builder --publish never",
"dist:mac": "pnpm build && electron-builder --mac --arm64 --publish never",
"dist:win": "pnpm build && electron-builder --win --x64 --publish never",
"dist:linux": "pnpm build && electron-builder --linux --x64 --arm64 --publish never",
"dist": "pnpm build && electron-builder --config electron-builder.config.cjs --publish never",
"dist:mac": "pnpm build && electron-builder --config electron-builder.config.cjs --mac --arm64 --publish never",
"dist:win": "pnpm build && electron-builder --config electron-builder.config.cjs --win --x64 --publish never",
"dist:linux": "pnpm build && electron-builder --config electron-builder.config.cjs --linux --x64 --arm64 --publish never",
"screenshots": "pnpm build && electron scripts/screenshot.cjs"
},
"devDependencies": {
@@ -35,62 +38,6 @@
"typescript": "^7.0.2",
"yauzl": "^3.2.0"
},
"build": {
"appId": "dev.traveler.frameloader",
"productName": "Frameloader",
"directories": {
"output": "release",
"buildResources": "build"
},
"files": [
"dist/**/*",
"package.json",
"!dist/test/**",
"!dist/**/*.map"
],
"extraResources": [
{
"from": "vendor/devkit-utils",
"to": "devkit-utils",
"filter": [
"**/*",
"!**/__pycache__/**"
]
}
],
"mac": {
"category": "public.app-category.utilities",
"target": [
"dmg",
"zip"
],
"artifactName": "Frameloader-mac-${arch}.${ext}",
"identity": "-",
"hardenedRuntime": false
},
"win": {
"target": [
"nsis",
"zip"
],
"artifactName": "Frameloader-win-${arch}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true
},
"linux": {
"target": [
"AppImage",
"deb"
],
"category": "Utility",
"artifactName": "Frameloader-linux-${arch}.${ext}",
"executableName": "frameloader"
},
"electronVersion": "44.5.1"
},
"homepage": "https://www.frameloader.com",
"repository": {
"type": "git",
+4
View File
@@ -4,6 +4,10 @@
"private": true,
"description": "The Frameloader website.",
"license": "MIT",
"author": {
"name": "Traveler Dev Ltd",
"email": "hi@traveler.dev"
},
"type": "module",
"scripts": {
"dev": "vite",
+4
View File
@@ -2,6 +2,10 @@
"name": "frameloader-monorepo",
"private": true,
"license": "MIT",
"author": {
"name": "Traveler Dev Ltd",
"email": "hi@traveler.dev"
},
"packageManager": "pnpm@11.24.0",
"scripts": {
"build": "pnpm -r build",
+4
View File
@@ -4,6 +4,10 @@
"private": true,
"description": "Shared color, type and shape tokens for the Frameloader app and website.",
"license": "MIT",
"author": {
"name": "Traveler Dev Ltd",
"email": "hi@traveler.dev"
},
"exports": {
"./tokens.css": "./tokens.css"
},