Build, Sign, Notarize & Publish (non-MAS) #35
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, Sign, Notarize & Publish (non-MAS) | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| create_release: | |
| description: 'Create a GitHub Release with the DMG' | |
| type: boolean | |
| default: true | |
| release_tag: | |
| description: 'Release tag (e.g. v1.0.33). Leave empty to auto-generate from package.json' | |
| type: string | |
| default: '' | |
| release_notes: | |
| description: 'Release notes. Leave empty to auto-read from CHANGELOG.md' | |
| type: string | |
| default: '' | |
| jobs: | |
| build-and-notarize: | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: 'yarn' | |
| - name: Install dependencies | |
| run: yarn install --frozen-lockfile | |
| - name: Import certificate to keychain | |
| env: | |
| APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12 | |
| KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db | |
| KEYCHAIN_PASSWORD=$(openssl rand -base64 32) | |
| echo -n "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode -o $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 "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH | |
| security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| security list-keychain -d user -s $KEYCHAIN_PATH | |
| # Import Developer ID G2 intermediate certificate | |
| curl -sO https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer | |
| security import DeveloperIDG2CA.cer -k $KEYCHAIN_PATH || true | |
| rm DeveloperIDG2CA.cer | |
| - name: Build app | |
| run: yarn make | |
| - name: Install create-dmg | |
| run: brew install create-dmg | |
| - name: Sign, notarize, and create DMG | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: sh ./sign-notarize.sh | |
| - name: Get version from package.json | |
| id: version | |
| run: echo "version=$(node -p 'require("./package.json").version')" >> $GITHUB_OUTPUT | |
| - name: Extract release notes from CHANGELOG.md | |
| id: changelog | |
| if: ${{ inputs.create_release && inputs.release_notes == '' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| # Find the last published release version to aggregate all unreleased entries | |
| LAST_TAG=$(gh release list --limit 1 --json tagName -q '.[0].tagName' 2>/dev/null || echo "") | |
| LAST_VER=$(echo "$LAST_TAG" | sed 's/^v//') | |
| # Extract all changelog sections from current version down to (but not including) last release | |
| if [ -n "$LAST_VER" ] && [ "$LAST_VER" != "$VERSION" ]; then | |
| NOTES=$(awk -v cur="## ${VERSION}" -v prev="## ${LAST_VER}" \ | |
| '$0 == cur {found=1} found && $0 == prev {exit} found' CHANGELOG.md | sed '/^$/d') | |
| else | |
| NOTES=$(awk -v ver="## ${VERSION}" '$0 == ver {found=1; next} found && /^## / {exit} found' CHANGELOG.md | sed '/^$/d') | |
| fi | |
| if [ -z "$NOTES" ]; then | |
| NOTES="Non-App Store notarized build v${VERSION}" | |
| fi | |
| # Write to file to avoid escaping issues | |
| echo "$NOTES" > /tmp/release-notes.txt | |
| echo "notes_file=/tmp/release-notes.txt" >> $GITHUB_OUTPUT | |
| - name: Create signed ZIP for auto-updater | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| cd ./out/CodeV-darwin-arm64 | |
| zip -r -y "../CodeV-darwin-arm64-${VERSION}.zip" CodeV.app | |
| echo "Created signed zip: ./out/CodeV-darwin-arm64-${VERSION}.zip" | |
| - name: Upload DMG artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: CodeV-${{ steps.version.outputs.version }}.dmg | |
| path: ./out/CodeV.dmg | |
| - name: Upload ZIP artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: CodeV-${{ steps.version.outputs.version }}.zip | |
| path: ./out/CodeV-darwin-arm64-${{ steps.version.outputs.version }}.zip | |
| - name: Create GitHub Release | |
| if: ${{ inputs.create_release }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| TAG="${{ inputs.release_tag }}" | |
| if [ -z "$TAG" ]; then | |
| TAG="v${{ steps.version.outputs.version }}" | |
| fi | |
| VERSION="${{ steps.version.outputs.version }}" | |
| ZIP_FILE="./out/CodeV-darwin-arm64-${VERSION}.zip" | |
| NOTES="${{ inputs.release_notes }}" | |
| if [ -z "$NOTES" ] && [ -f /tmp/release-notes.txt ]; then | |
| gh release create "$TAG" ./out/CodeV.dmg "$ZIP_FILE" \ | |
| --title "$TAG" \ | |
| --notes-file /tmp/release-notes.txt | |
| else | |
| gh release create "$TAG" ./out/CodeV.dmg "$ZIP_FILE" \ | |
| --title "$TAG" \ | |
| --notes "${NOTES:-Non-App Store notarized build}" | |
| fi | |
| - name: Cleanup keychain | |
| if: always() | |
| run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true |