From ab075e31838180c3a1426fd6656847dcbaa6d90c Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Tue, 23 Dec 2025 17:09:24 -0600 Subject: [PATCH 1/9] initial signing and notarization --- .github/entitlements.plist | 25 ++++ .github/scripts/sign-and-notarize.sh | 164 +++++++++++++++++++++++++++ .github/scripts/sign-dmg.sh | 36 ++++++ .github/workflows/pr-tests.yml | 89 ++++++++++++++- .github/workflows/release.yml | 39 +++++++ 5 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 .github/entitlements.plist create mode 100755 .github/scripts/sign-and-notarize.sh create mode 100755 .github/scripts/sign-dmg.sh diff --git a/.github/entitlements.plist b/.github/entitlements.plist new file mode 100644 index 0000000..b307dc5 --- /dev/null +++ b/.github/entitlements.plist @@ -0,0 +1,25 @@ + + + + + + com.apple.security.network.client + + + + com.apple.security.files.user-selected.read-write + + + + com.apple.security.files.downloads.read-write + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + + com.apple.security.cs.disable-library-validation + + + diff --git a/.github/scripts/sign-and-notarize.sh b/.github/scripts/sign-and-notarize.sh new file mode 100755 index 0000000..dfdb0b7 --- /dev/null +++ b/.github/scripts/sign-and-notarize.sh @@ -0,0 +1,164 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +# Check required environment variables +if [ -z "$APPLE_CERTIFICATE_P12_BASE64" ]; then + log_error "APPLE_CERTIFICATE_P12_BASE64 is not set" + exit 1 +fi + +if [ -z "$APPLE_CERTIFICATE_PASSWORD" ]; then + log_error "APPLE_CERTIFICATE_PASSWORD is not set" + exit 1 +fi + +if [ -z "$APPLE_ID" ]; then + log_error "APPLE_ID is not set" + exit 1 +fi + +if [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ]; then + log_error "APPLE_APP_SPECIFIC_PASSWORD is not set" + exit 1 +fi + +if [ -z "$APPLE_TEAM_ID" ]; then + log_error "APPLE_TEAM_ID is not set" + exit 1 +fi + +APP_PATH="dist/TextWave.app" +ENTITLEMENTS_PATH=".github/entitlements.plist" + +# Verify app exists +if [ ! -d "$APP_PATH" ]; then + log_error "App not found at $APP_PATH" + exit 1 +fi + +log_info "Starting code signing and notarization process..." + +# Create temporary keychain +KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" +KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + +log_info "Creating temporary keychain..." +security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" +security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" +security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + +# Decode and import certificate +log_info "Importing certificate..." +CERT_PATH="$RUNNER_TEMP/certificate.p12" +echo "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > "$CERT_PATH" +security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + +# Set keychain as default +security list-keychain -d user -s "$KEYCHAIN_PATH" +security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + +# Find signing identity +log_info "Finding Developer ID Application certificate..." +SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"') + +if [ -z "$SIGNING_IDENTITY" ]; then + log_error "No Developer ID Application certificate found" + security delete-keychain "$KEYCHAIN_PATH" || true + exit 1 +fi + +log_info "Using signing identity: $SIGNING_IDENTITY" + +# Sign all executables and frameworks inside the app +log_info "Signing embedded binaries and frameworks..." +find "$APP_PATH/Contents" -type f \( -name "*.dylib" -o -name "*.so" \) -exec codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime {} \; + +# Sign frameworks +if [ -d "$APP_PATH/Contents/Frameworks" ]; then + find "$APP_PATH/Contents/Frameworks" -type d -name "*.framework" | while read framework; do + log_info "Signing framework: $(basename "$framework")" + codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$framework" + done +fi + +# Sign the main executable +log_info "Signing main executable..." +codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ + --entitlements "$ENTITLEMENTS_PATH" \ + "$APP_PATH/Contents/MacOS/TextWave" + +# Sign the app bundle +log_info "Signing app bundle..." +codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ + --entitlements "$ENTITLEMENTS_PATH" \ + "$APP_PATH" + +# Verify signature +log_info "Verifying signature..." +codesign --verify --deep --strict --verbose=2 "$APP_PATH" +spctl --assess --type execute --verbose=4 "$APP_PATH" || log_warning "Gatekeeper assessment may fail before notarization" + +# Create zip for notarization (notarization requires zip, not just app bundle) +log_info "Creating archive for notarization..." +NOTARIZATION_ZIP="$RUNNER_TEMP/TextWave-notarization.zip" +ditto -c -k --keepParent "$APP_PATH" "$NOTARIZATION_ZIP" + +# Submit for notarization +log_info "Submitting to Apple notary service..." +xcrun notarytool submit "$NOTARIZATION_ZIP" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait \ + --timeout 30m + +# Check notarization status +NOTARIZATION_STATUS=$? +if [ $NOTARIZATION_STATUS -ne 0 ]; then + log_error "Notarization failed" + security delete-keychain "$KEYCHAIN_PATH" || true + exit 1 +fi + +log_info "Notarization successful!" + +# Staple the notarization ticket +log_info "Stapling notarization ticket..." +xcrun stapler staple "$APP_PATH" + +# Verify stapling +log_info "Verifying stapled ticket..." +xcrun stapler validate "$APP_PATH" + +# Final verification +log_info "Final signature verification..." +codesign --verify --deep --strict --verbose=2 "$APP_PATH" +spctl --assess --type execute --verbose=4 "$APP_PATH" + +log_info "Code signing and notarization complete!" + +# Clean up +log_info "Cleaning up temporary keychain..." +security delete-keychain "$KEYCHAIN_PATH" || true +rm -f "$CERT_PATH" +rm -f "$NOTARIZATION_ZIP" + +log_info "All done! The app is signed and notarized." diff --git a/.github/scripts/sign-dmg.sh b/.github/scripts/sign-dmg.sh new file mode 100755 index 0000000..ec9aa4f --- /dev/null +++ b/.github/scripts/sign-dmg.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +VERSION=$1 +DMG_PATH="TextWave-${VERSION}.dmg" + +if [ -z "$VERSION" ]; then + echo "Error: Version not specified" + exit 1 +fi + +if [ ! -f "$DMG_PATH" ]; then + echo "Error: DMG not found at $DMG_PATH" + exit 1 +fi + +echo "Signing DMG: $DMG_PATH" + +# Find signing identity (keychain already set up by calling workflow) +SIGNING_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"') + +if [ -z "$SIGNING_IDENTITY" ]; then + echo "Error: No Developer ID Application certificate found" + exit 1 +fi + +echo "Using signing identity: $SIGNING_IDENTITY" + +# Sign the DMG +codesign --force --sign "$SIGNING_IDENTITY" --timestamp "$DMG_PATH" + +# Verify DMG signature +echo "Verifying DMG signature..." +codesign --verify --verbose=2 "$DMG_PATH" + +echo "DMG signed successfully!" diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index dcfab5f..4acb335 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -6,6 +6,10 @@ on: push: branches: [main] +permissions: + contents: read + pull-requests: write + jobs: test: runs-on: macos-latest @@ -30,8 +34,19 @@ jobs: flake8 pdf2mp3_gui.py --max-line-length=120 --ignore=E203,W503 || true - name: Run unit tests + id: pytest + continue-on-error: true run: | - pytest tests/ -v --cov=pdf2mp3_gui --cov-report=xml --cov-report=term + set +e + pytest tests/ -v --cov=pdf2mp3_gui --cov-report=xml --cov-report=term | tee pytest-output.txt + echo "exit_code=$?" >> $GITHUB_OUTPUT + exit 0 + + - name: Extract coverage percentage + id: coverage + run: | + COVERAGE=$(grep -oP 'TOTAL.*\K\d+(?=%)' pytest-output.txt 2>/dev/null || echo "0") + echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 @@ -42,7 +57,79 @@ jobs: continue-on-error: true - name: Test app builds + id: build + continue-on-error: true run: | + set +e python -m pip install py2app python setup.py py2app test -d dist/TextWave.app + echo "exit_code=$?" >> $GITHUB_OUTPUT + exit 0 + + - name: Post test results comment + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const pytest_exit = '${{ steps.pytest.outputs.exit_code }}' || '0'; + const build_exit = '${{ steps.build.outputs.exit_code }}' || '0'; + const coverage = '${{ steps.coverage.outputs.percentage }}' || '0'; + + // Determine overall status + const all_passed = pytest_exit === '0' && build_exit === '0'; + const status_emoji = all_passed ? '✅' : '❌'; + const status_text = all_passed ? 'Passed' : 'Failed'; + + // Create comment body + const comment_body = `## ${status_emoji} Test Results + +**Status:** ${status_text} +**Coverage:** ${coverage}% + +### Test Summary +- **Linting:** ✅ flake8 checks completed +- **Unit Tests:** ${pytest_exit === '0' ? '✅ Passed' : '❌ Failed'} +- **Build Verification:** ${build_exit === '0' ? '✅ Passed' : '❌ Failed'} + +[View full workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId}) + +--- +*Last updated: ${new Date().toUTCString()}* +`; + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const bot_comment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('') + ); + + // Update or create comment + if (bot_comment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: bot_comment.id, + body: comment_body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment_body + }); + } + + - name: Fail workflow if tests failed + if: steps.pytest.outputs.exit_code != '0' || steps.build.outputs.exit_code != '0' + run: | + echo "Tests failed - failing workflow" + exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6880437..1a3c686 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,12 +54,49 @@ jobs: chmod +x .github/scripts/build-app.sh .github/scripts/build-app.sh + - name: Code sign and notarize app + if: steps.check_tag.outputs.exists == 'false' + env: + APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + chmod +x .github/scripts/sign-and-notarize.sh + .github/scripts/sign-and-notarize.sh + - name: Create DMG if: steps.check_tag.outputs.exists == 'false' run: | chmod +x .github/scripts/create-dmg.sh .github/scripts/create-dmg.sh ${{ steps.get_version.outputs.version }} + - name: Sign DMG + if: steps.check_tag.outputs.exists == 'false' + env: + APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + # Reimport certificate for DMG signing + KEYCHAIN_PATH="$RUNNER_TEMP/dmg-signing.keychain-db" + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + CERT_PATH="$RUNNER_TEMP/certificate.p12" + echo "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > "$CERT_PATH" + security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + chmod +x .github/scripts/sign-dmg.sh + .github/scripts/sign-dmg.sh ${{ steps.get_version.outputs.version }} + + security delete-keychain "$KEYCHAIN_PATH" || true + rm -f "$CERT_PATH" + - name: Create Release if: steps.check_tag.outputs.exists == 'false' uses: softprops/action-gh-release@v1 @@ -77,6 +114,8 @@ jobs: ### Installation Download `TextWave-${{ steps.get_version.outputs.version }}.dmg`, open it, and drag TextWave to your Applications folder. + ✅ **This release is code-signed and notarized by Apple** - no security warnings! + ### Requirements - macOS 10.15 or later - Internet connection for text-to-speech conversion From c9a654034cfdc5f21a47a0e5df8874a85d94e11a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 00:16:49 +0000 Subject: [PATCH 2/9] Fix YAML syntax error in pr-tests.yml caused by unindented lines in script block --- .github/workflows/pr-tests.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 4acb335..7ab2cab 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -85,19 +85,19 @@ jobs: // Create comment body const comment_body = `## ${status_emoji} Test Results -**Status:** ${status_text} -**Coverage:** ${coverage}% + **Status:** ${status_text} + **Coverage:** ${coverage}% -### Test Summary -- **Linting:** ✅ flake8 checks completed -- **Unit Tests:** ${pytest_exit === '0' ? '✅ Passed' : '❌ Failed'} -- **Build Verification:** ${build_exit === '0' ? '✅ Passed' : '❌ Failed'} + ### Test Summary + - **Linting:** ✅ flake8 checks completed + - **Unit Tests:** ${pytest_exit === '0' ? '✅ Passed' : '❌ Failed'} + - **Build Verification:** ${build_exit === '0' ? '✅ Passed' : '❌ Failed'} -[View full workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId}) + [View full workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId}) ---- -*Last updated: ${new Date().toUTCString()}* -`; + --- + *Last updated: ${new Date().toUTCString()}* + `; // Find existing comment const { data: comments } = await github.rest.issues.listComments({ From de166dc0802cbd969dcbbdd0d21ccf5cdf356a8d Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Tue, 23 Dec 2025 19:24:50 -0600 Subject: [PATCH 3/9] Fix coverage extraction to work on macOS without Perl regex --- .github/workflows/pr-tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 7ab2cab..dc9a277 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -45,7 +45,12 @@ jobs: - name: Extract coverage percentage id: coverage run: | - COVERAGE=$(grep -oP 'TOTAL.*\K\d+(?=%)' pytest-output.txt 2>/dev/null || echo "0") + # Extract coverage percentage using sed (more portable than grep -P) + COVERAGE=$(grep "TOTAL" pytest-output.txt 2>/dev/null | sed -E 's/.*TOTAL[[:space:]]+[0-9]+[[:space:]]+[0-9]+[[:space:]]+[0-9]+[[:space:]]+[0-9]+[[:space:]]+([0-9]+)%.*/\1/' || echo "0") + # Fallback: try alternative format if above didn't work + if [ "$COVERAGE" = "0" ] || [ -z "$COVERAGE" ]; then + COVERAGE=$(grep "TOTAL" pytest-output.txt 2>/dev/null | grep -o '[0-9]\+%' | head -1 | tr -d '%' || echo "0") + fi echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT - name: Upload coverage to Codecov From 33a4d75764b8094cb3048a0e8e8660ad1c544679 Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Tue, 23 Dec 2025 19:27:07 -0600 Subject: [PATCH 4/9] Fix coverage extraction and add signing/notarization note to PR comments --- .github/workflows/pr-tests.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index dc9a277..088eb45 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -45,11 +45,11 @@ jobs: - name: Extract coverage percentage id: coverage run: | - # Extract coverage percentage using sed (more portable than grep -P) - COVERAGE=$(grep "TOTAL" pytest-output.txt 2>/dev/null | sed -E 's/.*TOTAL[[:space:]]+[0-9]+[[:space:]]+[0-9]+[[:space:]]+[0-9]+[[:space:]]+[0-9]+[[:space:]]+([0-9]+)%.*/\1/' || echo "0") - # Fallback: try alternative format if above didn't work - if [ "$COVERAGE" = "0" ] || [ -z "$COVERAGE" ]; then - COVERAGE=$(grep "TOTAL" pytest-output.txt 2>/dev/null | grep -o '[0-9]\+%' | head -1 | tr -d '%' || echo "0") + # Extract just the percentage number from coverage output + COVERAGE=$(grep "TOTAL" pytest-output.txt 2>/dev/null | awk '{print $NF}' | tr -d '%' || echo "0") + # Validate it's a number + if ! [[ "$COVERAGE" =~ ^[0-9]+$ ]]; then + COVERAGE="0" fi echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT @@ -97,6 +97,7 @@ jobs: - **Linting:** ✅ flake8 checks completed - **Unit Tests:** ${pytest_exit === '0' ? '✅ Passed' : '❌ Failed'} - **Build Verification:** ${build_exit === '0' ? '✅ Passed' : '❌ Failed'} + - **Signing & Notarization:** ⏭️ Only on release (not tested in PRs) [View full workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId}) From bcecb8269060a21cbe4118e9742644cb04bd16a4 Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Tue, 23 Dec 2025 19:34:57 -0600 Subject: [PATCH 5/9] Update release.yml --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a3c686..1c27b85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,10 @@ name: Build and Release on: push: - branches: [main] + branches: + - main + - release/* + - kc9wwh-implement_code_signing paths-ignore: - "**.md" - "docs/**" From dfcd5cfb33358cd878157917a9b280f36e442371 Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Tue, 23 Dec 2025 19:40:25 -0600 Subject: [PATCH 6/9] Update versioning to create v0.6.0-beta release --- pdf2mp3_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdf2mp3_gui.py b/pdf2mp3_gui.py index 8d39cfd..afb8974 100644 --- a/pdf2mp3_gui.py +++ b/pdf2mp3_gui.py @@ -7,7 +7,7 @@ import webbrowser from pathlib import Path -__version__ = "0.5.2" +__version__ = "0.6.0-beta" def get_resource_path(filename): From 990a5aebe33c68a9f97f2a42d1d775faaeaa5d6f Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Tue, 23 Dec 2025 20:03:45 -0600 Subject: [PATCH 7/9] Fix code signing to handle all Qt frameworks and Python executable - Sign all .dylib and .so files - Sign Python executable and other binaries in MacOS directory - Properly sign Qt frameworks with nested binaries - Improve notarization error detection and reporting - Automatically fetch rejection logs on failure --- .github/scripts/sign-and-notarize.sh | 85 +++++++++++++++++++++------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/.github/scripts/sign-and-notarize.sh b/.github/scripts/sign-and-notarize.sh index dfdb0b7..ee998d9 100755 --- a/.github/scripts/sign-and-notarize.sh +++ b/.github/scripts/sign-and-notarize.sh @@ -87,25 +87,52 @@ fi log_info "Using signing identity: $SIGNING_IDENTITY" -# Sign all executables and frameworks inside the app -log_info "Signing embedded binaries and frameworks..." -find "$APP_PATH/Contents" -type f \( -name "*.dylib" -o -name "*.so" \) -exec codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime {} \; +# Sign all binaries - must be done in correct order: deepest first +log_info "Signing all binaries (this may take a few minutes)..." + +# Step 1: Sign all .dylib and .so files +log_info "Step 1/5: Signing .dylib and .so files..." +find "$APP_PATH/Contents/Resources" -type f \( -name "*.dylib" -o -name "*.so" \) -print0 | while IFS= read -r -d '' file; do + codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$file" 2>/dev/null || true +done + +# Step 2: Sign all executables in MacOS directory (including Python) +log_info "Step 2/5: Signing executables in MacOS directory..." +find "$APP_PATH/Contents/MacOS" -type f -perm +111 -print0 | while IFS= read -r -d '' file; do + log_info " Signing: $(basename "$file")" + codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$file" 2>/dev/null || true +done + +# Step 3: Sign Qt/PyQt6 frameworks (must sign nested binaries first, then framework) +log_info "Step 3/5: Signing Qt frameworks..." +if [ -d "$APP_PATH/Contents/Resources/lib/python3.11/PyQt6/Qt6/lib" ]; then + find "$APP_PATH/Contents/Resources/lib/python3.11/PyQt6/Qt6/lib" -name "*.framework" -print0 | while IFS= read -r -d '' framework; do + # First sign any binaries inside the framework + find "$framework" -type f \( -name "*.dylib" -o -perm +111 \) -print0 | while IFS= read -r -d '' binary; do + codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$binary" 2>/dev/null || true + done + # Then sign the framework itself + log_info " Signing framework: $(basename "$framework")" + codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$framework" 2>/dev/null || true + done +fi -# Sign frameworks +# Step 4: Sign any frameworks in Contents/Frameworks +log_info "Step 4/5: Signing frameworks in Contents/Frameworks..." if [ -d "$APP_PATH/Contents/Frameworks" ]; then - find "$APP_PATH/Contents/Frameworks" -type d -name "*.framework" | while read framework; do - log_info "Signing framework: $(basename "$framework")" - codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$framework" + find "$APP_PATH/Contents/Frameworks" -name "*.framework" -print0 | while IFS= read -r -d '' framework; do + log_info " Signing framework: $(basename "$framework")" + codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$framework" 2>/dev/null || true done fi -# Sign the main executable -log_info "Signing main executable..." +# Step 5: Sign the main executable +log_info "Step 5/5: Signing main executable..." codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ --entitlements "$ENTITLEMENTS_PATH" \ "$APP_PATH/Contents/MacOS/TextWave" -# Sign the app bundle +# Finally: Sign the app bundle itself log_info "Signing app bundle..." codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \ --entitlements "$ENTITLEMENTS_PATH" \ @@ -123,23 +150,43 @@ ditto -c -k --keepParent "$APP_PATH" "$NOTARIZATION_ZIP" # Submit for notarization log_info "Submitting to Apple notary service..." -xcrun notarytool submit "$NOTARIZATION_ZIP" \ +NOTARIZATION_OUTPUT=$(xcrun notarytool submit "$NOTARIZATION_ZIP" \ --apple-id "$APPLE_ID" \ --password "$APPLE_APP_SPECIFIC_PASSWORD" \ --team-id "$APPLE_TEAM_ID" \ --wait \ - --timeout 30m - -# Check notarization status -NOTARIZATION_STATUS=$? -if [ $NOTARIZATION_STATUS -ne 0 ]; then - log_error "Notarization failed" + --timeout 30m 2>&1) + +NOTARIZATION_EXIT_CODE=$? +echo "$NOTARIZATION_OUTPUT" + +# Extract submission ID from output +SUBMISSION_ID=$(echo "$NOTARIZATION_OUTPUT" | grep -o 'id: [a-f0-9-]*' | head -1 | cut -d' ' -f2) + +# Check if notarization was accepted +if echo "$NOTARIZATION_OUTPUT" | grep -q "status: Accepted"; then + log_info "Notarization accepted!" +elif echo "$NOTARIZATION_OUTPUT" | grep -q "status: Invalid"; then + log_error "Notarization was rejected by Apple!" + if [ -n "$SUBMISSION_ID" ]; then + log_error "Fetching detailed rejection log..." + xcrun notarytool log "$SUBMISSION_ID" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" || true + fi + security delete-keychain "$KEYCHAIN_PATH" || true + exit 1 +elif [ $NOTARIZATION_EXIT_CODE -ne 0 ]; then + log_error "Notarization failed with exit code $NOTARIZATION_EXIT_CODE" + security delete-keychain "$KEYCHAIN_PATH" || true + exit 1 +else + log_error "Notarization status unclear - check output above" security delete-keychain "$KEYCHAIN_PATH" || true exit 1 fi -log_info "Notarization successful!" - # Staple the notarization ticket log_info "Stapling notarization ticket..." xcrun stapler staple "$APP_PATH" From eb4aeddaafd2950fad5d8c49aca180c81e0e45b4 Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Tue, 23 Dec 2025 20:12:03 -0600 Subject: [PATCH 8/9] Sign standalone .dylib files in Contents/Frameworks Fixes signing of libtcl8.6.dylib, libtk8.6.dylib, libssl.3.dylib, and libcrypto.3.dylib --- .github/scripts/sign-and-notarize.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/scripts/sign-and-notarize.sh b/.github/scripts/sign-and-notarize.sh index ee998d9..311983d 100755 --- a/.github/scripts/sign-and-notarize.sh +++ b/.github/scripts/sign-and-notarize.sh @@ -117,9 +117,15 @@ if [ -d "$APP_PATH/Contents/Resources/lib/python3.11/PyQt6/Qt6/lib" ]; then done fi -# Step 4: Sign any frameworks in Contents/Frameworks -log_info "Step 4/5: Signing frameworks in Contents/Frameworks..." +# Step 4: Sign Contents/Frameworks (both .dylib files and .framework bundles) +log_info "Step 4/5: Signing Contents/Frameworks..." if [ -d "$APP_PATH/Contents/Frameworks" ]; then + # First sign all .dylib files + find "$APP_PATH/Contents/Frameworks" -type f -name "*.dylib" -print0 | while IFS= read -r -d '' dylib; do + log_info " Signing dylib: $(basename "$dylib")" + codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$dylib" 2>/dev/null || true + done + # Then sign .framework bundles find "$APP_PATH/Contents/Frameworks" -name "*.framework" -print0 | while IFS= read -r -d '' framework; do log_info " Signing framework: $(basename "$framework")" codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$framework" 2>/dev/null || true From aae54c8a7aa5ae00e29c539c203b051f5ffbe26e Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Wed, 24 Dec 2025 07:15:36 -0600 Subject: [PATCH 9/9] Prepare for v0.6.0 release --- .github/workflows/release.yml | 5 +---- pdf2mp3_gui.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c27b85..1a3c686 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,10 +2,7 @@ name: Build and Release on: push: - branches: - - main - - release/* - - kc9wwh-implement_code_signing + branches: [main] paths-ignore: - "**.md" - "docs/**" diff --git a/pdf2mp3_gui.py b/pdf2mp3_gui.py index afb8974..2000442 100644 --- a/pdf2mp3_gui.py +++ b/pdf2mp3_gui.py @@ -7,7 +7,7 @@ import webbrowser from pathlib import Path -__version__ = "0.6.0-beta" +__version__ = "0.6.0" def get_resource_path(filename):