Repository: Tyrrrz/DiscordChatExporter
Pull request #1375: Bundle the app for macOS
Author: login: Yudi
Labels: name: enhancement
Created: 2025-05-05T02:21:13Z | Merged: 2025-05-10T00:03:55Z

<!--

**Important**

This pull request should be linked to an issue that describes the problem it addresses.
If there is no corresponding issue yet, please create one first.

An open issue provides a good place to iron out technical requirements and discuss potential solutions.
Creating a pull request without prior discussion may very likely lead to wasted effort.

-->

<!-- Please specify the issue(s) addressed by this pull request: -->

Closes #1249

<!--

Also keep in mind:

- Pull requests should be as small as possible. Split larger changes into multiple pull requests if needed.
- Follow the coding style and conventions already established in the project. When in doubt, ask in the comments.
- You can add review comments to your own code. Use this to highlight something important or to seek further input from reviewers.

-->

TODO:
- [x] Docs (Gatekeeper)
- [x] ~~Icon~~
    - Use low resolution icon for now
- [x] Create the `.app` on pack instead. Then users will be able to use CI builds more easily
	- [x] ~~Check if chmod change breaks Linux releases~~
- [x] Test CLI
	- No changes required

This PR adds steps to the `pack` and `deploy` jobs.

`pack`:
- Set permissions for non-win releases
- Bundle the .app by arranging the files
	1. Create .app _folder_ structure inside a `staging/` directory
	    ```
	    DiscordChatExporter.app/
          Contents/
            Info.plist
            MacOS/
              [All publish files go here]
            Resources/
              AppIcon.icns
	    ```
	1. Move favicon.icns to `staging/.app/Contents/Resources/AppIcon.icns`
	1. Create [Information Property List](https://developer.apple.com/documentation/bundleresources/information-property-list) (Info.plist) inside the `publish/` directory
	1. Move all files from `publish` to `staging/.app/Contents/MacOS`
	1. Move .app to `publish` folder

`deploy`:
- Moved "set permissions" to `pack` job
	- Then users are able to run CI builds


<img width="282" alt="image" src="https://github.com/user-attachments/assets/04ff42fe-1a02-440a-acfe-a3a533de988d" />


Linked issue #1249: Bundle the app for MacOS
### Version

v2.43.3

### Flavor

GUI (Graphical User Interface), CLI (Command-Line Interface)

### Platform

OSX

### Export format

_No response_

### Steps to reproduce

Explained below

### Details

If you try to run the app after downloading, it won't launch due to macOS's security policies. You need to manually bypass the app signing check to work around this issue: https://github.com/Tyrrrz/DiscordChatExporter/discussions/1234#discussioncomment-9640812

Packaging the app as `.app` bundle may help resolve this issue. Ideally, we'd want to integrate the bundling stage in the existing CI process.

### Checklist

- [X] I have looked through existing issues to make sure that this bug has not been reported before
- [X] I have provided a descriptive title for this issue
- [X] I have made sure that this bug is reproducible on the latest version of the application
- [X] I have provided all the information needed to reproduce this bug as efficiently as possible
- [ ] I have sponsored this project
- [ ] I have not read any of the above and just checked all the boxes to submit the issue
Linked issue comments: user: login: toadchild
created_at: 2024-06-02T22:35:23Z
body: Commenting from the other thread, since I brought up the app bundle.

If you have an unsigned bundle, it is treated similarly to how Windows treats an unsigned .exe file. You have to right click -> open the first time in order to confirm that you want to run unsigned code, but after that it remembers the app and won't prompt you again.

I haven't done mac app stuff in a while, but iirc there's nothing magic about what xcode does when making a .app bundle. You just need the correct layout and an info.plist file that tells the OS how to set up the runtime.

https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19
author_association: NONE
user: login: DannyDorito24
created_at: 2024-11-18T19:43:07Z
body: Due to changes in Sequoia, it is likely this will no longer fix the issue, but it probably wouldn't have before either. The right click > open workaround now requires you to authenticate in settings (sometimes *twice*) to open the main process, and any child processes also have to be authenticated for (depending on how they are launched, I can't say I completely understand it). I've also heard rumors that [some people are having their machines completely refuse to run unsigned code](https://forums.macrumors.com/threads/bug-or-intentional-macos-15-1-completely-removes-ability-to-launch-unsigned-applications.2441792/) but I personally haven't seen this even on a completely stock machine straight out of the box, and people in the linked MacRumors thread call it out as fake.

