diff --git a/.github/actions/build-electron/action.yml b/.github/actions/build-electron/action.yml index 24e269aee..0ae36701d 100644 --- a/.github/actions/build-electron/action.yml +++ b/.github/actions/build-electron/action.yml @@ -1,3 +1,6 @@ +name: "Build Electron App" +description: "Builds and packages the Electron app for different platforms" + inputs: os: description: "One of the supported platforms: macos, linux, windows" @@ -8,13 +11,45 @@ inputs: extension: description: "Platform specific extensions to copy in the output: dmg, deb, rpm, exe, zip" required: true + runs: using: composite steps: - - name: Set up Python for appdmg to be installed + # Certificate setup + - name: Import Apple certificates + if: inputs.os == 'macos' + uses: apple-actions/import-codesign-certs@v2 + with: + p12-file-base64: ${{ env.APPLE_APP_CERTIFICATE_BASE64 }} + p12-password: ${{ env.APPLE_APP_CERTIFICATE_PASSWORD }} + keychain: build + keychain-password: ${{ github.run_id }} + + - name: Install Installer certificate + if: inputs.os == 'macos' + uses: apple-actions/import-codesign-certs@v2 + with: + p12-file-base64: ${{ env.APPLE_INSTALLER_CERTIFICATE_BASE64 }} + p12-password: ${{ env.APPLE_INSTALLER_CERTIFICATE_PASSWORD }} + keychain: build + keychain-password: ${{ github.run_id }} + # We don't need to create a keychain here because we're using the build keychain that was created in the previous step + create-keychain: false + + - name: Verify certificates + if: inputs.os == 'macos' + shell: bash + run: | + echo "Available signing identities:" + security find-identity -v -p codesigning build.keychain + + - name: Set up Python and other macOS dependencies if: ${{ inputs.os == 'macos' }} shell: bash - run: brew install python-setuptools + run: | + brew install python-setuptools + brew install create-dmg + - name: Install dependencies for RPM and Flatpak package building if: ${{ inputs.os == 'linux' }} shell: bash @@ -24,21 +59,160 @@ runs: FLATPAK_ARCH=$(if [[ ${{ inputs.arch }} = 'arm64' ]]; then echo 'aarch64'; else echo 'x86_64'; fi) FLATPAK_VERSION='24.08' flatpak install --user --no-deps --arch $FLATPAK_ARCH --assumeyes runtime/org.freedesktop.Platform/$FLATPAK_ARCH/$FLATPAK_VERSION runtime/org.freedesktop.Sdk/$FLATPAK_ARCH/$FLATPAK_VERSION org.electronjs.Electron2.BaseApp/$FLATPAK_ARCH/$FLATPAK_VERSION + + # Build setup - name: Install dependencies shell: bash run: npm ci + - name: Update build info shell: bash run: npm run chore:update-build-info - - name: Run electron-forge + + # Critical debugging configuration + - name: Run electron-forge build with enhanced logging shell: bash - run: npm run electron-forge:make -- --arch=${{ inputs.arch }} + env: + DEBUG: "electron-osx-sign*,@electron/notarize*,electron-forge:*" + ELECTRON_NOTARIZE_DEBUG: 1 + ELECTRON_ENABLE_LOGGING: 1 + ELECTRON_DEBUG_NOTARIZATION: 1 + # Pass through required environment variables for signing and notarization + APPLE_TEAM_ID: ${{ env.APPLE_TEAM_ID }} + APPLE_ID: ${{ env.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }} + run: | + # Map OS names to Electron Forge platform names + if [ "${{ inputs.os }}" = "macos" ]; then + PLATFORM="darwin" + elif [ "${{ inputs.os }}" = "windows" ]; then + PLATFORM="win32" + else + PLATFORM="${{ inputs.os }}" + fi + + npm run electron-forge:make -- \ + --arch=${{ inputs.arch }} \ + --platform=$PLATFORM \ + --verbose + + # Add DMG signing step + - name: Sign DMG + if: inputs.os == 'macos' + shell: bash + run: | + echo "Signing DMG file..." + dmg_file=$(find out -name "*.dmg" -print -quit) + if [ -n "$dmg_file" ]; then + echo "Found DMG: $dmg_file" + # Get the first valid signing identity from the keychain + SIGNING_IDENTITY=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application" | head -1 | sed -E 's/.*"([^"]+)".*/\1/') + if [ -z "$SIGNING_IDENTITY" ]; then + echo "Error: No valid Developer ID Application certificate found in keychain" + exit 1 + fi + echo "Using signing identity: $SIGNING_IDENTITY" + # Sign the DMG + codesign --force --sign "$SIGNING_IDENTITY" --options runtime --timestamp "$dmg_file" + # Notarize the DMG + xcrun notarytool submit "$dmg_file" --apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_TEAM_ID" --wait + # Staple the notarization ticket + xcrun stapler staple "$dmg_file" + else + echo "No DMG found to sign" + fi + + - name: Verify code signing + if: inputs.os == 'macos' + shell: bash + run: | + echo "Verifying code signing for all artifacts..." + + # First check the .app bundle + echo "Looking for .app bundle..." + app_bundle=$(find out -name "*.app" -print -quit) + if [ -n "$app_bundle" ]; then + echo "Found app bundle: $app_bundle" + echo "Verifying app bundle signing..." + codesign --verify --deep --strict --verbose=2 "$app_bundle" + echo "Displaying app bundle signing info..." + codesign --display --verbose=2 "$app_bundle" + + echo "Checking entitlements..." + codesign --display --entitlements :- "$app_bundle" + + echo "Checking notarization status..." + xcrun stapler validate "$app_bundle" || echo "Warning: App bundle not notarized yet" + else + echo "No .app bundle found to verify" + fi + + # Then check DMG if it exists + echo "Looking for DMG..." + dmg_file=$(find out -name "*.dmg" -print -quit) + if [ -n "$dmg_file" ]; then + echo "Found DMG: $dmg_file" + echo "Verifying DMG signing..." + codesign --verify --deep --strict --verbose=2 "$dmg_file" + echo "Displaying DMG signing info..." + codesign --display --verbose=2 "$dmg_file" + + echo "Checking DMG notarization..." + xcrun stapler validate "$dmg_file" || echo "Warning: DMG not notarized yet" + else + echo "No DMG found to verify" + fi + + # Finally check ZIP if it exists + echo "Looking for ZIP..." + zip_file=$(find out -name "*.zip" -print -quit) + if [ -n "$zip_file" ]; then + echo "Found ZIP: $zip_file" + echo "Note: ZIP files are not code signed, but their contents should be" + fi + - name: Prepare artifacts shell: bash run: | - mkdir -p upload; - for ext in ${{ inputs.extension }}; - do - file=$(find out/make -name "*.$ext" -print -quit); - cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}.$ext"; - done + mkdir -p upload + + if [ "${{ inputs.os }}" = "macos" ]; then + # For macOS, we need to look in specific directories based on the maker + echo "Collecting macOS artifacts..." + + # Look for DMG files recursively + echo "Looking for DMG files..." + dmg_file=$(find out -name "*.dmg" -print -quit) + if [ -n "$dmg_file" ]; then + echo "Found DMG: $dmg_file" + cp "$dmg_file" "upload/TriliumNextNotes-${{ github.ref_name }}-darwin-${{ inputs.arch }}.dmg" + else + echo "Warning: No DMG file found" + fi + + # Look for ZIP files recursively + echo "Looking for ZIP files..." + zip_file=$(find out -name "*.zip" -print -quit) + if [ -n "$zip_file" ]; then + echo "Found ZIP: $zip_file" + cp "$zip_file" "upload/TriliumNextNotes-${{ github.ref_name }}-darwin-${{ inputs.arch }}.zip" + else + echo "Warning: No ZIP file found" + fi + else + # For other platforms, use the existing logic but with better error handling + echo "Collecting artifacts for ${{ inputs.os }}..." + for ext in ${{ inputs.extension }}; do + echo "Looking for .$ext files..." + file=$(find out -name "*.$ext" -print -quit) + if [ -n "$file" ]; then + echo "Found $file for extension $ext" + cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}.$ext" + else + echo "Warning: No file found with extension .$ext" + fi + done + fi + + echo "Final contents of upload directory:" + ls -la upload/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d7e80cfed..b469ac79b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,6 +33,36 @@ jobs: runs-on: ${{ matrix.os.image }} steps: - uses: actions/checkout@v4 + + # Set up certificates and keychain for macOS + - name: Install Apple Certificates + if: matrix.os.name == 'macos' + env: + APP_CERTIFICATE_BASE64: ${{ secrets.APPLE_APP_CERTIFICATE_BASE64 }} + APP_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_CERTIFICATE_PASSWORD }} + INSTALLER_CERTIFICATE_BASE64: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_BASE64 }} + INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ github.run_id }} + run: | + # Create keychain + 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 -t 3600 -u build.keychain + + # Import application certificate + echo "$APP_CERTIFICATE_BASE64" | base64 --decode > application.p12 + security import application.p12 -k build.keychain -P "$APP_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + rm application.p12 + + # Import installer certificate + echo "$INSTALLER_CERTIFICATE_BASE64" | base64 --decode > installer.p12 + security import installer.p12 -k build.keychain -P "$INSTALLER_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + rm installer.p12 + + # Update keychain settings + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + - name: Set up node & dependencies uses: actions/setup-node@v4 with: @@ -43,6 +73,17 @@ jobs: os: ${{ matrix.os.name }} arch: ${{ matrix.arch }} extension: ${{ matrix.os.extension }} + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + + # Clean up keychain after build + - name: Clean up keychain + if: matrix.os.name == 'macos' && always() + run: | + security delete-keychain build.keychain + - name: Publish artifacts uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e9e9c851..dc523893b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,15 @@ jobs: os: ${{ matrix.os.name }} arch: ${{ matrix.arch }} extension: ${{ join(matrix.os.extension, ' ') }} + env: + APPLE_APP_CERTIFICATE_BASE64: ${{ secrets.APPLE_APP_CERTIFICATE_BASE64 }} + APPLE_APP_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_CERTIFICATE_PASSWORD }} + APPLE_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_BASE64 }} + APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + - name: Publish release uses: softprops/action-gh-release@v2 with: diff --git a/entitlements.plist b/entitlements.plist new file mode 100644 index 000000000..f975ed18a --- /dev/null +++ b/entitlements.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + \ No newline at end of file diff --git a/forge.config.cjs b/forge.config.cjs index 91000d63c..76e3ef81c 100644 --- a/forge.config.cjs +++ b/forge.config.cjs @@ -1,7 +1,7 @@ const path = require("path"); const fs = require("fs-extra"); -const APP_NAME = "TriliumNext Notes"; +const APP_NAME = "TriliumNextNotes"; const extraResourcesForPlatform = getExtraResourcesForPlatform(); const baseLinuxMakerConfigOptions = { @@ -17,33 +17,37 @@ module.exports = { overwrite: true, asar: true, icon: "./images/app-icons/icon", + osxSign: {}, + osxNotarize: { + appleId: process.env.APPLE_ID, + appleIdPassword: process.env.APPLE_ID_PASSWORD, + teamId: process.env.APPLE_TEAM_ID + }, extraResource: [ - // Moved to root - ...extraResourcesForPlatform, + // All resources should stay in Resources directory for macOS + ...(process.platform === "darwin" ? [] : extraResourcesForPlatform), - // Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS) + // These always go in Resources "translations/", "node_modules/@highlightjs/cdn-assets/styles" ], afterComplete: [ (buildPath, _electronVersion, platform, _arch, callback) => { - for (const resource of extraResourcesForPlatform) { - const baseName = path.basename(resource); + // Only move resources on non-macOS platforms + if (platform !== "darwin") { + for (const resource of extraResourcesForPlatform) { + const baseName = path.basename(resource); + const sourcePath = path.join(buildPath, "resources", baseName); + const destPath = (baseName !== "256x256.png") + ? path.join(buildPath, baseName) + : path.join(buildPath, "icon.png"); - // prettier-ignore - const sourcePath = (platform === "darwin") - ? path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName) - : path.join(buildPath, "resources", baseName); - - // prettier-ignore - const destPath = (baseName !== "256x256.png") - ? path.join(buildPath, baseName) - : path.join(buildPath, "icon.png"); - - // Copy files from resources folder to root - fs.move(sourcePath, destPath) - .then(() => callback()) - .catch((err) => callback(err)); + fs.move(sourcePath, destPath) + .then(() => callback()) + .catch((err) => callback(err)); + } + } else { + callback(); } } ]