# Release version 0.21.9 #178
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*.*.*" | |
| env: | |
| RUST_BACKTRACE: short | |
| HUSKY: 0 | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| create-draft-release: | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: write | |
| outputs: | |
| release_id: ${{ steps.release.outputs.release_id }} | |
| release_url: ${{ steps.release.outputs.release_url }} | |
| prerelease: ${{ steps.meta.outputs.prerelease }} | |
| release_body: ${{ steps.release.outputs.release_body }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - id: meta | |
| name: Resolve release channel | |
| shell: bash | |
| run: | | |
| if [[ "${GITHUB_REF_NAME}" == *"-rc"* ]]; then | |
| echo "prerelease=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "prerelease=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Check tag commit belongs to default branch | |
| shell: bash | |
| run: | | |
| DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" | |
| git fetch origin "${DEFAULT_BRANCH}" | |
| TAG_COMMIT=$(git rev-list -n 1 "${GITHUB_REF_NAME}") | |
| if git merge-base --is-ancestor "${TAG_COMMIT}" "origin/${DEFAULT_BRANCH}"; then | |
| echo "Tag ${GITHUB_REF_NAME} is based on ${DEFAULT_BRANCH}" | |
| else | |
| echo "Tag ${GITHUB_REF_NAME} is not based on ${DEFAULT_BRANCH}" | |
| exit 1 | |
| fi | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - name: Verify tag matches app version | |
| shell: bash | |
| run: | | |
| APP_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version") | |
| if [[ "v$APP_VERSION" != "${GITHUB_REF_NAME}" ]]; then | |
| echo "Tag ${GITHUB_REF_NAME} does not match app version v$APP_VERSION" | |
| exit 1 | |
| fi | |
| - id: release | |
| name: Create or reuse draft release | |
| uses: actions/github-script@v7 | |
| env: | |
| PRERELEASE: ${{ steps.meta.outputs.prerelease }} | |
| with: | |
| script: | | |
| const tag = context.ref.replace("refs/tags/", ""); | |
| const { owner, repo } = context.repo; | |
| const prerelease = process.env.PRERELEASE === "true"; | |
| const releaseName = `codeg ${tag}`; | |
| const { data: tagRef } = await github.rest.git.getRef({ | |
| owner, | |
| repo, | |
| ref: `tags/${tag}`, | |
| }); | |
| let commitSha = tagRef.object.sha; | |
| if (tagRef.object.type === "tag") { | |
| const { data: annotatedTag } = await github.rest.git.getTag({ | |
| owner, | |
| repo, | |
| tag_sha: commitSha, | |
| }); | |
| if (annotatedTag.object.type !== "commit") { | |
| core.setFailed( | |
| `Tag ${tag} points to ${annotatedTag.object.type}, not a commit.`, | |
| ); | |
| return; | |
| } | |
| commitSha = annotatedTag.object.sha; | |
| } | |
| const { data: commit } = await github.rest.repos.getCommit({ | |
| owner, | |
| repo, | |
| ref: commitSha, | |
| }); | |
| const releaseBody = | |
| commit.commit.message?.trim() || "_No commit message._"; | |
| let release; | |
| try { | |
| const existing = await github.rest.repos.getReleaseByTag({ | |
| owner, | |
| repo, | |
| tag, | |
| }); | |
| if (!existing.data.draft) { | |
| core.setFailed( | |
| `Release for tag ${tag} already exists and is not a draft.`, | |
| ); | |
| return; | |
| } | |
| release = existing.data; | |
| if ( | |
| release.prerelease !== prerelease || | |
| release.name !== releaseName || | |
| (release.body ?? "").trim() !== releaseBody | |
| ) { | |
| const updated = await github.rest.repos.updateRelease({ | |
| owner, | |
| repo, | |
| release_id: release.id, | |
| name: releaseName, | |
| prerelease, | |
| body: releaseBody, | |
| }); | |
| release = updated.data; | |
| } | |
| core.info(`Reusing existing draft release #${release.id}`); | |
| } catch (error) { | |
| if (error.status !== 404) { | |
| throw error; | |
| } | |
| const created = await github.rest.repos.createRelease({ | |
| owner, | |
| repo, | |
| tag_name: tag, | |
| name: releaseName, | |
| body: releaseBody, | |
| draft: true, | |
| prerelease, | |
| }); | |
| release = created.data; | |
| core.info(`Created draft release #${release.id}`); | |
| } | |
| core.setOutput("release_id", String(release.id)); | |
| core.setOutput("release_url", release.html_url); | |
| core.setOutput("release_body", releaseBody); | |
| build-tauri: | |
| needs: create-draft-release | |
| name: Build ${{ matrix.name }} | |
| permissions: | |
| contents: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: "macOS x64" | |
| runner: "macos-latest" | |
| target: "x86_64-apple-darwin" | |
| - name: "macOS arm64" | |
| runner: "macos-latest" | |
| target: "aarch64-apple-darwin" | |
| - name: "Linux x64" | |
| runner: "ubuntu-22.04" | |
| target: "x86_64-unknown-linux-gnu" | |
| - name: "Linux arm64" | |
| runner: "ubuntu-22.04" | |
| target: "aarch64-unknown-linux-gnu" | |
| - name: "Windows x64" | |
| runner: "windows-2022" | |
| target: "x86_64-pc-windows-msvc" | |
| - name: "Windows arm64" | |
| runner: "windows-latest" | |
| target: "aarch64-pc-windows-msvc" | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| run_install: false | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: "pnpm" | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Rust cache | |
| uses: swatinem/rust-cache@v2 | |
| with: | |
| workspaces: "./src-tauri -> target" | |
| shared-key: ${{ matrix.target }} | |
| - name: Install Linux x64 dependencies | |
| if: matrix.target == 'x86_64-unknown-linux-gnu' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libwebkit2gtk-4.1-dev \ | |
| libayatana-appindicator3-dev \ | |
| librsvg2-dev \ | |
| patchelf | |
| - name: Install Linux arm64 cross dependencies | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| run: | | |
| cat > /tmp/sources.list << 'EOF' | |
| deb [arch=amd64] http://archive.ubuntu.com/ubuntu jammy main restricted universe multiverse | |
| deb [arch=amd64] http://archive.ubuntu.com/ubuntu jammy-updates main restricted universe multiverse | |
| deb [arch=amd64] http://archive.ubuntu.com/ubuntu jammy-backports main restricted universe multiverse | |
| deb [arch=amd64] http://security.ubuntu.com/ubuntu jammy-security main restricted universe multiverse | |
| deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse | |
| deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse | |
| deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse | |
| deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse | |
| EOF | |
| sudo mv /etc/apt/sources.list /etc/apt/sources.list.default | |
| sudo mv /tmp/sources.list /etc/apt/sources.list | |
| sudo dpkg --add-architecture arm64 | |
| sudo apt-get update | |
| # libgraphite2-3 / libharfbuzz0b / libfreetype6 / libssl3 are | |
| # Multi-Arch: same — their :arm64 and :amd64 copies must be the | |
| # EXACT same version. The runner ships stale :amd64 copies, and a | |
| # jammy security bump (e.g. graphite2 1.3.14-1ubuntu0.1) made the | |
| # :arm64 webkit dev chain demand a newer version than the preinstalled | |
| # :amd64. apt then refuses the :arm64 copy ("not going to be | |
| # installed" / held broken packages). Upgrading the preinstalled | |
| # :amd64 copies first lets both arches land on the same version. | |
| # --only-upgrade never installs new packages, so any lib that is | |
| # absent is simply skipped. | |
| sudo apt-get install -y --only-upgrade \ | |
| libgraphite2-3 \ | |
| libharfbuzz0b \ | |
| libfreetype6 \ | |
| libssl3 | |
| sudo apt-get install -y \ | |
| gcc-aarch64-linux-gnu \ | |
| g++-aarch64-linux-gnu \ | |
| libwebkit2gtk-4.1-dev:arm64 \ | |
| libayatana-appindicator3-dev:arm64 \ | |
| librsvg2-dev:arm64 \ | |
| libssl-dev:arm64 \ | |
| patchelf | |
| - name: Configure Linux arm64 cross env | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| shell: bash | |
| run: | | |
| echo "PKG_CONFIG_ALLOW_CROSS=1" >> "$GITHUB_ENV" | |
| echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig" >> "$GITHUB_ENV" | |
| echo "PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig" >> "$GITHUB_ENV" | |
| echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu" >> "$GITHUB_ENV" | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV" | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_AR=aarch64-linux-gnu-gcc-ar" >> "$GITHUB_ENV" | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=-Clinker=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV" | |
| echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV" | |
| echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> "$GITHUB_ENV" | |
| echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc-ar" >> "$GITHUB_ENV" | |
| - name: Verify Linux arm64 cross toolchain | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| shell: bash | |
| run: | | |
| which aarch64-linux-gnu-gcc | |
| aarch64-linux-gnu-gcc -v | |
| rustup target list --installed | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=${CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER}" | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_AR=${CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_AR}" | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=${CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS}" | |
| - name: Install frontend dependencies | |
| run: pnpm install --frozen-lockfile | |
| # Build the `codeg-mcp` companion binary for the matrix target and | |
| # stage it as a Tauri sidecar at | |
| # `src-tauri/binaries/codeg-mcp-<triple>{.exe}`. `tauri-action` (below) | |
| # then bundles it via the `bundle.externalBin` entry in | |
| # `tauri.conf.json` — Tauri installs it next to the main executable | |
| # (Contents/MacOS on macOS, install root on Linux/Windows) where the | |
| # runtime `locate_codeg_mcp_binary()` finds it via `current_exe` | |
| # sibling lookup. The Linux arm64 cross env vars set above also | |
| # apply to this cargo invocation. | |
| - name: Stage codeg-mcp sidecar for Tauri bundle | |
| shell: bash | |
| run: pnpm tauri:prepare-sidecars --target ${{ matrix.target }} | |
| - name: Verify codeg-mcp sidecar landed | |
| shell: bash | |
| run: | | |
| ext="" | |
| case "${{ matrix.target }}" in | |
| *windows*) ext=".exe" ;; | |
| esac | |
| file="src-tauri/binaries/codeg-mcp-${{ matrix.target }}${ext}" | |
| if [ ! -f "$file" ]; then | |
| echo "FATAL: sidecar $file missing after prepare-sidecars" | |
| exit 1 | |
| fi | |
| ls -la "$file" | |
| - name: Import Apple Developer ID certificate | |
| if: contains(matrix.target, 'apple-darwin') | |
| shell: bash | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| missing=() | |
| for name in \ | |
| APPLE_CERTIFICATE \ | |
| APPLE_CERTIFICATE_PASSWORD \ | |
| KEYCHAIN_PASSWORD \ | |
| APPLE_ID \ | |
| APPLE_PASSWORD \ | |
| APPLE_TEAM_ID; do | |
| if [ -z "${!name:-}" ]; then | |
| missing+=("$name") | |
| fi | |
| done | |
| if [ "${#missing[@]}" -ne 0 ]; then | |
| printf '::error::Missing required Apple signing secret(s): %s\n' "${missing[*]}" | |
| exit 1 | |
| fi | |
| keychain_path="$RUNNER_TEMP/codeg-signing.keychain-db" | |
| certificate_path="$RUNNER_TEMP/developer-id.p12" | |
| keychain_search_list_path="$RUNNER_TEMP/codeg-keychains.txt" | |
| original_default_keychain="$( | |
| security default-keychain -d user \ | |
| | sed -e 's/^[[:space:]]*//' -e 's/^"//' -e 's/"$//' | |
| )" | |
| security list-keychains -d user \ | |
| | sed -e 's/^[[:space:]]*//' -e 's/^"//' -e 's/"$//' \ | |
| > "$keychain_search_list_path" | |
| echo "APPLE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV" | |
| echo "APPLE_CERTIFICATE_PATH=$certificate_path" >> "$GITHUB_ENV" | |
| echo "APPLE_KEYCHAIN_SEARCH_LIST_PATH=$keychain_search_list_path" >> "$GITHUB_ENV" | |
| echo "APPLE_ORIGINAL_DEFAULT_KEYCHAIN=$original_default_keychain" >> "$GITHUB_ENV" | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > "$certificate_path" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" | |
| existing_keychains=() | |
| while IFS= read -r existing_keychain; do | |
| if [ -n "$existing_keychain" ]; then | |
| existing_keychains+=("$existing_keychain") | |
| fi | |
| done < "$keychain_search_list_path" | |
| if [ "${#existing_keychains[@]}" -eq 0 ]; then | |
| security list-keychains -d user -s "$keychain_path" | |
| else | |
| security list-keychains -d user -s "$keychain_path" "${existing_keychains[@]}" | |
| fi | |
| security default-keychain -s "$keychain_path" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" | |
| security set-keychain-settings -t 21600 -u "$keychain_path" | |
| security import "$certificate_path" \ | |
| -k "$keychain_path" \ | |
| -P "$APPLE_CERTIFICATE_PASSWORD" \ | |
| -T /usr/bin/codesign \ | |
| -T /usr/bin/productbuild | |
| security set-key-partition-list \ | |
| -S apple-tool:,apple:,codesign: \ | |
| -s \ | |
| -k "$KEYCHAIN_PASSWORD" \ | |
| "$keychain_path" | |
| security find-identity -v -p codesigning "$keychain_path" | |
| - name: Resolve Apple Developer ID identity | |
| if: contains(matrix.target, 'apple-darwin') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cert_info="$( | |
| security find-identity -v -p codesigning "$APPLE_KEYCHAIN_PATH" \ | |
| | grep "Developer ID Application" \ | |
| | head -n 1 \ | |
| || true | |
| )" | |
| if [ -z "$cert_info" ]; then | |
| echo "::error::No Developer ID Application signing identity found in imported keychain." | |
| security find-identity -v -p codesigning "$APPLE_KEYCHAIN_PATH" || true | |
| exit 1 | |
| fi | |
| cert_id="$(echo "$cert_info" | awk -F'"' '{print $2}')" | |
| echo "APPLE_SIGNING_IDENTITY=$cert_id" >> "$GITHUB_ENV" | |
| echo "Using Apple signing identity: $cert_id" | |
| - name: Build and upload to draft release (Linux arm64) | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| uses: tauri-apps/tauri-action@v0.6.1 | |
| env: | |
| # The matrix-target sidecar is already staged and verified above. | |
| # Skip the duplicate beforeBuildCommand sidecar pass so cross builds | |
| # do not fall back to the runner host triple. | |
| CODEG_SKIP_SIDECAR: "1" | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| PKG_CONFIG_ALLOW_CROSS: 1 | |
| PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig | |
| PKG_CONFIG_LIBDIR: /usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig | |
| PKG_CONFIG_SYSROOT_DIR: /usr/aarch64-linux-gnu | |
| CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc | |
| CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_AR: aarch64-linux-gnu-gcc-ar | |
| CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS: -Clinker=aarch64-linux-gnu-gcc | |
| CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc | |
| CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++ | |
| AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc-ar | |
| with: | |
| releaseId: ${{ needs.create-draft-release.outputs.release_id }} | |
| tagName: ${{ github.ref_name }} | |
| releaseBody: ${{ needs.create-draft-release.outputs.release_body }} | |
| releaseDraft: true | |
| prerelease: ${{ needs.create-draft-release.outputs.prerelease }} | |
| tauriScript: pnpm tauri | |
| args: --target ${{ matrix.target }} --bundles deb,rpm | |
| includeUpdaterJson: false | |
| retryAttempts: 2 | |
| - name: Build and upload to draft release (Windows) | |
| if: contains(matrix.target, 'windows') | |
| uses: tauri-apps/tauri-action@v0.6.1 | |
| env: | |
| # The matrix-target sidecar is already staged and verified above. | |
| # Skip the duplicate beforeBuildCommand sidecar pass so cross builds | |
| # do not fall back to the runner host triple. | |
| CODEG_SKIP_SIDECAR: "1" | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| with: | |
| releaseId: ${{ needs.create-draft-release.outputs.release_id }} | |
| tagName: ${{ github.ref_name }} | |
| releaseBody: ${{ needs.create-draft-release.outputs.release_body }} | |
| releaseDraft: true | |
| prerelease: ${{ needs.create-draft-release.outputs.prerelease }} | |
| tauriScript: pnpm tauri | |
| # Skip MSI bundling on Windows: tauri-apps/tauri#14681 — light.exe | |
| # fails when externalBin is configured. NSIS installer + updater | |
| # artifacts cover the same distribution channel. | |
| args: --target ${{ matrix.target }} --bundles nsis,updater | |
| includeUpdaterJson: true | |
| retryAttempts: 2 | |
| - name: Build and upload to draft release (Linux x64) | |
| if: matrix.target == 'x86_64-unknown-linux-gnu' | |
| uses: tauri-apps/tauri-action@v0.6.1 | |
| env: | |
| # The matrix-target sidecar is already staged and verified above. | |
| # Skip the duplicate beforeBuildCommand sidecar pass so cross builds | |
| # do not fall back to the runner host triple. | |
| CODEG_SKIP_SIDECAR: "1" | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| with: | |
| releaseId: ${{ needs.create-draft-release.outputs.release_id }} | |
| tagName: ${{ github.ref_name }} | |
| releaseBody: ${{ needs.create-draft-release.outputs.release_body }} | |
| releaseDraft: true | |
| prerelease: ${{ needs.create-draft-release.outputs.prerelease }} | |
| tauriScript: pnpm tauri | |
| args: --target ${{ matrix.target }} | |
| includeUpdaterJson: true | |
| retryAttempts: 2 | |
| - name: Build, sign, and upload to draft release (macOS) | |
| if: contains(matrix.target, 'apple-darwin') | |
| uses: tauri-apps/tauri-action@v0.6.1 | |
| env: | |
| # The matrix-target sidecar is already staged and verified above. | |
| # Skip the duplicate beforeBuildCommand sidecar pass so cross builds | |
| # do not fall back to the runner host triple. | |
| CODEG_SKIP_SIDECAR: "1" | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| # Tauri notarizes the signed bundle when these three env vars are | |
| # present (in addition to the imported Developer ID cert + | |
| # APPLE_SIGNING_IDENTITY used for code signing). | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| with: | |
| releaseId: ${{ needs.create-draft-release.outputs.release_id }} | |
| tagName: ${{ github.ref_name }} | |
| releaseBody: ${{ needs.create-draft-release.outputs.release_body }} | |
| releaseDraft: true | |
| prerelease: ${{ needs.create-draft-release.outputs.prerelease }} | |
| tauriScript: pnpm tauri | |
| args: --target ${{ matrix.target }} | |
| includeUpdaterJson: true | |
| retryAttempts: 2 | |
| - name: Delete Apple signing keychain | |
| if: always() && contains(matrix.target, 'apple-darwin') | |
| shell: bash | |
| run: | | |
| if [ -n "${APPLE_ORIGINAL_DEFAULT_KEYCHAIN:-}" ]; then | |
| security default-keychain -s "$APPLE_ORIGINAL_DEFAULT_KEYCHAIN" || true | |
| fi | |
| if [ -n "${APPLE_KEYCHAIN_SEARCH_LIST_PATH:-}" ] && \ | |
| [ -f "$APPLE_KEYCHAIN_SEARCH_LIST_PATH" ]; then | |
| restored_keychains=() | |
| while IFS= read -r restored_keychain; do | |
| if [ -n "$restored_keychain" ]; then | |
| restored_keychains+=("$restored_keychain") | |
| fi | |
| done < "$APPLE_KEYCHAIN_SEARCH_LIST_PATH" | |
| if [ "${#restored_keychains[@]}" -ne 0 ]; then | |
| security list-keychains -d user -s "${restored_keychains[@]}" || true | |
| fi | |
| fi | |
| if [ -n "${APPLE_KEYCHAIN_PATH:-}" ]; then | |
| security delete-keychain "$APPLE_KEYCHAIN_PATH" || true | |
| fi | |
| for path in \ | |
| "${APPLE_CERTIFICATE_PATH:-}" \ | |
| "${APPLE_KEYCHAIN_SEARCH_LIST_PATH:-}"; do | |
| if [ -n "$path" ]; then | |
| rm -f "$path" | |
| fi | |
| done | |
| build-server: | |
| needs: create-draft-release | |
| name: Server ${{ matrix.name }} | |
| permissions: | |
| contents: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: "Linux x64" | |
| runner: "ubuntu-22.04" | |
| target: "x86_64-unknown-linux-gnu" | |
| artifact: "codeg-server-linux-x64" | |
| - name: "Linux arm64" | |
| runner: "ubuntu-22.04" | |
| target: "aarch64-unknown-linux-gnu" | |
| artifact: "codeg-server-linux-arm64" | |
| - name: "macOS x64" | |
| runner: "macos-latest" | |
| target: "x86_64-apple-darwin" | |
| artifact: "codeg-server-darwin-x64" | |
| - name: "macOS arm64" | |
| runner: "macos-latest" | |
| target: "aarch64-apple-darwin" | |
| artifact: "codeg-server-darwin-arm64" | |
| - name: "Windows x64" | |
| runner: "windows-2022" | |
| target: "x86_64-pc-windows-msvc" | |
| artifact: "codeg-server-windows-x64" | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Rust cache | |
| uses: swatinem/rust-cache@v2 | |
| with: | |
| workspaces: "./src-tauri -> target" | |
| shared-key: server-${{ matrix.target }} | |
| - name: Install Linux arm64 cross compiler | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| run: | | |
| cat > /tmp/sources.list << 'EOF' | |
| deb [arch=amd64] http://archive.ubuntu.com/ubuntu jammy main restricted universe multiverse | |
| deb [arch=amd64] http://archive.ubuntu.com/ubuntu jammy-updates main restricted universe multiverse | |
| deb [arch=amd64] http://archive.ubuntu.com/ubuntu jammy-backports main restricted universe multiverse | |
| deb [arch=amd64] http://security.ubuntu.com/ubuntu jammy-security main restricted universe multiverse | |
| deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse | |
| deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse | |
| deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse | |
| deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse | |
| EOF | |
| sudo mv /etc/apt/sources.list /etc/apt/sources.list.default | |
| sudo mv /tmp/sources.list /etc/apt/sources.list | |
| sudo dpkg --add-architecture arm64 | |
| sudo apt-get update | |
| # libssl3 is Multi-Arch: same; align the preinstalled :amd64 copy | |
| # with the version libssl-dev:arm64 pins so the cross install isn't | |
| # blocked by a held-back amd64 copy (same failure mode as the desktop | |
| # arm64 build — see the build-tauri step for the full rationale). | |
| sudo apt-get install -y --only-upgrade libssl3 | |
| sudo apt-get install -y \ | |
| gcc-aarch64-linux-gnu \ | |
| g++-aarch64-linux-gnu \ | |
| libssl-dev:arm64 | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV" | |
| echo "PKG_CONFIG_ALLOW_CROSS=1" >> "$GITHUB_ENV" | |
| echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig" >> "$GITHUB_ENV" | |
| echo "PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig" >> "$GITHUB_ENV" | |
| echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu" >> "$GITHUB_ENV" | |
| echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV" | |
| echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> "$GITHUB_ENV" | |
| echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc-ar" >> "$GITHUB_ENV" | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| run_install: false | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: "pnpm" | |
| - name: Build frontend | |
| run: | | |
| pnpm install --frozen-lockfile | |
| pnpm build | |
| - name: Build server + codeg-mcp companion | |
| working-directory: src-tauri | |
| # `codeg-mcp` is the stdio MCP companion the runtime injects per | |
| # session (see acp/delegation/companion.rs). Built with the same | |
| # `--no-default-features --target` flags as the server so it shares | |
| # the cross-compile env (Linux arm64) without dragging in tauri | |
| # runtime deps. | |
| run: | | |
| cargo build --release --bin codeg-server --no-default-features --target ${{ matrix.target }} | |
| cargo build --release --bin codeg-mcp --no-default-features --target ${{ matrix.target }} | |
| - name: Package (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| mkdir -p dist/${{ matrix.artifact }} | |
| cp src-tauri/target/${{ matrix.target }}/release/codeg-server dist/${{ matrix.artifact }}/ | |
| cp src-tauri/target/${{ matrix.target }}/release/codeg-mcp dist/${{ matrix.artifact }}/ | |
| chmod +x dist/${{ matrix.artifact }}/codeg-server dist/${{ matrix.artifact }}/codeg-mcp | |
| cp -r out dist/${{ matrix.artifact }}/web | |
| cd dist && tar czf ${{ matrix.artifact }}.tar.gz ${{ matrix.artifact }} | |
| - name: Package (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path dist/${{ matrix.artifact }} | |
| Copy-Item src-tauri/target/${{ matrix.target }}/release/codeg-server.exe dist/${{ matrix.artifact }}/ | |
| Copy-Item src-tauri/target/${{ matrix.target }}/release/codeg-mcp.exe dist/${{ matrix.artifact }}/ | |
| Copy-Item -Recurse out dist/${{ matrix.artifact }}/web | |
| Compress-Archive -Path dist/${{ matrix.artifact }} -DestinationPath dist/${{ matrix.artifact }}.zip | |
| - name: Smoke test packaged artifact (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| set -euo pipefail | |
| test -x "dist/${{ matrix.artifact }}/codeg-server" | |
| test -x "dist/${{ matrix.artifact }}/codeg-mcp" | |
| # `codeg-mcp --help` exits 0 without flags and prints the usage | |
| # line. Only run it for native targets; cross-compiled arm64 | |
| # binaries on x64 runners can't execute. | |
| if [ "${{ matrix.target }}" = "x86_64-unknown-linux-gnu" ] || \ | |
| [ "${{ matrix.target }}" = "x86_64-apple-darwin" ] || \ | |
| [ "${{ matrix.target }}" = "aarch64-apple-darwin" ]; then | |
| "dist/${{ matrix.artifact }}/codeg-mcp" --help | grep -F 'codeg-mcp --parent-connection-id' | |
| else | |
| echo "skipping codeg-mcp --help on cross-target ${{ matrix.target }}" | |
| fi | |
| - name: Smoke test packaged artifact (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| if (-not (Test-Path "dist/${{ matrix.artifact }}/codeg-server.exe")) { | |
| throw "codeg-server.exe missing from packaged artifact" | |
| } | |
| if (-not (Test-Path "dist/${{ matrix.artifact }}/codeg-mcp.exe")) { | |
| throw "codeg-mcp.exe missing from packaged artifact" | |
| } | |
| $help = & "dist/${{ matrix.artifact }}/codeg-mcp.exe" --help | |
| if ($help -notlike "*codeg-mcp --parent-connection-id*") { | |
| throw "codeg-mcp --help output unexpected: $help" | |
| } | |
| - name: Upload artifact for Docker build (Linux only) | |
| if: startsWith(matrix.target, 'x86_64-unknown-linux') || startsWith(matrix.target, 'aarch64-unknown-linux') | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact }} | |
| path: dist/${{ matrix.artifact }} | |
| retention-days: 1 | |
| # Sign the server bundle with the SAME minisign key the desktop | |
| # updater uses (`tauri signer sign` writes `<file>.sig`). The server's | |
| # `update::verify` module verifies this signature against the embedded | |
| # public key before installing — see src-tauri/src/update/verify.rs. | |
| - name: Sign and checksum bundle (Unix) | |
| if: runner.os != 'Windows' | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| pnpm tauri signer sign "dist/${{ matrix.artifact }}.tar.gz" | |
| test -f "dist/${{ matrix.artifact }}.tar.gz.sig" | |
| ( cd dist && shasum -a 256 "${{ matrix.artifact }}.tar.gz" > "${{ matrix.artifact }}.tar.gz.sha256" ) | |
| - name: Sign and checksum bundle (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| pnpm tauri signer sign "dist/${{ matrix.artifact }}.zip" | |
| if (-not (Test-Path "dist/${{ matrix.artifact }}.zip.sig")) { | |
| throw "signature not produced for ${{ matrix.artifact }}.zip" | |
| } | |
| $hash = (Get-FileHash -Algorithm SHA256 "dist/${{ matrix.artifact }}.zip").Hash.ToLower() | |
| "$hash ${{ matrix.artifact }}.zip" | Out-File -Encoding ascii "dist/${{ matrix.artifact }}.zip.sha256" | |
| - name: Upload to release (Unix) | |
| if: runner.os != 'Windows' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release upload "${{ github.ref_name }}" \ | |
| "dist/${{ matrix.artifact }}.tar.gz" \ | |
| "dist/${{ matrix.artifact }}.tar.gz.sig" \ | |
| "dist/${{ matrix.artifact }}.tar.gz.sha256" \ | |
| --clobber | |
| - name: Upload to release (Windows) | |
| if: runner.os == 'Windows' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: gh release upload "${{ github.ref_name }}" "dist/${{ matrix.artifact }}.zip" "dist/${{ matrix.artifact }}.zip.sig" "dist/${{ matrix.artifact }}.zip.sha256" --clobber | |
| build-docker: | |
| needs: | |
| - create-draft-release | |
| - build-server | |
| name: Build Docker image | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download Linux x64 artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: codeg-server-linux-x64 | |
| path: artifacts/linux-x64 | |
| - name: Download Linux arm64 artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: codeg-server-linux-arm64 | |
| path: artifacts/linux-arm64 | |
| - name: Prepare Docker build context | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist/amd64 dist/arm64 | |
| cp artifacts/linux-x64/codeg-server dist/amd64/codeg-server | |
| cp artifacts/linux-x64/codeg-mcp dist/amd64/codeg-mcp | |
| cp artifacts/linux-arm64/codeg-server dist/arm64/codeg-server | |
| cp artifacts/linux-arm64/codeg-mcp dist/arm64/codeg-mcp | |
| cp -r artifacts/linux-x64/web dist/web | |
| chmod +x dist/amd64/codeg-server dist/amd64/codeg-mcp \ | |
| dist/arm64/codeg-server dist/arm64/codeg-mcp | |
| # Fail fast if any companion went missing — Docker would otherwise | |
| # produce an image where delegation silently degrades. | |
| for f in dist/amd64/codeg-mcp dist/arm64/codeg-mcp; do | |
| test -x "$f" || { echo "FATAL: $f missing or non-exec"; exit 1; } | |
| done | |
| - name: Set up QEMU (for multi-arch manifest) | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract version from tag | |
| id: version | |
| run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: Dockerfile.ci | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: | | |
| ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} | |
| ghcr.io/${{ github.repository }}:latest | |
| ${{ secrets.DOCKERHUB_USERNAME }}/codeg:${{ steps.version.outputs.version }} | |
| ${{ secrets.DOCKERHUB_USERNAME }}/codeg:latest | |
| publish-release: | |
| needs: | |
| - create-draft-release | |
| - build-tauri | |
| - build-server | |
| - build-docker | |
| if: ${{ needs.build-tauri.result == 'success' && needs.build-server.result == 'success' && needs.build-docker.result == 'success' }} | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Publish draft release | |
| uses: actions/github-script@v7 | |
| env: | |
| RELEASE_ID: ${{ needs.create-draft-release.outputs.release_id }} | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const releaseId = Number(process.env.RELEASE_ID); | |
| const prerelease = | |
| "${{ needs.create-draft-release.outputs.prerelease }}" === "true"; | |
| await github.rest.repos.updateRelease({ | |
| owner, | |
| repo, | |
| release_id: releaseId, | |
| draft: false, | |
| prerelease, | |
| }); | |
| core.info(`Published release #${releaseId}`); |