|
| 1 | +# SPDX-FileCopyrightText: 2026 AprilNEA <dev@aprilnea.me> |
| 2 | +# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial |
| 3 | + |
| 4 | +name: Release |
| 5 | + |
| 6 | +on: |
| 7 | + push: |
| 8 | + tags: |
| 9 | + - "v*.*.*" |
| 10 | + workflow_dispatch: |
| 11 | + inputs: |
| 12 | + tag: |
| 13 | + description: "Release tag (e.g., v0.1.0)" |
| 14 | + required: false |
| 15 | + type: string |
| 16 | + |
| 17 | +concurrency: |
| 18 | + group: ${{ github.workflow }}-${{ github.ref }} |
| 19 | + cancel-in-progress: true |
| 20 | + |
| 21 | +env: |
| 22 | + CARGO_TERM_COLOR: always |
| 23 | + MACOSX_DEPLOYMENT_TARGET: "12.0" |
| 24 | + |
| 25 | +jobs: |
| 26 | + build-macos: |
| 27 | + name: Build macOS (${{ matrix.target }}) |
| 28 | + runs-on: ${{ matrix.runner }} |
| 29 | + strategy: |
| 30 | + fail-fast: false |
| 31 | + matrix: |
| 32 | + include: |
| 33 | + - target: aarch64-apple-darwin |
| 34 | + runner: macos-14 |
| 35 | + arch: arm64 |
| 36 | + - target: x86_64-apple-darwin |
| 37 | + runner: macos-13 |
| 38 | + arch: x64 |
| 39 | + |
| 40 | + steps: |
| 41 | + - uses: actions/checkout@v4 |
| 42 | + |
| 43 | + - name: Setup Rust |
| 44 | + run: | |
| 45 | + rustup toolchain install nightly-2026-01-18 --profile minimal |
| 46 | + rustup default nightly-2026-01-18 |
| 47 | + rustup target add ${{ matrix.target }} |
| 48 | +
|
| 49 | + - name: Setup Rust cache |
| 50 | + uses: Swatinem/rust-cache@v2 |
| 51 | + with: |
| 52 | + prefix-key: "v1-rust" |
| 53 | + shared-key: ${{ matrix.target }} |
| 54 | + |
| 55 | + - name: Install cargo-bundle |
| 56 | + run: cargo install cargo-bundle |
| 57 | + |
| 58 | + - name: Build release binary |
| 59 | + run: cargo build --release --target ${{ matrix.target }} |
| 60 | + |
| 61 | + - name: Bundle macOS app |
| 62 | + run: cargo bundle --release --target ${{ matrix.target }} |
| 63 | + |
| 64 | + - name: Upload app bundle |
| 65 | + uses: actions/upload-artifact@v4 |
| 66 | + with: |
| 67 | + name: ChatGPUI-app-${{ matrix.arch }} |
| 68 | + path: target/${{ matrix.target }}/release/bundle/osx/ChatGPUI.app |
| 69 | + if-no-files-found: error |
| 70 | + |
| 71 | + create-universal-binary: |
| 72 | + name: Create Universal Binary |
| 73 | + needs: build-macos |
| 74 | + runs-on: macos-14 |
| 75 | + steps: |
| 76 | + - uses: actions/checkout@v4 |
| 77 | + |
| 78 | + - name: Setup Rust |
| 79 | + run: | |
| 80 | + rustup toolchain install nightly-2026-01-18 --profile minimal |
| 81 | + rustup default nightly-2026-01-18 |
| 82 | + rustup target add aarch64-apple-darwin x86_64-apple-darwin |
| 83 | +
|
| 84 | + - name: Setup Rust cache |
| 85 | + uses: Swatinem/rust-cache@v2 |
| 86 | + with: |
| 87 | + prefix-key: "v1-rust" |
| 88 | + shared-key: "universal" |
| 89 | + |
| 90 | + - name: Install cargo-bundle |
| 91 | + run: cargo install cargo-bundle |
| 92 | + |
| 93 | + - name: Build both architectures |
| 94 | + run: | |
| 95 | + cargo build --release --target aarch64-apple-darwin |
| 96 | + cargo build --release --target x86_64-apple-darwin |
| 97 | +
|
| 98 | + - name: Create universal binary |
| 99 | + run: | |
| 100 | + mkdir -p target/universal-apple-darwin/release |
| 101 | + lipo -create \ |
| 102 | + target/aarch64-apple-darwin/release/chatgpui \ |
| 103 | + target/x86_64-apple-darwin/release/chatgpui \ |
| 104 | + -output target/universal-apple-darwin/release/chatgpui |
| 105 | +
|
| 106 | + - name: Bundle universal app |
| 107 | + run: | |
| 108 | + APP_NAME="ChatGPUI" |
| 109 | +
|
| 110 | + # Bundle from arm64 target first |
| 111 | + cargo bundle --release --target aarch64-apple-darwin |
| 112 | +
|
| 113 | + # Copy bundled app |
| 114 | + cp -r "target/aarch64-apple-darwin/release/bundle/osx/${APP_NAME}.app" \ |
| 115 | + "target/universal-apple-darwin/release/" |
| 116 | +
|
| 117 | + # Replace binary with universal binary |
| 118 | + cp target/universal-apple-darwin/release/chatgpui \ |
| 119 | + "target/universal-apple-darwin/release/${APP_NAME}.app/Contents/MacOS/chatgpui" |
| 120 | +
|
| 121 | + - name: Upload universal app bundle |
| 122 | + uses: actions/upload-artifact@v4 |
| 123 | + with: |
| 124 | + name: ChatGPUI-app-universal |
| 125 | + path: target/universal-apple-darwin/release/ChatGPUI.app |
| 126 | + if-no-files-found: error |
| 127 | + |
| 128 | + sign-and-notarize: |
| 129 | + name: Sign and Notarize (${{ matrix.arch }}) |
| 130 | + needs: [build-macos, create-universal-binary] |
| 131 | + runs-on: macos-14 |
| 132 | + strategy: |
| 133 | + fail-fast: false |
| 134 | + matrix: |
| 135 | + arch: [arm64, x64, universal] |
| 136 | + steps: |
| 137 | + - uses: actions/checkout@v4 |
| 138 | + with: |
| 139 | + sparse-checkout: bundle/ChatGPUI.entitlements |
| 140 | + sparse-checkout-cone-mode: false |
| 141 | + |
| 142 | + - name: Download app bundle |
| 143 | + uses: actions/download-artifact@v4 |
| 144 | + with: |
| 145 | + name: ChatGPUI-app-${{ matrix.arch }} |
| 146 | + path: ChatGPUI.app |
| 147 | + |
| 148 | + - name: Import certificates |
| 149 | + env: |
| 150 | + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} |
| 151 | + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} |
| 152 | + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} |
| 153 | + run: | |
| 154 | + # Create temporary keychain |
| 155 | + echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 |
| 156 | + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain |
| 157 | + security default-keychain -s build.keychain |
| 158 | + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain |
| 159 | + security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign |
| 160 | + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain |
| 161 | +
|
| 162 | + # Verify certificate is available |
| 163 | + security find-identity -v -p codesigning build.keychain |
| 164 | +
|
| 165 | + - name: Sign app |
| 166 | + env: |
| 167 | + APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }} |
| 168 | + run: | |
| 169 | + ENTITLEMENTS="bundle/ChatGPUI.entitlements" |
| 170 | +
|
| 171 | + # Sign all nested components first (frameworks, libraries, etc.) |
| 172 | + find ChatGPUI.app/Contents -type f \( -name "*.dylib" -o -name "*.so" -o -perm +111 \) -exec \ |
| 173 | + codesign --force --options runtime --sign "$APPLE_IDENTITY" --timestamp {} \; |
| 174 | +
|
| 175 | + # Sign the main app bundle with entitlements |
| 176 | + codesign --force --options runtime \ |
| 177 | + --sign "$APPLE_IDENTITY" \ |
| 178 | + --timestamp \ |
| 179 | + --entitlements "$ENTITLEMENTS" \ |
| 180 | + ChatGPUI.app |
| 181 | +
|
| 182 | + # Verify signature |
| 183 | + codesign --verify --deep --strict --verbose=2 ChatGPUI.app |
| 184 | +
|
| 185 | + # Display entitlements for verification |
| 186 | + codesign -d --entitlements :- ChatGPUI.app |
| 187 | +
|
| 188 | + - name: Create DMG |
| 189 | + run: | |
| 190 | + APP_NAME="ChatGPUI" |
| 191 | + DMG_NAME="ChatGPUI-${{ matrix.arch }}.dmg" |
| 192 | +
|
| 193 | + mkdir -p dmg-contents |
| 194 | + cp -r ChatGPUI.app dmg-contents/ |
| 195 | + ln -s /Applications dmg-contents/Applications |
| 196 | +
|
| 197 | + hdiutil create -volname "${APP_NAME}" \ |
| 198 | + -srcfolder dmg-contents \ |
| 199 | + -ov -format UDZO \ |
| 200 | + "${DMG_NAME}" |
| 201 | +
|
| 202 | + rm -rf dmg-contents |
| 203 | +
|
| 204 | + - name: Notarize DMG |
| 205 | + env: |
| 206 | + APPLE_ID: ${{ secrets.APPLE_ID }} |
| 207 | + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} |
| 208 | + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} |
| 209 | + run: | |
| 210 | + DMG_NAME="ChatGPUI-${{ matrix.arch }}.dmg" |
| 211 | +
|
| 212 | + # Submit for notarization |
| 213 | + xcrun notarytool submit "${DMG_NAME}" \ |
| 214 | + --apple-id "$APPLE_ID" \ |
| 215 | + --team-id "$APPLE_TEAM_ID" \ |
| 216 | + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ |
| 217 | + --wait |
| 218 | +
|
| 219 | + # Staple the notarization ticket |
| 220 | + xcrun stapler staple "${DMG_NAME}" |
| 221 | +
|
| 222 | + - name: Upload signed DMG |
| 223 | + uses: actions/upload-artifact@v4 |
| 224 | + with: |
| 225 | + name: ChatGPUI-macos-${{ matrix.arch }}-signed |
| 226 | + path: ChatGPUI-${{ matrix.arch }}.dmg |
| 227 | + if-no-files-found: error |
| 228 | + |
| 229 | + - name: Cleanup keychain |
| 230 | + if: always() |
| 231 | + run: | |
| 232 | + security delete-keychain build.keychain || true |
| 233 | +
|
| 234 | + release: |
| 235 | + name: Create Release |
| 236 | + needs: sign-and-notarize |
| 237 | + runs-on: ubuntu-latest |
| 238 | + if: startsWith(github.ref, 'refs/tags/v') |
| 239 | + permissions: |
| 240 | + contents: write |
| 241 | + steps: |
| 242 | + - uses: actions/checkout@v4 |
| 243 | + with: |
| 244 | + fetch-depth: 0 |
| 245 | + |
| 246 | + - name: Download all signed artifacts |
| 247 | + uses: actions/download-artifact@v4 |
| 248 | + with: |
| 249 | + pattern: ChatGPUI-macos-*-signed |
| 250 | + path: artifacts |
| 251 | + |
| 252 | + - name: Prepare release assets |
| 253 | + run: | |
| 254 | + mkdir -p release-assets |
| 255 | + find artifacts -name "*.dmg" -exec cp {} release-assets/ \; |
| 256 | + ls -la release-assets/ |
| 257 | +
|
| 258 | + - name: Generate changelog |
| 259 | + id: changelog |
| 260 | + run: | |
| 261 | + # Get the previous tag |
| 262 | + PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") |
| 263 | + CURRENT_TAG=${GITHUB_REF#refs/tags/} |
| 264 | +
|
| 265 | + echo "## What's Changed" > CHANGELOG.md |
| 266 | + echo "" >> CHANGELOG.md |
| 267 | +
|
| 268 | + if [ -n "$PREV_TAG" ]; then |
| 269 | + git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" >> CHANGELOG.md |
| 270 | + else |
| 271 | + git log --pretty=format:"- %s (%h)" -20 >> CHANGELOG.md |
| 272 | + fi |
| 273 | +
|
| 274 | + echo "" >> CHANGELOG.md |
| 275 | + echo "" >> CHANGELOG.md |
| 276 | + echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${CURRENT_TAG}" >> CHANGELOG.md |
| 277 | +
|
| 278 | + - name: Create GitHub Release |
| 279 | + uses: softprops/action-gh-release@v2 |
| 280 | + with: |
| 281 | + files: release-assets/* |
| 282 | + body_path: CHANGELOG.md |
| 283 | + draft: false |
| 284 | + prerelease: ${{ contains(github.ref, '-alpha') || contains(github.ref, '-beta') || contains(github.ref, '-rc') }} |
| 285 | + generate_release_notes: true |
| 286 | + env: |
| 287 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
0 commit comments