Anyway, likely the only way to avoid the error is to sign the binary, which of course requires a developer license from apple. Even before Sequoia, executables being in a bundle don't magically make them run, they still have to be signed. For now, some detailed instructions on how to work around this issue would be good, which I could certainly make if nobody else has a Mac (I daily drive one). I personally have gatekeeper [disabled](https://discussions.apple.com/thread/255759797) but this is *not* a good idea for the average user as it's akin to turning off SmartScreen and UAC in windows. So for now, the annoying workaround in settings is the best it'll be. 

In terms of the app bundle, while more user friendly for an applications folder, there would need to be some changes to how the app works. Applications on macOS are pretty static and use `~/Library/Application Support` to store any changing data, which as far as I can tell doesn't happen now (but may not be needed? I haven't read through the code thoroughly). 

All in all, I'm not sure it would accomplish anything, and macOS is pretty finicky with unsigned app bundles in general, especially if they're made through github actions in my experience, often claiming that the app is damaged, and there is no workaround for that (other than disabling gatekeeper).
author_association: NONE
user: login: Tyrrrz
created_at: 2024-11-19T12:59:06Z
body: > Anyway, likely the only way to avoid the error is to sign the binary, which of course requires a developer license from apple. Even before Sequoia, executables being in a bundle don't magically make them run, they still have to be signed.

I see. I guess with a bundle at least you'd only need to manually allow the app once, at least of once per every file?
author_association: OWNER
user: login: DannyDorito24
created_at: 2024-11-19T18:00:37Z
body: In the past that has been the case but with the restrictions now extending to dylibs, that may no longer be true. I can't easily test it without trying to put together the app myself, so maybe I'll try that. As toadchild mentioned, it's really just an info.plist and a basic folder structure. `.app` is basically just a styled folder, as are many packages on macOS.

