Skip to content

Commit 4965d05

Browse files
authored
feat: code signing (#69)
1 parent 2bd8563 commit 4965d05

File tree

5 files changed

+193
-36
lines changed

5 files changed

+193
-36
lines changed

.github/workflows/release.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ jobs:
7272
GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_TOKEN }}
7373
NODE_ENV: production
7474
APP_VERSION: ${{ needs.determine-version.outputs.version }}
75+
APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }}
76+
APPLE_ID: ${{ secrets.APPLE_ID }}
77+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
78+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
79+
APPLE_CODESIGN_CERT_BASE64: ${{ secrets.APPLE_CODESIGN_CERT_BASE64 }}
80+
APPLE_CODESIGN_CERT_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}
81+
APPLE_CODESIGN_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_CODESIGN_KEYCHAIN_PASSWORD }}
7582
steps:
7683
- name: Checkout
7784
uses: actions/checkout@v5
@@ -88,6 +95,26 @@ jobs:
8895
cache: "pnpm"
8996
- name: Install dependencies
9097
run: pnpm install --frozen-lockfile
98+
- name: Import code signing certificate
99+
if: env.APPLE_CODESIGN_IDENTITY != ''
100+
env:
101+
CERT_BASE64: ${{ env.APPLE_CODESIGN_CERT_BASE64 }}
102+
CERT_PASSWORD: ${{ env.APPLE_CODESIGN_CERT_PASSWORD }}
103+
KEYCHAIN_PASSWORD: ${{ env.APPLE_CODESIGN_KEYCHAIN_PASSWORD }}
104+
run: |
105+
if [ -z "$CERT_BASE64" ] || [ -z "$CERT_PASSWORD" ] || [ -z "$KEYCHAIN_PASSWORD" ]; then
106+
echo "Missing code signing certificate secrets"
107+
exit 1
108+
fi
109+
KEYCHAIN="$RUNNER_TEMP/codesign.keychain-db"
110+
echo "$CERT_BASE64" | base64 --decode > "$RUNNER_TEMP/certificate.p12"
111+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
112+
security set-keychain-settings -lut 21600 "$KEYCHAIN"
113+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
114+
security import "$RUNNER_TEMP/certificate.p12" -k "$KEYCHAIN" -P "$CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
115+
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
116+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
117+
rm "$RUNNER_TEMP/certificate.p12"
91118
- name: Verify package version
92119
run: |
93120
PACKAGE_VERSION=$(jq -r .version package.json)

