Infer provider from model for requested turns #94
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Release version (for example 1.2.3 or v1.2.3)" | |
| required: true | |
| type: string | |
| publish_cli: | |
| description: "Publish the okcodes CLI to npm after the desktop release finalizes" | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: write | |
| 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 }} | |
| make_latest: ${{ steps.release_meta.outputs.make_latest }} | |
| release_channel: ${{ steps.release_meta.outputs.release_channel }} | |
| npm_tag: ${{ steps.release_meta.outputs.npm_tag }} | |
| build_timestamp: ${{ steps.release_meta.outputs.build_timestamp }} | |
| ref: ${{ github.sha }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - id: release_meta | |
| name: Resolve release version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| 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 | |
| build_timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "tag=v$version" >> "$GITHUB_OUTPUT" | |
| echo "build_timestamp=$build_timestamp" >> "$GITHUB_OUTPUT" | |
| if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "is_prerelease=false" >> "$GITHUB_OUTPUT" | |
| echo "make_latest=true" >> "$GITHUB_OUTPUT" | |
| echo "release_channel=stable" >> "$GITHUB_OUTPUT" | |
| echo "npm_tag=latest" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_prerelease=true" >> "$GITHUB_OUTPUT" | |
| echo "make_latest=false" >> "$GITHUB_OUTPUT" | |
| echo "release_channel=prerelease" >> "$GITHUB_OUTPUT" | |
| echo "npm_tag=next" >> "$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: Install browser dependencies | |
| run: bun run --cwd apps/web test:browser:install | |
| - name: Format check | |
| run: bun run fmt:check | |
| - name: Lint | |
| run: bun run lint | |
| - name: Typecheck | |
| run: bun run typecheck | |
| - name: Test | |
| run: bun run test | |
| - name: Browser test | |
| run: bun run --cwd apps/web test:browser | |
| - name: Desktop smoke | |
| run: bun run test:desktop-smoke | |
| - name: Release smoke | |
| run: bun run release:smoke | |
| desktop_build: | |
| name: Desktop ${{ matrix.label }} | |
| needs: [preflight] | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - label: macOS arm64 | |
| runner: macos-14 | |
| platform: mac | |
| target: dmg | |
| arch: arm64 | |
| - 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 | |
| env: | |
| OKCODE_COMMIT_HASH: ${{ github.sha }} | |
| OKCODE_BUILD_TIMESTAMP: ${{ needs.preflight.outputs.build_timestamp }} | |
| OKCODE_RELEASE_CHANNEL: ${{ needs.preflight.outputs.release_channel }} | |
| 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: | | |
| set -euo pipefail | |
| args=( | |
| --platform "${{ matrix.platform }}" | |
| --target "${{ matrix.target }}" | |
| --arch "${{ matrix.arch }}" | |
| --build-version "${{ needs.preflight.outputs.version }}" | |
| --verbose | |
| ) | |
| require_values() { | |
| 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 | |
| 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" | |
| args+=(--signed --require-signed) | |
| elif [[ "${{ matrix.platform }}" == "win" ]]; then | |
| if require_values \ | |
| "$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 | |
| args+=(--signed --require-signed) | |
| else | |
| echo "Azure Trusted Signing secrets not configured; building unsigned Windows artifact." >&2 | |
| fi | |
| fi | |
| bun run dist:desktop:artifact -- "${args[@]}" | |
| - name: Validate packaged artifact outputs | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| case "${{ matrix.platform }}" in | |
| mac) | |
| dmg=(release/*.dmg) | |
| manifest=(release/latest-mac*.yml) | |
| [[ ${#dmg[@]} -gt 0 ]] || { echo "Missing macOS DMG artifact" >&2; exit 1; } | |
| [[ ${#manifest[@]} -gt 0 ]] || { echo "Missing macOS updater manifest" >&2; exit 1; } | |
| ;; | |
| linux) | |
| appimage=(release/*.AppImage) | |
| [[ ${#appimage[@]} -gt 0 ]] || { echo "Missing Linux AppImage artifact" >&2; exit 1; } | |
| ;; | |
| win) | |
| installer=(release/*.exe) | |
| [[ ${#installer[@]} -gt 0 ]] || { echo "Missing Windows installer artifact" >&2; exit 1; } | |
| ;; | |
| esac | |
| - 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 | |
| - name: Upload desktop artifact bundle | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: desktop-${{ matrix.platform }}-${{ matrix.arch }} | |
| path: release-publish/* | |
| if-no-files-found: error | |
| ios_signing_preflight: | |
| name: iOS signing preflight | |
| needs: [preflight] | |
| runs-on: ubuntu-24.04 | |
| outputs: | |
| enabled: ${{ steps.check.outputs.enabled }} | |
| missing: ${{ steps.check.outputs.missing }} | |
| steps: | |
| - id: check | |
| name: Check iOS signing secrets | |
| shell: bash | |
| env: | |
| IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }} | |
| IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} | |
| IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }} | |
| IOS_PROVISIONING_PROFILE_NAME: ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| run: | | |
| set -euo pipefail | |
| required_secrets=( | |
| IOS_CERTIFICATE_P12 | |
| IOS_CERTIFICATE_PASSWORD | |
| IOS_PROVISIONING_PROFILE | |
| IOS_PROVISIONING_PROFILE_NAME | |
| APPLE_TEAM_ID | |
| APPLE_API_KEY | |
| APPLE_API_KEY_ID | |
| APPLE_API_ISSUER | |
| ) | |
| missing=() | |
| for secret_name in "${required_secrets[@]}"; do | |
| if [[ -z "${!secret_name}" ]]; then | |
| missing+=("$secret_name") | |
| fi | |
| done | |
| if (( ${#missing[@]} == 0 )); then | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| echo "missing=" >> "$GITHUB_OUTPUT" | |
| echo "All required iOS signing secrets are configured." | |
| else | |
| missing_csv="$(IFS=,; echo "${missing[*]}")" | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "missing=$missing_csv" >> "$GITHUB_OUTPUT" | |
| echo "Skipping iOS TestFlight because the following required secrets are missing: $missing_csv" | |
| fi | |
| ios_testflight: | |
| name: iOS TestFlight | |
| needs: [preflight, ios_signing_preflight] | |
| if: ${{ needs.ios_signing_preflight.outputs.enabled == 'true' }} | |
| runs-on: macos-14 | |
| env: | |
| RELEASE_VERSION: ${{ needs.preflight.outputs.version }} | |
| OKCODE_COMMIT_HASH: ${{ github.sha }} | |
| OKCODE_BUILD_TIMESTAMP: ${{ needs.preflight.outputs.build_timestamp }} | |
| OKCODE_RELEASE_CHANNEL: ${{ needs.preflight.outputs.release_channel }} | |
| 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: Log iOS build metadata | |
| run: | | |
| echo "version=$RELEASE_VERSION" | |
| echo "commit=$OKCODE_COMMIT_HASH" | |
| echo "build_timestamp=$OKCODE_BUILD_TIMESTAMP" | |
| echo "channel=$OKCODE_RELEASE_CHANNEL" | |
| - 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 | |
| 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 | |
| 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" | |
| 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" | |
| 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: Simulator smoke build | |
| run: | | |
| set -euo pipefail | |
| xcodebuild build \ | |
| -project apps/mobile/ios/App/App.xcodeproj \ | |
| -scheme App \ | |
| -configuration Debug \ | |
| -destination 'platform=iOS Simulator,name=iPhone 15' \ | |
| COMPILER_INDEX_STORE_ENABLE=NO | |
| - 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 | |
| exit 1 | |
| fi | |
| xcrun altool --upload-app \ | |
| -f "$IPA_FILE" \ | |
| -t ios \ | |
| --apiKey "$APPLE_API_KEY_ID" \ | |
| --apiIssuer "$APPLE_API_ISSUER" | |
| - 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 | |
| publish_cli: | |
| name: Publish CLI | |
| needs: [preflight, finalize] | |
| if: ${{ !cancelled() && needs.finalize.result == 'success' && github.event_name == 'workflow_dispatch' && inputs.publish_cli }} | |
| continue-on-error: true | |
| runs-on: ubuntu-24.04 | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} | |
| OKCODE_COMMIT_HASH: ${{ github.sha }} | |
| OKCODE_BUILD_TIMESTAMP: ${{ needs.preflight.outputs.build_timestamp }} | |
| OKCODE_RELEASE_CHANNEL: ${{ needs.preflight.outputs.release_channel }} | |
| steps: | |
| - name: Checkout finalized main | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| 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 | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Assert package versions match stated release version | |
| env: | |
| RELEASE_VERSION: ${{ needs.preflight.outputs.version }} | |
| run: | | |
| node <<'NODE' | |
| const fs = require('fs'); | |
| const files = [ | |
| 'apps/server/package.json', | |
| 'apps/desktop/package.json', | |
| 'apps/web/package.json', | |
| 'apps/mobile/package.json', | |
| 'packages/contracts/package.json', | |
| ]; | |
| const expected = process.env.RELEASE_VERSION; | |
| const mismatches = files.filter((file) => { | |
| const version = JSON.parse(fs.readFileSync(file, 'utf8')).version; | |
| return version !== expected; | |
| }); | |
| if (mismatches.length > 0) { | |
| console.error(`Package version mismatch for release ${expected}: ${mismatches.join(', ')}`); | |
| process.exit(1); | |
| } | |
| NODE | |
| - name: Build CLI package | |
| run: bun run build --filter=@okcode/web --filter=okcodes | |
| - name: Verify npm pack | |
| working-directory: apps/server | |
| run: npm pack | |
| - name: Verify local CLI entrypoints | |
| working-directory: apps/server | |
| run: | | |
| node dist/index.mjs --version | |
| node dist/index.mjs --help >/dev/null | |
| node dist/index.mjs doctor --help >/dev/null | |
| - name: Publish okcodes | |
| run: > | |
| node apps/server/scripts/cli.ts publish | |
| --tag "${{ needs.preflight.outputs.npm_tag }}" | |
| --app-version "${{ needs.preflight.outputs.version }}" | |
| --verbose | |
| - name: Verify published CLI | |
| run: | | |
| npx --yes okcodes@${{ needs.preflight.outputs.version }} --version | |
| npx --yes okcodes@${{ needs.preflight.outputs.version }} --help >/dev/null | |
| release: | |
| name: Publish GitHub Release | |
| needs: [preflight, desktop_build, ios_signing_preflight, ios_testflight] | |
| if: ${{ always() && !cancelled() && needs.preflight.result == 'success' && needs.desktop_build.result == 'success' && needs.ios_signing_preflight.result == 'success' && (needs.ios_testflight.result == 'success' || needs.ios_testflight.result == 'skipped') }} | |
| 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 desktop artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: desktop-* | |
| merge-multiple: true | |
| path: release-assets | |
| - name: Stage release documentation | |
| env: | |
| RELEASE_VERSION: ${{ needs.preflight.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| notes="docs/releases/v${RELEASE_VERSION}.md" | |
| manifest="docs/releases/v${RELEASE_VERSION}/assets.md" | |
| [[ -f "$notes" ]] || { echo "Missing release notes: $notes" >&2; exit 1; } | |
| [[ -f "$manifest" ]] || { echo "Missing asset manifest: $manifest" >&2; exit 1; } | |
| 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] | |
| if: ${{ !cancelled() && needs.release.result == 'success' }} | |
| 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 apps/mobile/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 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 apps/mobile/package.json packages/contracts/package.json bun.lock | |
| git commit -m "chore(release): prepare $RELEASE_TAG" | |
| git push origin HEAD:main |