diff --git a/.github/workflows/release_test.yml b/.github/workflows/release_test.yml new file mode 100644 index 0000000..875a160 --- /dev/null +++ b/.github/workflows/release_test.yml @@ -0,0 +1,119 @@ +name: release (macOS signing test) + +on: + pull_request: + branches: + - "**" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # pin@3.2.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # pin@3.6.1 + - name: Version + id: version + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "VERSION=pr-${{ github.event.pull_request.number }}-${{ github.run_number }}" >> "$GITHUB_OUTPUT" + else + echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + fi + - name: Login to DockerHub + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # pin@v2.1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Release + run: | + make all + bash ./build/release.sh -xe + env: + API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.VERSION }} + - name: Upload built binaries + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + - name: Create draft GitHub Release + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # pin@v1 + with: + draft: true + name: ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + sign_and_notarize: + runs-on: macos-latest + needs: release + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download built binaries + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + - name: Sign CLI binaries + run: bash ./build/sign_and_notarize.sh + env: + SIGNING_CERTIFICATE: ${{ secrets.SIGNING_CERTIFICATE }} + CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} + SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + DIST_DIR: "dist" + NOTARIZATION_APPLE_ID: ${{ secrets.NOTARIZATION_APPLE_ID }} + NOTARIZATION_APP_PASSWORD: ${{ secrets.NOTARIZATION_APP_PASSWORD }} + NOTARIZATION_TEAM_ID: ${{ secrets.NOTARIZATION_TEAM_ID }} + - name: Upload signed binaries to Draft Release + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # pin@v1 + with: + files: | + dist/phrase_macosx_*.zip + dist/*.tar.gz + dist/phrase_windows_*.exe.zip + dist/phrase_windows_*.exe + fail_on_unmatched_files: true + overwrite: true + name: ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + draft: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # - name: Publish GitHub Release + # uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # pin@v1 + # with: + # draft: false + # name: ${{ github.ref_name }} + # tag_name: ${{ github.ref_name }} + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # brew: + # runs-on: ubuntu-latest + # needs: sign_and_notarize + # steps: + # - name: Checkout + # uses: actions/checkout@v4 + # - name: Update Homebrew formula + # uses: dawidd6/action-homebrew-bump-formula@75ed025ff3ad1d617862838b342b06d613a0ddf3 # v3.10.1 + # with: + # # Required, custom GitHub access token with the 'public_repo' and 'workflow' scopes + # token: ${{secrets.GH_ACCESS_TOKEN}} + # # Formula name, required + # formula: phrase-cli + # # Optional, will be determined automatically + # tag: ${{github.ref_name}} + # # Optional, will be determined automatically + # revision: ${{github.sha}} + # # Optional, if don't want to check for already open PRs + # force: false diff --git a/build/release.sh b/build/release.sh index e29198c..c3de46f 100755 --- a/build/release.sh +++ b/build/release.sh @@ -25,52 +25,4 @@ cp dist/phrase_linux_arm64 dist/linux/arm64 docker buildx build --tag "${IMAGE}" --tag ${IMAGE_LATEST} --platform linux/amd64,linux/arm64 -f ./Dockerfile --push . -# Create release -function create_release_data() -{ - cat < /dev/null - echo Hash: $(sha256sum $file) - fi -done - -echo "Publishing release" -curl \ - --silent \ - -X PATCH \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - -H "Accept: application/vnd.github.v3+json" \ - "https://api.github.com/repos/phrase/phrase-cli/releases/${release_id}" \ - -d '{"draft": false}' > /dev/null - -echo "Release successful" +echo "Artifacts built and ready in dist/ directory. GitHub Release creation handled in GitHub Actions workflow." diff --git a/build/sign_and_notarize.sh b/build/sign_and_notarize.sh new file mode 100644 index 0000000..bcec9d7 --- /dev/null +++ b/build/sign_and_notarize.sh @@ -0,0 +1,144 @@ +#!/bin/bash +set -euo pipefail +umask 077 + +CERTIFICATE_BASE64="${SIGNING_CERTIFICATE}" +P12_PASSWORD="${CERTIFICATE_PASSWORD}" +SIGNING_IDENTITY="${SIGNING_IDENTITY}" +KEYCHAIN_PASSWORD="${KEYCHAIN_PASSWORD}" +DIST_DIR="${DIST_DIR:-dist}" + +CERTIFICATE_PATH="$(pwd)/build_certificate.p12" +KEYCHAIN_PATH="$(pwd)/my-signing.keychain-db" + +# Basic env validation to fail fast +require_env() { + local name="$1" value="$2" + if [[ -z "$value" ]]; then + echo "โŒ Missing required environment variable: $name" >&2 + exit 1 + fi +} + +require_env "SIGNING_CERTIFICATE" "${CERTIFICATE_BASE64}" +require_env "CERTIFICATE_PASSWORD" "${P12_PASSWORD}" +require_env "SIGNING_IDENTITY" "${SIGNING_IDENTITY}" +require_env "KEYCHAIN_PASSWORD" "${KEYCHAIN_PASSWORD}" +require_env "NOTARIZATION_APPLE_ID" "${NOTARIZATION_APPLE_ID:-}" +require_env "NOTARIZATION_APP_PASSWORD" "${NOTARIZATION_APP_PASSWORD:-}" +require_env "NOTARIZATION_TEAM_ID" "${NOTARIZATION_TEAM_ID:-}" + + +cleanup() { + echo "๐Ÿงน Cleaning up keychain and certificate..." + # Attempt to delete the temporary keychain + security delete-keychain "$KEYCHAIN_PATH" || true + # Remove certificate file + rm -f "$CERTIFICATE_PATH" || true +} +trap cleanup EXIT + +echo "๐Ÿ” Setting up certificate and keychain..." + +# Decode the certificate (macOS-only) +echo "$CERTIFICATE_BASE64" | /usr/bin/base64 -D > "$CERTIFICATE_PATH" +# Restrict permissions on sensitive certificate material +chmod 600 "$CERTIFICATE_PATH" + +# Create 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" + +# Add keychain to the search list (prepend to existing list) +# This is required for codesign to find the identity +EXISTING_KEYCHAINS=$(security list-keychains -d user | tr -d '"' | tr '\n' ' ') +security list-keychains -d user -s "$KEYCHAIN_PATH" $EXISTING_KEYCHAINS + +# Import certificate into keychain +security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + +# Set the custom keychain as the default for this session +security default-keychain -s "$KEYCHAIN_PATH" + +# Debug: show keychain search list +echo "๐Ÿ”Ž Keychain search list:" +security list-keychains -d user + +# Show available signing identities for visibility +echo "๐Ÿ”Ž Available signing identities (codesigning):" +security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true + +# Also check all keychains +echo "๐Ÿ”Ž All available signing identities:" +security find-identity -v -p codesigning || true + +# Extract the SHA-1 hash from the keychain for reliable signing +# This avoids issues with identity name matching +IDENTITY_HASH=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk '{print $2}') +if [[ -z "$IDENTITY_HASH" ]]; then + echo "โŒ No Developer ID Application identity found in keychain" >&2 + exit 1 +fi +echo "๐Ÿ”‘ Using identity hash: $IDENTITY_HASH" + +# Find and sign all macOS binaries dynamically +echo "๐Ÿ”Ž Searching for macOS binaries in $DIST_DIR..." + +shopt -s nullglob +for binary in "$DIST_DIR"/phrase_macosx_*; do + [[ "$binary" == *.tar.gz ]] && continue + [[ ! -f "$binary" ]] && continue + echo "๐Ÿ” Signing $binary..." + codesign --force --timestamp --options runtime --keychain "$KEYCHAIN_PATH" --sign "$IDENTITY_HASH" "$binary" + codesign --verify --verbose=2 "$binary" +done + +echo "โœ… All macOS binaries signed successfully." + +# --- Recreate tar.gz with signed binaries (for Homebrew) --- +echo "๐Ÿ“ฆ Recreating tar.gz archives with signed binaries..." +for binary in "$DIST_DIR"/phrase_macosx_*; do + [[ "$binary" == *.tar.gz ]] && continue + [[ "$binary" == *.zip ]] && continue + [[ ! -f "$binary" ]] && continue + relbin="${binary#${DIST_DIR}/}" + # Remove old tar.gz if exists + rm -f "$DIST_DIR/${relbin}.tar.gz" + # Create new tar.gz with signed binary renamed to 'phrase' + echo "Creating $DIST_DIR/${relbin}.tar.gz with signed binary..." + ( + cd "$DIST_DIR" + cp "$relbin" phrase + tar --create phrase | gzip -n > "${relbin}.tar.gz" + rm phrase + ) +done + +# --- Zip artifacts for notarization --- +echo "๐Ÿ“ฆ Zipping macOS binaries for notarization..." +shopt -s nullglob +for bin in "$DIST_DIR"/phrase_macosx_*; do + [[ "$bin" == *.tar.gz ]] && continue + relbin="${bin#${DIST_DIR}/}" + echo "Creating $DIST_DIR/${relbin}.zip" + ( + cd "$DIST_DIR" && /usr/bin/zip -o "${relbin}.zip" "${relbin}" + ) +done + +# --- Notarization via Apple notarytool (Apple ID + app-specific password) --- +echo "๐Ÿ“ Notarizing zipped binaries with Apple Notary (Apple ID)..." +for zip in "$DIST_DIR"/phrase_macosx_*.zip; do + [[ -e "$zip" ]] || continue + echo "Submitting $zip to Apple Notary..." + xcrun notarytool submit "$zip" \ + --apple-id "$NOTARIZATION_APPLE_ID" \ + --password "$NOTARIZATION_APP_PASSWORD" \ + --team-id "$NOTARIZATION_TEAM_ID" \ + --wait + echo "โ„น๏ธ Notarization complete for $zip." +done + +echo "๐ŸŽ‰ Signing and notarization finished." \ No newline at end of file