README.md

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,41 +32,6 @@ pnpm run make
3232
pnpm run check:write # Linting & typecheck
3333
```
3434

35-
### Building Distributables
36-
37-
To create production distributables (DMG, ZIP):
38-
39-
```bash
40-
# Package the app
41-
pnpm package
42-
43-
# Create distributables (DMG + ZIP)
44-
pnpm make
45-
```
46-
47-
Output will be in:
48-
- `out/Array-darwin-arm64/Array.app` - Packaged app
49-
- `out/make/Array-*.dmg` - macOS installer
50-
- `out/make/zip/` - ZIP archives
51-
52-
**Note:** Native modules for the DMG maker are automatically compiled via the `prePackage` hook. If you need to manually rebuild them, run:
53-
54-
```bash
55-
pnpm build-native
56-
```
57-
58-
### Auto Updates & Releases
59-
60-
Array uses Electron's built-in `autoUpdater` pointed at the public `update.electronjs.org` service for `PostHog/Array`. Every time a non-draft GitHub release is published with the platform archives, packaged apps will automatically download and install the newest version on macOS and Windows.
61-
62-
Publishing a new release:
63-
64-
1. Export a GitHub token with `repo` scope as `GH_PUBLISH_TOKEN`; set both `GH_TOKEN` and `GITHUB_TOKEN` to its value locally (e.g., in `.envrc`). In GitHub, store the token as the `GH_PUBLISH_TOKEN` repository secret.
65-
2. Run `pnpm run make` locally to sanity check artifacts, then bump `package.json`'s version (e.g., `pnpm version patch`).
66-
3. Merge the version bump into `main`. The `Publish Release` GitHub Action auto-detects the new version, tags `vX.Y.Z`, runs `pnpm run publish`, and uploads the release artifacts. You can also run the workflow manually (`workflow_dispatch`) and supply a tag if you need to re-publish.
67-
68-
Set `ELECTRON_DISABLE_AUTO_UPDATE=1` if you ever need to ship a build with auto updates disabled.
69-
7035
### Liquid Glass Icon (macOS 26+)
7136

7237
The app supports macOS liquid glass icons for a modern, layered appearance. The icon configuration is in `build/icon.icon/`.
@@ -122,3 +87,77 @@ array/
12287
- `⌘R` - Refresh task list
12388
- `⌘⇧[/]` - Switch between tabs
12489
- `⌘W` - Close current tab
90+
91+
92+
### Building Distributables
93+
94+
To create production distributables (DMG, ZIP):
95+
96+
```bash
97+
# Package the app
98+
pnpm package
99+
100+
# Create distributables (DMG + ZIP)
101+
pnpm make
102+
```
103+
104+
Output will be in:
105+
- `out/Array-darwin-arm64/Array.app` - Packaged app
106+
- `out/make/Array-*.dmg` - macOS installer
107+
- `out/make/zip/` - ZIP archives
108+
109+
**Note:** Native modules for the DMG maker are automatically compiled via the `prePackage` hook. If you need to manually rebuild them, run:
110+
111+
```bash
112+
pnpm build-native
113+
```
114+
115+
### Auto Updates & Releases
116+
117+
Array uses Electron's built-in `autoUpdater` pointed at the public `update.electronjs.org` service for `PostHog/Array`. Every time a non-draft GitHub release is published with the platform archives, packaged apps will automatically download and install the newest version on macOS and Windows.
118+
119+
Publishing a new release:
120+
121+
1. Export a GitHub token with `repo` scope as `GH_PUBLISH_TOKEN`; set both `GH_TOKEN` and `GITHUB_TOKEN` to its value locally (e.g., in `.envrc`). In GitHub, store the token as the `GH_PUBLISH_TOKEN` repository secret.
122+
2. Run `pnpm run make` locally to sanity check artifacts, then bump `package.json`'s version (e.g., `pnpm version patch`).
123+
3. Merge the version bump into `main`. The `Publish Release` GitHub Action auto-detects the new version, tags `vX.Y.Z`, runs `pnpm run publish`, and uploads the release artifacts. You can also run the workflow manually (`workflow_dispatch`) and supply a tag if you need to re-publish.
124+
125+
Set `ELECTRON_DISABLE_AUTO_UPDATE=1` if you ever need to ship a build with auto updates disabled.
126+
127+
### macOS Code Signing & Notarization
128+
129+
macOS packages are signed and notarized automatically when these environment variables are present:
130+
131+
```bash
132+
export APPLE_CODESIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)"
133+
export APPLE_ID="[email protected]"
134+
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
135+
export APPLE_TEAM_ID="TEAMID"
136+
```
137+
138+
For CI releases, configure matching GitHub Actions secrets:
139+
140+
- `APPLE_CODESIGN_IDENTITY`
141+
- `APPLE_ID`
142+
- `APPLE_APP_SPECIFIC_PASSWORD`
143+
- `APPLE_TEAM_ID`
144+
- `APPLE_CODESIGN_CERT_BASE64` – Base64-encoded `.p12` export of the Developer ID Application certificate (include the private key)
145+
- `APPLE_CODESIGN_CERT_PASSWORD` – Password used when exporting the `.p12`
146+
- `APPLE_CODESIGN_KEYCHAIN_PASSWORD` – Password for the temporary keychain the workflow creates on the runner
147+
148+
The `Publish Release` workflow imports the certificate into a temporary keychain, signs each artifact with hardened runtime enabled (using Electron’s default entitlements), and notarizes it before upload whenever these secrets are available.
149+
150+
For local testing, copy `codesign.env.example` to `.env.codesign`, fill in the real values, and load it before running `pnpm run make`:
151+
152+
```bash
153+
set -a
154+
source .env.codesign
155+
set +a
156+
pnpm run make
157+
```
158+
159+
Set `SKIP_NOTARIZE=1` if you need to generate signed artifacts without submitting to Apple (e.g., while debugging credentials):
160+
161+
```bash
162+
SKIP_NOTARIZE=1 pnpm run make
163+
```

codesign.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Developer ID signing / notarization credentials
2+
# Copy this file to `.env.codesign` (or similar) and fill in real values.
3+
4+
APPLE_CODESIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)"
5+
6+
APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
7+
APPLE_TEAM_ID="TEAMID"
8+
9+
APPLE_CODESIGN_CERT_BASE64="xxx"
10+
APPLE_CODESIGN_CERT_PASSWORD="xxx"
11+
APPLE_CODESIGN_KEYCHAIN_PASSWORD="xxx"

forge.config.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,67 @@ import { VitePlugin } from "@electron-forge/plugin-vite";
77
import { PublisherGithub } from "@electron-forge/publisher-github";
88
import type { ForgeConfig } from "@electron-forge/shared-types";
99

10+
const appleCodesignIdentity = process.env.APPLE_CODESIGN_IDENTITY;
11+
const appleTeamId = process.env.APPLE_TEAM_ID;
12+
const appleId = process.env.APPLE_ID;
13+
const appleIdPassword =
14+
process.env.APPLE_APP_SPECIFIC_PASSWORD ?? process.env.APPLE_ID_PASSWORD;
15+
const appleApiKey = process.env.APPLE_API_KEY;
16+
const appleApiKeyId = process.env.APPLE_API_KEY_ID;
17+
const appleApiIssuer = process.env.APPLE_API_ISSUER;
18+
const appleNotarizeKeychainProfile =
19+
process.env.APPLE_NOTARIZE_KEYCHAIN_PROFILE;
20+
const appleNotarizeKeychain = process.env.APPLE_NOTARIZE_KEYCHAIN;
21+
const shouldSignMacApp = Boolean(appleCodesignIdentity);
22+
const skipNotarize = process.env.SKIP_NOTARIZE === "1";
23+
24+
type NotaryToolCredentials =
25+
| {
26+
appleId: string;
27+
appleIdPassword: string;
28+
teamId: string;
29+
}
30+
| {
31+
appleApiKey: string;
32+
appleApiKeyId: string;
33+
appleApiIssuer: string;
34+
}
35+
| {
36+
keychainProfile: string;
37+
keychain?: string;
38+
};
39+
40+
let notarizeCredentials: NotaryToolCredentials | undefined;
41+
42+
if (appleId && appleIdPassword && appleTeamId) {
43+
notarizeCredentials = {
44+
appleId: appleId!,
45+
appleIdPassword: appleIdPassword!,
46+
teamId: appleTeamId!,
47+
};
48+
} else if (appleApiKey && appleApiKeyId && appleApiIssuer) {
49+
notarizeCredentials = {
50+
appleApiKey,
51+
appleApiKeyId,
52+
appleApiIssuer,
53+
};
54+
} else if (appleNotarizeKeychainProfile) {
55+
notarizeCredentials = {
56+
keychainProfile: appleNotarizeKeychainProfile,
57+
...(appleNotarizeKeychain ? { keychain: appleNotarizeKeychain } : {}),
58+
};
59+
}
60+
61+
const notarizeConfig =
62+
!skipNotarize && shouldSignMacApp && notarizeCredentials
63+
? notarizeCredentials
64+
: undefined;
65+
const osxSignConfig = shouldSignMacApp
66+
? ({
67+
identity: appleCodesignIdentity!,
68+
} satisfies Record<string, unknown>)
69+
: undefined;
70+
1071
function copyNativeDependency(
1172
dependency: string,
1273
destinationRoot: string,
@@ -50,12 +111,30 @@ const config: ForgeConfig = {
50111
CFBundleIconName: "Icon",
51112
}
52113
: {},
114+
...(osxSignConfig
115+
? {
116+
osxSign: osxSignConfig,
117+
}
118+
: {}),
119+
...(notarizeConfig
120+
? {
121+
osxNotarize: notarizeConfig,
122+
}
123+
: {}),
53124
},
54125
rebuildConfig: {},
55126
makers: [
56127
new MakerDMG({
57128
icon: "./build/app-icon.icns",
58129
format: "ULFO",
130+
...(shouldSignMacApp
131+
? {
132+
"code-sign": {
133+
"signing-identity": appleCodesignIdentity!,
134+
identifier: "com.posthog.array",
135+
},
136+
}
137+
: {}),
59138
}),
60139
new MakerZIP({}, ["darwin", "linux", "win32"]),
61140
],
@@ -87,6 +166,7 @@ const config: ForgeConfig = {
87166
},
88167
packageAfterCopy: async (_forgeConfig, buildPath) => {
89168
copyNativeDependency("node-pty", buildPath);
169+
copyNativeDependency("@recallai/desktop-sdk", buildPath);
90170
},
91171
},
92172
publishers: [

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "array",
3-
"version": "0.1.2",
3+
"version": "0.1.6",
44
"description": "Array - PostHog desktop task manager",
55
"main": ".vite/build/index.js",
66
"versionHash": "dynamic",

0 commit comments

Comments
 (0)