Skip to content

# Release version 0.21.9 #178

# Release version 0.21.9

# Release version 0.21.9 #178

Workflow file for this run

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}`);