Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion clients/cli/.github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
tags:
- '*'

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -32,9 +35,66 @@ jobs:
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@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@v1
with:
files: |
dist/phrase_macosx_*.zip
dist/*.tar.gz
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@v1
with:
draft: false
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
Comment on lines +87 to +92
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Publishing the release immediately after uploading signed binaries does not include any verification step. Consider adding a manual approval step or automated verification that all expected signed artifacts are present before automatically publishing the release.

Copilot uses AI. Check for mistakes.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
brew:
runs-on: ubuntu-latest
needs: release
needs: sign_and_notarize
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
50 changes: 1 addition & 49 deletions clients/cli/build/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
{
"tag_name": "${VERSION}",
"name": "${VERSION}",
"draft": true,
"prerelease": false,
"body": "https://github.com/phrase/phrase-cli/blob/master/CHANGELOG.md"
}
EOF
}

echo "Create release $VERSION"
api_url="https://api.github.com/repos/phrase/phrase-cli/releases"
response="$(curl -H "Authorization: token ${GITHUB_TOKEN}" --data "$(create_release_data)" ${api_url})"
release_id=$(echo $response | python -c "import sys, json; print(json.load(sys.stdin).get('id', ''))")

if [ -z "$release_id" ]
then
echo "Failed to create GitHub release"
echo $response
exit 1
else
echo "New release created created with id: ${release_id}"
fi

# Upload artifacts
DIST_DIR="./dist"
for file in "$DIST_DIR"/*; do
if [ -f "$file" ]; then
echo "Uploading ${file}"
asset="https://uploads.github.com/repos/phrase/phrase-cli/releases/${release_id}/assets?name=$(basename "$file")"
curl -sS --data-binary @"$file" -H "Authorization: token ${GITHUB_TOKEN}" -H "Content-Type: application/octet-stream" $asset > /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."
97 changes: 97 additions & 0 deletions clients/cli/build/sign_and_notarize.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/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="./build_certificate.p12"
KEYCHAIN_PATH="./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"
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The certificate file is created with default permissions that may be world-readable. Set restrictive permissions (e.g., chmod 600) on the certificate file immediately after creation to prevent unauthorized access to sensitive cryptographic material.

Suggested change
echo "$CERTIFICATE_BASE64" | /usr/bin/base64 -D > "$CERTIFICATE_PATH"
echo "$CERTIFICATE_BASE64" | /usr/bin/base64 -D > "$CERTIFICATE_PATH"
chmod 600 "$CERTIFICATE_PATH"

Copilot uses AI. Check for mistakes.
# 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"

# 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: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

# Show available signing identities for visibility
echo "🔎 Available signing identities (codesigning):"
security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true

# Find and sign all macOS binaries dynamically
echo "🔎 Searching for macOS binaries in $DIST_DIR..."

find "$DIST_DIR" -type f \( -name "phrase_macosx_*" ! -name "*.tar.gz" \) -print0 | while IFS= read -r -d '' binary; do
echo "🔏 Signing $binary..."
codesign --timestamp --options runtime --keychain "$KEYCHAIN_PATH" --sign "$SIGNING_IDENTITY" "$binary"
codesign --verify --verbose=2 --keychain "$KEYCHAIN_PATH" "$binary"
done

echo "✅ All macOS binaries signed successfully."

# --- 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."
Loading