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
229 changes: 229 additions & 0 deletions .github/workflows/release-ios.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
name: Release iOS

on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
version:
description: "Release version (for example 1.2.3 or v1.2.3)"
required: true
type: string

permissions:
contents: read

jobs:
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 }}
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"
else
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
fi

build-ios:
name: Build & Upload to TestFlight
needs: [preflight]
runs-on: macos-14
env:
RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
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 "$RELEASE_VERSION"

- name: Update iOS version in Xcode project
run: node scripts/update-ios-version.ts "$RELEASE_VERSION" --build-number "$GITHUB_RUN_NUMBER"

- name: Build mobile web bundle
run: bun run --cwd apps/mobile build

- name: Sync Capacitor iOS
run: bunx cap sync ios --deployment
working-directory: apps/mobile

- name: Install Apple certificate and provisioning profile
env:
IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }}
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }}
run: |
set -euo pipefail

# Validate required secrets
for secret_name in IOS_CERTIFICATE_P12 IOS_CERTIFICATE_PASSWORD IOS_PROVISIONING_PROFILE; do
if [[ -z "${!secret_name}" ]]; then
echo "Missing required secret: $secret_name" >&2
exit 1
fi
done

# Create temporary keychain
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 16)"
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 distribution certificate
CERT_PATH="$RUNNER_TEMP/certificate.p12"
echo "$IOS_CERTIFICATE_P12" | base64 --decode > "$CERT_PATH"
security import "$CERT_PATH" \
-P "$IOS_CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"

# Install provisioning profile
PROFILE_PATH="$RUNNER_TEMP/profile.mobileprovision"
echo "$IOS_PROVISIONING_PROFILE" | base64 --decode > "$PROFILE_PATH"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/

- name: Build iOS archive
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail

xcodebuild archive \
-project apps/mobile/ios/App/App.xcodeproj \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/App.xcarchive" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
CODE_SIGN_IDENTITY="iPhone Distribution" \
PROVISIONING_PROFILE_SPECIFIER="${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}" \
-allowProvisioningUpdates \
COMPILER_INDEX_STORE_ENABLE=NO

- name: Generate ExportOptions.plist
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
cat > "$RUNNER_TEMP/ExportOptions.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>destination</key>
<string>upload</string>
<key>teamID</key>
<string>${APPLE_TEAM_ID}</string>
<key>uploadSymbols</key>
<true/>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>com.openknots.okcode.mobile</key>
<string>${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}</string>
</dict>
</dict>
</plist>
PLIST

- name: Export IPA
run: |
set -euo pipefail

xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/App.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist"

- name: Write App Store Connect API key
env:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: |
set -euo pipefail
KEY_DIR="$HOME/private_keys"
mkdir -p "$KEY_DIR"
printf '%s' "$APPLE_API_KEY" > "$KEY_DIR/AuthKey_${APPLE_API_KEY_ID}.p8"

- name: Upload to TestFlight
env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: |
set -euo pipefail

IPA_FILE=$(find "$RUNNER_TEMP/export" -name "*.ipa" -print -quit)
if [[ -z "$IPA_FILE" ]]; then
echo "No IPA file found in export directory" >&2
ls -la "$RUNNER_TEMP/export/"
exit 1
fi

echo "Uploading $IPA_FILE to TestFlight..."

xcrun altool --upload-app \
-f "$IPA_FILE" \
-t ios \
--apiKey "$APPLE_API_KEY_ID" \
--apiIssuer "$APPLE_API_ISSUER"

echo "Upload to TestFlight complete!"

- name: Cleanup keychain
if: always()
run: |
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
if [[ -f "$KEYCHAIN_PATH" ]]; then
security delete-keychain "$KEYCHAIN_PATH" || true
fi
rm -f "$HOME/private_keys/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8" || true
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ jobs:

- 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
run: bunx oxfmt apps/server/package.json apps/desktop/package.json apps/web/package.json apps/mobile/package.json packages/contracts/package.json

