From 346cd2e29869b1dcdfc87da888ba56f44425eb2a Mon Sep 17 00:00:00 2001 From: Radwan Parvez Date: Sat, 25 Jul 2026 12:24:19 -0400 Subject: [PATCH] Publish free source-built release --- .github/workflows/ci.yml | 7 -- .github/workflows/notarized-release.yml | 139 ++++++++++++++++++++++++ .github/workflows/release.yml | 107 ++---------------- CHANGELOG.md | 4 +- README.md | 93 ++++++++++------ install.sh | 69 ++++++++++++ uninstall.sh | 31 ++++++ 7 files changed, 309 insertions(+), 141 deletions(-) create mode 100644 .github/workflows/notarized-release.yml create mode 100755 install.sh create mode 100755 uninstall.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27e3459..31a8ca0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,3 @@ jobs: env: RUN_LAUNCH_SERVICES_TESTS: "1" run: ./scripts/test.sh - - - name: Upload development build - uses: actions/upload-artifact@v4 - with: - name: JLabTextQuickLook-development - path: build/JLabTextQuickLook.app - if-no-files-found: error diff --git a/.github/workflows/notarized-release.yml b/.github/workflows/notarized-release.yml new file mode 100644 index 0000000..1341dad --- /dev/null +++ b/.github/workflows/notarized-release.yml @@ -0,0 +1,139 @@ +name: Future notarized binary release + +on: + workflow_dispatch: + inputs: + tag: + description: Existing source-release tag to build + required: true + type: string + +permissions: + contents: write + +defaults: + run: + shell: zsh -eo pipefail {0} + +jobs: + release: + runs-on: macos-15 + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag }} + + - name: Validate tag and required secrets + env: + TAG: ${{ inputs.tag }} + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }} + run: | + version=$(/usr/libexec/PlistBuddy -c \ + 'Print :CFBundleShortVersionString' App-Info.plist) + [[ "$TAG" == "v$version" ]] || { + echo "Tag $TAG does not match bundle version $version" >&2 + exit 1 + } + for name in MACOS_CERTIFICATE MACOS_CERTIFICATE_PASSWORD \ + APPLE_TEAM_ID APPLE_API_ISSUER_ID APPLE_API_KEY_ID \ + APPLE_API_PRIVATE_KEY; do + [[ -n "${(P)name}" ]] || { + echo "Required secret $name is missing" >&2 + exit 1 + } + done + + - name: Import Developer ID certificate + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + run: | + certificate="$RUNNER_TEMP/developer-id.p12" + keychain="$RUNNER_TEMP/release.keychain-db" + keychain_password=$(openssl rand -hex 24) + print -r -- "$MACOS_CERTIFICATE" | base64 --decode > "$certificate" + security create-keychain -p "$keychain_password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + security import "$certificate" -k "$keychain" \ + -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$keychain_password" "$keychain" + security list-keychains -d user -s "$keychain" + identity=$(security find-identity -v -p codesigning "$keychain" | + sed -n 's/.*"\\(Developer ID Application:.*\\)"/\\1/p' | head -1) + [[ -n "$identity" ]] || { + echo "Developer ID Application identity not found" >&2 + exit 1 + } + print "SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" + + - name: Build and verify signed application + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + ./build.sh + codesign --verify --deep --strict --verbose=2 \ + build/JLabTextQuickLook.app + signed_team=$(codesign -dv --verbose=4 \ + build/JLabTextQuickLook.app 2>&1 | + sed -n 's/^TeamIdentifier=//p') + [[ "$signed_team" == "$APPLE_TEAM_ID" ]] || { + echo "Signed TeamIdentifier $signed_team does not match APPLE_TEAM_ID" >&2 + exit 1 + } + ./scripts/test.sh + + - name: Notarize and staple application + env: + APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }} + run: | + api_key="$RUNNER_TEMP/AuthKey_$APPLE_API_KEY_ID.p8" + print -r -- "$APPLE_API_PRIVATE_KEY" > "$api_key" + ditto -c -k --keepParent \ + build/JLabTextQuickLook.app "$RUNNER_TEMP/JLabTextQuickLook.zip" + xcrun notarytool submit "$RUNNER_TEMP/JLabTextQuickLook.zip" \ + --key "$api_key" \ + --key-id "$APPLE_API_KEY_ID" \ + --issuer "$APPLE_API_ISSUER_ID" \ + --wait + xcrun stapler staple build/JLabTextQuickLook.app + xcrun stapler validate build/JLabTextQuickLook.app + + - name: Create, notarize, and validate DMG + env: + APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + run: | + api_key="$RUNNER_TEMP/AuthKey_$APPLE_API_KEY_ID.p8" + ./scripts/create-dmg.sh + xcrun notarytool submit dist/JLabTextQuickLook.dmg \ + --key "$api_key" \ + --key-id "$APPLE_API_KEY_ID" \ + --issuer "$APPLE_API_ISSUER_ID" \ + --wait + xcrun stapler staple dist/JLabTextQuickLook.dmg + xcrun stapler validate dist/JLabTextQuickLook.dmg + spctl --assess --type open --context context:primary-signature \ + --verbose=2 dist/JLabTextQuickLook.dmg + shasum -a 256 dist/JLabTextQuickLook.dmg \ + > dist/JLabTextQuickLook.dmg.sha256 + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.tag }} + run: | + gh release upload "$TAG" \ + dist/JLabTextQuickLook.dmg \ + dist/JLabTextQuickLook.dmg.sha256 \ + --clobber diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19e106f..ff8ccc8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Signed release +name: Source release on: push: @@ -13,22 +13,16 @@ defaults: shell: zsh -eo pipefail {0} jobs: - release: + validate-and-release: runs-on: macos-15 - timeout-minutes: 45 + timeout-minutes: 20 steps: - uses: actions/checkout@v4 - - name: Validate tag and required secrets + - name: Validate tag matches application version env: TAG: ${{ github.ref_name }} - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }} run: | version=$(/usr/libexec/PlistBuddy -c \ 'Print :CFBundleShortVersionString' App-Info.plist) @@ -36,100 +30,17 @@ jobs: echo "Tag $TAG does not match bundle version $version" >&2 exit 1 } - for name in MACOS_CERTIFICATE MACOS_CERTIFICATE_PASSWORD \ - APPLE_TEAM_ID APPLE_API_ISSUER_ID APPLE_API_KEY_ID \ - APPLE_API_PRIVATE_KEY; do - [[ -n "${(P)name}" ]] || { - echo "Required secret $name is missing" >&2 - exit 1 - } - done - - name: Import Developer ID certificate + - name: Test universal build and Quick Look registration env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - run: | - certificate="$RUNNER_TEMP/developer-id.p12" - keychain="$RUNNER_TEMP/release.keychain-db" - keychain_password=$(openssl rand -hex 24) - print -r -- "$MACOS_CERTIFICATE" | base64 --decode > "$certificate" - security create-keychain -p "$keychain_password" "$keychain" - security set-keychain-settings -lut 21600 "$keychain" - security unlock-keychain -p "$keychain_password" "$keychain" - security import "$certificate" -k "$keychain" \ - -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: \ - -s -k "$keychain_password" "$keychain" - security list-keychains -d user -s "$keychain" - identity=$(security find-identity -v -p codesigning "$keychain" | - sed -n 's/.*"\\(Developer ID Application:.*\\)"/\\1/p' | head -1) - [[ -n "$identity" ]] || { - echo "Developer ID Application identity not found" >&2 - exit 1 - } - print "SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" - - - name: Build and verify signed application - env: - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - ./build.sh - codesign --verify --deep --strict --verbose=2 \ - build/JLabTextQuickLook.app - signed_team=$(codesign -dv --verbose=4 \ - build/JLabTextQuickLook.app 2>&1 | - sed -n 's/^TeamIdentifier=//p') - [[ "$signed_team" == "$APPLE_TEAM_ID" ]] || { - echo "Signed TeamIdentifier $signed_team does not match APPLE_TEAM_ID" >&2 - exit 1 - } - ./scripts/test.sh - - - name: Notarize and staple application - env: - APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }} - run: | - api_key="$RUNNER_TEMP/AuthKey_$APPLE_API_KEY_ID.p8" - print -r -- "$APPLE_API_PRIVATE_KEY" > "$api_key" - ditto -c -k --keepParent \ - build/JLabTextQuickLook.app "$RUNNER_TEMP/JLabTextQuickLook.zip" - xcrun notarytool submit "$RUNNER_TEMP/JLabTextQuickLook.zip" \ - --key "$api_key" \ - --key-id "$APPLE_API_KEY_ID" \ - --issuer "$APPLE_API_ISSUER_ID" \ - --wait - xcrun stapler staple build/JLabTextQuickLook.app - xcrun stapler validate build/JLabTextQuickLook.app - - - name: Create, notarize, and validate DMG - env: - APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - run: | - api_key="$RUNNER_TEMP/AuthKey_$APPLE_API_KEY_ID.p8" - ./scripts/create-dmg.sh - xcrun notarytool submit dist/JLabTextQuickLook.dmg \ - --key "$api_key" \ - --key-id "$APPLE_API_KEY_ID" \ - --issuer "$APPLE_API_ISSUER_ID" \ - --wait - xcrun stapler staple dist/JLabTextQuickLook.dmg - xcrun stapler validate dist/JLabTextQuickLook.dmg - spctl --assess --type open --context context:primary-signature \ - --verbose=2 dist/JLabTextQuickLook.dmg - shasum -a 256 dist/JLabTextQuickLook.dmg \ - > dist/JLabTextQuickLook.dmg.sha256 + RUN_LAUNCH_SERVICES_TESTS: "1" + run: ./scripts/test.sh - - name: Publish GitHub release + - name: Create source-only GitHub release env: GH_TOKEN: ${{ github.token }} run: | - gh release create "${GITHUB_REF_NAME}" \ - dist/JLabTextQuickLook.dmg \ - dist/JLabTextQuickLook.dmg.sha256 \ + gh release create "$GITHUB_REF_NAME" \ --verify-tag \ --generate-notes \ --title "JLabTextQuickLook ${GITHUB_REF_NAME#v}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 89bd033..f8d0fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ Versioning. - Prepare the project for public distribution. - Build a universal Apple silicon and Intel application. - Set the minimum supported operating system to macOS 13. -- Add automated validation and signed, notarized DMG releases. +- Add one-command, user-level installation and uninstallation from source. +- Publish a source-only GitHub release with no unsigned binary downloads. +- Preserve notarized binary release automation for possible future use. - Register `.inp`, `.hist`, `.report`, `.dat`, `.gen`, and `.geni` as plain text formats for Finder Quick Look. diff --git a/README.md b/README.md index 5b28fb6..6052f2f 100644 --- a/README.md +++ b/README.md @@ -28,25 +28,57 @@ that conforms to `public.plain-text`; Apple provides the actual preview UI. - macOS 13 Ventura or later - Apple silicon or Intel Mac +- Apple's free Command Line Tools ## Install -1. Download `JLabTextQuickLook.dmg` from the - [latest release](https://github.com/radwanparvez/JLabTextQuickLook/releases/latest). -2. Open the DMG and drag `JLabTextQuickLook.app` to **Applications**. -3. Launch the app once. -4. In Finder, select a supported file and press **Space**. +Install Apple's free Command Line Tools if they are not already present: -Public release artifacts are Developer ID signed and notarized by Apple. -Compare the downloaded DMG with the accompanying `.sha256` file if you want to -verify its checksum. +```sh +xcode-select --install +``` + +Then clone the source and run the installer: + +```sh +git clone https://github.com/radwanparvez/JLabTextQuickLook.git +cd JLabTextQuickLook +./install.sh +``` + +The installer builds a universal application, ad-hoc signs it locally, and +installs it at `~/Applications/JLabTextQuickLook.app`. It does not use `sudo` +or modify supported files. Select one of those files in Finder and press +**Space**. + +This free release is intentionally source-only. The application is built on +your Mac and is not Apple-notarized. The GitHub release does not contain an +unsigned `.app`, `.zip`, or `.dmg` download. + +## Upgrade and uninstall + +To upgrade or repair the installation, update the clone and rerun the +installer: + +```sh +git pull --ff-only +./install.sh +``` + +To unregister and remove only the installed application: + +```sh +./uninstall.sh +``` + +Uninstalling the app does not remove or modify any JLab data files. ## Troubleshooting If Finder still shows a generic icon or an empty preview: -1. Confirm the app is in `/Applications`. -2. Launch `JLabTextQuickLook.app` once. +1. Confirm the app is in `~/Applications`. +2. Rerun `./install.sh`. 3. Close existing Quick Look windows. 4. Restart Finder, or run: @@ -61,6 +93,11 @@ preview for the authoritative test. Other applications may also claim broad extensions such as `.dat`. The active type association can therefore depend on the applications installed on a Mac. +Syntax Highlight and PreviewText do not have to be removed automatically. +Temporarily disable their Quick Look extensions in **System Settings → +General → Login Items & Extensions → Quick Look**, refresh Finder, and test +again to identify a conflict. + Please open an issue with the output of: ```sh @@ -90,34 +127,20 @@ To exercise Launch Services and Quick Look thumbnail integration: RUN_LAUNCH_SERVICES_TESTS=1 ./scripts/test.sh ``` -To create an unsigned-development DMG after building: - -```sh -./scripts/create-dmg.sh -``` - -The GitHub release workflow is the only supported process for publishing -official binaries. It signs with Developer ID, enables hardened runtime, -notarizes and staples both the app and DMG, validates Gatekeeper acceptance, -and publishes a SHA-256 checksum. - -## Release setup - -Repository administrators must configure these GitHub Actions secrets: +GitHub Actions validates the same universal build but does not upload the +ad-hoc-signed application. Official free releases contain source archives +generated by GitHub only. -- `MACOS_CERTIFICATE`: base64-encoded Developer ID Application `.p12` -- `MACOS_CERTIFICATE_PASSWORD`: password protecting that `.p12` -- `APPLE_TEAM_ID` -- `APPLE_API_ISSUER_ID` -- `APPLE_API_KEY_ID` -- `APPLE_API_PRIVATE_KEY`: contents of the App Store Connect `.p8` key +## Maintainer release process -Create a release by updating the plist and changelog, committing the change, -and pushing a matching signed tag such as `v1.3.0`. Missing credentials or a -version mismatch stops the workflow before any release is published. +Update the plist and changelog, commit the change, and push a matching tag such +as `v1.3.0`. The source-release workflow validates the version and all six +formats before creating a GitHub Release without binary attachments. -Homebrew installation will be added after the first notarized GitHub release -has been validated. +A separate manually triggered workflow preserves support for a future +Developer ID–signed and Apple-notarized DMG. It cannot run successfully until +the required Apple credentials are configured. A conventional Homebrew cask is +likewise deferred until a notarized binary is available. ## Contributing and security diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..91cf363 --- /dev/null +++ b/install.sh @@ -0,0 +1,69 @@ +#!/bin/zsh +set -euo pipefail + +project_dir=${0:A:h} +destination_dir="$HOME/Applications" +destination="$destination_dir/JLabTextQuickLook.app" +lsregister=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister + +fail() { + print -u2 "error: $1" + exit 1 +} + +[[ "$(uname -s)" == "Darwin" ]] || + fail "JLabTextQuickLook can only be installed on macOS" + +macos_major=$(sw_vers -productVersion | cut -d. -f1) +(( macos_major >= 13 )) || + fail "macOS 13 Ventura or later is required" + +xcode-select -p >/dev/null 2>&1 || + fail "Apple Command Line Tools are required. Run: xcode-select --install" + +for tool in clang codesign lipo plutil; do + command -v "$tool" >/dev/null 2>&1 || + fail "required build tool not found: $tool" +done +[[ -x "$lsregister" ]] || fail "Launch Services registration tool not found" + +print "Building JLabTextQuickLook..." +"$project_dir/build.sh" >/dev/null + +built_app="$project_dir/build/JLabTextQuickLook.app" +executable="$built_app/Contents/MacOS/JLabTextQuickLook" +[[ -x "$executable" ]] || fail "build did not produce the expected executable" +codesign --verify --deep --strict "$built_app" + +architectures=" $(lipo -archs "$executable") " +[[ "$architectures" == *" arm64 "* && "$architectures" == *" x86_64 "* ]] || + fail "the application is not a universal arm64 + x86_64 build" + +mkdir -p "$destination_dir" +staging="$destination_dir/.JLabTextQuickLook.app.installing.$$" + +cleanup() { + [[ -e "$staging" ]] && rm -rf "$staging" +} +trap cleanup EXIT + +ditto "$built_app" "$staging" +codesign --verify --deep --strict "$staging" + +if [[ -e "$destination" ]]; then + "$lsregister" -u "$destination" >/dev/null 2>&1 || true + rm -rf "$destination" +fi +mv "$staging" "$destination" +trap - EXIT + +"$lsregister" -f "$destination" +qlmanage -r cache >/dev/null 2>&1 || true +killall Finder >/dev/null 2>&1 || true + +print +print "Installed: $destination" +print "Select an .inp, .hist, .report, .dat, .gen, or .geni file in Finder" +print "and press Space to preview it." +print +print "To remove JLabTextQuickLook, run: $project_dir/uninstall.sh" diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..d25a538 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,31 @@ +#!/bin/zsh +set -euo pipefail + +destination="$HOME/Applications/JLabTextQuickLook.app" +expected_identifier=org.jlab.TextQuickLook +lsregister=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister + +fail() { + print -u2 "error: $1" + exit 1 +} + +if [[ ! -e "$destination" ]]; then + print "JLabTextQuickLook is not installed at $destination" + exit 0 +fi + +actual_identifier=$(/usr/libexec/PlistBuddy \ + -c 'Print :CFBundleIdentifier' "$destination/Contents/Info.plist" 2>/dev/null || + true) +[[ "$actual_identifier" == "$expected_identifier" ]] || + fail "refusing to remove an application with bundle identifier '$actual_identifier'" + +[[ -x "$lsregister" ]] && + "$lsregister" -u "$destination" >/dev/null 2>&1 || true +rm -rf "$destination" +qlmanage -r cache >/dev/null 2>&1 || true +killall Finder >/dev/null 2>&1 || true + +print "Removed $destination" +print "Your .inp, .hist, .report, .dat, .gen, and .geni files were not changed."