name: PreRelease App on: push: branches: - dev - feat/windows-sidecar permissions: contents: write concurrency: group: prerelease-${{ github.ref }} cancel-in-progress: true jobs: prepare-release: name: Prepare Prerelease runs-on: blacksmith-4vcpu-ubuntu-2404 outputs: release_tag: ${{ steps.prerelease-meta.outputs.release_tag }} release_name: ${{ steps.prerelease-meta.outputs.release_name }} release_body: ${{ steps.prerelease-meta.outputs.release_body }} steps: - name: Set prerelease metadata id: prerelease-meta shell: bash run: | set -euo pipefail short_sha=$(echo "$GITHUB_SHA" | cut -c1-7) tag="v${short_sha}-dev" name="OpenWork ${tag}" body="Automated prerelease from ${GITHUB_REF_NAME} (${GITHUB_SHA})." echo "RELEASE_TAG=$tag" >> "$GITHUB_ENV" echo "RELEASE_NAME=$name" >> "$GITHUB_ENV" { echo "RELEASE_BODY<<__OPENWORK_RELEASE_BODY_EOF__" echo "$body" echo "__OPENWORK_RELEASE_BODY_EOF__" } >> "$GITHUB_ENV" echo "release_tag=$tag" >> "$GITHUB_OUTPUT" echo "release_name=$name" >> "$GITHUB_OUTPUT" { echo "release_body<<__OPENWORK_RELEASE_BODY_EOF__" echo "$body" echo "__OPENWORK_RELEASE_BODY_EOF__" } >> "$GITHUB_OUTPUT" - name: Create prerelease shell: bash env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail BODY_FILE="$RUNNER_TEMP/release_body.md" printf '%s\n' "$RELEASE_BODY" > "$BODY_FILE" if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "Prerelease $RELEASE_TAG already exists; skipping create." exit 0 fi gh release create "$RELEASE_TAG" \ --repo "$GITHUB_REPOSITORY" \ --title "$RELEASE_NAME" \ --notes-file "$BODY_FILE" \ --prerelease publish-tauri: name: Build + Publish (${{ matrix.target }}) needs: prepare-release # Set OPENWORK_LINUX_X64_RUNNER_LABEL to route only the Linux x86_64 build to a larger runner. runs-on: ${{ matrix.target == 'x86_64-unknown-linux-gnu' && vars.OPENWORK_LINUX_X64_RUNNER_LABEL != '' && vars.OPENWORK_LINUX_X64_RUNNER_LABEL || matrix.platform }} timeout-minutes: 360 env: RELEASE_TAG: ${{ needs.prepare-release.outputs.release_tag }} RELEASE_NAME: ${{ needs.prepare-release.outputs.release_name }} RELEASE_BODY: ${{ needs.prepare-release.outputs.release_body }} MACOS_NOTARIZE: ${{ vars.MACOS_NOTARIZE || 'false' }} OPENCODE_GITHUB_REPO: ${{ vars.OPENCODE_GITHUB_REPO || 'anomalyco/opencode' }} OPENCODE_VERSION: ${{ vars.OPENCODE_VERSION || '1.2.20' }} strategy: fail-fast: false matrix: include: - platform: macos-14 os_type: macos target: aarch64-apple-darwin args: "--target aarch64-apple-darwin --bundles dmg,app" - platform: macos-14 os_type: macos target: x86_64-apple-darwin args: "--target x86_64-apple-darwin --bundles dmg,app" - platform: ubuntu-22.04 os_type: linux target: x86_64-unknown-linux-gnu args: "--target x86_64-unknown-linux-gnu --bundles deb,rpm" - platform: ubuntu-22.04-arm os_type: linux target: aarch64-unknown-linux-gnu args: "--target aarch64-unknown-linux-gnu --bundles deb,rpm" - platform: windows-2022 os_type: windows target: x86_64-pc-windows-msvc args: "--target x86_64-pc-windows-msvc --bundles msi" steps: - name: Log runner selection shell: bash run: | echo "Requested larger runner label: ${RUNNER_LABEL:-}" echo "Effective runs-on: ${EFFECTIVE_RUNS_ON}" env: RUNNER_LABEL: ${{ vars.OPENWORK_LINUX_X64_RUNNER_LABEL }} EFFECTIVE_RUNS_ON: ${{ matrix.target == 'x86_64-unknown-linux-gnu' && vars.OPENWORK_LINUX_X64_RUNNER_LABEL != '' && vars.OPENWORK_LINUX_X64_RUNNER_LABEL || matrix.platform }} - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.sha }} - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10.27.0 - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: "1.3.6" - name: Install dependencies run: pnpm install --frozen-lockfile - name: Install OpenTUI x64 core (macOS x86_64) if: matrix.os_type == 'macos' && matrix.target == 'x86_64-apple-darwin' run: pnpm add -w --ignore-workspace-root-check @opentui/core-darwin-x64@0.1.77 - name: Install Linux build dependencies if: matrix.os_type == 'linux' run: | sudo apt-get update sudo apt-get install -y \ libgtk-3-dev \ libglib2.0-dev \ libayatana-appindicator3-dev \ libsoup-3.0-dev \ libwebkit2gtk-4.1-dev \ libssl-dev \ rpm \ libdbus-1-dev \ librsvg2-dev - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Resolve OpenCode version id: opencode-version shell: bash env: GITHUB_TOKEN: ${{ github.token }} run: | node <<'NODE' >> "$GITHUB_OUTPUT" const fs = require('fs'); const repo = (process.env.OPENCODE_GITHUB_REPO || 'anomalyco/opencode').trim() || 'anomalyco/opencode'; async function resolveLatest() { const token = (process.env.GITHUB_TOKEN || '').trim(); const headers = { 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'openwork-ci', }; if (token) headers.Authorization = `Bearer ${token}`; // Prefer API, but fall back to the web "latest" redirect if rate-limited (403) or otherwise blocked. try { const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { headers }); if (res.ok) { const data = await res.json(); const tag = (typeof data.tag_name === 'string' ? data.tag_name : '').trim(); let v = tag.startsWith('v') ? tag.slice(1) : tag; v = v.trim(); if (v) return v; } if (res.status !== 403) { throw new Error(`Failed to resolve latest OpenCode version (HTTP ${res.status})`); } } catch { // continue to fallback } const web = await fetch(`https://github.com/${repo}/releases/latest`, { headers: { 'User-Agent': 'openwork-ci' }, redirect: 'follow', }); const url = (web && web.url) ? String(web.url) : ''; const match = url.match(/\/tag\/v([^/?#]+)/); if (!match) throw new Error('Failed to resolve latest OpenCode version (web redirect).'); return match[1]; } async function main() { const pkg = JSON.parse(fs.readFileSync('./apps/desktop/package.json', 'utf8')); const configuredRaw = (process.env.OPENCODE_VERSION || pkg.opencodeVersion || '').toString().trim(); if (configuredRaw && configuredRaw.toLowerCase() !== 'latest') { const normalized = configuredRaw.startsWith('v') ? configuredRaw.slice(1) : configuredRaw; const resolved = normalized.trim(); if (process.env.GITHUB_ENV) { fs.appendFileSync(process.env.GITHUB_ENV, `OPENCODE_VERSION=${resolved}\n`); } console.log('version=' + resolved); return; } const latest = await resolveLatest(); if (process.env.GITHUB_ENV) { fs.appendFileSync(process.env.GITHUB_ENV, `OPENCODE_VERSION=${latest}\n`); } console.log('version=' + latest); } main().catch((err) => { console.error(err); process.exit(1); }); NODE - name: Download OpenCode sidecar shell: bash env: OPENCODE_VERSION: ${{ steps.opencode-version.outputs.version }} run: | set -euo pipefail case "${{ matrix.target }}" in aarch64-apple-darwin) opencode_asset="opencode-darwin-arm64.zip" ;; x86_64-apple-darwin) opencode_asset="opencode-darwin-x64-baseline.zip" ;; x86_64-unknown-linux-gnu) opencode_asset="opencode-linux-x64-baseline.tar.gz" ;; aarch64-unknown-linux-gnu) opencode_asset="opencode-linux-arm64.tar.gz" ;; x86_64-pc-windows-msvc) opencode_asset="opencode-windows-x64-baseline.zip" ;; *) echo "Unsupported target: ${{ matrix.target }}" exit 1 ;; esac repo="${OPENCODE_GITHUB_REPO:-anomalyco/opencode}" url="https://github.com/${repo}/releases/download/v${OPENCODE_VERSION}/${opencode_asset}" tmp_dir="$RUNNER_TEMP/opencode" extract_dir="$tmp_dir/extracted" rm -rf "$tmp_dir" mkdir -p "$extract_dir" curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 -o "$tmp_dir/$opencode_asset" "$url" if [[ "$opencode_asset" == *.tar.gz ]]; then tar -xzf "$tmp_dir/$opencode_asset" -C "$extract_dir" else if command -v unzip >/dev/null 2>&1; then unzip -q "$tmp_dir/$opencode_asset" -d "$extract_dir" elif command -v 7z >/dev/null 2>&1; then 7z x "$tmp_dir/$opencode_asset" -o"$extract_dir" >/dev/null else echo "No unzip utility available" exit 1 fi fi if [ -f "$extract_dir/opencode" ]; then bin_path="$extract_dir/opencode" elif [ -f "$extract_dir/opencode.exe" ]; then bin_path="$extract_dir/opencode.exe" else echo "OpenCode binary not found in archive" ls -la "$extract_dir" exit 1 fi target_name="opencode-${{ matrix.target }}" if [ "${{ matrix.os_type }}" = "windows" ]; then target_name="${target_name}.exe" fi mkdir -p apps/desktop/src-tauri/sidecars cp "$bin_path" "apps/desktop/src-tauri/sidecars/${target_name}" chmod 755 "apps/desktop/src-tauri/sidecars/${target_name}" - name: Write notary API key if: matrix.os_type == 'macos' && env.MACOS_NOTARIZE == 'true' env: APPLE_NOTARY_API_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }} run: | set -euo pipefail NOTARY_KEY_PATH="$RUNNER_TEMP/AuthKey.p8" printf '%s' "$APPLE_NOTARY_API_KEY_P8_BASE64" | base64 --decode > "$NOTARY_KEY_PATH" chmod 600 "$NOTARY_KEY_PATH" echo "NOTARY_KEY_PATH=$NOTARY_KEY_PATH" >> "$GITHUB_ENV" - name: Build + upload (notarized) if: matrix.os_type == 'macos' && env.MACOS_NOTARIZE == 'true' uses: tauri-apps/tauri-action@v0.5.17 env: CI: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Tauri updater signing TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} # macOS signing APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CODESIGN_CERT_P12_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }} # macOS notarization (App Store Connect API key) APPLE_API_KEY: ${{ secrets.APPLE_NOTARY_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_NOTARY_API_ISSUER_ID }} APPLE_API_KEY_PATH: ${{ env.NOTARY_KEY_PATH }} with: tagName: ${{ env.RELEASE_TAG }} releaseName: ${{ env.RELEASE_NAME }} releaseBody: ${{ env.RELEASE_BODY }} prerelease: true releaseDraft: false projectPath: apps/desktop tauriScript: pnpm exec tauri -vvv args: ${{ matrix.args }} retryAttempts: 3 - name: Build + upload if: matrix.os_type != 'macos' || env.MACOS_NOTARIZE != 'true' uses: tauri-apps/tauri-action@v0.5.17 env: CI: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Tauri updater signing TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} # macOS signing APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CODESIGN_CERT_P12_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }} with: tagName: ${{ env.RELEASE_TAG }} releaseName: ${{ env.RELEASE_NAME }} releaseBody: ${{ env.RELEASE_BODY }} prerelease: true releaseDraft: false projectPath: apps/desktop tauriScript: pnpm exec tauri -vvv args: ${{ matrix.args }} retryAttempts: 3