Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/desktop-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false

- name: Set up pinned Node.js
Expand All @@ -37,8 +38,12 @@ jobs:

- name: Check build host and define canonical assets
id: metadata
env:
SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
echo "Desktop source commit: $SOURCE_SHA" >> "$GITHUB_STEP_SUMMARY"
test "$(uname -s)" = Linux
test "$(uname -m)" = x86_64
test "$(getconf GNU_LIBC_VERSION)" = "glibc 2.35"
Expand Down Expand Up @@ -137,6 +142,7 @@ jobs:
- name: Checkout smoke harness
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false

- name: Set up Node.js for the smoke harness
Expand Down
285 changes: 285 additions & 0 deletions .github/workflows/server-linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
name: Linux server archive

on:
workflow_dispatch:
inputs:
source_sha:
description: 'Exact 40-character source commit (empty uses the dispatched commit)'
required: false
type: string
pull_request:
paths:
- '.github/workflows/server-linux.yml'
- '.github/workflows/ci.yml'
- 'src/**'
- 'server/**'
- 'shared/**'
- 'native/**'
- 'scripts/**'
- 'public/**'
- 'packaging/systemd/**'
- 'docker/**'
- 'package*.json'
- '*config*'
- '.npmrc'
- 'index.html'
- 'LICENSE'
- 'NOTICE'
- 'docs/SELF-HOST.md'
- 'docs/INSTALL.md'
- 'docs/SERVER-LINUX-ACCEPTANCE.md'
push:
branches: [main]
paths:
- '.github/workflows/server-linux.yml'
- '.github/workflows/ci.yml'
- 'src/**'
- 'server/**'
- 'shared/**'
- 'native/**'
- 'scripts/**'
- 'public/**'
- 'packaging/systemd/**'
- 'docker/**'
- 'package*.json'
- '*config*'
- '.npmrc'
- 'index.html'
- 'LICENSE'
- 'NOTICE'
- 'docs/SELF-HOST.md'
- 'docs/INSTALL.md'
- 'docs/SERVER-LINUX-ACCEPTANCE.md'

permissions:
contents: read

concurrency:
group: server-linux-${{ github.workflow }}-${{ inputs.source_sha || github.event.pull_request.head.sha || github.sha }}
cancel-in-progress: true

# Acceptance/build artifacts only. Source verification is the separate CI gate.
# No release environment, signing/provider credentials, publication or dispatch.
jobs:
build:
name: Build Linux x64 Node 22 archive (glibc 2.35)
runs-on: ubuntu-22.04
timeout-minutes: 45
env:
SOURCE_SHA: ${{ inputs.source_sha || github.event.pull_request.head.sha || github.sha }}
outputs:
source_sha: ${{ steps.metadata.outputs.source_sha }}
asset_name: ${{ steps.metadata.outputs.asset_name }}
steps:
- name: Require an immutable source commit
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]

- name: Checkout exact source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.SOURCE_SHA }}
persist-credentials: false

- name: Set up pinned Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22.22.2'
architecture: x64
cache: npm

- name: Check source and build host
id: metadata
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
test "$(uname -s)" = Linux
test "$(uname -m)" = x86_64
test "$(getconf GNU_LIBC_VERSION)" = "glibc 2.35"
test "$(node --version)" = v22.22.2
VERSION="$(node -p "require('./package.json').version")"
echo "source_sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT"
echo "asset_name=gajae-app-server-${VERSION}-linux-x64-node22.tar.gz" >> "$GITHUB_OUTPUT"
echo "SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD)" >> "$GITHUB_ENV"

- name: Install native build prerequisites
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y build-essential python3 pkg-config libssl-dev binutils git ca-certificates

- name: Set up Rust
run: |
set -euo pipefail
rustup toolchain install 1.85.1 --profile minimal
rustup default 1.85.1
rustup target add x86_64-unknown-linux-gnu

- name: Seed verified ripgrep install cache
run: node scripts/release/prime-ripgrep-cache.mjs

- name: Install dependencies
run: npm ci

- name: Fetch and check pinned Bun runtime
run: |
set -euo pipefail
node scripts/fetch-bun.mjs
test "$(dist-native/bun --version)" = 1.4.0

- name: Check smoke harness regressions
run: node --test scripts/release/packaged-server-paths.test.mjs scripts/release/server-archive-smoke.test.mjs

- name: Build canonical server archive
run: npm run server:bundle

