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: 3 additions & 3 deletions .github/workflows/desktop-package-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ jobs:
run: pnpm broker-packs:build
timeout-minutes: 10

# Keep this until the Windows managed Git runtime fully replaces dugite's
# embedded payload. It catches a skipped dugite postinstall before a
# package artifact reaches a tester.
# macOS still uses dugite's embedded Git. Windows keeps only dugite's JS
# wrapper and routes it to managed PortableGit in the packaged smoke.
- name: Verify bundled git (dugite) was fetched
if: runner.os == 'macOS'
shell: bash
run: |
node -e "const{existsSync}=require('fs');const{dirname,join}=require('path');const g=join(dirname(require.resolve('dugite/package.json')),'git');if(!existsSync(g)){console.error('dugite embedded git MISSING at '+g);process.exit(1)}console.log('dugite embedded git OK '+g)"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ jobs:
- name: Build optional Broker Packs
run: pnpm broker-packs:build

# Fail loud if dugite's postinstall was skipped (e.g. dropped from
# pnpm.onlyBuiltDependencies) — otherwise node_modules/dugite/git/ is
# silently empty and workspace creation breaks at runtime, not build.
# macOS still uses dugite's embedded Git. Windows keeps only dugite's JS
# wrapper and routes it to managed PortableGit in the packaged smoke.
- name: Verify bundled git (dugite) was fetched
if: runner.os == 'macOS'
run: |
node -e "const{existsSync}=require('fs');const{dirname,join}=require('path');const g=join(dirname(require.resolve('dugite/package.json')),'git');if(!existsSync(g)){console.error('dugite embedded git MISSING at '+g);process.exit(1)}console.log('dugite embedded git OK '+g)"

Expand Down
27 changes: 17 additions & 10 deletions docs/managed-workspace-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,11 @@ Source/dev falls back to `node` from the contributor environment. Keep the publi
JavaScript directly makes behavior depend on the host Node version and the
nearest `package.json` module type.

The Windows package currently retains dugite's embedded Git payload as well as
PortableGit. This duplication is intentional until all Workspace Git call
sites have moved behind an OpenAlice-owned wrapper.
The Windows package retains dugite's JavaScript execution wrapper but excludes
its embedded Git payload. `LOCAL_GIT_DIRECTORY` routes every dugite call to the
same pinned PortableGit tree that supplies Workspace Bash. macOS continues to
ship dugite's embedded Git because its packaged path does not need a separate
managed Unix shell payload.

### Windows workspace shell preference

Expand Down Expand Up @@ -344,9 +346,9 @@ Keep these true together:
- `vendor/**` remains in the Electron builder file list.
- `asar` remains disabled while packaged scripts and binaries are executed
from the resource tree.
- `dugite` remains in `pnpm.onlyBuiltDependencies` until its embedded payload
is deliberately removed; skipping its postinstall silently produces an
incomplete package.
- `dugite` remains in `pnpm.onlyBuiltDependencies` because macOS packages use
its embedded Git. The Windows builder excludes `node_modules/dugite/git/**`,
keeps the JS wrapper, and must route it through managed PortableGit.
- Pi and PortableGit versions, download URLs, and checksums remain pinned in
`scripts/vendor-managed-runtime.mjs`.
- Managed `fd` and `ripgrep` versions, release URLs, checksums, binaries, and
Expand Down Expand Up @@ -384,6 +386,10 @@ contract:
when structured assistant output is decoded and the created issue is visible
from the external `/api/issues` surface.

The focused Windows toolchain smoke additionally loads the packaged dugite JS
wrapper with no embedded dugite Git present, then performs a real
`init`/`add`/`commit`/`status` cycle through managed PortableGit.

The second assertion deliberately uses an observable Workspace side effect,
not a model claiming that a command succeeded. The run emits a versioned JSON
receipt whose individual checks make PATH, injection, CLI transport, runtime
Expand Down Expand Up @@ -475,10 +481,11 @@ A release-facing change should also verify a clean-machine flow:

## Known Follow-up

PortableGit and dugite's embedded Git are still duplicated on Windows. The
next cleanup is to introduce an OpenAlice-owned Git execution wrapper, migrate
Workspace/template call sites to it, and remove dugite only after macOS and
Windows packaged smokes remain green.
OpenAlice still imports dugite directly at several Workspace and template call
sites. A future OpenAlice-owned Git execution wrapper can centralize timeouts,
errors, and environment policy and eventually replace the dugite dependency.
That refactor is no longer required to keep duplicate Git binaries out of the
Windows package.

