ci: add signed + notarized release workflow #1
Workflow file for this run
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: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag to release (e.g. v1.0.0)' | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| env: | |
| SIGN_IDENTITY: "Developer ID Application: FOURNINE CLOUD SOLUTIONS PRIVATE LIMITED (XUKA94C5G4)" | |
| PYTHON_VERSION: "3.13" | |
| jobs: | |
| build: | |
| name: Build, sign, notarize & publish | |
| runs-on: macos-15 | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.inputs.tag || github.ref }} | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Show toolchain | |
| run: | | |
| python3 --version | |
| xcrun --version | |
| uname -m | |
| - name: Install build dependencies | |
| run: | | |
| python3 -m pip install --upgrade pip setuptools wheel | |
| python3 -m pip install py2app pyobjc | |
| python3 -m pip install -r requirements.txt | |
| - name: Import Developer ID certificate | |
| env: | |
| P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" | |
| CERT_PATH="$RUNNER_TEMP/cert.p12" | |
| P12_PASSWORD="$(printf '%s' "$P12_PASSWORD" | tr -d '\r\n')" | |
| PWD_LEN=${#P12_PASSWORD} | |
| PWD_FP=$(printf '%s' "$P12_PASSWORD" | shasum -a 256 | cut -c1-12) | |
| echo "Password length: $PWD_LEN" | |
| echo "Password sha256 prefix: $PWD_FP" | |
| printf '%s' "$P12_BASE64" | base64 --decode > "$CERT_PATH" | |
| P12_SIZE=$(wc -c < "$CERT_PATH") | |
| echo "Decoded .p12 is $P12_SIZE bytes." | |
| if [ "$P12_SIZE" -lt 1000 ]; then | |
| echo "::error::Decoded .p12 looks too small — secret MACOS_CERTIFICATE_P12_BASE64 may be truncated or mis-encoded." | |
| exit 1 | |
| fi | |
| if /usr/bin/openssl pkcs12 -in "$CERT_PATH" -nokeys \ | |
| -passin "env:P12_PASSWORD" >/dev/null 2>&1; then | |
| echo "Password accepted by openssl." | |
| elif /usr/bin/openssl pkcs12 -in "$CERT_PATH" -nokeys -legacy \ | |
| -passin "env:P12_PASSWORD" >/dev/null 2>&1; then | |
| echo "Password accepted by openssl (-legacy)." | |
| else | |
| echo "::error::Password rejected by openssl. The CI sha256 prefix above must match printf '%s' 'YOUR_PASSWORD' | shasum -a 256 | cut -c1-12 on your machine." | |
| exit 1 | |
| fi | |
| 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 "$CERT_PATH" \ | |
| -P "$P12_PASSWORD" \ | |
| -A -t cert -f pkcs12 \ | |
| -k "$KEYCHAIN_PATH" \ | |
| -T /usr/bin/codesign -T /usr/bin/security | |
| security set-key-partition-list \ | |
| -S apple-tool:,apple:,codesign: \ | |
| -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') | |
| security default-keychain -s "$KEYCHAIN_PATH" | |
| rm "$CERT_PATH" | |
| echo "Available signing identities:" | |
| security find-identity -v -p codesigning | |
| - name: Determine version | |
| id: version | |
| env: | |
| INPUT_TAG: ${{ github.event.inputs.tag }} | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| TAG="${INPUT_TAG:-$REF_NAME}" | |
| VERSION="${TAG#v}" | |
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "Releasing $TAG (CFBundle version $VERSION)" | |
| - name: Build .app via py2app | |
| run: | | |
| set -euo pipefail | |
| rm -rf build dist | |
| python3 setup.py py2app | |
| ls -la dist/ | |
| - name: Stamp version into Info.plist | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| PLIST="dist/TimeTracker.app/Contents/Info.plist" | |
| plutil -replace CFBundleShortVersionString -string "$VERSION" "$PLIST" | |
| plutil -replace CFBundleVersion -string "$VERSION" "$PLIST" | |
| plutil -replace LSApplicationCategoryType -string "public.app-category.productivity" "$PLIST" | |
| echo "--- Info.plist ---" | |
| plutil -p "$PLIST" | |
| - name: Sign app bundle (hardened runtime) | |
| run: | | |
| set -euo pipefail | |
| APP="dist/TimeTracker.app" | |
| ENTITLEMENTS="$GITHUB_WORKSPACE/entitlements.plist" | |
| echo "::group::Signing nested binaries" | |
| find "$APP" -type f \( -name "*.so" -o -name "*.dylib" \) -print0 | \ | |
| while IFS= read -r -d '' f; do | |
| codesign --force --options runtime --timestamp \ | |
| --sign "$SIGN_IDENTITY" "$f" | |
| done | |
| echo "::endgroup::" | |
| PYTHON_FW=$(find "$APP/Contents/Frameworks" -path "*/Python.framework/Versions/*/Python" -type f 2>/dev/null | head -1 || true) | |
| if [ -n "${PYTHON_FW:-}" ]; then | |
| echo "Signing embedded Python: $PYTHON_FW" | |
| codesign --force --options runtime --timestamp \ | |
| --sign "$SIGN_IDENTITY" "$PYTHON_FW" | |
| fi | |
| for f in "$APP/Contents/MacOS/"*; do | |
| [ -f "$f" ] || continue | |
| codesign --force --options runtime --timestamp \ | |
| --sign "$SIGN_IDENTITY" "$f" | |
| done | |
| codesign --force --options runtime --timestamp \ | |
| --entitlements "$ENTITLEMENTS" \ | |
| --sign "$SIGN_IDENTITY" "$APP" | |
| echo "::group::Verify signature" | |
| codesign --verify --deep --strict --verbose=2 "$APP" | |
| codesign -dvv "$APP" | |
| echo "::endgroup::" | |
| - name: Package zip for notarization | |
| run: | | |
| set -euo pipefail | |
| cd dist | |
| ditto -c -k --sequesterRsrc --keepParent TimeTracker.app TimeTracker.zip | |
| ls -la TimeTracker.zip | |
| - name: Notarize and staple | |
| env: | |
| NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }} | |
| NOTARY_PASSWORD: ${{ secrets.NOTARY_PASSWORD }} | |
| NOTARY_TEAM_ID: ${{ secrets.NOTARY_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| cd dist | |
| echo "Submitting to notary..." | |
| xcrun notarytool submit TimeTracker.zip \ | |
| --apple-id "$NOTARY_APPLE_ID" \ | |
| --password "$NOTARY_PASSWORD" \ | |
| --team-id "$NOTARY_TEAM_ID" \ | |
| --wait | |
| echo "Stapling..." | |
| xcrun stapler staple TimeTracker.app | |
| xcrun stapler validate TimeTracker.app | |
| rm -f TimeTracker.zip | |
| ditto -c -k --sequesterRsrc --keepParent TimeTracker.app TimeTracker.zip | |
| echo "Stapled and re-zipped." | |
| - name: Verify Gatekeeper acceptance | |
| run: | | |
| xcrun stapler validate dist/TimeTracker.app | |
| spctl -a -vvv -t install dist/TimeTracker.app || true | |
| - name: Compute checksums | |
| run: | | |
| cd dist | |
| shasum -a 256 TimeTracker.zip > SHA256SUMS.txt | |
| cat SHA256SUMS.txt | |
| - name: Clean up keychain | |
| if: always() | |
| run: | | |
| security delete-keychain "$RUNNER_TEMP/build.keychain-db" || true | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.tag }} | |
| name: ${{ steps.version.outputs.tag }} | |
| draft: false | |
| prerelease: false | |
| generate_release_notes: true | |
| fail_on_unmatched_files: true | |
| body: | | |
| ## TimeTracker ${{ steps.version.outputs.tag }} | |
| Local-first macOS activity tracker with menu-bar UI and Flask dashboard. | |
| ### Downloads | |
| | Asset | Description | | |
| |---|---| | |
| | `TimeTracker.zip` | Signed and notarized macOS app bundle | | |
| | `SHA256SUMS.txt` | Checksums | | |
| ### Install | |
| ```bash | |
| unzip TimeTracker.zip -d /Applications/ | |
| open /Applications/TimeTracker.app | |
| ``` | |
| Signed with Apple Developer ID and notarized — Gatekeeper accepts on first launch with no warnings. macOS will still prompt for Apple Events, Accessibility, and (optionally) Screen Recording permissions; see the [README](https://github.com/FournineCS/timetracker#permissions) for what each one is for. | |
| files: | | |
| dist/TimeTracker.zip | |
| dist/SHA256SUMS.txt |