- name: Verify canonical assets and record source provenance
env:
ASSET_NAME: ${{ steps.metadata.outputs.asset_name }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
git diff --exit-code
shopt -s nullglob dotglob
assets=(release/server/*)
test "${#assets[@]}" -eq 2
test -f "release/server/$ASSET_NAME"
test -f "release/server/$ASSET_NAME.sha256"
(cd release/server && sha256sum --check "$ASSET_NAME.sha256")
mkdir -p "$RUNNER_TEMP/server-acceptance"
node --input-type=module <<'NODE'
import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
const asset = process.env.ASSET_NAME;
const sha256 = readFileSync(`release/server/${asset}.sha256`, 'utf8').split(/\s+/)[0];
const evidence = { sourceSha: process.env.SOURCE_SHA, asset, sha256, node: process.versions.node, glibc: process.report.getReport().header.glibcVersionRuntime, sourceDateEpoch: process.env.SOURCE_DATE_EPOCH };
writeFileSync(path.join(process.env.RUNNER_TEMP, 'server-acceptance', 'build.json'), JSON.stringify(evidence, null, 2) + '\n');
NODE
cat "$RUNNER_TEMP/server-acceptance/build.json" >> "$GITHUB_STEP_SUMMARY"

- name: Upload canonical archive and checksum
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: gajae-app-server-linux-${{ steps.metadata.outputs.source_sha }}
path: release/server/*
if-no-files-found: error
compression-level: 0
retention-days: 14

- name: Upload build provenance
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: gajae-server-linux-provenance-${{ steps.metadata.outputs.source_sha }}
path: ${{ runner.temp }}/server-acceptance/build.json
if-no-files-found: error
retention-days: 14

archive-smoke:
name: Accept server archive on ${{ matrix.os }}
needs: build
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
glibc: '2.35'
- os: ubuntu-24.04
glibc: '2.39'
env:
SOURCE_SHA: ${{ needs.build.outputs.source_sha }}
ASSET_NAME: ${{ needs.build.outputs.asset_name }}
EXPECTED_GLIBC: ${{ matrix.glibc }}
steps:
- name: Checkout smoke harness at the build source commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.build.outputs.source_sha }}
persist-credentials: false

- name: Set up pinned host Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22.22.2'
architecture: x64

- name: Check validation host
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
test "$(uname -s)" = Linux
test "$(uname -m)" = x86_64
test "$(getconf GNU_LIBC_VERSION)" = "glibc $EXPECTED_GLIBC"
test "$(node --version)" = v22.22.2
# Intentionally no npm install in validation: resolve only shipped deps.
test ! -e node_modules
ARCHIVE_ROOT="$RUNNER_TEMP/gajae-server-archive"
EVIDENCE_ROOT="$RUNNER_TEMP/server-acceptance"
echo "ARCHIVE_ROOT=$ARCHIVE_ROOT" >> "$GITHUB_ENV"
echo "EVIDENCE_ROOT=$EVIDENCE_ROOT" >> "$GITHUB_ENV"
mkdir -p "$EVIDENCE_ROOT"
printf 'source=%s\nhost=%s\nnode=%s\n' "$SOURCE_SHA" "$(getconf GNU_LIBC_VERSION)" "$(node --version)" > "$EVIDENCE_ROOT/host.txt"

- name: Download the build archive
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: gajae-app-server-linux-${{ needs.build.outputs.source_sha }}
path: ${{ runner.temp }}/server-assets

- name: Download build provenance
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: gajae-server-linux-provenance-${{ needs.build.outputs.source_sha }}
path: ${{ runner.temp }}/server-acceptance

- name: Verify and extract the same archive outside the checkout
run: |
set -euo pipefail
shopt -s nullglob dotglob
assets=("$RUNNER_TEMP/server-assets/"*)
test "${#assets[@]}" -eq 2
test -f "$RUNNER_TEMP/server-assets/$ASSET_NAME"
test -f "$RUNNER_TEMP/server-assets/$ASSET_NAME.sha256"
(cd "$RUNNER_TEMP/server-assets" && sha256sum --check "$ASSET_NAME.sha256")
node --input-type=module <<'NODE'
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
const evidence = JSON.parse(readFileSync(path.join(process.env.EVIDENCE_ROOT, 'build.json'), 'utf8'));
const checksum = readFileSync(path.join(process.env.RUNNER_TEMP, 'server-assets', `${process.env.ASSET_NAME}.sha256`), 'utf8').split(/\s+/)[0];
assert.equal(evidence.sourceSha, process.env.SOURCE_SHA);
assert.equal(evidence.asset, process.env.ASSET_NAME);
assert.equal(evidence.sha256, checksum);
assert.equal(evidence.node, '22.22.2');
assert.equal(evidence.glibc, '2.35');
NODE
mkdir "$ARCHIVE_ROOT"
tar -xzf "$RUNNER_TEMP/server-assets/$ASSET_NAME" -C "$ARCHIVE_ROOT"
VERSION="$(node -p "require('./package.json').version")"
ARCHIVE_VERSION="$(node -p 'require(process.env.ARCHIVE_ROOT + "/package.json").version')"
test "$ARCHIVE_VERSION" = "$VERSION"

- name: Check authenticated startup, native closure, worker, abort and replay
run: |
set -euo pipefail
node scripts/release/smoke-packaged-server.mjs --server-archive-root "$ARCHIVE_ROOT" 2>&1 | tee "$EVIDENCE_ROOT/standard.log"

- name: Check data survival across graceful shutdown and restart
run: |
set -euo pipefail
node scripts/release/smoke-packaged-server.mjs --server-archive-root "$ARCHIVE_ROOT" --data-survival 2>&1 | tee "$EVIDENCE_ROOT/data-survival.log"

- name: Record acceptance
run: |
set -euo pipefail
printf 'Accepted %s on %s (glibc %s), source %s\n' "$ASSET_NAME" "$RUNNER_OS" "$EXPECTED_GLIBC" "$SOURCE_SHA" >> "$GITHUB_STEP_SUMMARY"

- name: Upload acceptance evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: gajae-server-linux-evidence-${{ matrix.os }}-${{ needs.build.outputs.source_sha }}
path: ${{ runner.temp }}/server-acceptance/
if-no-files-found: warn
retention-days: 14
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ All notable changes to Gajae Code App are documented in this file. Current and
future desktop and server artifacts are published only through
[GitHub Releases](https://github.com/devswha/gajae-code-app/releases).

## 2.0.0-beta.9 (2026-09-06)

- Managed session worktrees and visible, persistent goals with pause, resume
and cancellation. Delegated work inherits the parent's account, model,
permissions and cancellation through app-owned sessions.
- GJC conversation search, provider-qualified model choices, stable model
picker layout, and correct replay/history reconciliation across reconnects.
- Preserve streaming tool metadata and errors, and wait for confirmed worker
cleanup before reporting a stopped run.
- Native Linux deb/AppImage packaging and separate Linux server archive
acceptance on Ubuntu 22.04 and 24.04.
- Desktop UI preferences survive relaunch through a preserved loopback origin.
macOS acceptance supports separate WebKit and server profiles without copying
the user's data or credentials.
- GJC SDK 0.16.4, signed-release validation, exact server archive identity
checks, and source-bound artifact verification.

## 2.0.0-beta.8 (2026-09-03)

Same-day follow-up to beta.7, driven by first-install testing on a clean
Expand Down
50 changes: 50 additions & 0 deletions docs/DESKTOP-QA-PROFILE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Isolated macOS desktop acceptance

The installed binary accepts an explicit `--qa-profile /absolute/directory`
on macOS 14 or newer. Ordinary launches keep the normal user profile. QA
launches use a separate persistent WebKit data-store UUID, application/agent
database, home, scratch projects, browser caches and instance lock. The server
environment is rebuilt from an allowlist, so parent provider keys, custom
agent paths, Node options and shell configuration are not inherited.

Use a fresh empty directory outside the checkout and an accepted app copy:

```sh
QA_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/gajae-desktop-qa.XXXXXX")"
QA_ROOT="$(cd "$QA_ROOT" && pwd -P)"
"/absolute/path/Gajae Code App.app/Contents/MacOS/gajae-app-desktop" \
--qa-profile "$QA_ROOT"
```

Only empty directories or existing profiles with a matching root-bound
`desktop-qa-profile.json` are accepted. Existing user/project directories,
copied profile manifests and symlinked managed directories are refused. Do not
copy the normal agent credential database into QA. The profile does not grant
credentials, change app permissions, or disable workflow guards.

Quit and relaunch the same binary with the same argument to check persistence.
The desktop stores its first verified loopback port in `desktop-port` in the
profile's app data directory and reuses it. This keeps local UI settings on the
same origin. Per-launch API credentials still rotate, and PID/health identity
verification precedes navigation. An occupied saved port causes recovery rather
than silently choosing a different origin; release the conflicting listener
and Retry. The normal desktop uses its own app-local data directory for this
record. A first launch cannot recover preferences from older random ports.
Use a second empty profile to check that sidebar data and appearance settings
are independent. Verify the supervised Node process exits after quitting.
The dedicated WebKit store is managed by macOS and persists independently of
the filesystem profile; keep its UUID with the QA evidence. Deleting the QA
directory alone does not erase that WebKit store. QA profiles are not portable.
No production browser profile is inspected or copied by this mechanism.

macOS 11–13 still support normal app launches, but QA mode refuses startup
there rather than silently falling back to WebKit's default store. Other
platforms reject this option. A disposable OS account remains useful for
first-install permissions/LaunchServices testing and is required on older
macOS versions. Profile-based GUI checks do not claim clean-machine coverage.

Record the source commit/tree, artifact hashes, profile UUIDs and separate
results for launch, sign-in link, settings/project persistence, fresh-profile
isolation, quit and child-process cleanup. Do not put authentication callback
URLs, cookies, keys or passwords in the record. OMG skill execution is outside
the current acceptance scope at the user's request.
Loading