There's also the issue of Microsoft's [discontinuing of VS for macOS](https://learn.microsoft.com/en-us/visualstudio/releases/2022/what-happened-to-vs-for-mac), instead directing users to VScode which does not have the same support as the old VS did... Thanks Microsoft. That's going to make things a bit more difficult.
author_association: NONE
user: login: DannyDorito24
created_at: 2024-11-19T19:03:32Z
body: With a bit of googling, it looks like publishing directly to an app bundle is annoying. However, I got it to launch by just moving files into place and creating app.plist manually. Cursed, but it works. Sadly it's only for one architecture at a time, but I suppose that's what it is now. I can't tell what gatekeeper is doing to it since it was made on my local machine, but I'll try it on another machine later.
author_association: NONE
user: login: Tyrrrz
created_at: 2024-12-01T17:02:12Z
body: > However, I got it to launch by just moving files into place and creating app.plist manually.

Can't we also just do that as a way to "publish directly to an app bundle"? Or is there something else to it that we'd be missing?
author_association: OWNER
user: login: toadchild
created_at: 2024-12-02T07:34:52Z
body: That had been my initial thought, yeah. Make a build rule that copies all the files into the directory structure and adds a very basic plist.
author_association: NONE
user: login: Lewiscowles1986
created_at: 2024-12-28T13:31:29Z
body: So I do see download for OSX arm64, however they contain a lot of dll files...
author_association: NONE
user: login: DannyDorito24
created_at: 2024-12-28T18:06:53Z
body: That’s unavoidable. There is no official app bundle yet (afaik) but even when there is one it’s still going to be .NET on macOS so DLLs are to be expected. 
author_association: NONE
user: login: DannyDorito24
created_at: 2024-12-28T18:08:53Z
body: > Can't we also just do that as a way to "publish directly to an app bundle"? Or is there something else to it that we'd be missing?

That should work, but I didn’t find a way to do that directly in the editors that exist on macOS. Not sure about VS on Windows. It should be super easy to script it though. Not sure what the Microsoft intended way to do it is, if there is one. 
author_association: NONE
user: login: Tyrrrz
created_at: 2025-01-01T11:17:37Z
body: > It should be super easy to script it though.

It would have to be scripted either way since it'd be running on CI
author_association: OWNER
user: login: laurikskl
created_at: 2025-02-11T00:01:33Z
body: `sudo spctl --master-disable`

Should disable all protection and helps avoid having to click through those pesky "allow anyway" messages. 

After running the command you have to open Privacy & Security and there will be a new option there called "Always".

Remember to enable it again after you're done.
author_association: NONE
user: login: DannyDorito24
created_at: 2025-03-12T20:18:23Z
body: Disabling gatekeeper entirely is not a good solution for the average user, and is akin to disabling windows defender and smartscreen. A signed app bundle is a better way to go, but at the very least and without paying apple, finding a way to bundle that is more apple approved and doesn't require disabling gatekeeper would be ideal. In my opinion, going into settings to allow an app like this once is far better than disabling gatekeeper.
author_association: NONE
user: login: Tyrrrz
created_at: 2025-03-13T19:13:34Z
body: Agree @DannyDorito24.

I think we have a general idea of what bundling the app for macOS would entail (i.e. creating a proper file/directory structure during publish), just need someone to actually put the effort and automate it.
author_association: OWNER
user: login: lyh16
created_at: 2025-04-08T11:44:52Z
body: I agree that an app bundle would be ideal. But until we can have one, here's an easier and safer workaround compared to other approaches which involves disabling protection, etc.

1. On the terminal, `cd` into the `DiscordChatExporter.osx-ARCHITECTURE` directory
2. Then run `xattr -dr com.apple.quarantine ./`

Since macOS cannot verify the app, it sets the quarantine flag on any executables it finds in the app/project directory. This results in the the main process and any children being prevented from running. The workaround I'm sharing is just asking macOS to recursively remove the quarantine flag from the app/project root directory; allowing the app to run without any hurdles.
author_association: NONE

Diff:
diff --git a/.docs/Troubleshooting.md b/.docs/Troubleshooting.md
index 76e206a37..024a8dbb6 100644
--- a/.docs/Troubleshooting.md
+++ b/.docs/Troubleshooting.md
@@ -146,6 +146,12 @@ Red Hat: `cert-sync --user /etc/pki/tls/certs/ca-bundle.crt`
 
 If it still doesn't work, try mozroots: `mozroots --import --ask-remove`
 
+## macOS-specific
+
+### DiscordChatExporter is damaged and can’t be opened. You should move it to the Trash.
+
+Check the [Using the GUI page](Using-the-GUI.md#step-1) for instructions on how to run the app.
+
 ---
 
 > ❓ If you still have unanswered questions, feel free to [create a new discussion](https://github.com/Tyrrrz/DiscordChatExporter/discussions/new).
diff --git a/.docs/Using-the-GUI.md b/.docs/Using-the-GUI.md
index 27417f720..d175765bd 100644
--- a/.docs/Using-the-GUI.md
+++ b/.docs/Using-the-GUI.md
@@ -10,7 +10,22 @@
 
 ### Step 1
 
-After extracting the `.zip`, run `DiscordChatExporter.exe` (Windows), or `DiscordChatExporter` (Mac OS and Linux).
+After extracting the `.zip`, run `DiscordChatExporter.exe` **(Windows)**, or `DiscordChatExporter` **(Linux)**.
+
+If you're using **macOS**, you'll need to manually grant permission for the app to run.  
+If you skip these steps, the "DiscordChatExporter is damaged and can’t be opened" error will be shown.
+
+1. Open Terminal.app. You can search for it in Spotlight (press <kbd>⌘</kbd> + <kbd>Space</kbd> and type "Terminal").
+2. Paste the following into the terminal window:
+   ```bash
+   xattr -rd com.apple.quarantine
+   ```
+3. Hit <kbd>Space</kbd> once to add a space after the command
+4. Drag and drop DiscordChatExporter.app into the terminal window
+5. Press <kbd>Return</kbd> to run the command
+6. Open DiscordChatExporter.app normally
+
+> Apple requires apps to be notarized and signed in order to run on macOS without warnings, which in turn requires an Apple Developer membership ($99/year). This open-source project is distributed for free and without commercial intent.
 
 ### Step 2
 
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5bea0629f..cb5e2054b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -138,6 +138,16 @@ jobs:
           --runtime ${{ matrix.rid }}
           --self-contained
 
+      - name: Generate macOS .app bundle resources
+        if: ${{ startsWith(matrix.rid, 'osx-') && matrix.app == 'DiscordChatExporter.Gui' }}
+        shell: pwsh
+        run: >
+          ./bundle-macos-app.ps1
+          -BundleName "${{ matrix.asset }}"
+          -PublishDir "${{ matrix.app }}/bin/publish/"
+          -Version "${{ github.ref_type == 'tag' && github.ref_name || '999.9.9'}}"
+          -GitHubSha "${{ github.sha }}"
+
       - name: Upload artifacts
         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
         with:
@@ -210,9 +220,13 @@ jobs:
           path: ${{ matrix.app }}/
 
       - name: Set permissions
-        if: ${{ !startsWith(matrix.rid, 'win-') }}
+        if: ${{ !startsWith(matrix.rid, 'win-') && !(startsWith(matrix.rid, 'osx-') && matrix.app == 'DiscordChatExporter.Gui') }}
         run: chmod +x ${{ matrix.app }}/${{ matrix.asset }}
 
+      - name: Set permissions for macOS .app bundle
+        if: ${{ startsWith(matrix.rid, 'osx-') && matrix.app == 'DiscordChatExporter.Gui' }}
+        run: chmod +x ${{ matrix.app }}/${{ matrix.asset }}.app/Contents/MacOS/${{ matrix.asset }}
+
       - name: Create package
         # Change into the artifacts directory to avoid including the directory itself in the zip archive
         working-directory: ${{ matrix.app }}/
diff --git a/bundle-macos-app.ps1 b/bundle-macos-app.ps1
new file mode 100644
index 000000000..343cdfd43
--- /dev/null
+++ b/bundle-macos-app.ps1
@@ -0,0 +1,71 @@
+param(
+    [Parameter(Mandatory=$true)]
+    [string]$BundleName,
+
+    [Parameter(Mandatory=$true)]
+    [string]$PublishDir,
+
+    [Parameter(Mandatory=$true)]
+    [string]$Version,
+    
+    [Parameter(Mandatory=$true)]
+    [string]$GitHubSha
+)
+
+# Setup paths
+$appName = "$BundleName.app"
+$appDir = Join-Path "bundle-macos-app-staging" $appName
+$contentsDir = Join-Path $appDir "Contents"
+$macosDir = Join-Path $contentsDir "MacOS"
+$resourcesDir = Join-Path $contentsDir "Resources"
+
+# Create the macOS .app bundle directory structure
+New-Item -ItemType Directory -Path $macosDir -Force
+New-Item -ItemType Directory -Path $resourcesDir -Force
+
+# Copy icon into the .app's Resources folder
+Copy-Item -Path "favicon.icns" -Destination (Join-Path $resourcesDir "AppIcon.icns") -Force
+
+# Generate Info.plist metadata file with app information
+$plistContent = @"
+<?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>
+    <key>CFBundleDisplayName</key>
+    <string>$BundleName</string>
+    <key>CFBundleName</key>
+    <string>$BundleName</string>
+    <key>CFBundleExecutable</key>
+    <string>$BundleName</string>
+    <key>NSHumanReadableCopyright</key>
+    <string>© Oleksii Holub</string>
+    <key>CFBundleIdentifier</key>
+    <string>me.Tyrrrz.$BundleName</string>
+    <key>CFBundleSpokenName</key>
+    <string>Discord Chat Exporter</string>
+    <key>CFBundleIconFile</key>
+    <string>AppIcon</string>
+    <key>CFBundleIconName</key>
+    <string>AppIcon</string>
+    <key>CFBundleVersion</key>
+    <string>$GitHubSha</string>
+    <key>CFBundleShortVersionString</key>
+    <string>$Version</string>
+    <key>NSHighResolutionCapable</key>
+    <true/>
+    <key>CFBundlePackageType</key>
+    <string>APPL</string>
+  </dict>
+</plist>
+"@
+
+Set-Content -Path (Join-Path $contentsDir "Info.plist") -Value $plistContent
+
+# Move all files from the publish directory into the MacOS directory
+Get-ChildItem -Path $PublishDir | ForEach-Object {
+    Move-Item -Path $_.FullName -Destination $macosDir -Force
+}
+
+# Move the final .app bundle into the publish directory for upload
+Move-Item -Path $appDir -Destination $PublishDir -Force
\ No newline at end of file
diff --git a/favicon.icns b/favicon.icns
new file mode 100644
index 000000000..85a0d257a
Binary files /dev/null and b/favicon.icns differ


Reviews:
user: login: Tyrrrz
body: Thanks a lot, that's very useful effort. I added some comments/questions.
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T17:58:06Z
user: login: Yudi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T18:05:39Z
user: login: Yudi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T18:09:34Z
user: login: Yudi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T19:21:09Z
user: login: Tyrrrz
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T14:40:05Z
user: login: Tyrrrz
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T21:04:34Z
user: login: Tyrrrz
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T21:05:00Z
user: login: Tyrrrz
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T21:05:59Z
user: login: Tyrrrz
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T22:50:00Z
user: login: Tyrrrz
body: Thanks!
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-10T00:03:20Z

Review comments:
diff_hunk: @@ -209,10 +270,6 @@ jobs:
           name: ${{ matrix.asset }}.${{ matrix.rid }}
           path: ${{ matrix.app }}/
 
-      - name: Set permissions
-        if: ${{ !startsWith(matrix.rid, 'win-') }}
-        run: chmod +x ${{ matrix.app }}/${{ matrix.asset }}
-
path: .github/workflows/main.yml
user: login: Tyrrrz
body: Last time, I remember looking into this and found that GitHub Actions artifacts don't retain file permissions. It was a while ago so maybe that changed. Can you confirm that the downloaded release assets will have the correct permissions assigned to them?
created_at: 2025-05-07T17:49:38Z
author_association: OWNER
diff_hunk: @@ -138,6 +138,67 @@ jobs:
           --runtime ${{ matrix.rid }}
           --self-contained
 
+      - name: Set permissions
+        if: ${{ !startsWith(matrix.rid, 'win-') }}
+        run: chmod +x ${{ matrix.app }}/bin/publish/${{ matrix.asset }}
+
+      - name: Generate macOS .app bundle resources
+        if: matrix.app == 'DiscordChatExporter.Gui' && startsWith(matrix.rid, 'osx-')
path: .github/workflows/main.yml
user: login: Tyrrrz
body: Do we not also want to do that for the CLI?
created_at: 2025-05-07T17:50:06Z
author_association: OWNER
diff_hunk: @@ -138,6 +138,67 @@ jobs:
           --runtime ${{ matrix.rid }}
           --self-contained
 
+      - name: Set permissions
+        if: ${{ !startsWith(matrix.rid, 'win-') }}
+        run: chmod +x ${{ matrix.app }}/bin/publish/${{ matrix.asset }}
+
+      - name: Generate macOS .app bundle resources
+        if: matrix.app == 'DiscordChatExporter.Gui' && startsWith(matrix.rid, 'osx-')
+        run: |
+          APP_NAME="${{ matrix.asset }}.app"
+          STAGING_DIR="app-bundle-staging"
+          APP_DIR="$STAGING_DIR/$APP_NAME"
+          CONTENTS_DIR="$APP_DIR/Contents"
+          MACOS_DIR="$CONTENTS_DIR/MacOS"
+          RESOURCES_DIR="$CONTENTS_DIR/Resources"
+          PUBLISH_DIR="${{ matrix.app }}/bin/publish"
+
+          # Create the macOS .app bundle directory structure
+          mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
+
+          # Move icon into the .app's Resources folder
+          mv "favicon.icns" "$RESOURCES_DIR/AppIcon.icns"
+
+          # Generate Info.plist metadata file with app information
+          cat > $CONTENTS_DIR/Info.plist << EOF
+            <?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>
+                <key>CFBundleDisplayName</key>
+                <string>${{ matrix.asset }}</string>
+                <key>CFBundleName</key>
+                <string>${{ matrix.asset }}</string>
+                <key>CFBundleExecutable</key>
+                <string>${{ matrix.asset }}</string>
+                <key>NSHumanReadableCopyright</key>
+                <string>© Oleksii Holub</string>
+                <key>CFBundleIdentifier</key>
+                <string>com.Tyrrrz.${{ matrix.asset }}</string>
+                <key>CFBundleSpokenName</key>
+                <string>Discord Chat Exporter</string>
+                <key>CFBundleIconFile</key>
+                <string>AppIcon</string>
+                <key>CFBundleIconName</key>
+                <string>AppIcon</string>
+                <key>CFBundleVersion</key>
+                <string>${{ github.sha }}</string>
+                <key>CFBundleShortVersionString</key>
+                <string>${{ github.ref_type == 'tag' && github.ref_name || '999.9.9-ci' }}</string>
+                <key>NSHighResolutionCapable</key>
+                <true/>
+                <key>CFBundlePackageType</key>
+                <string>APPL</string>
+              </dict>
+            </plist>
+          EOF
+
+          # Copy all built application files into the .app's MacOS directory
+          find "$PUBLISH_DIR" -maxdepth 1 -mindepth 1 -exec mv "{}" "$MACOS_DIR/" \;
+
+          # Move the final .app bundle into the publish directory for upload
+          mv "$APP_DIR" "$PUBLISH_DIR"
path: .github/workflows/main.yml
user: login: Tyrrrz
body: Can we move this to a parametrized script file instead? Ideally `pwsh` because that's x-platform, but `bash` is fine too.
created_at: 2025-05-07T17:51:09Z
author_association: OWNER
diff_hunk: @@ -209,10 +270,6 @@ jobs:
           name: ${{ matrix.asset }}.${{ matrix.rid }}
           path: ${{ matrix.app }}/
 
-      - name: Set permissions
-        if: ${{ !startsWith(matrix.rid, 'win-') }}
-        run: chmod +x ${{ matrix.app }}/${{ matrix.asset }}
-
path: .github/workflows/main.yml
user: login: Yudi
body: ~~+x permission is preserved~~
Not preserved, fixed
created_at: 2025-05-07T18:05:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,6 +138,67 @@ jobs:
           --runtime ${{ matrix.rid }}
           --self-contained
 
+      - name: Set permissions
+        if: ${{ !startsWith(matrix.rid, 'win-') }}
+        run: chmod +x ${{ matrix.app }}/bin/publish/${{ matrix.asset }}
+
+      - name: Generate macOS .app bundle resources
+        if: matrix.app == 'DiscordChatExporter.Gui' && startsWith(matrix.rid, 'osx-')
path: .github/workflows/main.yml
user: login: Yudi
body: `.app` bundles are only for graphical macOS applications
CLI programs are run from the Terminal just like on Linux (e.g. `./DiscordChatExporter.Cli`)
created_at: 2025-05-07T18:09:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,6 +138,67 @@ jobs:
           --runtime ${{ matrix.rid }}
           --self-contained
 
+      - name: Set permissions
+        if: ${{ !startsWith(matrix.rid, 'win-') }}
+        run: chmod +x ${{ matrix.app }}/bin/publish/${{ matrix.asset }}
+
+      - name: Generate macOS .app bundle resources
+        if: matrix.app == 'DiscordChatExporter.Gui' && startsWith(matrix.rid, 'osx-')
+        run: |
+          APP_NAME="${{ matrix.asset }}.app"
+          STAGING_DIR="app-bundle-staging"
+          APP_DIR="$STAGING_DIR/$APP_NAME"
+          CONTENTS_DIR="$APP_DIR/Contents"
+          MACOS_DIR="$CONTENTS_DIR/MacOS"
+          RESOURCES_DIR="$CONTENTS_DIR/Resources"
+          PUBLISH_DIR="${{ matrix.app }}/bin/publish"
+
+          # Create the macOS .app bundle directory structure
+          mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
+
+          # Move icon into the .app's Resources folder
+          mv "favicon.icns" "$RESOURCES_DIR/AppIcon.icns"
+
+          # Generate Info.plist metadata file with app information
+          cat > $CONTENTS_DIR/Info.plist << EOF
+            <?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>
+                <key>CFBundleDisplayName</key>
+                <string>${{ matrix.asset }}</string>
+                <key>CFBundleName</key>
+                <string>${{ matrix.asset }}</string>
+                <key>CFBundleExecutable</key>
+                <string>${{ matrix.asset }}</string>
+                <key>NSHumanReadableCopyright</key>
+                <string>© Oleksii Holub</string>
+                <key>CFBundleIdentifier</key>
+                <string>com.Tyrrrz.${{ matrix.asset }}</string>
+                <key>CFBundleSpokenName</key>
+                <string>Discord Chat Exporter</string>
+                <key>CFBundleIconFile</key>
+                <string>AppIcon</string>
+                <key>CFBundleIconName</key>
+                <string>AppIcon</string>
+                <key>CFBundleVersion</key>
+                <string>${{ github.sha }}</string>
+                <key>CFBundleShortVersionString</key>
+                <string>${{ github.ref_type == 'tag' && github.ref_name || '999.9.9-ci' }}</string>
+                <key>NSHighResolutionCapable</key>
+                <true/>
+                <key>CFBundlePackageType</key>
+                <string>APPL</string>
+              </dict>
+            </plist>
+          EOF
+
+          # Copy all built application files into the .app's MacOS directory
+          find "$PUBLISH_DIR" -maxdepth 1 -mindepth 1 -exec mv "{}" "$MACOS_DIR/" \;
+
+          # Move the final .app bundle into the publish directory for upload
+          mv "$APP_DIR" "$PUBLISH_DIR"
path: .github/workflows/main.yml
user: login: Yudi
body: Done
created_at: 2025-05-07T19:21:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,6 +138,23 @@ jobs:
           --runtime ${{ matrix.rid }}
           --self-contained
 
+      - name: Generate macOS .app bundle resources
+        if: ${{ startsWith(matrix.rid, 'osx-') && matrix.app == 'DiscordChatExporter.Gui' }}
+        shell: pwsh
+        run: >
+          ./bundle-macos-app.ps1
+          -MatrixAssetName "${{ matrix.asset }}"
+          -MatrixAppPath "${{ matrix.app }}"
+          -PublishDir "${{ matrix.app }}/bin/publish/"
+          -Version "${{ github.ref_type == 'tag' && github.ref_name || '999.9.9'}}"
+          -GitHubSha "${{ github.sha }}"
+          -GitHubRefType "${{ github.ref_type }}"
+          -GitHubRefName "${{ github.ref_name }}"
+          -BundleIdentifier "com.Tyrrrz.${{ matrix.asset }}"
+          -SpokenName "Discord Chat Exporter"
+          -Copyright "© Oleksii Holub"
+          -IconPath "favicon.icns"
path: .github/workflows/main.yml
user: login: Tyrrrz
body: These last 4 parameters could probably just be hard-codded instead since they don't really need to be parametrized by the CI/CD job
created_at: 2025-05-08T14:37:25Z
author_association: OWNER
diff_hunk: @@ -138,6 +138,23 @@ jobs:
           --runtime ${{ matrix.rid }}
           --self-contained
 
+      - name: Generate macOS .app bundle resources
+        if: ${{ startsWith(matrix.rid, 'osx-') && matrix.app == 'DiscordChatExporter.Gui' }}
+        shell: pwsh
+        run: >
+          ./bundle-macos-app.ps1
+          -MatrixAssetName "${{ matrix.asset }}"
+          -MatrixAppPath "${{ matrix.app }}"
+          -PublishDir "${{ matrix.app }}/bin/publish/"
+          -Version "${{ github.ref_type == 'tag' && github.ref_name || '999.9.9'}}"
+          -GitHubSha "${{ github.sha }}"
+          -GitHubRefType "${{ github.ref_type }}"
+          -GitHubRefName "${{ github.ref_name }}"
+          -BundleIdentifier "com.Tyrrrz.${{ matrix.asset }}"
path: .github/workflows/main.yml
user: login: Tyrrrz
body: Let's use `me.Tyrrrz.${{ matrix.asset }}` since I own that domain
created_at: 2025-05-08T14:37:41Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,80 @@
+param(
+    [Parameter(Mandatory=$true)]
+    [string]$MatrixAssetName,
+    
+    [Parameter(Mandatory=$true)]
+    [string]$MatrixAppPath,
+
+    [Parameter(Mandatory=$true)]
+    [string]$PublishDir,
+
+    [Parameter(Mandatory=$true)]
+    [string]$Version,
+    
+    [Parameter(Mandatory=$true)]
+    [string]$GitHubSha,
+    
+    [Parameter(Mandatory=$true)]
+    [string]$GitHubRefType,
+    
+    [Parameter(Mandatory=$true)]
+    [string]$GitHubRefName
path: bundle-macos-app.ps1
user: login: Tyrrrz
body: Looks like these two parameters are unused, correct?
created_at: 2025-05-09T21:04:34Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,80 @@
+param(
+    [Parameter(Mandatory=$true)]
+    [string]$MatrixAssetName,
+    
+    [Parameter(Mandatory=$true)]
+    [string]$MatrixAppPath,
path: bundle-macos-app.ps1
user: login: Tyrrrz
body: This one also appears unused
created_at: 2025-05-09T21:05:00Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,80 @@
+param(
+    [Parameter(Mandatory=$true)]
+    [string]$MatrixAssetName,
path: bundle-macos-app.ps1
user: login: Tyrrrz
body: Let's rename this to something specific to the bundling process and not the CI/CD process

```suggestion
    [string]$BundleName,
```
created_at: 2025-05-09T21:05:59Z
author_association: OWNER
diff_hunk: @@ -138,6 +138,18 @@ jobs:
           --runtime ${{ matrix.rid }}
           --self-contained
 
+      - name: Generate macOS .app bundle resources
+        if: ${{ startsWith(matrix.rid, 'osx-') && matrix.app == 'DiscordChatExporter.Gui' }}
+        shell: pwsh
+        run: >
+          ./bundle-macos-app.ps1
+          -BundleName "${{ matrix.asset }}"
+          -PublishDir "${{ matrix.app }}/bin/publish/"
+          -Version "${{ github.ref_type == 'tag' && github.ref_name || '999.9.9'}}"
+          -GitHubSha "${{ github.sha }}"
+          -GitHubRefType "${{ github.ref_type }}"
+          -GitHubRefName "${{ github.ref_name }}"
path: .github/workflows/main.yml
user: login: Tyrrrz
body: These two can be removed since they're no longer defined in the script file
created_at: 2025-05-09T22:50:00Z
author_association: OWNER

PR comments:
user: login: Yudi
created_at: 2025-05-05T02:24:06Z
body: @Tyrrrz If you still have the original favicon files, would you mind sending them to me here or on Discord? 
macOS icons should be 1024x1024 and `favicon.png` is only 256x256.
author_association: CONTRIBUTOR
user: login: Tyrrrz
created_at: 2025-05-05T13:27:23Z
body: > @Tyrrrz If you still have the original favicon files, would you mind sending them to me here or on Discord? macOS icons should be 1024x1024 and `favicon.png` is only 256x256.

Unfortunately, I do not :(
author_association: OWNER
user: login: Yudi
created_at: 2025-05-06T13:12:43Z
body: Works on latest macOS.

There’s some additional work that could be done, like adding menu controls and the icon resolution issue. I’ll look into those in separate PRs when I find some time.

<img width="545" alt="image" src="https://github.com/user-attachments/assets/a15480e5-8363-46bf-9928-a45e59479345" />
author_association: CONTRIBUTOR
user: login: Tyrrrz
created_at: 2025-05-07T20:57:46Z
body: @Yudi did you close this by accident?
author_association: OWNER
user: login: Yudi
created_at: 2025-05-07T20:59:26Z
body: I checked the wrong folder for permissions, I'm fixing and testing it
Sorry lol
author_association: CONTRIBUTOR
user: login: Yudi
created_at: 2025-05-07T21:05:51Z
body: Done

https://github.com/yudi-forks/DiscordChatExporter/releases/tag/4.0.1
author_association: CONTRIBUTOR