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
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: 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 }}" |