Skip to content
Merged
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
107 changes: 86 additions & 21 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: [ self-hosted, macOS, ARM64 ]
timeout-minutes: 30

steps:
- name: Checkout Repository
Expand Down Expand Up @@ -50,17 +50,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
Expand All @@ -70,19 +78,76 @@ 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 binaries with a Developer ID certificate stored in GitHub
# secrets. The certificate must be exported as a base64-encoded .p12 file.
- name: Sign macOS Binaries
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-arm64 \
dist/embed-code-macos-x64

# Notarize the macOS ZIPs that will be published as release assets.
- name: Notarize macOS Binaries
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-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
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, 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);
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,
release_id: release.data.id,
name: assetName,
headers: {
'content-type': 'application/octet-stream',
'content-length': assetData.length,
},
data: assetData,
});
const durationSeconds = ((Date.now() - startedAt) / 1000).toFixed(1);
core.info(`Uploaded ${assetName} in ${durationSeconds}s.`);
}
6 changes: 5 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,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, 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<version>` from `VERSION`. When the release already exists,
the workflow emits a warning and finishes successfully without rebuilding or
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ 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-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-arm64.zip
./embed-code-macos-arm64 -mode=check -config-path=showcase/embedding/embed-code.yml
```

Or run it with Go:

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.3
1.2.4
52 changes: 52 additions & 0 deletions scripts/release/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Release Scripts

These scripts support the `release-binaries` GitHub workflow. They are intended
for the self-hosted macOS ARM64 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-arm64 \
dist/embed-code-macos-x64
```

## 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-arm64 \
dist/embed-code-macos-arm64.zip
scripts/release/notarize-macos-zip.sh \
dist/embed-code-macos-x64 \
dist/embed-code-macos-x64.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
Loading
Loading