Skip to content
Draft
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
28 changes: 26 additions & 2 deletions .github/workflows/release-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -73,6 +73,30 @@ 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: |
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
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: |
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.
- name: Publish GitHub Release
if: steps.release.outputs.publish == 'true'
Expand Down
5 changes: 4 additions & 1 deletion PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -56,7 +58,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<version>` from `VERSION`. When the release already exists,
the workflow emits a warning and finishes successfully without rebuilding or
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
47 changes: 47 additions & 0 deletions scripts/release/README.md
Original file line number Diff line number Diff line change
@@ -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
```
73 changes: 73 additions & 0 deletions scripts/release/notarize-macos-zip.sh
Original file line number Diff line number Diff line change
@@ -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 <signed-macos-binary-path> <zip-output-path>" >&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
111 changes: 111 additions & 0 deletions scripts/release/sign-macos-binary.sh
Original file line number Diff line number Diff line change
@@ -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 <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

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