That cleanup must not weaken the first-run contract: install OpenAlice,
configure a credential, open a Workspace, and let Alice work.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@
"target": [
"nsis"
],
"icon": "apps/desktop/build/icon.ico"
"icon": "apps/desktop/build/icon.ico",
"files": [
"!node_modules/dugite/git/**"
]
},
"nsis": {
"oneClick": false,
Expand Down
13 changes: 12 additions & 1 deletion scripts/assert-desktop-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const BASE_REQUIRED_FILES = [
'src/workspaces/cli/bin/alice-uta.cmd',
'src/workspaces/templates/_common.mjs',
'src/workspaces/templates/chat/bootstrap.mjs',
'node_modules/dugite/package.json',
'node_modules/dugite/build/lib/index.js',
'vendor/manifest.json',
'vendor/pi/package.json',
'vendor/pi/node_modules/@earendil-works/pi-coding-agent/dist/cli.js',
Expand Down Expand Up @@ -124,6 +126,13 @@ export function assertDesktopPackage(options = {}) {
}

if (platform === 'win32') {
const embeddedDugiteGit = join(appRoot, 'node_modules', 'dugite', 'git')
if (existsSync(embeddedDugiteGit)) {
errors.push(
'[desktop-package] Windows must use managed PortableGit; dugite\'s embedded Git payload is forbidden',
)
}

const git = manifest?.git?.[platformArch]
if (!git) {
errors.push(`[desktop-package] expected manifest.git.${platformArch} for Windows managed Git Bash`)
Expand Down Expand Up @@ -171,7 +180,9 @@ function platformRequiredFiles(platform, platformArch) {
`vendor/git/${platformArch}/bin/bash.exe`,
`vendor/git/${platformArch}/bin/sh.exe`,
]
: []
: platform === 'darwin'
? ['node_modules/dugite/git/bin/git']
: []
return [...searchTools, ...git]
}

Expand Down
34 changes: 34 additions & 0 deletions scripts/assert-desktop-package.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('assertDesktopPackage', () => {
const appRoot = join(root, 'mac-arm64/OpenAlice.app/Contents/Resources/app')
writeBasePackage(appRoot, { ...piManifest(), ...searchToolsManifest('darwin-arm64') })
writeSearchToolFiles(appRoot, 'darwin-arm64')
writePackageFile(appRoot, 'node_modules/dugite/git/bin/git')

const result = assertDesktopPackage({ packageRoot: root, repoRoot: root, arch: 'arm64' })

Expand Down Expand Up @@ -125,12 +126,45 @@ describe('assertDesktopPackage', () => {
}
})

it('rejects dugite embedded Git in Windows packages', () => {
const root = mkdtempSync(join(tmpdir(), 'openalice-package-win-dugite-git-'))
try {
const appRoot = join(root, 'win-unpacked/resources/app')
writeBasePackage(appRoot, {
...piManifest(),
...searchToolsManifest('win32-x64', true),
git: {
'win32-x64': {
version: '2.55.0.2',
path: 'vendor/git/win32-x64',
gitBin: 'cmd/git.exe',
shellPath: 'bin/bash.exe',
shPath: 'bin/sh.exe',
},
},
})
writePackageFile(appRoot, 'vendor/git/win32-x64/cmd/git.exe')
writePackageFile(appRoot, 'vendor/git/win32-x64/bin/bash.exe')
writePackageFile(appRoot, 'vendor/git/win32-x64/bin/sh.exe')
writeSearchToolFiles(appRoot, 'win32-x64', true)
writePackageFile(appRoot, 'node_modules/dugite/git/cmd/git.exe')

const result = assertDesktopPackage({ packageRoot: root, repoRoot: root, arch: 'x64' })

expect(result.ok).toBe(false)
expect(result.errors.join('\n')).toContain('dugite\'s embedded Git payload is forbidden')
} finally {
rmSync(root, { recursive: true, force: true })
}
})

it('rejects optional broker SDKs bundled into the desktop app', () => {
const root = mkdtempSync(join(tmpdir(), 'openalice-package-broker-sdk-'))
try {
const appRoot = join(root, 'mac-arm64/OpenAlice.app/Contents/Resources/app')
writeBasePackage(appRoot, { ...piManifest(), ...searchToolsManifest('darwin-arm64') })
writeSearchToolFiles(appRoot, 'darwin-arm64')
writePackageFile(appRoot, 'node_modules/dugite/git/bin/git')
writePackageFile(appRoot, 'node_modules/.pnpm/ccxt@4.5.38/package.json')
writePackageFile(appRoot, 'node_modules/longbridge/package.json')

Expand Down
39 changes: 39 additions & 0 deletions scripts/smoke-packaged-toolchain.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function buildPackagedToolchainSmokePlan(packageResult) {
const gitExe = join(gitRoot, git.gitBin)
const bashExe = join(gitRoot, git.shellPath)
const shExe = join(gitRoot, git.shPath)
const dugiteEntry = join(packageResult.appRoot, 'node_modules', 'dugite', 'build', 'lib', 'index.js')
const workspaceCliDir = join(packageResult.appRoot, 'src', 'workspaces', 'cli', 'bin')
const toolchainPath = (Array.isArray(git.toolchainPaths) ? git.toolchainPaths : ['cmd', 'bin', 'usr/bin'])
.map((entry) => join(gitRoot, entry))
Expand All @@ -136,6 +137,44 @@ export function buildPackagedToolchainSmokePlan(packageResult) {
args: ['--version'],
expectStdout: /^git version /m,
})
const dugiteProbe = `
const { mkdtempSync, rmSync, writeFileSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join } = require('node:path')
const { exec } = require(${JSON.stringify(dugiteEntry)})
const root = mkdtempSync(join(tmpdir(), 'openalice-dugite-portable-git-'))
const run = async (args) => {
const result = await exec(args, root)
if (result.exitCode !== 0) {
throw new Error('git ' + args.join(' ') + ' failed: ' + String(result.stderr).slice(0, 500))
}
return result
}
;(async () => {
try {
await run(['init', '-q'])
writeFileSync(join(root, 'smoke.txt'), 'portable git through dugite\\n')
await run(['add', 'smoke.txt'])
await run(['-c', 'user.email=smoke@openalice.local', '-c', 'user.name=OpenAlice Smoke', 'commit', '-q', '-m', 'smoke'])
const status = await run(['status', '--porcelain'])
if (status.stdout.trim()) throw new Error('git status was not clean: ' + status.stdout)
const version = await run(['--version'])
console.log('OPENALICE_DUGITE_PORTABLE_GIT_OK ' + version.stdout.trim())
} finally {
rmSync(root, { recursive: true, force: true })
}
})().catch((error) => { console.error(error); process.exit(1) })
`.trim()
commands.push({
label: 'dugite JS wrapper routes Git operations through managed PortableGit',
command: electron,
args: ['-e', dugiteProbe],
env: {
ELECTRON_RUN_AS_NODE: '1',
LOCAL_GIT_DIRECTORY: gitRoot,
},
expectStdout: /OPENALICE_DUGITE_PORTABLE_GIT_OK git version /,
})
commands.push({
label: 'managed bash.exe',
command: bashExe,
Expand Down
13 changes: 9 additions & 4 deletions scripts/smoke-packaged-toolchain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,23 @@ describe('buildPackagedToolchainSmokePlan', () => {
'managed Pi resolves packaged fd/rg without download',
'workspace CLI payload through packaged Electron Node',
'managed git.exe',
'dugite JS wrapper routes Git operations through managed PortableGit',
'managed bash.exe',
'managed sh.exe can resolve git and bash on PATH',
'Workspace CLI launcher through managed Git Bash',
'Workspace CLI transport env through managed Git Bash',
])
expect(plan.commands[2].command.replaceAll('\\', '/')).toContain('vendor/tools/win32-x64/bin/fd.exe')
expect(plan.commands[6].command.replaceAll('\\', '/')).toContain('vendor/git/win32-x64/cmd/git.exe')
expect(plan.commands[8].env?.PATH.replaceAll('\\', '/')).toContain('vendor/git/win32-x64/mingw64/bin')
expect(plan.commands[8].env?.PATH.replaceAll('\\', '/')).toContain('vendor/tools/win32-x64/bin')
expect(plan.commands[9].env?.OPENALICE_MANAGED_PI_NODE_PATH.replaceAll('\\', '/'))
expect(plan.commands[7].env?.LOCAL_GIT_DIRECTORY.replaceAll('\\', '/'))
.toContain('vendor/git/win32-x64')
expect(plan.commands[7].args.join('\n').replaceAll('\\', '/').replaceAll('//', '/'))
.toContain('node_modules/dugite/build/lib/index.js')
expect(plan.commands[9].env?.PATH.replaceAll('\\', '/')).toContain('vendor/git/win32-x64/mingw64/bin')
expect(plan.commands[9].env?.PATH.replaceAll('\\', '/')).toContain('vendor/tools/win32-x64/bin')
expect(plan.commands[10].env?.OPENALICE_MANAGED_PI_NODE_PATH.replaceAll('\\', '/'))
.toContain('win-unpacked/OpenAlice.exe')
expect(plan.commands[10].env?.OPENALICE_TOOL_URL).toBe('/cli')
expect(plan.commands[11].env?.OPENALICE_TOOL_URL).toBe('/cli')
} finally {
rmSync(root, { recursive: true, force: true })
}
Expand Down
Loading