Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Merge PR #37: fix(llm): preserve prompt cache stability with 2-part s… #304

Merge PR #37: fix(llm): preserve prompt cache stability with 2-part s…

Merge PR #37: fix(llm): preserve prompt cache stability with 2-part s… #304

Workflow file for this run

name: desktop-release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing v* or v*-desktop tag to build and publish"
required: true
type: string
permissions: write-all
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
concurrency:
group: ${{ github.workflow }}-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false
jobs:
resolve:
runs-on: ubuntu-latest
outputs:
enabled: ${{ steps.resolve.outputs.enabled }}
source_tag: ${{ steps.resolve.outputs.source_tag }}
desktop_tag: ${{ steps.resolve.outputs.desktop_tag }}
version: ${{ steps.resolve.outputs.version }}
steps:
- id: resolve
shell: bash
run: |
set -euo pipefail
tag="${{ inputs.tag || github.ref_name }}"
if [ -z "$tag" ]; then
echo "A release tag is required." >&2
exit 1
fi
if [[ "$tag" != v* ]]; then
echo "Release tags must start with v." >&2
exit 1
fi
# `v<x.y.z>-mobile` is the mobile-release sibling tag and is
# NOT a desktop-release trigger. Skip without doing anything so
# the desktop release isn't accidentally re-built / re-promoted
# on every mobile-tag push.
if [[ "$tag" == *-mobile ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "source_tag=${tag%-mobile}" >> "$GITHUB_OUTPUT"
echo "desktop_tag=${tag%-mobile}-desktop" >> "$GITHUB_OUTPUT"
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$tag" == *-desktop ]]; then
source_tag="${tag%-desktop}"
desktop_tag="$tag"
else
source_tag="$tag"
desktop_tag="${tag}-desktop"
fi
if [[ "${{ github.event_name }}" == "push" && "$tag" == *-desktop ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "source_tag=$source_tag" >> "$GITHUB_OUTPUT"
echo "desktop_tag=$desktop_tag" >> "$GITHUB_OUTPUT"
echo "version=${source_tag#v}" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "enabled=true" >> "$GITHUB_OUTPUT"
echo "source_tag=$source_tag" >> "$GITHUB_OUTPUT"
echo "desktop_tag=$desktop_tag" >> "$GITHUB_OUTPUT"
echo "version=${source_tag#v}" >> "$GITHUB_OUTPUT"
create-release:
needs: resolve
if: ${{ needs.resolve.outputs.enabled == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: refs/tags/${{ needs.resolve.outputs.source_tag }}
- name: Create or sync desktop release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh_release() {
local attempt
for attempt in 1 2 3; do
if gh release "$@"; then
return 0
fi
sleep $((attempt * 5))
done
return 1
}
title="${{ needs.resolve.outputs.source_tag }}"
desktop_tag="${{ needs.resolve.outputs.desktop_tag }}"
source_sha="$(git rev-parse HEAD)"
notes_file="$(mktemp)"
gh release view "${{ needs.resolve.outputs.source_tag }}" --repo "${{ github.repository }}" --json name,body > base-release.json
source_title="$(jq -r '.name // empty' base-release.json)"
if [ -n "$source_title" ]; then
title="$source_title"
fi
jq -r '.body // ""' base-release.json > "$notes_file"
if ! grep -q '[^[:space:]]' "$notes_file"; then
echo "Source release ${{ needs.resolve.outputs.source_tag }} is missing notes." >&2
exit 1
fi
if grep -Eiq '^[[:space:]]*No notable changes\.?[[:space:]]*$|^BLOCKED:|rolls forward in-flight|bumps version metadata|<area>|^## CLI · npm release$|^## Desktop Shell$|^## Mobile Shell$' "$notes_file"; then
echo "Source release ${{ needs.resolve.outputs.source_tag }} still contains placeholder or boilerplate notes." >&2
exit 1
fi
if ! git ls-remote --exit-code --tags origin "refs/tags/${desktop_tag}" >/dev/null 2>&1; then
git tag "$desktop_tag" "$source_sha"
git push origin "refs/tags/${desktop_tag}"
fi
if gh release view "$desktop_tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then
gh_release edit "$desktop_tag" \
--title "$title" \
--notes-file "$notes_file" \
--repo "${{ github.repository }}"
exit 0
fi
# Create as a real release (not draft). Assets are uploaded as
# individual platform build jobs finish; the publish-release job
# at the end of the workflow still runs to set --latest. This way
# the release is visible immediately and downloads start landing
# on it as soon as the first platform finishes building, instead
# of being hidden as a draft until every platform completes.
gh_release create "$desktop_tag" \
--target "$source_sha" \
--title "$title" \
--notes-file "$notes_file" \
--repo "${{ github.repository }}"
build-desktop:
needs:
- resolve
- create-release
if: ${{ needs.resolve.outputs.enabled == 'true' }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-14
target: mac
package_command: bunx electron-builder --mac dmg zip --x64 --arm64 --publish never
- os: ubuntu-latest
target: linux
package_command: bunx electron-builder --linux AppImage deb tar.gz --x64 --publish never
- os: windows-2025
target: win
package_command: bunx electron-builder --win nsis zip --x64 --publish never
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
ref: refs/tags/${{ needs.resolve.outputs.source_tag }}
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
path: .github-workflow
- uses: ./.github-workflow/.github/actions/setup-bun
- name: Verify desktop version
shell: bash
run: |
set -euo pipefail
version=$(bun --eval 'console.log((await Bun.file("packages/desktop/package.json").json()).version)')
if [ "$version" != "${{ needs.resolve.outputs.version }}" ]; then
echo "packages/desktop/package.json version $version does not match ${{ needs.resolve.outputs.version }}" >&2
exit 1
fi
- name: Build desktop bundle
working-directory: packages/desktop
run: bun run build
env:
CODEPLANE_VERSION: ${{ needs.resolve.outputs.version }}
# Configure macOS Developer ID signing only when the cert secret
# exists. Has to be a separate step that conditionally appends to
# $GITHUB_ENV — GHA cannot omit a per-step `env:` value via
# expression, only set it (possibly to empty string), and
# electron-builder treats CSC_LINK="" as a file path, which then
# tries to resolve `''` against cwd → `packages/desktop` →
# `not a file` → exit 1. So we must keep CSC_LINK fully UNSET
# in the no-secret case.
#
# Single inlined step (instead of two) because GHA `if:` conditions
# can't reference secrets directly for security reasons, but they
# CAN reference env vars set from secrets within the same step
# via $MAC_CERTS check + early exit. Skipping inline keeps the
# flow obvious and avoids a sentinel-env-var dance between steps.
# Default CSC_IDENTITY_AUTO_DISCOVERY to false everywhere via
# $GITHUB_ENV (NOT a per-step env: block, because step env
# OVERRIDES $GITHUB_ENV — and we need the next step to be able
# to flip this to true via $GITHUB_ENV when secrets are present).
# Keychain probing on Linux/Windows runners or unsigned-mac
# runners would fail or hang; this keeps them safe.
- name: Default code signing config
shell: bash
run: echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV"
# Configure macOS Developer ID signing only when the cert secret
# exists. Has to write to $GITHUB_ENV (not per-step env:) because
# GHA can't omit a step env value via expression — it always
# produces SOME string (possibly empty), and electron-builder
# treats CSC_LINK="" as a file path, which then resolves `''`
# against cwd → `packages/desktop` → `not a file` → exit 1.
# So we must keep CSC_LINK fully UNSET in the no-secret case.
#
# `if:` conditions can't reference secrets directly for security
# reasons, but they CAN reference env vars set from secrets within
# the same step via $MAC_CERTS check + early exit. Single inlined
# step keeps the flow obvious.
- name: Configure macOS code signing
if: matrix.target == 'mac'
shell: bash
env:
MAC_CERTS: ${{ secrets.MAC_CERTS }}
MAC_CERTS_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
if [ -z "$MAC_CERTS" ]; then
echo "Mac signing secrets not configured — building unsigned (ad-hoc) for this runner."
exit 0
fi
{
echo "CSC_LINK=$MAC_CERTS"
echo "CSC_KEY_PASSWORD=$MAC_CERTS_PASSWORD"
echo "APPLE_ID=$APPLE_ID"
echo "APPLE_APP_SPECIFIC_PASSWORD=$APPLE_APP_SPECIFIC_PASSWORD"
echo "APPLE_TEAM_ID=$APPLE_TEAM_ID"
echo "CSC_IDENTITY_AUTO_DISCOVERY=true"
} >> "$GITHUB_ENV"
echo "Mac signing secrets present — Developer ID signing + notarization will run."
- name: Package desktop binaries
working-directory: packages/desktop
run: ${{ matrix.package_command }}
env:
CODEPLANE_VERSION: ${{ needs.resolve.outputs.version }}
- name: Create stable download aliases
working-directory: packages/desktop/release
shell: bash
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
resolve() {
local pattern="$1"
local file
file=$(compgen -G "$pattern" | head -n 1 || true)
if [ -z "$file" ]; then
echo "No file matched: $pattern" >&2
exit 1
fi
printf '%s\n' "$file"
}
case "${{ matrix.target }}" in
mac)
cp "$(resolve "codeplane-desktop-${VERSION}-mac-arm64.dmg")" codeplane-desktop-macos-apple-silicon.dmg
cp "$(resolve "codeplane-desktop-${VERSION}-mac-x64.dmg")" codeplane-desktop-macos-intel.dmg
;;
linux)
cp "$(resolve "codeplane-desktop-${VERSION}-linux-*.AppImage")" codeplane-desktop-linux-x64.AppImage
;;
win)
cp "$(resolve "codeplane-desktop-${VERSION}-win-x64.exe")" codeplane-desktop-windows-x64.exe
;;
esac
- name: Upload desktop binaries
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
shopt -s nullglob
cd packages/desktop/release
files=(codeplane-desktop-* latest*.yml)
if [ ${#files[@]} -eq 0 ]; then
echo "No desktop binaries were produced." >&2
exit 1
fi
gh release upload "${{ needs.resolve.outputs.desktop_tag }}" "${files[@]}" \
--clobber \
--repo "${{ github.repository }}"
publish-release:
needs:
- resolve
- build-desktop
# Always run so a single platform build failing or hanging does not leave
# the desktop release un-promoted. The release is created up-front (not
# as a draft) so it stays visible regardless; this step just ensures the
# title/notes mirror the source release and --latest is set.
if: ${{ needs.resolve.outputs.enabled == 'true' && always() }}
runs-on: ubuntu-latest
steps:
- name: Publish release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh_release() {
local attempt
for attempt in 1 2 3; do
if gh release "$@"; then
return 0
fi
sleep $((attempt * 5))
done
return 1
}
title="${{ needs.resolve.outputs.source_tag }}"
notes_file="$(mktemp)"
gh release view "${{ needs.resolve.outputs.source_tag }}" --repo "${{ github.repository }}" --json name,body > base-release.json
source_title="$(jq -r '.name // empty' base-release.json)"
if [ -n "$source_title" ]; then
title="$source_title"
fi
jq -r '.body // ""' base-release.json > "$notes_file"
if grep -Eiq '^[[:space:]]*No notable changes\.?[[:space:]]*$|^BLOCKED:|rolls forward in-flight|bumps version metadata|<area>|^## CLI · npm release$|^## Desktop Shell$|^## Mobile Shell$' "$notes_file"; then
echo "Source release ${{ needs.resolve.outputs.source_tag }} still contains placeholder or boilerplate notes." >&2
exit 1
fi
gh_release edit "${{ needs.resolve.outputs.desktop_tag }}" \
--draft=false \
--latest \
--title "$title" \
--notes-file "$notes_file" \
--repo "${{ github.repository }}"