From 9a1fc992529eb585c96e92a2bec9fc40c7272059 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 13:59:53 +0200 Subject: [PATCH 01/10] Provide macOs binary signing and notarization. --- .github/workflows/release-binaries.yml | 99 +++++++++++++++++++++++++- PROJECT.md | 3 +- README.md | 11 +-- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 122550d..7a35426 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -17,8 +17,8 @@ concurrency: jobs: release: - runs-on: ubuntu-latest - timeout-minutes: 10 + runs-on: macos-latest + timeout-minutes: 30 steps: - name: Checkout Repository @@ -73,6 +73,101 @@ jobs: GOOS=darwin GOARCH=amd64 go build -trimpath -o dist/embed-code-macos main.go chmod +x dist/embed-code-linux dist/embed-code-macos + # Sign the macOS binary with a Developer ID certificate stored in GitHub + # secrets. The certificate must be exported as a base64-encoded .p12 file. + - name: Sign macOS Binary + if: steps.release.outputs.publish == 'true' + shell: bash + env: + MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} + run: | + missing=() + [[ -n "$MACOS_CERTIFICATE_P12_BASE64" ]] || missing+=("MACOS_CERTIFICATE_P12_BASE64") + [[ -n "$MACOS_CERTIFICATE_PASSWORD" ]] || missing+=("MACOS_CERTIFICATE_PASSWORD") + [[ -n "$MACOS_CODESIGN_IDENTITY" ]] || missing+=("MACOS_CODESIGN_IDENTITY") + if (( ${#missing[@]} > 0 )); then + printf 'Missing required GitHub secret: %s\n' "${missing[@]}" >&2 + exit 1 + fi + + keychain_path="$RUNNER_TEMP/embed-code-signing.keychain-db" + certificate_path="$RUNNER_TEMP/embed-code-signing-certificate.p12" + keychain_password="$(uuidgen)" + + # GNU base64 supports --decode; macOS base64 uses -D. + if printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path" 2>/dev/null; then + : + else + printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 -D > "$certificate_path" + fi + + security create-keychain -p "$keychain_password" "$keychain_path" + security set-keychain-settings -lut 21600 "$keychain_path" + security unlock-keychain -p "$keychain_password" "$keychain_path" + + curl -fsSL https://www.apple.com/certificateauthority/AppleRootCA-G3.cer \ + -o "$RUNNER_TEMP/AppleRootCA-G3.cer" + curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer \ + -o "$RUNNER_TEMP/AppleWWDRCAG6.cer" + curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer \ + -o "$RUNNER_TEMP/DeveloperIDG2CA.cer" + + security import "$RUNNER_TEMP/AppleRootCA-G3.cer" -k "$keychain_path" + security import "$RUNNER_TEMP/AppleWWDRCAG6.cer" -k "$keychain_path" + security import "$RUNNER_TEMP/DeveloperIDG2CA.cer" -k "$keychain_path" + security import "$certificate_path" \ + -P "$MACOS_CERTIFICATE_PASSWORD" \ + -A \ + -t cert \ + -f pkcs12 \ + -k "$keychain_path" + security list-keychains -d user -s "$keychain_path" + security set-key-partition-list \ + -S apple-tool:,apple: \ + -s \ + -k "$keychain_password" \ + "$keychain_path" + security find-identity -v -p codesigning "$keychain_path" + + codesign \ + --force \ + --options runtime \ + --timestamp \ + --sign "$MACOS_CODESIGN_IDENTITY" \ + dist/embed-code-macos + codesign --verify --verbose=4 dist/embed-code-macos + + # Notarize the macOS ZIP that will be published as the release asset. + - name: Notarize macOS Binary + if: steps.release.outputs.publish == 'true' + shell: bash + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + run: | + missing=() + [[ -n "$APPLE_ID" ]] || missing+=("APPLE_ID") + [[ -n "$APPLE_TEAM_ID" ]] || missing+=("APPLE_TEAM_ID") + [[ -n "$APPLE_APP_SPECIFIC_PASSWORD" ]] || missing+=("APPLE_APP_SPECIFIC_PASSWORD") + if (( ${#missing[@]} > 0 )); then + printf 'Missing required GitHub secret: %s\n' "${missing[@]}" >&2 + exit 1 + fi + + pushd dist + ditto -c -k --keepParent embed-code-macos embed-code-macos.zip + popd + + xcrun notarytool submit dist/embed-code-macos.zip \ + --apple-id "$APPLE_ID" \ + --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --wait + rm dist/embed-code-macos + # Create the release for the current commit and attach all generated binaries. - name: Publish GitHub Release if: steps.release.outputs.publish == 'true' diff --git a/PROJECT.md b/PROJECT.md index f2076c5..6c38188 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -56,7 +56,8 @@ This repository is configured with these GitHub workflows: - `check`: runs linting, the normal Go test suite, and the showcase end-to-end tests across supported platforms. - `release-binaries`: reads `VERSION`, builds Linux, macOS, and Windows - binaries, and creates the matching GitHub Release on pushes to `master`. + binaries, signs and notarizes the macOS ZIP, and creates the matching GitHub + Release on pushes to `master`. The release tag is `v` from `VERSION`. When the release already exists, the workflow emits a warning and finishes successfully without rebuilding or diff --git a/README.md b/README.md index 7f070e0..d20fac6 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,12 @@ On Linux, for example: > chmod +x embed-code-linux > ``` -> Since binary file for macOS is not signed, it may be necessary -> to change its attributes to allow execution: -> ```bash -> xattr -d com.apple.quarantine embed-code-macos -> ``` +On macOS, download `embed-code-macos.zip`, unzip it, and run the binary: + +```bash +unzip embed-code-macos.zip +./embed-code-macos -mode=check -config-path=showcase/embedding/embed-code.yml +``` Or run it with Go: From 3e6b272ff99bb7197aef29b933f5717ee25242b6 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 14:20:54 +0200 Subject: [PATCH 02/10] Extract bash scripts. --- .github/workflows/release-binaries.yml | 75 +---------------- PROJECT.md | 2 + scripts/release/README.md | 47 +++++++++++ scripts/release/notarize-macos-zip.sh | 73 ++++++++++++++++ scripts/release/sign-macos-binary.sh | 111 +++++++++++++++++++++++++ 5 files changed, 235 insertions(+), 73 deletions(-) create mode 100644 scripts/release/README.md create mode 100755 scripts/release/notarize-macos-zip.sh create mode 100755 scripts/release/sign-macos-binary.sh diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 7a35426..51fb8b6 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -83,61 +83,7 @@ jobs: MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} run: | - missing=() - [[ -n "$MACOS_CERTIFICATE_P12_BASE64" ]] || missing+=("MACOS_CERTIFICATE_P12_BASE64") - [[ -n "$MACOS_CERTIFICATE_PASSWORD" ]] || missing+=("MACOS_CERTIFICATE_PASSWORD") - [[ -n "$MACOS_CODESIGN_IDENTITY" ]] || missing+=("MACOS_CODESIGN_IDENTITY") - if (( ${#missing[@]} > 0 )); then - printf 'Missing required GitHub secret: %s\n' "${missing[@]}" >&2 - exit 1 - fi - - keychain_path="$RUNNER_TEMP/embed-code-signing.keychain-db" - certificate_path="$RUNNER_TEMP/embed-code-signing-certificate.p12" - keychain_password="$(uuidgen)" - - # GNU base64 supports --decode; macOS base64 uses -D. - if printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path" 2>/dev/null; then - : - else - printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 -D > "$certificate_path" - fi - - security create-keychain -p "$keychain_password" "$keychain_path" - security set-keychain-settings -lut 21600 "$keychain_path" - security unlock-keychain -p "$keychain_password" "$keychain_path" - - curl -fsSL https://www.apple.com/certificateauthority/AppleRootCA-G3.cer \ - -o "$RUNNER_TEMP/AppleRootCA-G3.cer" - curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer \ - -o "$RUNNER_TEMP/AppleWWDRCAG6.cer" - curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer \ - -o "$RUNNER_TEMP/DeveloperIDG2CA.cer" - - security import "$RUNNER_TEMP/AppleRootCA-G3.cer" -k "$keychain_path" - security import "$RUNNER_TEMP/AppleWWDRCAG6.cer" -k "$keychain_path" - security import "$RUNNER_TEMP/DeveloperIDG2CA.cer" -k "$keychain_path" - security import "$certificate_path" \ - -P "$MACOS_CERTIFICATE_PASSWORD" \ - -A \ - -t cert \ - -f pkcs12 \ - -k "$keychain_path" - security list-keychains -d user -s "$keychain_path" - security set-key-partition-list \ - -S apple-tool:,apple: \ - -s \ - -k "$keychain_password" \ - "$keychain_path" - security find-identity -v -p codesigning "$keychain_path" - - codesign \ - --force \ - --options runtime \ - --timestamp \ - --sign "$MACOS_CODESIGN_IDENTITY" \ - dist/embed-code-macos - codesign --verify --verbose=4 dist/embed-code-macos + scripts/release/sign-macos-binary.sh dist/embed-code-macos # Notarize the macOS ZIP that will be published as the release asset. - name: Notarize macOS Binary @@ -148,24 +94,7 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} run: | - missing=() - [[ -n "$APPLE_ID" ]] || missing+=("APPLE_ID") - [[ -n "$APPLE_TEAM_ID" ]] || missing+=("APPLE_TEAM_ID") - [[ -n "$APPLE_APP_SPECIFIC_PASSWORD" ]] || missing+=("APPLE_APP_SPECIFIC_PASSWORD") - if (( ${#missing[@]} > 0 )); then - printf 'Missing required GitHub secret: %s\n' "${missing[@]}" >&2 - exit 1 - fi - - pushd dist - ditto -c -k --keepParent embed-code-macos embed-code-macos.zip - popd - - xcrun notarytool submit dist/embed-code-macos.zip \ - --apple-id "$APPLE_ID" \ - --team-id "$APPLE_TEAM_ID" \ - --password "$APPLE_APP_SPECIFIC_PASSWORD" \ - --wait + scripts/release/notarize-macos-zip.sh dist/embed-code-macos dist/embed-code-macos.zip rm dist/embed-code-macos # Create the release for the current commit and attach all generated binaries. diff --git a/PROJECT.md b/PROJECT.md index 6c38188..fc120d9 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -30,6 +30,8 @@ them inside code fences, and checks whether existing snippets are up-to-date. - `type/`: YAML-compatible string and named-path list types. The import path segment is `type`, but the Go package identifier is `_type` because `type` is a Go keyword. +- `scripts/release/`: helper scripts used by release workflows for signing and + notarizing macOS binaries. - `test/resources/`: parser, embedding, configuration, and source-code fixtures. - `showcase/`: executable user guide and end-to-end example suite. diff --git a/scripts/release/README.md b/scripts/release/README.md new file mode 100644 index 0000000..3755ffb --- /dev/null +++ b/scripts/release/README.md @@ -0,0 +1,47 @@ +# Release Scripts + +These scripts support the `release-binaries` GitHub workflow. +They are intended for the macOS release job. + +## Signing + +Use `sign-macos-binary.sh` to import the Developer ID certificate into a +temporary keychain and sign the macOS binary. + +Required environment variables: + +- `MACOS_CERTIFICATE_P12_BASE64`: base64-encoded `.p12` export of the Developer + ID Application certificate and private key. +- `MACOS_CERTIFICATE_PASSWORD`: password used when exporting the `.p12` file. +- `MACOS_CODESIGN_IDENTITY`: full Developer ID Application identity, such as + `Developer ID Application: Company Name (TEAMID)`. + +Optional environment variable: + +- `MACOS_KEYCHAIN_PASSWORD`: password for the temporary keychain. When omitted, + the script generates one. + +Example: + +```bash +scripts/release/sign-macos-binary.sh dist/embed-code-macos +``` + +## Notarization + +Use `notarize-macos-zip.sh` to package the signed CLI binary as a ZIP archive +and submit that archive to Apple notarization. + +Required environment variables: + +- `APPLE_ID`: Apple Account email used for notarization. +- `APPLE_TEAM_ID`: 10-character Apple Developer Team ID. +- `APPLE_APP_SPECIFIC_PASSWORD`: app-specific password for the Apple Account. + +Example: + +```bash +scripts/release/notarize-macos-zip.sh \ + dist/embed-code-macos \ + dist/embed-code-macos.zip +``` diff --git a/scripts/release/notarize-macos-zip.sh b/scripts/release/notarize-macos-zip.sh new file mode 100755 index 0000000..957050e --- /dev/null +++ b/scripts/release/notarize-macos-zip.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# Packages and notarizes the signed macOS CLI binary. +# +# Usage: +# scripts/release/notarize-macos-zip.sh \ +# dist/embed-code-macos \ +# dist/embed-code-macos.zip +# +# The script expects these environment variables: +# APPLE_ID +# APPLE_TEAM_ID +# APPLE_APP_SPECIFIC_PASSWORD +# +# Apple notarization accepts an archive/package submission, so the CLI binary is +# wrapped in a ZIP instead of being uploaded as a loose executable. +# +# The script writes the ZIP path passed as its second argument and waits for +# Apple to accept or reject the notarization submission. +set -euo pipefail + +# Verifies, that secrets are set. +required_vars=( + APPLE_ID + APPLE_TEAM_ID + APPLE_APP_SPECIFIC_PASSWORD +) + +missing=() +for var_name in "${required_vars[@]}"; do + if [[ -z "${!var_name:-}" ]]; then + missing+=("$var_name") + fi +done +if (( ${#missing[@]} > 0 )); then + printf 'Missing required environment variable: %s\n' "${missing[@]}" >&2 + exit 1 +fi + +if (( $# != 2 )); then + echo "Usage: $0 " >&2 + exit 1 +fi + +binary_path="$1" +zip_path="$2" + +if [[ ! -f "$binary_path" ]]; then + echo "Signed macOS binary does not exist: $binary_path" >&2 + exit 1 +fi + +binary_dir="$(cd "$(dirname "$binary_path")" && pwd)" +binary_name="$(basename "$binary_path")" +zip_dir="$(dirname "$zip_path")" +zip_name="$(basename "$zip_path")" + +# Resolve the ZIP path before changing directories. +mkdir -p "$zip_dir" +zip_dir="$(cd "$zip_dir" && pwd)" +zip_path="$zip_dir/$zip_name" + +pushd "$binary_dir" >/dev/null +ditto -c -k --keepParent "$binary_name" "$zip_path" +popd >/dev/null + +# Wait for the notarization result before publishing. +# ZIP archives do not support stapling, so the release publishes the accepted archive as-is. +xcrun notarytool submit "$zip_path" \ + --apple-id "$APPLE_ID" \ + --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --wait diff --git a/scripts/release/sign-macos-binary.sh b/scripts/release/sign-macos-binary.sh new file mode 100755 index 0000000..91cebe2 --- /dev/null +++ b/scripts/release/sign-macos-binary.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Signs the macOS CLI binary with a Developer ID Application certificate. +# +# Usage: +# scripts/release/sign-macos-binary.sh dist/embed-code-macos +# +# The script expects these environment variables: +# MACOS_CERTIFICATE_P12_BASE64 +# MACOS_CERTIFICATE_PASSWORD +# MACOS_CODESIGN_IDENTITY +# +# The certificate value must be a base64-encoded `.p12` export that contains the +# Developer ID Application certificate and private key. +# `MACOS_CODESIGN_IDENTITY` is the full identity printed by +# `security find-identity`, such as: +# +# Developer ID Application: Company Name (TEAMID) +# +# For local diagnostics, `MACOS_KEYCHAIN_PASSWORD` can be set to reuse a known +# temporary keychain password. +# +# The script writes temporary certificate and keychain files under RUNNER_TEMP, +# or TMPDIR when RUNNER_TEMP is not set. It signs the binary in place and verifies +# the resulting signature. +set -euo pipefail + +# Verifies, that secrets are set. +required_vars=( + MACOS_CERTIFICATE_P12_BASE64 + MACOS_CERTIFICATE_PASSWORD + MACOS_CODESIGN_IDENTITY +) + +missing=() +for var_name in "${required_vars[@]}"; do + if [[ -z "${!var_name:-}" ]]; then + missing+=("$var_name") + fi +done +if (( ${#missing[@]} > 0 )); then + printf 'Missing required environment variable: %s\n' "${missing[@]}" >&2 + exit 1 +fi + +if (( $# != 1 )); then + echo "Usage: $0 " >&2 + exit 1 +fi + +binary_path="$1" +if [[ ! -f "$binary_path" ]]; then + echo "macOS binary does not exist: $binary_path" >&2 + exit 1 +fi + +temp_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" +keychain_path="$temp_dir/embed-code-signing.keychain-db" +certificate_path="$temp_dir/embed-code-signing-certificate.p12" +keychain_password="${MACOS_KEYCHAIN_PASSWORD:-$(uuidgen)}" + +# Decode the certificate at runtime so only the temporary runner filesystem ever +# contains the `.p12`. +if printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path" 2>/dev/null; then + : +else + printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 -D > "$certificate_path" +fi + +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 Apple's public certificate chain into the temporary keychain. +# Some runners do not have the current Developer ID intermediate certificates. +curl -fsSL https://www.apple.com/certificateauthority/AppleRootCA-G3.cer \ + -o "$temp_dir/AppleRootCA-G3.cer" +curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer \ + -o "$temp_dir/AppleWWDRCAG6.cer" +curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer \ + -o "$temp_dir/DeveloperIDG2CA.cer" + +security import "$temp_dir/AppleRootCA-G3.cer" -k "$keychain_path" +security import "$temp_dir/AppleWWDRCAG6.cer" -k "$keychain_path" +security import "$temp_dir/DeveloperIDG2CA.cer" -k "$keychain_path" + +# Import the private signing identity and allow Apple signing tools to access +# the key non-interactively. +security import "$certificate_path" \ + -P "$MACOS_CERTIFICATE_PASSWORD" \ + -A \ + -t cert \ + -f pkcs12 \ + -k "$keychain_path" +security list-keychains -d user -s "$keychain_path" +security set-key-partition-list \ + -S apple-tool:,apple: \ + -s \ + -k "$keychain_password" \ + "$keychain_path" +security find-identity -v -p codesigning "$keychain_path" + +# Use the hardened runtime and a trusted timestamp so Gatekeeper can evaluate +# the signature after the Developer ID certificate eventually expires. +codesign \ + --force \ + --options runtime \ + --timestamp \ + --sign "$MACOS_CODESIGN_IDENTITY" \ + "$binary_path" +codesign --verify --verbose=4 "$binary_path" From ebcdf8c9a404e637b9df822d36f9edaa0cc4c098 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 7 Jul 2026 11:57:16 +0200 Subject: [PATCH 03/10] Test signing. --- .github/workflows/release-binaries.yml | 1 + VERSION | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 51fb8b6..1cc18ae 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -2,6 +2,7 @@ name: Release Binaries on: + pull_request: push: branches: [ master ] diff --git a/VERSION b/VERSION index 0495c4a..e8ea05d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.3 +1.2.4 From 4df8cb24d9bd0d8424666993fffc5f2d7407f326 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 7 Jul 2026 12:03:03 +0200 Subject: [PATCH 04/10] Remove test. --- .github/workflows/release-binaries.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 1cc18ae..51fb8b6 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -2,7 +2,6 @@ name: Release Binaries on: - pull_request: push: branches: [ master ] From b36516ffa860ea5d81d961b677c374d6fb4af7b9 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 8 Jul 2026 12:29:35 +0200 Subject: [PATCH 05/10] Provide self-hosted runner. --- .github/workflows/release-binaries.yml | 25 ++++++++++++++----------- PROJECT.md | 5 +++-- README.md | 7 ++++--- scripts/release/README.md | 14 +++++++++----- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 51fb8b6..e6354f1 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -17,7 +17,7 @@ concurrency: jobs: release: - runs-on: macos-latest + runs-on: [ self-hosted, macOS, ARM64 ] timeout-minutes: 30 steps: @@ -70,12 +70,13 @@ jobs: mkdir -p dist GOOS=windows GOARCH=amd64 go build -trimpath -o dist/embed-code-windows.exe main.go GOOS=linux GOARCH=amd64 go build -trimpath -o dist/embed-code-linux main.go - GOOS=darwin GOARCH=amd64 go build -trimpath -o dist/embed-code-macos main.go - chmod +x dist/embed-code-linux dist/embed-code-macos + GOOS=darwin GOARCH=arm64 go build -trimpath -o dist/embed-code-macos-arm64 main.go + GOOS=darwin GOARCH=amd64 go build -trimpath -o dist/embed-code-macos-x64 main.go + chmod +x dist/embed-code-linux dist/embed-code-macos-arm64 dist/embed-code-macos-x64 - # Sign the macOS binary with a Developer ID certificate stored in GitHub + # Sign the macOS binaries with a Developer ID certificate stored in GitHub # secrets. The certificate must be exported as a base64-encoded .p12 file. - - name: Sign macOS Binary + - name: Sign macOS Binaries if: steps.release.outputs.publish == 'true' shell: bash env: @@ -83,10 +84,11 @@ jobs: MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} run: | - scripts/release/sign-macos-binary.sh dist/embed-code-macos + scripts/release/sign-macos-binary.sh dist/embed-code-macos-arm64 + scripts/release/sign-macos-binary.sh dist/embed-code-macos-x64 - # Notarize the macOS ZIP that will be published as the release asset. - - name: Notarize macOS Binary + # Notarize the macOS ZIPs that will be published as release assets. + - name: Notarize macOS Binaries if: steps.release.outputs.publish == 'true' shell: bash env: @@ -94,8 +96,9 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} run: | - scripts/release/notarize-macos-zip.sh dist/embed-code-macos dist/embed-code-macos.zip - rm dist/embed-code-macos + scripts/release/notarize-macos-zip.sh dist/embed-code-macos-arm64 dist/embed-code-macos-arm64.zip + scripts/release/notarize-macos-zip.sh dist/embed-code-macos-x64 dist/embed-code-macos-x64.zip + rm dist/embed-code-macos-arm64 dist/embed-code-macos-x64 # Create the release for the current commit and attach all generated binaries. - name: Publish GitHub Release @@ -105,7 +108,7 @@ jobs: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ steps.version.outputs.tag }} run: | - release_notes="$(printf 'Embed Code %s\n\nThis release contains pre-built Embed Code binaries for macOS, Linux, and Windows.' "$RELEASE_TAG")" + release_notes="$(printf 'Embed Code %s\n\nThis release contains pre-built Embed Code binaries for macOS ARM64, macOS x64, Linux, and Windows.' "$RELEASE_TAG")" gh release create "$RELEASE_TAG" dist/* \ --target "$GITHUB_SHA" \ --title "embed-code $RELEASE_TAG" \ diff --git a/PROJECT.md b/PROJECT.md index fc120d9..ad07a74 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -58,8 +58,9 @@ This repository is configured with these GitHub workflows: - `check`: runs linting, the normal Go test suite, and the showcase end-to-end tests across supported platforms. - `release-binaries`: reads `VERSION`, builds Linux, macOS, and Windows - binaries, signs and notarizes the macOS ZIP, and creates the matching GitHub - Release on pushes to `master`. + binaries, signs and notarizes the macOS ARM64 and x64 ZIPs, and creates the + matching GitHub Release on pushes to `master`. It runs on a self-hosted macOS + ARM64 runner because Apple signing and notarization require macOS tooling. The release tag is `v` from `VERSION`. When the release already exists, the workflow emits a warning and finishes successfully without rebuilding or diff --git a/README.md b/README.md index d20fac6..d49893f 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,12 @@ On Linux, for example: > chmod +x embed-code-linux > ``` -On macOS, download `embed-code-macos.zip`, unzip it, and run the binary: +On macOS, download `embed-code-macos-arm64.zip` for Apple silicon or +`embed-code-macos-x64.zip` for Intel Macs. Then unzip it and run the binary: ```bash -unzip embed-code-macos.zip -./embed-code-macos -mode=check -config-path=showcase/embedding/embed-code.yml +unzip embed-code-macos-arm64.zip +./embed-code-macos-arm64 -mode=check -config-path=showcase/embedding/embed-code.yml ``` Or run it with Go: diff --git a/scripts/release/README.md b/scripts/release/README.md index 3755ffb..baf4099 100644 --- a/scripts/release/README.md +++ b/scripts/release/README.md @@ -1,7 +1,7 @@ # Release Scripts -These scripts support the `release-binaries` GitHub workflow. -They are intended for the macOS release job. +These scripts support the `release-binaries` GitHub workflow. They are intended +for the self-hosted macOS ARM64 release job. ## Signing @@ -24,7 +24,8 @@ Optional environment variable: Example: ```bash -scripts/release/sign-macos-binary.sh dist/embed-code-macos +scripts/release/sign-macos-binary.sh dist/embed-code-macos-arm64 +scripts/release/sign-macos-binary.sh dist/embed-code-macos-x64 ``` ## Notarization @@ -42,6 +43,9 @@ Example: ```bash scripts/release/notarize-macos-zip.sh \ - dist/embed-code-macos \ - dist/embed-code-macos.zip + dist/embed-code-macos-arm64 \ + dist/embed-code-macos-arm64.zip +scripts/release/notarize-macos-zip.sh \ + dist/embed-code-macos-x64 \ + dist/embed-code-macos-x64.zip ``` From 49bd5bdc9fb74226befa576aa11ee741eced284f Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 8 Jul 2026 12:30:14 +0200 Subject: [PATCH 06/10] Test workflow. --- .github/workflows/release-binaries.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index e6354f1..072dfec 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -2,6 +2,7 @@ name: Release Binaries on: + pull_request: push: branches: [ master ] From 3cd38958bc4b387bf707b217e317f2d9b249ca03 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 8 Jul 2026 12:41:56 +0200 Subject: [PATCH 07/10] Fix keychain issue. --- .github/workflows/release-binaries.yml | 5 +- scripts/release/README.md | 5 +- scripts/release/sign-macos-binary.sh | 74 ++++++++++++++++---------- 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 072dfec..19bd1ef 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -85,8 +85,9 @@ jobs: MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} run: | - scripts/release/sign-macos-binary.sh dist/embed-code-macos-arm64 - scripts/release/sign-macos-binary.sh dist/embed-code-macos-x64 + scripts/release/sign-macos-binary.sh \ + dist/embed-code-macos-arm64 \ + dist/embed-code-macos-x64 # Notarize the macOS ZIPs that will be published as release assets. - name: Notarize macOS Binaries diff --git a/scripts/release/README.md b/scripts/release/README.md index baf4099..006f6f9 100644 --- a/scripts/release/README.md +++ b/scripts/release/README.md @@ -24,8 +24,9 @@ Optional environment variable: Example: ```bash -scripts/release/sign-macos-binary.sh dist/embed-code-macos-arm64 -scripts/release/sign-macos-binary.sh dist/embed-code-macos-x64 +scripts/release/sign-macos-binary.sh \ + dist/embed-code-macos-arm64 \ + dist/embed-code-macos-x64 ``` ## Notarization diff --git a/scripts/release/sign-macos-binary.sh b/scripts/release/sign-macos-binary.sh index 91cebe2..ec58b03 100755 --- a/scripts/release/sign-macos-binary.sh +++ b/scripts/release/sign-macos-binary.sh @@ -1,9 +1,11 @@ #!/usr/bin/env bash -# Signs the macOS CLI binary with a Developer ID Application certificate. +# Signs macOS CLI binaries with a Developer ID Application certificate. # # Usage: -# scripts/release/sign-macos-binary.sh dist/embed-code-macos +# scripts/release/sign-macos-binary.sh \ +# dist/embed-code-macos-arm64 \ +# dist/embed-code-macos-x64 # # The script expects these environment variables: # MACOS_CERTIFICATE_P12_BASE64 @@ -21,8 +23,8 @@ # temporary keychain password. # # The script writes temporary certificate and keychain files under RUNNER_TEMP, -# or TMPDIR when RUNNER_TEMP is not set. It signs the binary in place and verifies -# the resulting signature. +# or TMPDIR when RUNNER_TEMP is not set. It signs each binary in place and +# verifies the resulting signatures. set -euo pipefail # Verifies, that secrets are set. @@ -43,21 +45,37 @@ if (( ${#missing[@]} > 0 )); then exit 1 fi -if (( $# != 1 )); then - echo "Usage: $0 " >&2 +if (( $# == 0 )); then + echo "Usage: $0 [macos-binary-path...]" >&2 exit 1 fi -binary_path="$1" -if [[ ! -f "$binary_path" ]]; then - echo "macOS binary does not exist: $binary_path" >&2 - exit 1 -fi +binary_paths=("$@") +for binary_path in "${binary_paths[@]}"; do + if [[ ! -f "$binary_path" ]]; then + echo "macOS binary does not exist: $binary_path" >&2 + exit 1 + fi +done temp_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" -keychain_path="$temp_dir/embed-code-signing.keychain-db" -certificate_path="$temp_dir/embed-code-signing-certificate.p12" +signing_id="$(uuidgen)" +keychain_path="$temp_dir/embed-code-signing-$signing_id.keychain-db" +certificate_path="$temp_dir/embed-code-signing-certificate-$signing_id.p12" keychain_password="${MACOS_KEYCHAIN_PASSWORD:-$(uuidgen)}" +apple_root_certificate_path="$temp_dir/AppleRootCA-G3-$signing_id.cer" +apple_wwdr_certificate_path="$temp_dir/AppleWWDRCAG6-$signing_id.cer" +developer_id_certificate_path="$temp_dir/DeveloperIDG2CA-$signing_id.cer" + +cleanup() { + security delete-keychain "$keychain_path" >/dev/null 2>&1 || true + rm -f \ + "$certificate_path" \ + "$apple_root_certificate_path" \ + "$apple_wwdr_certificate_path" \ + "$developer_id_certificate_path" +} +trap cleanup EXIT # Decode the certificate at runtime so only the temporary runner filesystem ever # contains the `.p12`. @@ -74,15 +92,15 @@ security unlock-keychain -p "$keychain_password" "$keychain_path" # Import Apple's public certificate chain into the temporary keychain. # Some runners do not have the current Developer ID intermediate certificates. curl -fsSL https://www.apple.com/certificateauthority/AppleRootCA-G3.cer \ - -o "$temp_dir/AppleRootCA-G3.cer" + -o "$apple_root_certificate_path" curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer \ - -o "$temp_dir/AppleWWDRCAG6.cer" + -o "$apple_wwdr_certificate_path" curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer \ - -o "$temp_dir/DeveloperIDG2CA.cer" + -o "$developer_id_certificate_path" -security import "$temp_dir/AppleRootCA-G3.cer" -k "$keychain_path" -security import "$temp_dir/AppleWWDRCAG6.cer" -k "$keychain_path" -security import "$temp_dir/DeveloperIDG2CA.cer" -k "$keychain_path" +security import "$apple_root_certificate_path" -k "$keychain_path" +security import "$apple_wwdr_certificate_path" -k "$keychain_path" +security import "$developer_id_certificate_path" -k "$keychain_path" # Import the private signing identity and allow Apple signing tools to access # the key non-interactively. @@ -92,7 +110,6 @@ security import "$certificate_path" \ -t cert \ -f pkcs12 \ -k "$keychain_path" -security list-keychains -d user -s "$keychain_path" security set-key-partition-list \ -S apple-tool:,apple: \ -s \ @@ -102,10 +119,13 @@ security find-identity -v -p codesigning "$keychain_path" # Use the hardened runtime and a trusted timestamp so Gatekeeper can evaluate # the signature after the Developer ID certificate eventually expires. -codesign \ - --force \ - --options runtime \ - --timestamp \ - --sign "$MACOS_CODESIGN_IDENTITY" \ - "$binary_path" -codesign --verify --verbose=4 "$binary_path" +for binary_path in "${binary_paths[@]}"; do + codesign \ + --force \ + --options runtime \ + --timestamp \ + --keychain "$keychain_path" \ + --sign "$MACOS_CODESIGN_IDENTITY" \ + "$binary_path" + codesign --verify --verbose=4 "$binary_path" +done From d67388327743f0e9a46bea9648e19bc06668a1c0 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 8 Jul 2026 12:44:54 +0200 Subject: [PATCH 08/10] Fix signing script. --- scripts/release/sign-macos-binary.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/release/sign-macos-binary.sh b/scripts/release/sign-macos-binary.sh index ec58b03..6713d97 100755 --- a/scripts/release/sign-macos-binary.sh +++ b/scripts/release/sign-macos-binary.sh @@ -66,8 +66,18 @@ keychain_password="${MACOS_KEYCHAIN_PASSWORD:-$(uuidgen)}" apple_root_certificate_path="$temp_dir/AppleRootCA-G3-$signing_id.cer" apple_wwdr_certificate_path="$temp_dir/AppleWWDRCAG6-$signing_id.cer" developer_id_certificate_path="$temp_dir/DeveloperIDG2CA-$signing_id.cer" +original_keychains=() + +while IFS= read -r keychain; do + keychain="${keychain#\"}" + keychain="${keychain%\"}" + original_keychains+=("$keychain") +done < <(security list-keychains -d user) cleanup() { + if (( ${#original_keychains[@]} > 0 )); then + security list-keychains -d user -s "${original_keychains[@]}" >/dev/null 2>&1 || true + fi security delete-keychain "$keychain_path" >/dev/null 2>&1 || true rm -f \ "$certificate_path" \ @@ -88,6 +98,7 @@ fi security create-keychain -p "$keychain_password" "$keychain_path" security set-keychain-settings -lut 21600 "$keychain_path" security unlock-keychain -p "$keychain_password" "$keychain_path" +security list-keychains -d user -s "$keychain_path" "${original_keychains[@]}" # Import Apple's public certificate chain into the temporary keychain. # Some runners do not have the current Developer ID intermediate certificates. @@ -124,7 +135,6 @@ for binary_path in "${binary_paths[@]}"; do --force \ --options runtime \ --timestamp \ - --keychain "$keychain_path" \ --sign "$MACOS_CODESIGN_IDENTITY" \ "$binary_path" codesign --verify --verbose=4 "$binary_path" From 44e2107416b66f54774e09f6ee9956144a1472e5 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 8 Jul 2026 12:55:31 +0200 Subject: [PATCH 09/10] Update script to work on self-hosted runner. --- .github/workflows/release-binaries.yml | 66 +++++++++++++++++++------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 19bd1ef..01c5caf 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -51,17 +51,25 @@ jobs: # If current app version already exist in release, emits a warning, but flow counts as passed. - name: Check Release Version id: release - shell: bash + uses: actions/github-script@v8 env: - GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ steps.version.outputs.tag }} - run: | - if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - echo "::warning title=Release already exists::Release $RELEASE_TAG already exists. Bump VERSION to publish a new release." - echo "publish=false" >> "$GITHUB_OUTPUT" - else - echo "publish=true" >> "$GITHUB_OUTPUT" - fi + with: + script: | + try { + await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: process.env.RELEASE_TAG, + }); + core.warning(`Release ${process.env.RELEASE_TAG} already exists. Bump VERSION to publish a new release.`); + core.setOutput('publish', 'false'); + } catch (error) { + if (error.status !== 404) { + throw error; + } + core.setOutput('publish', 'true'); + } # Build binaries only for new releases. - name: Build Binaries @@ -105,13 +113,37 @@ jobs: # Create the release for the current commit and attach all generated binaries. - name: Publish GitHub Release if: steps.release.outputs.publish == 'true' - shell: bash + uses: actions/github-script@v8 env: - GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ steps.version.outputs.tag }} - run: | - release_notes="$(printf 'Embed Code %s\n\nThis release contains pre-built Embed Code binaries for macOS ARM64, macOS x64, Linux, and Windows.' "$RELEASE_TAG")" - gh release create "$RELEASE_TAG" dist/* \ - --target "$GITHUB_SHA" \ - --title "embed-code $RELEASE_TAG" \ - --notes "$release_notes" + with: + script: | + const fs = require('fs'); + const path = require('path'); + + const releaseTag = process.env.RELEASE_TAG; + const releaseNotes = `Embed Code ${releaseTag}\n\nThis release contains pre-built Embed Code binaries for macOS ARM64, macOS x64, Linux, and Windows.`; + const release = await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: releaseTag, + target_commitish: context.sha, + name: `embed-code ${releaseTag}`, + body: releaseNotes, + }); + + for (const assetName of fs.readdirSync('dist').sort()) { + const assetPath = path.join('dist', assetName); + const assetData = fs.readFileSync(assetPath); + await github.rest.repos.uploadReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.data.id, + name: assetName, + headers: { + 'content-type': 'application/octet-stream', + 'content-length': assetData.length, + }, + data: assetData, + }); + } From fa494ac567dbd164c4399c2b0d9ab3973d21ec02 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 8 Jul 2026 13:13:28 +0200 Subject: [PATCH 10/10] Remove test trigger. --- .github/workflows/release-binaries.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 01c5caf..287e9a7 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -2,7 +2,6 @@ name: Release Binaries on: - pull_request: push: branches: [ master ] @@ -135,6 +134,9 @@ jobs: for (const assetName of fs.readdirSync('dist').sort()) { const assetPath = path.join('dist', assetName); const assetData = fs.readFileSync(assetPath); + const sizeMb = (assetData.length / 1024 / 1024).toFixed(2); + const startedAt = Date.now(); + core.info(`Uploading ${assetName} (${sizeMb} MB).`); await github.rest.repos.uploadReleaseAsset({ owner: context.repo.owner, repo: context.repo.repo, @@ -146,4 +148,6 @@ jobs: }, data: assetData, }); + const durationSeconds = ((Date.now() - startedAt) / 1000).toFixed(1); + core.info(`Uploaded ${assetName} in ${durationSeconds}s.`); }