Release Gajae Code App #5
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 Gajae App | |
| on: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| release-policy: | |
| name: Validate protected release ref | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Require main branch | |
| env: | |
| RELEASE_REF: ${{ github.ref }} | |
| run: | | |
| if [ "$RELEASE_REF" != "refs/heads/main" ]; then | |
| echo "Releases may only be dispatched from refs/heads/main." >&2 | |
| exit 1 | |
| fi | |
| build: | |
| name: Build Node 22 Linux x64 server artifact | |
| needs: release-policy | |
| timeout-minutes: 30 | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: read | |
| outputs: | |
| artifact_name: ${{ steps.metadata.outputs.artifact_name }} | |
| release_tag: ${{ steps.metadata.outputs.release_tag }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version: 22 | |
| architecture: x64 | |
| cache: npm | |
| - name: Define canonical release asset | |
| id: metadata | |
| run: | | |
| set -euo pipefail | |
| case "$(node --version)" in | |
| v22.*) ;; | |
| *) echo "Node 22 is required." >&2; exit 1 ;; | |
| esac | |
| case "$(getconf GNU_LIBC_VERSION)" in | |
| "glibc 2.35") ;; | |
| *) echo "The release artifact must be built on glibc 2.35." >&2; exit 1 ;; | |
| esac | |
| VERSION="$(node -p "require('./package.json').version")" | |
| ASSET_NAME="gajae-app-server-${VERSION}-linux-x64-node22.tar.gz" | |
| { | |
| echo "artifact_name=$ASSET_NAME" | |
| echo "release_tag=v$VERSION" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Seed verified ripgrep install cache | |
| run: node scripts/release/prime-ripgrep-cache.mjs | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Fetch pinned Bun runtime | |
| run: node scripts/fetch-bun.mjs | |
| - name: Verify source | |
| run: npm run verify | |
| - name: Build server artifact | |
| run: npm run server:bundle | |
| - name: Stage and verify canonical release assets | |
| env: | |
| ASSET_NAME: ${{ steps.metadata.outputs.artifact_name }} | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| archives=(release/server/*.tar.gz) | |
| if [ "${#archives[@]}" -ne 1 ]; then | |
| echo "Expected exactly one generated server archive." >&2 | |
| exit 1 | |
| fi | |
| source_archive="${archives[0]}" | |
| if [ "$(basename "$source_archive")" != "$ASSET_NAME" ]; then | |
| echo "Generated archive name does not match canonical release asset: $source_archive" >&2 | |
| exit 1 | |
| fi | |
| source_checksum="${source_archive}.sha256" | |
| test -f "$source_checksum" | |
| ( | |
| cd "$(dirname "$source_archive")" | |
| sha256sum --check "$(basename "$source_checksum")" | |
| ) | |
| mkdir release-assets | |
| cp "$source_archive" "release-assets/$ASSET_NAME" | |
| ( | |
| cd release-assets | |
| sha256sum "$ASSET_NAME" > "$ASSET_NAME.sha256" | |
| sha256sum --check "$ASSET_NAME.sha256" | |
| ) | |
| assets=(release-assets/*) | |
| if [ "${#assets[@]}" -ne 2 ] || | |
| [ ! -f "release-assets/$ASSET_NAME" ] || | |
| [ ! -f "release-assets/$ASSET_NAME.sha256" ]; then | |
| echo "Release staging must contain only the canonical archive and checksum." >&2 | |
| exit 1 | |
| fi | |
| - name: Smoke extracted server artifact | |
| env: | |
| ASSET_NAME: ${{ steps.metadata.outputs.artifact_name }} | |
| run: | | |
| set -euo pipefail | |
| mkdir extracted | |
| tar -xzf "release-assets/$ASSET_NAME" -C extracted | |
| test -f extracted/package.json | |
| expected_version="${ASSET_NAME#gajae-app-server-}" | |
| expected_version="${expected_version%-linux-x64-node22.tar.gz}" | |
| actual_version="$(node -p "require('./extracted/package.json').version")" | |
| test "$actual_version" = "$expected_version" | |
| node extracted/scripts/gajae-app-runtime.mjs --help | |
| - name: Upload canonical server release assets | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 | |
| with: | |
| name: gajae-app-server-release | |
| path: release-assets/* | |
| if-no-files-found: error | |
| desktop-macos: | |
| name: Build macOS arm64 desktop DMG | |
| needs: release-policy | |
| timeout-minutes: 60 | |
| runs-on: macos-14 | |
| permissions: | |
| contents: read | |
| outputs: | |
| artifact_name: ${{ steps.metadata.outputs.artifact_name }} | |
| release_tag: ${{ steps.metadata.outputs.release_tag }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version: 22 | |
| architecture: arm64 | |
| cache: npm | |
| - name: Define canonical desktop release asset | |
| id: metadata | |
| run: | | |
| set -euo pipefail | |
| test "$(uname -s)" = "Darwin" | |
| test "$(uname -m)" = "arm64" | |
| case "$(node --version)" in | |
| v22.*) ;; | |
| *) echo "Node 22 is required." >&2; exit 1 ;; | |
| esac | |
| RELEASE_VERSION="$(node -p "require('./package.json').version")" | |
| DESKTOP_VERSION="$(node -p "require('./package.json').desktopVersion")" | |
| ASSET_NAME="gajae-app-desktop-${RELEASE_VERSION}-macos-arm64.dmg" | |
| { | |
| echo "artifact_name=$ASSET_NAME" | |
| echo "desktop_version=$DESKTOP_VERSION" | |
| echo "release_tag=v$RELEASE_VERSION" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Set up Rust | |
| run: | | |
| set -euo pipefail | |
| rustup toolchain install 1.85.1 --profile minimal | |
| rustup default 1.85.1 | |
| rustup target add aarch64-apple-darwin | |
| rustc --version | |
| - name: Seed verified ripgrep install cache | |
| run: node scripts/release/prime-ripgrep-cache.mjs | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build and verify embedded macOS server payload | |
| run: npm run server:payload:macos | |
| - name: Build ad-hoc signed Tauri app bundle | |
| run: env -u CI npm run tauri -- build --bundles app | |
| - name: Build canonical installable DMG | |
| env: | |
| RELEASE_VERSION: ${{ steps.metadata.outputs.release_tag }} | |
| run: npm run desktop:dmg:macos -- --artifact-version "${RELEASE_VERSION#v}" | |
| - name: Stage and verify canonical desktop assets | |
| env: | |
| ASSET_NAME: ${{ steps.metadata.outputs.artifact_name }} | |
| run: | | |
| set -euo pipefail | |
| SOURCE="src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/$ASSET_NAME" | |
| test -f "$SOURCE" | |
| test -f "$SOURCE.sha256" | |
| ( | |
| cd "$(dirname "$SOURCE")" | |
| shasum -a 256 --check "$(basename "$SOURCE.sha256")" | |
| ) | |
| mkdir release-assets | |
| cp "$SOURCE" "$SOURCE.sha256" release-assets/ | |
| assets=(release-assets/*) | |
| if [ "${#assets[@]}" -ne 2 ] || | |
| [ ! -f "release-assets/$ASSET_NAME" ] || | |
| [ ! -f "release-assets/$ASSET_NAME.sha256" ]; then | |
| echo "Desktop release staging must contain only the canonical DMG and checksum." >&2 | |
| exit 1 | |
| fi | |
| hdiutil verify "release-assets/$ASSET_NAME" | |
| - name: Smoke the app installed from the DMG | |
| env: | |
| ASSET_NAME: ${{ steps.metadata.outputs.artifact_name }} | |
| DESKTOP_VERSION: ${{ steps.metadata.outputs.desktop_version }} | |
| run: | | |
| set -euo pipefail | |
| MOUNT_POINT="$RUNNER_TEMP/gajae-app-dmg" | |
| mkdir "$MOUNT_POINT" | |
| hdiutil attach "release-assets/$ASSET_NAME" -nobrowse -readonly -mountpoint "$MOUNT_POINT" | |
| trap 'hdiutil detach "$MOUNT_POINT"' EXIT | |
| APP="$MOUNT_POINT/Gajae App.app" | |
| test -d "$APP" | |
| codesign --verify --deep --strict "$APP" | |
| test "$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$APP/Contents/Info.plist")" = "app.gajae.desktop" | |
| test "$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$APP/Contents/Info.plist")" = "$DESKTOP_VERSION" | |
| lipo "$APP/Contents/MacOS/gajae-app-desktop" -verify_arch arm64 | |
| lipo "$APP/Contents/MacOS/gajae-app-server" -verify_arch arm64 | |
| node scripts/release/smoke-packaged-server.mjs --tauri-app "$APP" | |
| hdiutil detach "$MOUNT_POINT" | |
| trap - EXIT | |
| - name: Upload canonical desktop release assets | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 | |
| with: | |
| name: gajae-app-desktop-release | |
| path: release-assets/* | |
| if-no-files-found: error | |
| ubuntu-24-compatibility: | |
| name: Smoke server artifact on Ubuntu 24.04 | |
| needs: build | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Set up Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version: 22 | |
| architecture: x64 | |
| - name: Download canonical server release assets | |
| uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 | |
| with: | |
| name: gajae-app-server-release | |
| path: release-assets | |
| - name: Verify checksum and smoke extracted artifact | |
| env: | |
| ASSET_NAME: ${{ needs.build.outputs.artifact_name }} | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| assets=(release-assets/*) | |
| if [ "${#assets[@]}" -ne 2 ] || | |
| [ ! -f "release-assets/$ASSET_NAME" ] || | |
| [ ! -f "release-assets/$ASSET_NAME.sha256" ]; then | |
| echo "Compatibility job received non-canonical release assets." >&2 | |
| exit 1 | |
| fi | |
| ( | |
| cd release-assets | |
| sha256sum --check "$ASSET_NAME.sha256" | |
| ) | |
| mkdir extracted | |
| tar -xzf "release-assets/$ASSET_NAME" -C extracted | |
| test -f extracted/package.json | |
| expected_version="${ASSET_NAME#gajae-app-server-}" | |
| expected_version="${expected_version%-linux-x64-node22.tar.gz}" | |
| actual_version="$(node -p "require('./extracted/package.json').version")" | |
| test "$actual_version" = "$expected_version" | |
| node extracted/scripts/gajae-app-runtime.mjs --help | |
| ( | |
| cd extracted | |
| node -e "const Database=require('better-sqlite3'); const pty=require('node-pty'); const db=new Database(':memory:'); db.exec('select 1'); db.close(); const child=pty.spawn(process.execPath,['-e','process.exit(0)']); child.onExit(() => process.exit(0)); setTimeout(() => process.exit(1),5000)" | |
| node -e "const {spawnSync}=require('node:child_process'); const rg=require('@vscode/ripgrep').rgPath; const result=spawnSync(rg,['--version'],{stdio:'inherit'}); process.exit(result.status ?? 1)" | |
| ) | |
| export HOME="$RUNNER_TEMP/gajae-app-home" | |
| mkdir -p "$HOME" | |
| SERVER_PORT=39001 HOST=127.0.0.1 node extracted/scripts/gajae-app-runtime.mjs start >"$RUNNER_TEMP/gajae-app-server.log" 2>&1 & | |
| server_pid=$! | |
| trap 'kill "$server_pid" 2>/dev/null || true' EXIT | |
| for _ in {1..30}; do | |
| if curl --fail --silent http://127.0.0.1:39001/health >/dev/null; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| curl --fail --silent http://127.0.0.1:39001/health | |
| kill "$server_pid" | |
| wait "$server_pid" || true | |
| trap - EXIT | |
| publish: | |
| name: Publish GitHub Release | |
| needs: | |
| - build | |
| - desktop-macos | |
| - ubuntu-24-compatibility | |
| runs-on: ubuntu-22.04 | |
| environment: release | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download canonical server release assets | |
| uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 | |
| with: | |
| name: gajae-app-server-release | |
| path: release-assets | |
| - name: Download canonical desktop release assets | |
| uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 | |
| with: | |
| name: gajae-app-desktop-release | |
| path: release-assets | |
| - name: Verify publish inputs | |
| env: | |
| SERVER_ASSET_NAME: ${{ needs.build.outputs.artifact_name }} | |
| DESKTOP_ASSET_NAME: ${{ needs.desktop-macos.outputs.artifact_name }} | |
| SERVER_RELEASE_TAG: ${{ needs.build.outputs.release_tag }} | |
| DESKTOP_RELEASE_TAG: ${{ needs.desktop-macos.outputs.release_tag }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$SERVER_RELEASE_TAG" != "$DESKTOP_RELEASE_TAG" ]; then | |
| echo "Server and desktop release tags do not match." >&2 | |
| exit 1 | |
| fi | |
| shopt -s nullglob | |
| assets=(release-assets/*) | |
| if [ "${#assets[@]}" -ne 4 ] || | |
| [ ! -f "release-assets/$SERVER_ASSET_NAME" ] || | |
| [ ! -f "release-assets/$SERVER_ASSET_NAME.sha256" ] || | |
| [ ! -f "release-assets/$DESKTOP_ASSET_NAME" ] || | |
| [ ! -f "release-assets/$DESKTOP_ASSET_NAME.sha256" ]; then | |
| echo "Refusing to publish non-canonical release assets." >&2 | |
| exit 1 | |
| fi | |
| ( | |
| cd release-assets | |
| sha256sum --check "$SERVER_ASSET_NAME.sha256" | |
| sha256sum --check "$DESKTOP_ASSET_NAME.sha256" | |
| ) | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| SERVER_ASSET_NAME: ${{ needs.build.outputs.artifact_name }} | |
| DESKTOP_ASSET_NAME: ${{ needs.desktop-macos.outputs.artifact_name }} | |
| RELEASE_TAG: ${{ needs.build.outputs.release_tag }} | |
| run: | | |
| set -euo pipefail | |
| prerelease_args=() | |
| if [[ "$RELEASE_TAG" == *-* ]]; then | |
| prerelease_args+=(--prerelease) | |
| fi | |
| gh release create "$RELEASE_TAG" \ | |
| "release-assets/$SERVER_ASSET_NAME" \ | |
| "release-assets/$SERVER_ASSET_NAME.sha256" \ | |
| "release-assets/$DESKTOP_ASSET_NAME" \ | |
| "release-assets/$DESKTOP_ASSET_NAME.sha256" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --target "$GITHUB_SHA" \ | |
| --title "Gajae App $RELEASE_TAG" \ | |
| "${prerelease_args[@]}" \ | |
| --generate-notes |