fix: use buildHostToolEnv and simplify HTTP request types #256
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build All Platforms | |
| on: | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - README.md | |
| - "**/*.md" | |
| - "docs/**" | |
| - "landing/**" | |
| pull_request: | |
| branches: [main] | |
| paths-ignore: | |
| - "docs/**" | |
| - "landing/**" | |
| jobs: | |
| determine-version: | |
| name: Determine and Set Version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| new_version: ${{ steps.version.outputs.new_version }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Determine semantic version | |
| id: version | |
| run: | | |
| NEW_VERSION=$(node scripts/version.js) | |
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "Determined version: $NEW_VERSION" | |
| - name: Commit package.json version when it changed | |
| run: | | |
| git config --global user.name "GitHub Actions" | |
| git config --global user.email "actions@github.com" | |
| git add package.json | |
| if ! git diff --cached --quiet; then | |
| git commit -m "chore: update version to v${{ steps.version.outputs.new_version }} [skip ci]" | |
| git push | |
| else | |
| echo "No version change needed" | |
| fi | |
| build-macos-arm64: | |
| name: Build macOS ARM64 (Apple Silicon) | |
| needs: determine-version | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Sync package.json version with computed release version | |
| run: | | |
| node -e "const fs=require('fs'); const path='package.json'; const pkg=JSON.parse(fs.readFileSync(path,'utf8')); pkg.version='${{ needs.determine-version.outputs.new_version }}'; fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); console.log('package.json version set to', pkg.version)" | |
| - name: Determine macOS signing mode | |
| id: mac_signing | |
| env: | |
| BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_CERTIFICATE || secrets.MACOS_CERTIFICATE_P12_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PWD || secrets.MACOS_CERTIFICATE_P12_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD || secrets.MAC_CERTIFICATE_PWD }} | |
| CSC_NAME: ${{ secrets.MAC_DEVELOPER_ID }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| HAS_CERT_SECRETS=true | |
| HAS_NOTARY_SECRETS=true | |
| if [ -z "$BUILD_CERTIFICATE_BASE64" ] || [ -z "$P12_PASSWORD" ] || [ -z "$KEYCHAIN_PASSWORD" ]; then | |
| HAS_CERT_SECRETS=false | |
| fi | |
| if [ -z "$APPLE_ID" ] || [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] || [ -z "$APPLE_TEAM_ID" ]; then | |
| HAS_NOTARY_SECRETS=false | |
| fi | |
| if [ "$HAS_CERT_SECRETS" = "true" ]; then | |
| echo "signing_enabled=true" >> "$GITHUB_OUTPUT" | |
| if [ "$HAS_NOTARY_SECRETS" = "true" ]; then | |
| echo "notarize_enabled=true" >> "$GITHUB_OUTPUT" | |
| echo "macOS build mode: signed + notarized" | |
| else | |
| echo "notarize_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "macOS build mode: signed only (missing notarization secrets)" | |
| fi | |
| else | |
| echo "signing_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "notarize_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "macOS build mode: unsigned (missing signing certificate secrets)" | |
| fi | |
| - name: Prepare macOS signing keychain | |
| if: steps.mac_signing.outputs.signing_enabled == 'true' | |
| env: | |
| BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_CERTIFICATE || secrets.MACOS_CERTIFICATE_P12_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PWD || secrets.MACOS_CERTIFICATE_P12_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD || secrets.MAC_CERTIFICATE_PWD }} | |
| run: | | |
| CERTIFICATE_PATH="$RUNNER_TEMP/certificate.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychain -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"/"//g') | |
| - name: Build renderer | |
| run: npm run build:renderer | |
| - name: Build Electron main process | |
| run: npm run build:electron | |
| - name: Build macOS ARM64 (Apple Silicon, signed/notarized) | |
| if: steps.mac_signing.outputs.signing_enabled == 'true' | |
| run: npx electron-builder --mac dmg --arm64 --publish never --config.mac.notarize=${{ steps.mac_signing.outputs.notarize_enabled }} | |
| env: | |
| GH_TOKEN: ${{ secrets.TOKEN }} | |
| CSC_IDENTITY_AUTO_DISCOVERY: true | |
| CSC_FOR_PULL_REQUEST: true | |
| CSC_FOR_PULL_REQUEST_IF_PRIVATE: true | |
| CSC_NAME: ${{ secrets.MAC_DEVELOPER_ID }} | |
| DEBUG: electron-notarize* | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| USE_HARD_LINKS: false | |
| - name: Build macOS ARM64 (Apple Silicon, unsigned) | |
| if: steps.mac_signing.outputs.signing_enabled != 'true' | |
| run: npx electron-builder --mac dmg --arm64 --publish never --config.mac.notarize=false | |
| env: | |
| GH_TOKEN: ${{ secrets.TOKEN }} | |
| - name: Validate signed/notarized DMGs | |
| if: steps.mac_signing.outputs.signing_enabled == 'true' | |
| run: | | |
| shopt -s nullglob | |
| RETRY_MAX=4 | |
| RETRY_DELAY_SECONDS=30 | |
| staple_with_retry() { | |
| local dmg="$1" | |
| local attempt=1 | |
| while [ "$attempt" -le "$RETRY_MAX" ]; do | |
| echo "Staple attempt $attempt/$RETRY_MAX for $dmg" | |
| if xcrun stapler staple "$dmg"; then | |
| if xcrun stapler validate "$dmg"; then | |
| echo "Stapling and validation succeeded for $dmg" | |
| return 0 | |
| fi | |
| echo "Stapler validation failed for $dmg on attempt $attempt" | |
| else | |
| echo "Stapler staple failed for $dmg on attempt $attempt" | |
| fi | |
| if [ "$attempt" -lt "$RETRY_MAX" ]; then | |
| echo "Ticket may still be propagating in Apple services; waiting ${RETRY_DELAY_SECONDS}s before retry" | |
| sleep "$RETRY_DELAY_SECONDS" | |
| fi | |
| attempt=$((attempt + 1)) | |
| done | |
| echo "Failed to staple and validate $dmg after $RETRY_MAX attempts" | |
| return 1 | |
| } | |
| DMGS=(release/*.dmg) | |
| if [ ${#DMGS[@]} -eq 0 ]; then | |
| echo "No DMG artifacts found in release/." | |
| exit 1 | |
| fi | |
| for dmg in "${DMGS[@]}"; do | |
| echo "Validating $dmg" | |
| if [ "${{ steps.mac_signing.outputs.notarize_enabled }}" = "true" ]; then | |
| echo "Notarization enabled; attempting to staple DMG ticket for $dmg (best effort)" | |
| if ! staple_with_retry "$dmg"; then | |
| echo "Warning: DMG staple did not succeed for $dmg. Continuing because notarization tickets may exist only for the app bundle." | |
| fi | |
| else | |
| echo "Notarization disabled; skipping stapler validation for $dmg" | |
| fi | |
| echo "Checking DMG container signature (non-blocking)" | |
| if codesign --verify --verbose=2 "$dmg"; then | |
| spctl -a -t open --context context:primary-signature -v "$dmg" | |
| else | |
| echo "Warning: DMG container is not signed/verifiable; continuing because shipped trust decision is enforced on enclosed app." | |
| fi | |
| MOUNT_POINT="$RUNNER_TEMP/mount-$(basename "$dmg" .dmg)" | |
| mkdir -p "$MOUNT_POINT" | |
| hdiutil attach "$dmg" -nobrowse -readonly -mountpoint "$MOUNT_POINT" | |
| APP_PATH=$(find "$MOUNT_POINT" -maxdepth 1 -name "*.app" | head -n 1) | |
| if [ -z "$APP_PATH" ]; then | |
| echo "No .app found inside $dmg" | |
| hdiutil detach "$MOUNT_POINT" -force || true | |
| exit 1 | |
| fi | |
| echo "Checking signature for $APP_PATH" | |
| codesign --verify --deep --strict --verbose=2 "$APP_PATH" | |
| APP_VALIDATION_DIR="$RUNNER_TEMP/validate-$(basename "$dmg" .dmg)" | |
| APP_VALIDATION_PATH="$APP_VALIDATION_DIR/$(basename "$APP_PATH")" | |
| rm -rf "$APP_VALIDATION_DIR" | |
| mkdir -p "$APP_VALIDATION_DIR" | |
| ditto "$APP_PATH" "$APP_VALIDATION_PATH" | |
| if [ "${{ steps.mac_signing.outputs.notarize_enabled }}" = "true" ]; then | |
| echo "Notarization enabled; stapling/validating app bundle ticket for writable copy $APP_VALIDATION_PATH" | |
| staple_with_retry "$APP_VALIDATION_PATH" | |
| fi | |
| spctl -a -vv -t exec "$APP_VALIDATION_PATH" | |
| rm -rf "$APP_VALIDATION_DIR" | |
| hdiutil detach "$MOUNT_POINT" | |
| done | |
| - name: Upload macOS ARM64 artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Chaton-macOS-ARM64 | |
| path: release/*.dmg | |
| if-no-files-found: error | |
| build-macos-x64: | |
| name: Build macOS x64 (Intel) | |
| needs: determine-version | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Sync package.json version with computed release version | |
| run: | | |
| node -e "const fs=require('fs'); const path='package.json'; const pkg=JSON.parse(fs.readFileSync(path,'utf8')); pkg.version='${{ needs.determine-version.outputs.new_version }}'; fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); console.log('package.json version set to', pkg.version)" | |
| - name: Determine macOS signing mode | |
| id: mac_signing | |
| env: | |
| BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_CERTIFICATE || secrets.MACOS_CERTIFICATE_P12_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PWD || secrets.MACOS_CERTIFICATE_P12_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD || secrets.MAC_CERTIFICATE_PWD }} | |
| CSC_NAME: ${{ secrets.MAC_DEVELOPER_ID }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| HAS_CERT_SECRETS=true | |
| HAS_NOTARY_SECRETS=true | |
| if [ -z "$BUILD_CERTIFICATE_BASE64" ] || [ -z "$P12_PASSWORD" ] || [ -z "$KEYCHAIN_PASSWORD" ]; then | |
| HAS_CERT_SECRETS=false | |
| fi | |
| if [ -z "$APPLE_ID" ] || [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] || [ -z "$APPLE_TEAM_ID" ]; then | |
| HAS_NOTARY_SECRETS=false | |
| fi | |
| if [ "$HAS_CERT_SECRETS" = "true" ]; then | |
| echo "signing_enabled=true" >> "$GITHUB_OUTPUT" | |
| if [ "$HAS_NOTARY_SECRETS" = "true" ]; then | |
| echo "notarize_enabled=true" >> "$GITHUB_OUTPUT" | |
| echo "macOS build mode: signed + notarized" | |
| else | |
| echo "notarize_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "macOS build mode: signed only (missing notarization secrets)" | |
| fi | |
| else | |
| echo "signing_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "notarize_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "macOS build mode: unsigned (missing signing certificate secrets)" | |
| fi | |
| - name: Prepare macOS signing keychain | |
| if: steps.mac_signing.outputs.signing_enabled == 'true' | |
| env: | |
| BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_CERTIFICATE || secrets.MACOS_CERTIFICATE_P12_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PWD || secrets.MACOS_CERTIFICATE_P12_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD || secrets.MAC_CERTIFICATE_PWD }} | |
| run: | | |
| CERTIFICATE_PATH="$RUNNER_TEMP/certificate.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychain -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"/"//g') | |
| - name: Build renderer | |
| run: npm run build:renderer | |
| - name: Build Electron main process | |
| run: npm run build:electron | |
| - name: Build macOS x64 (Intel, signed/notarized) | |
| if: steps.mac_signing.outputs.signing_enabled == 'true' | |
| run: npx electron-builder --mac dmg --x64 --publish never --config.mac.notarize=${{ steps.mac_signing.outputs.notarize_enabled }} | |
| env: | |
| GH_TOKEN: ${{ secrets.TOKEN }} | |
| CSC_IDENTITY_AUTO_DISCOVERY: true | |
| CSC_FOR_PULL_REQUEST: true | |
| CSC_FOR_PULL_REQUEST_IF_PRIVATE: true | |
| CSC_NAME: ${{ secrets.MAC_DEVELOPER_ID }} | |
| DEBUG: electron-notarize* | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| USE_HARD_LINKS: false | |
| - name: Build macOS x64 (Intel, unsigned) | |
| if: steps.mac_signing.outputs.signing_enabled != 'true' | |
| run: npx electron-builder --mac dmg --x64 --publish never --config.mac.notarize=false | |
| env: | |
| GH_TOKEN: ${{ secrets.TOKEN }} | |
| - name: Validate signed/notarized DMGs | |
| if: steps.mac_signing.outputs.signing_enabled == 'true' | |
| run: | | |
| shopt -s nullglob | |
| RETRY_MAX=4 | |
| RETRY_DELAY_SECONDS=30 | |
| staple_with_retry() { | |
| local dmg="$1" | |
| local attempt=1 | |
| while [ "$attempt" -le "$RETRY_MAX" ]; do | |
| echo "Staple attempt $attempt/$RETRY_MAX for $dmg" | |
| if xcrun stapler staple "$dmg"; then | |
| if xcrun stapler validate "$dmg"; then | |
| echo "Stapling and validation succeeded for $dmg" | |
| return 0 | |
| fi | |
| echo "Stapler validation failed for $dmg on attempt $attempt" | |
| else | |
| echo "Stapler staple failed for $dmg on attempt $attempt" | |
| fi | |
| if [ "$attempt" -lt "$RETRY_MAX" ]; then | |
| echo "Ticket may still be propagating in Apple services; waiting ${RETRY_DELAY_SECONDS}s before retry" | |
| sleep "$RETRY_DELAY_SECONDS" | |
| fi | |
| attempt=$((attempt + 1)) | |
| done | |
| echo "Failed to staple and validate $dmg after $RETRY_MAX attempts" | |
| return 1 | |
| } | |
| DMGS=(release/*.dmg) | |
| if [ ${#DMGS[@]} -eq 0 ]; then | |
| echo "No DMG artifacts found in release/." | |
| exit 1 | |
| fi | |
| for dmg in "${DMGS[@]}"; do | |
| echo "Validating $dmg" | |
| if [ "${{ steps.mac_signing.outputs.notarize_enabled }}" = "true" ]; then | |
| echo "Notarization enabled; attempting to staple DMG ticket for $dmg (best effort)" | |
| if ! staple_with_retry "$dmg"; then | |
| echo "Warning: DMG staple did not succeed for $dmg. Continuing because notarization tickets may exist only for the app bundle." | |
| fi | |
| else | |
| echo "Notarization disabled; skipping stapler validation for $dmg" | |
| fi | |
| echo "Checking DMG container signature (non-blocking)" | |
| if codesign --verify --verbose=2 "$dmg"; then | |
| spctl -a -t open --context context:primary-signature -v "$dmg" | |
| else | |
| echo "Warning: DMG container is not signed/verifiable; continuing because shipped trust decision is enforced on enclosed app." | |
| fi | |
| MOUNT_POINT="$RUNNER_TEMP/mount-$(basename "$dmg" .dmg)" | |
| mkdir -p "$MOUNT_POINT" | |
| hdiutil attach "$dmg" -nobrowse -readonly -mountpoint "$MOUNT_POINT" | |
| APP_PATH=$(find "$MOUNT_POINT" -maxdepth 1 -name "*.app" | head -n 1) | |
| if [ -z "$APP_PATH" ]; then | |
| echo "No .app found inside $dmg" | |
| hdiutil detach "$MOUNT_POINT" -force || true | |
| exit 1 | |
| fi | |
| echo "Checking signature for $APP_PATH" | |
| codesign --verify --deep --strict --verbose=2 "$APP_PATH" | |
| APP_VALIDATION_DIR="$RUNNER_TEMP/validate-$(basename "$dmg" .dmg)" | |
| APP_VALIDATION_PATH="$APP_VALIDATION_DIR/$(basename "$APP_PATH")" | |
| rm -rf "$APP_VALIDATION_DIR" | |
| mkdir -p "$APP_VALIDATION_DIR" | |
| ditto "$APP_PATH" "$APP_VALIDATION_PATH" | |
| if [ "${{ steps.mac_signing.outputs.notarize_enabled }}" = "true" ]; then | |
| echo "Notarization enabled; stapling/validating app bundle ticket for writable copy $APP_VALIDATION_PATH" | |
| staple_with_retry "$APP_VALIDATION_PATH" | |
| fi | |
| spctl -a -vv -t exec "$APP_VALIDATION_PATH" | |
| rm -rf "$APP_VALIDATION_DIR" | |
| hdiutil detach "$MOUNT_POINT" | |
| done | |
| - name: Upload macOS x64 artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Chaton-macOS-x64 | |
| path: release/*.dmg | |
| if-no-files-found: error | |
| build-windows: | |
| name: Build Windows App | |
| needs: determine-version | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Sync package.json version with computed release version | |
| run: | | |
| node -e "const fs=require('fs'); const path='package.json'; const pkg=JSON.parse(fs.readFileSync(path,'utf8')); pkg.version='${{ needs.determine-version.outputs.new_version }}'; fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); console.log('package.json version set to', pkg.version)" | |
| - name: Build renderer | |
| run: npm run build:renderer | |
| - name: Build Electron main process | |
| run: npm run build:electron | |
| - name: Build Windows | |
| run: npm run dist:win | |
| env: | |
| GH_TOKEN: ${{ secrets.TOKEN }} | |
| - name: Upload Windows artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Chaton-Windows | |
| path: release/*.exe | |
| if-no-files-found: error | |
| build-linux: | |
| name: Build Linux App | |
| needs: determine-version | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Sync package.json version with computed release version | |
| run: | | |
| node -e "const fs=require('fs'); const path='package.json'; const pkg=JSON.parse(fs.readFileSync(path,'utf8')); pkg.version='${{ needs.determine-version.outputs.new_version }}'; fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); console.log('package.json version set to', pkg.version)" | |
| - name: Build renderer | |
| run: npm run build:renderer | |
| - name: Build Electron main process | |
| run: npm run build:electron | |
| - name: Build Linux | |
| run: npm run dist:linux | |
| env: | |
| GH_TOKEN: ${{ secrets.TOKEN }} | |
| - name: Upload Linux artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Chaton-Linux | |
| path: release/*.AppImage | |
| if-no-files-found: error | |
| release: | |
| permissions: | |
| contents: write | |
| name: Create Release | |
| needs: | |
| [ | |
| determine-version, | |
| build-macos-arm64, | |
| build-macos-x64, | |
| build-windows, | |
| build-linux, | |
| ] | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Get current version from package.json | |
| id: current_version | |
| run: | | |
| CURRENT_VERSION=$(node -p "require('./package.json').version") | |
| echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| echo "Current version: $CURRENT_VERSION" | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: List all artifacts | |
| run: find artifacts -type f | |
| - name: Generate Release Notes with AI | |
| id: generate_notes | |
| continue-on-error: true | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // Get the latest published release to use as baseline | |
| let sinceDate = null; | |
| let previousTag = null; | |
| try { | |
| const { data: latestRelease } = await github.rest.repos.getLatestRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| sinceDate = latestRelease.published_at; | |
| previousTag = latestRelease.tag_name; | |
| } catch (e) { | |
| // No previous release exists, include everything | |
| } | |
| // Fetch commits since the last release | |
| const commitParams = { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: 'main', | |
| per_page: 100, | |
| }; | |
| if (sinceDate) commitParams.since = sinceDate; | |
| const { data: commits } = await github.rest.repos.listCommits(commitParams); | |
| // Fetch closed PRs merged since the last release | |
| const { data: prs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'closed', | |
| base: 'main', | |
| sort: 'updated', | |
| direction: 'desc', | |
| per_page: 50, | |
| }); | |
| const changes = []; | |
| // Collect PRs merged after the previous release | |
| for (const pr of prs) { | |
| if (!pr.merged_at) continue; | |
| if (sinceDate && new Date(pr.merged_at) <= new Date(sinceDate)) continue; | |
| changes.push(`- ${pr.title} (PR #${pr.number})`); | |
| } | |
| // Collect conventional commits not already covered by a PR | |
| const prCommitShas = new Set(prs.filter(pr => pr.merge_commit_sha).map(pr => pr.merge_commit_sha)); | |
| for (const commit of commits) { | |
| if (prCommitShas.has(commit.sha)) continue; | |
| const match = commit.commit.message.match(/^(feat|fix|docs|style|refactor|perf|test|chore)(?:\(.+\))?: (.+)/); | |
| if (match) { | |
| changes.push(`- ${match[1]}: ${match[2]}`); | |
| } | |
| } | |
| const compareBase = previousTag || `v${process.env.CURRENT_VERSION}`; | |
| const releaseNotes = [ | |
| '## What\'s Changed', | |
| '', | |
| changes.length > 0 ? changes.join('\n') : '- Minor improvements and bug fixes', | |
| '', | |
| '## Downloads', | |
| '', | |
| '- **macOS**: Apple Silicon (ARM64) and Intel (x64) .dmg files', | |
| '- **Windows**: .exe installer', | |
| '- **Linux**: .AppImage', | |
| '', | |
| `Full Changelog: https://github.com/${context.repo.owner}/${context.repo.repo}/compare/${compareBase}...v${process.env.NEW_VERSION}`, | |
| ].join('\n'); | |
| core.setOutput('release_notes', releaseNotes); | |
| return releaseNotes; | |
| - name: Create Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ needs.determine-version.outputs.new_version }} | |
| name: Release v${{ needs.determine-version.outputs.new_version }} | |
| body: ${{ steps.generate_notes.outputs.release_notes || format('Automatic multi-platform build from commit {0}', github.sha) }} | |
| draft: false | |
| prerelease: false | |
| files: artifacts/**/* | |
| env: | |
| TOKEN: ${{ secrets.TOKEN }} | |
| CURRENT_VERSION: ${{ steps.current_version.outputs.current_version }} | |
| NEW_VERSION: ${{ needs.determine-version.outputs.new_version }} |