This document describes the complete CI/CD pipeline for building, signing, and distributing GnuNae across all platforms.
- Overview
- Architecture
- Docker Image Versioning
- Platform-Specific Packaging
- Environment Variables
- Local Development Setup
- GitHub Actions Workflow
- Troubleshooting
GnuNae uses a multi-platform build pipeline that:
- Builds on
macos-latest,windows-latest, andubuntu-latestrunners - Signs binaries using platform-appropriate certificates
- Creates a GitHub Release with all artifacts when a version tag is pushed
The release workflow triggers on version tags:
git tag v0.8.14
git push --tags
GnuNae requires Node.js, npm, and Codex CLI to function. Runtime provisioning differs by platform:
| Aspect | Windows EXE | Windows APPX | macOS DMG/ZIP | macOS MAS | Linux |
|---|---|---|---|---|---|
| npm Build | pack:win |
pack:win |
pack:mac |
pack:mac-mas |
pack:linux |
| GitHub Actions | ✅ Yes | ✅ Yes (auto-upload) | ✅ Yes | ❌ Local (deploy:mas) |
✅ Yes |
| Output Format | .exe (NSIS) |
.appx |
.dmg .zip |
.pkg |
.AppImage .deb |
| Code Signing | Azure Trusted Signing | Unsigned (MS Store signs) | Developer ID + Notarization | 3rd Party Mac Developer | GPG |
| Node.js | ✅ Embedded | ✅ Embedded | ✅ Embedded | ✅ Embedded | ⬇️ Auto-download |
| npm | ✅ Bundled | ✅ Bundled | ✅ Bundled | ✅ Bundled | ⬇️ Bundled with Node |
| Codex CLI | ✅ Pre-installed | ✅ Pre-installed | ✅ Pre-installed | ✅ Pre-installed | ⬇️ npm install |
| Storage | %LOCALAPPDATA%/GnuNae/ |
%LOCALAPPDATA%/GnuNae/ |
App Resources | App Resources | ~/.config/GnuNae/ |
Legend: ✅ = Included/Yes, ⬇️ = Downloaded automatically on first run, ❌ = Not included
Important
MAS is NOT built via GitHub Actions. Build locally:
- MAS:
npm run deploy:mas→ Builds and uploads to App Store Connect automatically
APPX is fully automated. The build-msstore job in release.yml builds the APPX and uploads it to Partner Center using the msstore CLI.
On app startup, RuntimeManager.ensureRuntime() checks if runtime is ready:
- Windows: Runtime is embedded, no download needed
- macOS/MAS/Linux: If not ready, downloads Node.js from nodejs.org and runs
npm install @openai/codex
The runtime is stored in the user's app data directory (Application Support/AppData), which is accessible in all sandbox environments including MAS.
┌─────────────────────────────────────────────────────────────────────────────┐
│ GitHub Actions Release Workflow │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─── build job ────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ macOS Runner │ │ Windows │ │ Linux Runner │ │ │
│ │ │ DMG + ZIP │ │ Runner │ │ AppImage │ │ │
│ │ │ (Developer │ │ NSIS │ │ DEB │ │ │
│ │ │ ID signed + │ │ (Azure │ │ (GPG signed) │ │ │
│ │ │ notarized) │ │ Signing) │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ ↓ GitHub Release ↓ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── build-msstore job (parallel) ─────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────┐ │ │
│ │ │ Windows │ │ │
│ │ │ Runner │──→ msstore CLI ──→ Partner Center │ │
│ │ │ APPX │ │ │
│ │ │ (unsigned) │ │ │
│ │ └──────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ LOCAL BUILDS (not in CI/CD) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ npm run │ │
│ │ deploy:mas │──→ xcrun altool ──→ App Store Connect │
│ │ (PKG arm64) │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The sandbox Docker image is versioned to match the app version automatically.
The docker.yml workflow triggers on:
- Push to
mainwith changes indocker/** - Version tags (
v*) - triggered bynpm version
on:
push:
tags:
- 'v*' # Triggers on npm version tagsWhen you run npm version patch (pushes v0.8.33):
| Docker Tag | Value |
|---|---|
ghcr.io/fkiller/gnunae/sandbox:0.8.33 |
Exact version |
ghcr.io/fkiller/gnunae/sandbox:0.8 |
Major.minor |
ghcr.io/fkiller/gnunae/sandbox:latest |
Latest stable |
docker-manager.ts reads the app version from package.json and requests the matching Docker image:
function getDockerImageName(): string {
const version = require('../../package.json').version;
return `ghcr.io/fkiller/gnunae/sandbox:${version}`;
}- Make changes to app and/or
docker/Dockerfile - Run
npm version patch - GitHub Actions builds app releases AND Docker image with matching version
- App v0.8.33 automatically requests Docker image
0.8.33
Purpose: Direct distribution via GitHub releases, website download, or homebrew.
Signing: Apple Developer ID Application certificate + Notarization
Output: GnuNae-mac-{arch}.dmg, GnuNae-mac-{arch}.zip
| Certificate Type | Description | Where to Get |
|---|---|---|
| Developer ID Application | Signs the .app bundle | Apple Developer Portal → Certificates |
package.json:
{
"build": {
"mac": {
"hardenedRuntime": true,
"gatekeeperAssess": false,
"notarize": true,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist"
}
}
}npm run pack:mac
# Equivalent to: npm run build && node scripts/load-env.js --mac dmg zip| Variable | Description | GitHub Secret |
|---|---|---|
APPLE_DEVELOPER_ID_APPLICATION_P12 |
Base64-encoded .p12 certificate | ✅ |
APPLE_CERTIFICATE_PASSWORD |
Password for .p12 file | ✅ |
APPLE_TEAM_ID |
10-character Apple Team ID | ✅ |
APPLE_ID |
Apple ID email (for notarization) | ✅ |
APPLE_APP_SPECIFIC_PASSWORD |
App-specific password for notarization | ✅ |
macOS DMG/ZIP packages include embedded Node.js and Codex CLI so users don't need to download anything:
| Component | Build Step | Package Location |
|---|---|---|
| Node.js + npm | npm run download-node-darwin-{arch} |
resources/runtime-darwin-{arch}/ |
| Codex CLI | npm run install-codex |
resources/codex/ |
The pack:mac script runs both before packaging:
npm run download-node-darwin-arm64 && npm run download-node-darwin-x64 && \
npm run install-codex && npm run build && electron-builder --mac dmg zipPurpose: Distribution via Mac App Store.
Signing:
- 3rd Party Mac Developer Application (signs the app)
- 3rd Party Mac Developer Installer (signs the pkg)
Output: GnuNae-mac-{arch}.pkg
| Certificate Type | Description | Where to Get |
|---|---|---|
| 3rd Party Mac Developer Application | Signs the .app for App Store | Apple Developer Portal |
| 3rd Party Mac Developer Installer | Signs the .pkg installer | Apple Developer Portal |
| Provisioning Profile | Links app to App Store Connect | Apple Developer Portal → Profiles |
package.json:
{
"build": {
"mas": {
"entitlements": "build/entitlements.mas.plist",
"entitlementsInherit": "build/entitlements.mas.inherit.plist",
"hardenedRuntime": false,
"provisioningProfile": "certs/GnuNae.provisionprofile",
"extendInfo": {
"ElectronTeamID": "${env.APPLE_TEAM_ID}"
}
}
}
}# Build only (no upload):
npm run pack:mac-mas
# Build + upload to App Store Connect (fully automated):
npm run deploy:mas| Variable | Description | Source |
|---|---|---|
APPLE_CERTIFICATE_APPLICATION_P12 |
Base64-encoded 3rd Party Mac Developer Application .p12 | GitHub Secret |
APPLE_CERTIFICATE_INSTALLER_P12 |
Base64-encoded 3rd Party Mac Developer Installer .p12 | GitHub Secret |
APPLE_CERTIFICATE_PASSWORD |
Password for .p12 files | .env.local |
APPLE_PROVISIONING_PROFILE |
Base64-encoded .provisionprofile | GitHub Secret |
APPLE_TEAM_ID |
10-character Apple Team ID | .env.local |
ASC_API_KEY_ID |
App Store Connect API Key ID | .env.local |
ASC_API_ISSUER_ID |
App Store Connect Issuer ID | .env.local |
The deploy:mas script uses xcrun altool with API Key authentication to upload.
- Go to App Store Connect → Users and Access → Integrations → API Keys
- Generate a new key with App Manager role
- Note the Key ID and Issuer ID
- Download the
.p8file (can only be downloaded once!) - Save the
.p8file to:~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8 - Add to
.env.local:ASC_API_KEY_ID=YOUR_KEY_ID ASC_API_ISSUER_ID=YOUR_ISSUER_ID
build/entitlements.mas.plist (App Store sandbox):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>MAS packages include embedded Node.js and Codex CLI so users don't need to download anything:
| Component | Build Step | Package Location |
|---|---|---|
| Node.js + npm | npm run download-node-darwin-{arch} |
resources/runtime-darwin-{arch}/ |
| Codex CLI | npm run install-codex |
resources/codex/ |
The pack:mac-mas script runs both before packaging:
npm run download-node-darwin-arm64 && npm run download-node-darwin-x64 && \
npm run install-codex && npm run build && electron-builder --mac masThe afterPack.js hook copies node_modules directories (which electron-builder excludes by default):
// scripts/afterPack.js
exports.default = async function(context) {
// Copies resources/{runtime,codex}/node_modules to packaged app
};At runtime, the app detects MAS builds via process.mas and uses embedded resources.
Purpose: Direct distribution via GitHub releases, website download.
Signing: Azure Trusted Signing (Azure Code Signing)
Output: GnuNae-win-x64.exe (NSIS installer), portable .exe
- Azure subscription with Trusted Signing resource
- Code Signing Account created
- Certificate Profile created
- App registration for authentication
The signing is configured dynamically via CLI flags (not in package.json for security):
GitHub Actions Command:
npx electron-builder --win nsis portable --publish never \
--config.win.signAndEditExecutable=true \
--config.win.azureSignOptions.endpoint="https://eus.codesigning.azure.net/" \
--config.win.azureSignOptions.codeSigningAccountName="$AZURE_CODE_SIGNING_NAME" \
--config.win.azureSignOptions.certificateProfileName="$AZURE_CERT_PROFILE_NAME" \
--config.win.azureSignOptions.publisherName="$BUILD_PUBLISHER_NAME"npm run pack:win
# Includes: download-node, install-codex, build, electron-builderWindows packages include embedded Node.js and Codex CLI so users don't need to download anything:
| Component | Build Step | Package Location |
|---|---|---|
| Node.js + npm | npm run download-node |
resources/runtime/ |
| Codex CLI | npm run install-codex |
resources/codex/ |
The pack:win script runs both before packaging:
npm run download-node && npm run install-codex && npm run build && electron-builderThe afterPack.js hook copies node_modules directories (which electron-builder excludes by default):
// scripts/afterPack.js
exports.default = async function(context) {
// Copies resources/{runtime,codex}/node_modules to packaged app
};At runtime, the embedded runtime is migrated to %LOCALAPPDATA%/GnuNae/ for stability (avoids temp folder deletion issues with portable apps).
| Variable | Description | GitHub Secret |
|---|---|---|
AZURE_TENANT_ID |
Azure Active Directory tenant ID | ✅ |
AZURE_CLIENT_ID |
App registration client ID | ✅ |
AZURE_CLIENT_SECRET |
App registration client secret | ✅ |
AZURE_CODE_SIGNING_NAME |
Code Signing Account name | ✅ |
AZURE_CERT_PROFILE_NAME |
Certificate Profile name | ✅ |
BUILD_PUBLISHER_NAME |
Publisher CN (e.g., "CN=Company, O=...") | ✅ |
Purpose: Distribution via Microsoft Store.
Signing: NOT signed locally - Microsoft Store signs the package during submission.
Output: GnuNae-win-x64.appx
package.json:
{
"build": {
"appx": {
"identityName": "BigDad.GnuNae",
"publisher": "${env.MSSTORE_PUBLISHER_CN}",
"publisherDisplayName": "BigDad",
"displayName": "GnuNae",
"applicationId": "GnuNae",
"backgroundColor": "#0a0a0f"
}
}
}npm run pack:win
# This builds NSIS, Portable, and APPX targets
# Upload the .appx file to MS Store Partner Center| Variable | Description | GitHub Secret |
|---|---|---|
MSSTORE_PUBLISHER_CN |
Publisher CN from Partner Center (e.g., "CN=12345678-1234-...") | ✅ |
CSC_IDENTITY_AUTO_DISCOVERY |
Set to false to disable signing |
N/A |
- Go to Microsoft Partner Center
- Navigate to: Apps → Your App → Product Identity
- Copy the Publisher value (format:
CN=GUID)
Purpose: Distribution via GitHub releases, package managers.
Signing: GPG signature
Output: GnuNae-linux-x64.AppImage, GnuNae-linux-x64.deb
package.json:
{
"build": {
"linux": {
"icon": "assets/gnunae.png",
"target": ["AppImage", "deb"],
"category": "Network"
}
}
}npm run pack:linux| Variable | Description | GitHub Secret |
|---|---|---|
GPG_PRIVATE_KEY |
Base64-encoded GPG private key | ✅ |
GPG_KEY |
GPG key ID (e.g., C90FF75C007E7301) |
Hardcoded in workflow |
APPLE_CERTIFICATE_PASSWORD |
Passphrase for GPG key (reused) | ✅ |
# Create key (4096-bit RSA, no expiration)
gpg --full-generate-key
# Export for GitHub Secret
gpg --pinentry-mode loopback --armor --export-secret-keys YOUR_KEY_ID | base64 | pbcopy| Variable | Description | Local | GitHub |
|---|---|---|---|
BUILD_AUTHOR_NAME |
Author name for package.json | .env.local |
Secret |
BUILD_AUTHOR_EMAIL |
Author email for package.json | .env.local |
Secret |
BUILD_PUBLISHER_NAME |
Publisher CN for Windows signing | .env.local |
Secret |
| Variable | Description | Local | GitHub |
|---|---|---|---|
APPLE_TEAM_ID |
Apple Developer Team ID | .env.local |
Secret |
APPLE_ID |
Apple ID email | .env.local |
Secret |
APPLE_APP_SPECIFIC_PASSWORD |
Notarization password | .env.local |
Secret |
APPLE_DEVELOPER_ID_APPLICATION_P12 |
Developer ID cert (base64) | Keychain | Secret |
APPLE_CERTIFICATE_APPLICATION_P12 |
App Store app cert (base64) | Keychain | Secret |
APPLE_CERTIFICATE_INSTALLER_P12 |
App Store installer cert (base64) | Keychain | Secret |
APPLE_CERTIFICATE_PASSWORD |
.p12 password | .env.local |
Secret |
APPLE_PROVISIONING_PROFILE |
Provisioning profile (base64) | File | Secret |
| Variable | Description | Local | GitHub |
|---|---|---|---|
ASC_API_KEY_ID |
API Key ID (from App Store Connect) | .env.local |
N/A |
ASC_API_ISSUER_ID |
Issuer ID (from App Store Connect) | .env.local |
N/A |
API Key .p8 file |
Stored at ~/.appstoreconnect/private_keys/ |
File | N/A |
| Variable | Description | Local | GitHub |
|---|---|---|---|
AZURE_TENANT_ID |
Azure AD tenant ID | .env.local |
Secret |
AZURE_CLIENT_ID |
App registration client ID | .env.local |
Secret |
AZURE_CLIENT_SECRET |
App registration secret | .env.local |
Secret |
AZURE_CODE_SIGNING_NAME |
Code Signing Account name | .env.local |
Secret |
AZURE_CERT_PROFILE_NAME |
Certificate Profile name | .env.local |
Secret |
| Variable | Description | Local | GitHub |
|---|---|---|---|
MSSTORE_PUBLISHER_CN |
Publisher CN from Partner Center | .env.local |
Secret |
Create .env.local in the project root (gitignored):
# Build Configuration
BUILD_AUTHOR_NAME=Your Name
BUILD_AUTHOR_EMAIL=your@email.com
BUILD_PUBLISHER_NAME=CN=Your Name, O=Your Company, L=City, S=State, C=US
# Apple (macOS builds)
APPLE_TEAM_ID=XXXXXXXXXX
APPLE_ID=your@apple.id
APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx
APPLE_CERTIFICATE_PASSWORD=your_p12_password
# App Store Connect API (for deploy:mas)
ASC_API_KEY_ID=XXXXXXXXXX
ASC_API_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Note: Also place AuthKey_<KEY_ID>.p8 in ~/.appstoreconnect/private_keys/
# Azure Trusted Signing (Windows builds)
AZURE_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_CLIENT_SECRET=your_client_secret
AZURE_CODE_SIGNING_NAME=YourCodeSigningAccount
AZURE_CERT_PROFILE_NAME=YourCertProfile
# Microsoft Store (APPX builds)
MSSTORE_PUBLISHER_CN=CN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx- Export certificates from Apple Developer Portal as .p12
- Double-click to import into Keychain
- For local builds, macOS will use Keychain certificates automatically
For local Windows builds with Azure signing:
az login
az account set --subscription "Your Subscription"In your GitHub repository, go to Settings → Secrets and variables → Actions and add:
BUILD_AUTHOR_NAMEBUILD_AUTHOR_EMAILBUILD_PUBLISHER_NAME
APPLE_TEAM_IDAPPLE_IDAPPLE_APP_SPECIFIC_PASSWORDAPPLE_CERTIFICATE_PASSWORDAPPLE_DEVELOPER_ID_APPLICATION_P12(base64)APPLE_CERTIFICATE_APPLICATION_P12(base64)APPLE_CERTIFICATE_INSTALLER_P12(base64)APPLE_PROVISIONING_PROFILE(base64)
Note
App Store Connect API credentials (ASC_API_KEY_ID, ASC_API_ISSUER_ID, .p8 key) are local-only — they are not needed in GitHub Secrets since MAS builds run locally via npm run deploy:mas.
AZURE_TENANT_IDAZURE_CLIENT_IDAZURE_CLIENT_SECRETAZURE_CODE_SIGNING_NAMEAZURE_CERT_PROFILE_NAME
MSSTORE_PUBLISHER_CNMSSTORE_SELLER_ID(from Partner Center → Account settings → Identifiers)MSSTORE_PRODUCT_ID(your app's Store Product ID)
# macOS/Linux
base64 -i certificate.p12 -o certificate.p12.base64
cat certificate.p12.base64
# Windows PowerShell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.p12"))- Ensure
hardenedRuntime: truein build config - Check notarization completed successfully
Error: Your app uses or references the following non-public or deprecated APIs: rg with PCRE2 symbols
Cause: If @openai/codex is in dependencies, it gets bundled into the app's node_modules/. The package includes rg (ripgrep) binary which statically links PCRE2 library - Apple considers these non-public APIs.
Solution: @openai/codex is in devDependencies (not dependencies), so it's NOT bundled in the app.
- macOS installs Codex CLI at runtime to
~/Library/Application Support/GnuNae/codex/ - Windows uses
resources/codex/viaextraResources - The app only spawns Codex as an external CLI, never imports it as a module
If you see this error, ensure @openai/codex is NOT in dependencies in package.json.
Error: Your app appears to be associated with ChatGPT, which does not have requisite permits to operate in China
Cause: Chinese regulations require DST (Deep Synthesis Technologies) permits for ChatGPT/OpenAI services.
Solution: Exclude China from App Store availability:
- In App Store Connect → Pricing and Availability → Deselect "China mainland"
This is the standard approach for ChatGPT-integrated apps.
- Verify sandbox entitlements in
entitlements.mas.plist - Ensure no hardened runtime entitlements in MAS build
- The
afterPack.jshook copies node_modules after packaging - Run
npm run download-node && npm run install-codexbefore building
- Ensure
MSSTORE_PUBLISHER_CNenvironment variable is set - Format must be:
CN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
- Verify Azure CLI is logged in:
az login - Check app registration has signing permissions
- Ensure certificate profile is active
- Update version in
package.json - Commit changes
- Create and push tag:
git tag v0.x.x && git push --tags - Monitor GitHub Actions workflow
- Verify GitHub Release created with all artifacts (DMG, ZIP, EXE, AppImage, DEB)
- Verify APPX uploaded to MS Partner Center (check
build-msstorejob) - Run
npm run deploy:masto build and upload MAS .pkg to App Store Connect