fix(desktop): bootstrap falls back to installed agent install.sh on GitHub 404

Packaged Desktop first-launch bootstrap no longer dies with a fatal HTTP
404 when install-stamp.json pins a commit that isn't fetchable from GitHub.

This only happens for locally-built desktop apps: write-build-stamp.cjs's
fromLocalGit() pins `git rev-parse HEAD`, which can be an unpushed commit
or dirty tree. CI builds stamp $GITHUB_SHA and are unaffected. The fix
unblocks the dev / self-builder workflow.

resolveInstallScript() now wraps the GitHub download in try/catch; on
failure it resolves ~/.hermes/hermes-agent/scripts/install.sh (the
already-installed agent checkout), copies it into bootstrap-cache, and
returns it as source 'installed-agent'. If the cache copy fails (read-only
FS), it uses the source path directly. With no installed checkout to fall
back to, the original error rethrows unchanged.

Download is now injectable via an optional _download param so the fallback
path is tested hermetically (no network).

Reported with a precise repro and suggested fix by @Tamaz-sujashvili (#40815).

Co-authored-by: Tamaz-sujashvili <56168197+Tamaz-sujashvili@users.noreply.github.com>
This commit is contained in:
teknium1
2026-06-07 03:46:12 -07:00
committed by Teknium
co-authored by Tamaz-sujashvili
parent 9e63109522
commit 02aad08acf
2 changed files with 159 additions and 5 deletions
+47 -4
View File
@@ -76,6 +76,21 @@ function bootstrapCacheDir(hermesHome) {
return path.join(hermesHome, 'bootstrap-cache')
}
// The install.sh / install.ps1 that ships inside the already-installed agent
// checkout under ~/.hermes/hermes-agent. Used as a last-resort fallback when
// the pinned commit can't be fetched from GitHub (e.g. a locally-built desktop
// app stamped to an unpushed HEAD).
function installedAgentInstallScript(hermesHome) {
if (!hermesHome) return null
const candidate = path.join(hermesHome, 'hermes-agent', 'scripts', installScriptName())
try {
fs.accessSync(candidate, fs.constants.R_OK)
return candidate
} catch {
return null
}
}
function cachedScriptPath(hermesHome, commit) {
return path.join(bootstrapCacheDir(hermesHome), `install-${commit}.${process.platform === 'win32' ? 'ps1' : 'sh'}`)
}
@@ -155,7 +170,7 @@ function downloadInstallScript(commit, destPath) {
})
}
async function resolveInstallScript({ installStamp, sourceRepoRoot, hermesHome, emit }) {
async function resolveInstallScript({ installStamp, sourceRepoRoot, hermesHome, emit, _download = downloadInstallScript }) {
// 1. Dev shortcut: prefer a local checkout's installer so we can iterate
// without pushing. SOURCE_REPO_ROOT comes from main.cjs (path.resolve
// of APP_ROOT/../..).
@@ -189,9 +204,35 @@ async function resolveInstallScript({ installStamp, sourceRepoRoot, hermesHome,
type: 'log',
line: `[bootstrap] fetching ${installScriptName()} for ${installStamp.commit.slice(0, 12)} from GitHub`
})
await downloadInstallScript(installStamp.commit, cached)
emit({ type: 'log', line: `[bootstrap] saved to ${cached}` })
return { path: cached, source: 'download', commit: installStamp.commit, kind: installScriptKind() }
try {
await _download(installStamp.commit, cached)
emit({ type: 'log', line: `[bootstrap] saved to ${cached}` })
return { path: cached, source: 'download', commit: installStamp.commit, kind: installScriptKind() }
} catch (err) {
// The pinned commit may not be fetchable from GitHub -- most commonly a
// locally-built desktop app stamped to an unpushed HEAD (see
// write-build-stamp.cjs fromLocalGit). Fall back to the installer that
// ships inside the already-installed agent checkout so dev/self-builds can
// still bootstrap instead of dying with a fatal 404.
const installed = installedAgentInstallScript(hermesHome)
if (installed) {
emit({
type: 'log',
line:
`[bootstrap] GitHub fetch failed (${err.message}); ` +
`falling back to installed agent ${installScriptName()} at ${installed}`
})
try {
fs.mkdirSync(path.dirname(cached), { recursive: true })
fs.copyFileSync(installed, cached)
return { path: cached, source: 'installed-agent', commit: installStamp.commit, kind: installScriptKind() }
} catch {
// Cache copy failed (read-only FS, etc.) -- use the source path directly.
return { path: installed, source: 'installed-agent', commit: installStamp.commit, kind: installScriptKind() }
}
}
throw err
}
}
// ---------------------------------------------------------------------------
@@ -673,5 +714,7 @@ module.exports = {
// Exposed for testability
parseStageResult,
resolveLocalInstallScript,
resolveInstallScript,
installedAgentInstallScript,
cachedScriptPath
}