From 024376e5c0dd03210de6f228c8b9c57ea2793674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Br=C3=A4nn?= Date: Thu, 16 Apr 2026 10:03:11 +0200 Subject: [PATCH 1/4] mac: Switch mac package to .zip instead of .tar.gz This is required for the MacOS notarization, but tar.gz is more commonly supported on linux. --- .github/workflows/release.yml | 32 ++++++++++++++++-------- install.sh | 18 ++++++++++++-- src/commands/update.test.ts | 41 ++++++++++++++++++++++++++++++- src/commands/update.ts | 46 +++++++++++++++++++++++++++-------- 4 files changed, 114 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 412b6c6..dcfbb1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,11 +6,13 @@ on: - "v*" permissions: - contents: write + contents: read jobs: test: runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v4 @@ -25,17 +27,19 @@ jobs: build: needs: test + permissions: + contents: read strategy: matrix: include: - target: bun-darwin-arm64 - artifact: polar-darwin-arm64 - os: ubuntu-latest + archive: polar-darwin-arm64.zip + os: macos-15 - target: bun-darwin-x64 - artifact: polar-darwin-x64 - os: ubuntu-latest + archive: polar-darwin-x64.zip + os: macos-15 - target: bun-linux-x64 - artifact: polar-linux-x64 + archive: polar-linux-x64.tar.gz os: ubuntu-latest runs-on: ${{ matrix.os }} @@ -53,16 +57,23 @@ jobs: run: bun build ./src/cli.ts --compile --target=${{ matrix.target }} --outfile polar - name: Package binary - run: tar -czf ${{ matrix.artifact }}.tar.gz polar + run: | + if [[ "${{ matrix.archive }}" == *.zip ]]; then + ditto -c -k --keepParent polar "${{ matrix.archive }}" + else + tar -czf "${{ matrix.archive }}" polar + fi - uses: actions/upload-artifact@v4 with: - name: ${{ matrix.artifact }} - path: ${{ matrix.artifact }}.tar.gz + name: ${{ matrix.archive }} + path: ${{ matrix.archive }} release: needs: build runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/download-artifact@v4 @@ -70,12 +81,13 @@ jobs: merge-multiple: true - name: Generate checksums - run: sha256sum *.tar.gz > checksums.txt + run: sha256sum *.tar.gz *.zip > checksums.txt - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: files: | *.tar.gz + *.zip checksums.txt generate_release_notes: true diff --git a/install.sh b/install.sh index e7b9cb7..d641301 100755 --- a/install.sh +++ b/install.sh @@ -50,6 +50,15 @@ get_latest_version() { echo "$version" } +get_archive_name() { + local platform="$1" + + case "$platform" in + darwin-*) echo "${BINARY_NAME}-${platform}.zip" ;; + *) echo "${BINARY_NAME}-${platform}.tar.gz" ;; + esac +} + main() { local platform version url @@ -61,7 +70,8 @@ main() { version="$(get_latest_version)" info "Version: ${version}" - local archive="${BINARY_NAME}-${platform}.tar.gz" + local archive + archive="$(get_archive_name "$platform")" local url="https://github.com/${REPO}/releases/download/${version}/${archive}" local checksums_url="https://github.com/${REPO}/releases/download/${version}/checksums.txt" @@ -94,7 +104,11 @@ main() { info "Checksum verified" info "Extracting..." - tar -xzf "${tmpdir}/${archive}" -C "$tmpdir" + case "$archive" in + *.zip) ditto -x -k "${tmpdir}/${archive}" "$tmpdir" ;; + *.tar.gz) tar -xzf "${tmpdir}/${archive}" -C "$tmpdir" ;; + *) error "Unsupported archive format: ${archive}" ;; + esac info "Installing to ${INSTALL_DIR}..." if [ -w "$INSTALL_DIR" ]; then diff --git a/src/commands/update.test.ts b/src/commands/update.test.ts index 88d2420..7aaae8b 100644 --- a/src/commands/update.test.ts +++ b/src/commands/update.test.ts @@ -3,12 +3,51 @@ import { mkdtemp, rm, writeFile, readFile, stat } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { Effect } from "effect"; -import { replaceBinary } from "./update"; +import { + getArchiveExtractionCommand, + getReleaseArchiveName, + replaceBinary, +} from "./update"; async function makeTemp() { return mkdtemp(join(tmpdir(), "polar-test-")); } +describe("getReleaseArchiveName", () => { + test("uses zip archives for darwin releases", () => { + expect(getReleaseArchiveName({ os: "darwin", arch: "arm64" })).toBe( + "polar-darwin-arm64.zip", + ); + expect(getReleaseArchiveName({ os: "darwin", arch: "x64" })).toBe( + "polar-darwin-x64.zip", + ); + }); + + test("uses tar.gz archives for linux releases", () => { + expect(getReleaseArchiveName({ os: "linux", arch: "x64" })).toBe( + "polar-linux-x64.tar.gz", + ); + }); +}); + +describe("getArchiveExtractionCommand", () => { + test("uses ditto for zip archives", () => { + expect(getArchiveExtractionCommand("/tmp/polar.zip", "/tmp/out")).toEqual([ + "ditto", + "-x", + "-k", + "/tmp/polar.zip", + "/tmp/out", + ]); + }); + + test("uses tar for tar.gz archives", () => { + expect( + getArchiveExtractionCommand("/tmp/polar.tar.gz", "/tmp/out"), + ).toEqual(["tar", "-xzf", "/tmp/polar.tar.gz", "-C", "/tmp/out"]); + }); +}); + describe("replaceBinary", () => { let dir: string; let newBinaryPath: string; diff --git a/src/commands/update.ts b/src/commands/update.ts index db1177e..b8825f4 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -114,6 +114,29 @@ function detectPlatform(): { os: string; arch: string } { return { os, arch: normalizedArch }; } +export function getReleaseArchiveName(platform: { + os: string; + arch: string; +}): string { + const baseName = `polar-${platform.os}-${platform.arch}`; + return platform.os === "darwin" ? `${baseName}.zip` : `${baseName}.tar.gz`; +} + +export function getArchiveExtractionCommand( + archivePath: string, + destinationDir: string, +): string[] { + if (archivePath.endsWith(".zip")) { + return ["ditto", "-x", "-k", archivePath, destinationDir]; + } + + if (archivePath.endsWith(".tar.gz")) { + return ["tar", "-xzf", archivePath, "-C", destinationDir]; + } + + throw new Error(`Unsupported archive format: ${archivePath}`); +} + const downloadAndUpdate = ( release: typeof GitHubRelease.Type, latestVersion: string, @@ -127,7 +150,7 @@ const downloadAndUpdate = ( const { os, arch } = detectPlatform(); const platform = `${os}-${arch}`; - const archiveName = `polar-${platform}.tar.gz`; + const archiveName = getReleaseArchiveName({ os, arch }); const asset = release.assets.find((a) => a.name === archiveName); if (!asset) { @@ -216,20 +239,23 @@ const downloadAndUpdate = ( yield* Console.log(`${dim}Extracting...${reset}`); - const tar = Bun.spawn(["tar", "-xzf", archivePath, "-C", tempDir], { - stdout: "ignore", - stderr: "pipe", - }); + const extract = Bun.spawn( + getArchiveExtractionCommand(archivePath, tempDir), + { + stdout: "ignore", + stderr: "pipe", + }, + ); - const tarExitCode = yield* Effect.tryPromise({ - try: () => tar.exited, + const extractExitCode = yield* Effect.tryPromise({ + try: () => extract.exited, catch: () => new Error("Failed to extract archive"), }); - if (tarExitCode !== 0) { + if (extractExitCode !== 0) { const stderr = yield* Effect.tryPromise({ - try: () => new Response(tar.stderr).text(), - catch: () => new Error("Failed to read tar stderr"), + try: () => new Response(extract.stderr).text(), + catch: () => new Error("Failed to read archive extractor stderr"), }); return yield* Effect.fail( new Error(`Failed to extract archive: ${stderr}`), From 6a6d741f9d41b372ab3b62ef9ed92a8b72d0696e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Br=C3=A4nn?= Date: Thu, 16 Apr 2026 10:40:00 +0200 Subject: [PATCH 2/4] ci: Notarize the cli for MacOS --- .github/workflows/release.yml | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dcfbb1b..f8d04a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,12 +35,18 @@ jobs: - target: bun-darwin-arm64 archive: polar-darwin-arm64.zip os: macos-15 + sign: true + notarize: true - target: bun-darwin-x64 archive: polar-darwin-x64.zip os: macos-15 + sign: true + notarize: true - target: bun-linux-x64 archive: polar-linux-x64.tar.gz os: ubuntu-latest + sign: false + notarize: false runs-on: ${{ matrix.os }} @@ -53,9 +59,54 @@ jobs: - run: bun install + - name: Validate macOS signing configuration + if: ${{ matrix.sign }} + env: + MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} + APP_STORE_CONNECT_API_KEY_P8: ${{ secrets.APP_STORE_CONNECT_API_KEY_P8 }} + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + run: | + test -n "$MACOS_CERTIFICATE_P12_BASE64" || { echo "Missing MACOS_CERTIFICATE_P12_BASE64 secret"; exit 1; } + test -n "$MACOS_CERTIFICATE_PASSWORD" || { echo "Missing MACOS_CERTIFICATE_PASSWORD secret"; exit 1; } + test -n "$MACOS_SIGNING_IDENTITY" || { echo "Missing MACOS_SIGNING_IDENTITY secret"; exit 1; } + test -n "$APP_STORE_CONNECT_API_KEY_P8" || { echo "Missing APP_STORE_CONNECT_API_KEY_P8 secret"; exit 1; } + test -n "$APP_STORE_CONNECT_API_KEY_ID" || { echo "Missing APP_STORE_CONNECT_API_KEY_ID secret"; exit 1; } + test -n "$APP_STORE_CONNECT_ISSUER_ID" || { echo "Missing APP_STORE_CONNECT_ISSUER_ID secret"; exit 1; } + + - name: Import macOS signing certificate + if: ${{ matrix.sign }} + uses: apple-actions/import-codesign-certs@v6 + with: + p12-file-base64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + - name: Build binary run: bun build ./src/cli.ts --compile --target=${{ matrix.target }} --outfile polar + - name: Sign macOS binary + if: ${{ matrix.sign }} + env: + MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} + run: codesign --force --options runtime --sign "$MACOS_SIGNING_IDENTITY" --timestamp ./polar + + - name: Verify macOS signature + if: ${{ matrix.sign }} + run: codesign --verify --strict --verbose=2 ./polar + + - name: Write App Store Connect API key + if: ${{ matrix.notarize }} + env: + APP_STORE_CONNECT_API_KEY_P8: ${{ secrets.APP_STORE_CONNECT_API_KEY_P8 }} + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} + run: | + key_path="$RUNNER_TEMP/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8" + printf '%s' "$APP_STORE_CONNECT_API_KEY_P8" > "$key_path" + chmod 600 "$key_path" + echo "APP_STORE_CONNECT_API_KEY_PATH=$key_path" >> "$GITHUB_ENV" + - name: Package binary run: | if [[ "${{ matrix.archive }}" == *.zip ]]; then @@ -64,6 +115,18 @@ jobs: tar -czf "${{ matrix.archive }}" polar fi + - name: Notarize macOS archive + if: ${{ matrix.notarize }} + env: + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + run: | + xcrun notarytool submit "${{ matrix.archive }}" \ + --key "$APP_STORE_CONNECT_API_KEY_PATH" \ + --key-id "$APP_STORE_CONNECT_API_KEY_ID" \ + --issuer "$APP_STORE_CONNECT_ISSUER_ID" \ + --wait + - uses: actions/upload-artifact@v4 with: name: ${{ matrix.archive }} From 33e72b7cec6f18d1958b2d8530f76cce417ce920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Br=C3=A4nn?= Date: Thu, 16 Apr 2026 10:42:11 +0200 Subject: [PATCH 3/4] ci: Add entitlements plist for MacOS codesign --- .github/macos-entitlements.plist | 16 ++++++++++++++++ .github/workflows/release.yml | 7 ++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .github/macos-entitlements.plist diff --git a/.github/macos-entitlements.plist b/.github/macos-entitlements.plist new file mode 100644 index 0000000..b777ab4 --- /dev/null +++ b/.github/macos-entitlements.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8d04a7..31ea4c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,7 +90,12 @@ jobs: if: ${{ matrix.sign }} env: MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} - run: codesign --force --options runtime --sign "$MACOS_SIGNING_IDENTITY" --timestamp ./polar + run: | + codesign --force --options runtime \ + --entitlements ./.github/macos-entitlements.plist \ + --sign "$MACOS_SIGNING_IDENTITY" \ + --timestamp \ + ./polar - name: Verify macOS signature if: ${{ matrix.sign }} From 52715bf37f4d95dc59d6fc2a0db99c5bf37d0ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20Br=C3=A4nn?= Date: Thu, 16 Apr 2026 10:45:30 +0200 Subject: [PATCH 4/4] ci: Add draft release flow --- .github/workflows/release.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31ea4c4..20c364d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,12 @@ on: push: tags: - "v*" + workflow_dispatch: + inputs: + tag_name: + description: "Draft verification tag to create, e.g. verify-signing-2026-04-16" + required: true + type: string permissions: contents: read @@ -154,6 +160,11 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: + tag_name: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || github.ref_name }} + target_commitish: ${{ github.sha }} + draft: ${{ github.event_name == 'workflow_dispatch' }} + prerelease: ${{ github.event_name == 'workflow_dispatch' }} + name: ${{ github.event_name == 'workflow_dispatch' && format('Signing Verification {0}', inputs.tag_name) || github.ref_name }} files: | *.tar.gz *.zip