Skip to content

Release

Release #28

Workflow file for this run

name: Release Desktop
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
version:
description: "Release version (for example 1.2.3 or v1.2.3)"
required: true
type: string
mac_arm64_only:
description: "Apple Silicon (M-series) Mac only — build and publish only the macOS arm64 DMG (skip Intel Mac, Linux, and Windows). Enabled by default."
required: false
type: boolean
default: true
permissions:
contents: write
jobs:
configure:
name: Configure build matrix
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.set.outputs.matrix }}
mac_arm64_only: ${{ steps.set.outputs.mac_arm64_only }}
steps:
- id: set
name: Select desktop targets
shell: bash
run: |
set -euo pipefail
mac_arm64_only="true"
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
input="${{ github.event.inputs.mac_arm64_only }}"
if [[ "$input" == "false" ]]; then
mac_arm64_only="false"
fi
fi
echo "mac_arm64_only=$mac_arm64_only" >> "$GITHUB_OUTPUT"
if [[ "$mac_arm64_only" == "true" ]]; then
matrix_json='[{"label":"macOS arm64","runner":"macos-14","platform":"mac","target":"dmg","arch":"arm64"}]'
else
matrix_json='[{"label":"macOS arm64","runner":"macos-14","platform":"mac","target":"dmg","arch":"arm64"},{"label":"macOS x64","runner":"macos-15-intel","platform":"mac","target":"dmg","arch":"x64"},{"label":"Linux x64","runner":"ubuntu-24.04","platform":"linux","target":"AppImage","arch":"x64"},{"label":"Windows x64","runner":"windows-2022","platform":"win","target":"nsis","arch":"x64"}]'
fi
{
echo "matrix<<MATRIX_JSON_EOF"
echo "$matrix_json"
echo "MATRIX_JSON_EOF"
} >> "$GITHUB_OUTPUT"
preflight:
name: Preflight
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.release_meta.outputs.version }}
tag: ${{ steps.release_meta.outputs.tag }}
is_prerelease: ${{ steps.release_meta.outputs.is_prerelease }}
make_latest: ${{ steps.release_meta.outputs.make_latest }}
ref: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v6
- id: release_meta
name: Resolve release version
shell: bash
run: |
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
raw="${{ github.event.inputs.version }}"
else
raw="${GITHUB_REF_NAME}"
fi
version="${raw#v}"
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid release version: $raw" >&2
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=v$version" >> "$GITHUB_OUTPUT"
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
echo "make_latest=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
echo "make_latest=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: package.json
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Format
run: bun run fmt:check
- name: Lint
run: bun run lint
- name: Typecheck
run: bun run typecheck
- name: Test
run: bun run test
- name: Release smoke
run: bun run release:smoke
build:
name: Build ${{ matrix.label }}
needs: [preflight, configure]
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.configure.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: package.json
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Align package versions to release version
run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"
- name: Build desktop artifact
shell: bash
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }}
run: |
args=(
--platform "${{ matrix.platform }}"
--target "${{ matrix.target }}"
--arch "${{ matrix.arch }}"
--build-version "${{ needs.preflight.outputs.version }}"
--verbose
)
has_all() {
for value in "$@"; do
if [[ -z "$value" ]]; then
return 1
fi
done
return 0
}
if [[ "${{ matrix.platform }}" == "mac" ]]; then
required_mac_signing_secrets=(
CSC_LINK
CSC_KEY_PASSWORD
APPLE_API_KEY
APPLE_API_KEY_ID
APPLE_API_ISSUER
)
missing_mac_signing_secrets=()
for secret_name in "${required_mac_signing_secrets[@]}"; do
if [[ -z "${!secret_name}" ]]; then
missing_mac_signing_secrets+=("$secret_name")
fi
done
if (( ${#missing_mac_signing_secrets[@]} > 0 )); then
echo "Missing required macOS signing/notarization secrets: ${missing_mac_signing_secrets[*]}" >&2
echo "Refusing to publish an unsigned or unnotarized macOS release." >&2
exit 1
fi
key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
printf '%s' "$APPLE_API_KEY" > "$key_path"
export APPLE_API_KEY="$key_path"
echo "macOS signing + notarization enabled."
args+=(--signed --require-signed)
elif [[ "${{ matrix.platform }}" == "win" ]]; then
if has_all \
"$AZURE_TENANT_ID" \
"$AZURE_CLIENT_ID" \
"$AZURE_CLIENT_SECRET" \
"$AZURE_TRUSTED_SIGNING_ENDPOINT" \
"$AZURE_TRUSTED_SIGNING_ACCOUNT_NAME" \
"$AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME" \
"$AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"; then
echo "Windows signing enabled (Azure Trusted Signing)."
args+=(--signed)
else
echo "Windows signing disabled (missing one or more Azure Trusted Signing secrets)."
fi
else
echo "Signing disabled for ${{ matrix.platform }}."
fi
bun run dist:desktop:artifact -- "${args[@]}"
- name: Collect release assets
shell: bash
run: |
set -euo pipefail
mkdir -p release-publish
shopt -s nullglob
for pattern in \
"release/*.dmg" \
"release/*.zip" \
"release/*.AppImage" \
"release/*.exe" \
"release/*.blockmap" \
"release/latest*.yml"; do
for file in $pattern; do
cp "$file" release-publish/
done
done
if [[ "${{ matrix.platform }}" == "mac" && "${{ matrix.arch }}" != "arm64" ]]; then
if [[ -f release-publish/latest-mac.yml ]]; then
mv release-publish/latest-mac.yml "release-publish/latest-mac-${{ matrix.arch }}.yml"
fi
fi
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
path: release-publish/*
if-no-files-found: error
release:
name: Publish GitHub Release
needs: [preflight, build, configure]
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: package.json
- name: Download all desktop artifacts
uses: actions/download-artifact@v8
with:
pattern: desktop-*
merge-multiple: true
path: release-assets
- name: Merge macOS updater manifests
run: |
set -euo pipefail
if [[ "${{ needs.configure.outputs.mac_arm64_only }}" == "true" ]]; then
echo "macOS arm64-only build: using latest-mac.yml from the arm64 builder as-is."
elif [[ -f release-assets/latest-mac-x64.yml ]]; then
node scripts/merge-mac-update-manifests.ts \
release-assets/latest-mac.yml \
release-assets/latest-mac-x64.yml
rm -f release-assets/latest-mac-x64.yml
else
echo "No latest-mac-x64.yml present; using latest-mac.yml as-is."
fi
- name: Stage documentation for release assets
env:
RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
v="${RELEASE_VERSION}"
notes="docs/releases/v${v}.md"
manifest="docs/releases/v${v}/assets.md"
if [[ ! -f "$notes" ]]; then
echo "Missing release notes: $notes" >&2
exit 1
fi
if [[ ! -f "$manifest" ]]; then
echo "Missing asset manifest: $manifest" >&2
exit 1
fi
cp CHANGELOG.md release-assets/okcode-CHANGELOG.md
cp "$notes" release-assets/okcode-RELEASE-NOTES.md
cp "$manifest" release-assets/okcode-ASSETS-MANIFEST.md
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.preflight.outputs.tag }}
target_commitish: ${{ needs.preflight.outputs.ref }}
name: OK Code v${{ needs.preflight.outputs.version }}
body_path: docs/releases/v${{ needs.preflight.outputs.version }}.md
prerelease: ${{ needs.preflight.outputs.is_prerelease }}
make_latest: ${{ needs.preflight.outputs.make_latest }}
files: release-assets/*
fail_on_unmatched_files: true
finalize:
name: Finalize release
needs: [preflight, release]
runs-on: ubuntu-24.04
steps:
- name: Validate release app secrets
shell: bash
env:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
run: |
set -euo pipefail
[[ -n "$RELEASE_APP_ID" ]] || { echo "Missing secret RELEASE_APP_ID"; exit 1; }
[[ "$RELEASE_APP_ID" =~ ^[0-9]+$ ]] || { echo "RELEASE_APP_ID must be a numeric GitHub App ID"; exit 1; }
[[ -n "$RELEASE_APP_PRIVATE_KEY" ]] || { echo "Missing secret RELEASE_APP_PRIVATE_KEY"; exit 1; }
- id: app_token
name: Mint release app token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Checkout
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
persist-credentials: true
- id: app_bot
name: Resolve GitHub App bot identity
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
APP_SLUG: ${{ steps.app_token.outputs.app-slug }}
run: |
user_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)"
echo "name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT"
echo "email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: package.json
- id: update_versions
name: Update version strings
env:
RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
run: node scripts/update-release-package-versions.ts "$RELEASE_VERSION" --github-output
- name: Format package.json files
if: steps.update_versions.outputs.changed == 'true'
run: bunx oxfmt apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json
- name: Refresh lockfile
if: steps.update_versions.outputs.changed == 'true'
run: bun install --lockfile-only --ignore-scripts
- name: Commit and push version bump
if: steps.update_versions.outputs.changed == 'true'
shell: bash
env:
RELEASE_TAG: ${{ needs.preflight.outputs.tag }}
run: |
if git diff --quiet -- apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json bun.lock; then
echo "No version changes to commit."
exit 0
fi
git config user.name "${{ steps.app_bot.outputs.name }}"
git config user.email "${{ steps.app_bot.outputs.email }}"
git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json bun.lock
git commit -m "chore(release): prepare $RELEASE_TAG"
git push origin HEAD:main