- name: Refresh lockfile
if: steps.update_versions.outputs.changed == 'true'
Expand All @@ -383,14 +383,14 @@ jobs:
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
if git diff --quiet -- apps/server/package.json apps/desktop/package.json apps/web/package.json apps/mobile/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 add apps/server/package.json apps/desktop/package.json apps/web/package.json apps/mobile/package.json packages/contracts/package.json bun.lock
git commit -m "chore(release): prepare $RELEASE_TAG"
git push origin HEAD:main
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okcode/desktop",
"version": "0.0.12",
"version": "0.10.0",
"private": true,
"main": "dist-electron/main.js",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 0.10.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.openknots.okcode.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -325,7 +325,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 0.10.0;
PRODUCT_BUNDLE_IDENTIFIER = com.openknots.okcode.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okcode/mobile",
"version": "0.0.4",
"version": "0.10.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "okcodes",
"version": "0.0.12",
"version": "0.10.0",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okcode/web",
"version": "0.0.12",
"version": "0.10.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
"dist:desktop:win": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64",
"release:prepare": "node scripts/prepare-release.ts",
"release:smoke": "node scripts/release-smoke.ts",
"dist:mobile:build": "bun run --cwd apps/mobile build",
"dist:mobile:sync:ios": "bunx cap sync ios --deployment",
"dist:mobile:update-ios-version": "node scripts/update-ios-version.ts",
"clean": "rm -rf node_modules apps/*/node_modules packages/*/node_modules apps/*/dist apps/*/dist-electron packages/*/dist .turbo apps/*/.turbo packages/*/.turbo",
"sync:vscode-icons": "node scripts/sync-vscode-icons.mjs",
"regenerate:brand-assets": "python3 scripts/generate-brand-assets.py",
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okcode/contracts",
"version": "0.0.12",
"version": "0.10.0",
"private": true,
"files": [
"dist"
Expand Down
23 changes: 18 additions & 5 deletions scripts/prepare-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ ${highlights || "- See changelog for detailed changes."}

- **CLI:** \`npm install -g okcodes@${version}\` (after the package is published to npm manually).
- **Desktop:** Download from [GitHub Releases](${REPO_URL}/releases/tag/v${version}). Filenames are listed in [assets.md](v${version}/assets.md).
- **iOS:** Available via TestFlight (uploaded automatically by the Release iOS workflow).

## Known limitations

Expand Down Expand Up @@ -301,6 +302,16 @@ After the workflow completes, expect **installer and updater** artifacts similar
| \`latest.yml\` | Windows update manifest |
| \`*.blockmap\` | Differential download block maps |

## iOS (TestFlight)

The iOS build is uploaded directly to App Store Connect / TestFlight by the [Release iOS workflow](../../.github/workflows/release-ios.yml). No IPA artifact is attached to the GitHub Release.

| Detail | Value |
| ----------------- | ---------------------------------------- |
| Bundle ID | \`com.openknots.okcode.mobile\` |
| Marketing version | \`${version}\` |
| Build number | Set from \`GITHUB_RUN_NUMBER\` at build time |

## Checksums

SHA-256 checksums are not committed here; verify downloads via GitHub's release UI or \`gh release download\` if you use the GitHub CLI.
Expand Down Expand Up @@ -713,11 +724,13 @@ async function main(): Promise<void> {

if (!skipCommit && !dryRun) {
console.log(" Next steps:");
console.log(` 1. Monitor the release workflow: ${REPO_URL}/actions`);
console.log(` 2. Verify the GitHub Release: ${REPO_URL}/releases/tag/${tag}`);
console.log(" 3. Test downloaded installers on each platform.");
console.log(" 4. Verify auto-update from the previous version.");
console.log(` 5. Confirm version bump commit on main: git log origin/main --oneline -5`);
console.log(` 1. Monitor the desktop release workflow: ${REPO_URL}/actions/workflows/release.yml`);
console.log(` 2. Monitor the iOS TestFlight workflow: ${REPO_URL}/actions/workflows/release-ios.yml`);
console.log(` 3. Verify the GitHub Release: ${REPO_URL}/releases/tag/${tag}`);
console.log(" 4. Test downloaded installers on each platform.");
console.log(" 5. Verify auto-update from the previous version.");
console.log(" 6. Verify TestFlight build in App Store Connect.");
console.log(` 7. Confirm version bump commit on main: git log origin/main --oneline -5`);
console.log("");
} else if (skipCommit) {
console.log(" Documentation generated. To finish the release manually:");
Expand Down
Loading
Loading