fix(desktop): stop hard-failing pack on non-git checkouts + fix ZIP-path autocrlf (supersedes #67643) (#67730)

* fix(desktop): allow write-build-stamp from non-git checkouts

Stop hard-failing npm pack when neither GITHUB_SHA nor git HEAD is
available (ZIP installs / broken .git). Emit an explicit fallback stamp
instead so local Windows desktop builds can finish (#50823).

* fix(desktop): treat fallback stamps as unpinned; harden Windows install

Keep all-zero fallback commits out of -Commit/--commit pins and fetch
install.ps1 by branch instead. After bootstrap, pin the marker to the
checkout HEAD so isBootstrapComplete accepts it. On Windows, force ZIP
checkout, seed GITHUB_SHA (ASCII-only install.ps1), and avoid the pack
stamp failure.

* fix(install): pin core.autocrlf=false before ZIP-path checkout (#50823 review)

The ZIP-fallback path added in #67643 runs `git checkout -f FETCH_HEAD`
before core.autocrlf gets pinned (which only happened later, on the
shared clone-path config). On Git for Windows -- where core.autocrlf
defaults to true -- that renormalizes the repo's LF text files to CRLF in
the working tree during checkout, leaving the freshly-created managed
checkout dirty versus HEAD and aborting the next `hermes update`. That is
the exact "dirty tree the user never touched" failure the surrounding
code already guards against (install.ps1:1461-1469, 1750-1753).

Move the `config core.autocrlf false` pin to run immediately after
`git init`, before the fetch/checkout. The later idempotent pin on the
shared clone path is retained so git-clone installs are unaffected.

Addresses teknium1's review on #67643 and supersedes it, preserving the
original author's two commits.

Co-authored-by: HexLab98 <8422520+HexLab98@users.noreply.github.com>

* chore(contributors): map austinpickett commit email for attribution

The check-attribution CI gate flagged austinpickett@users.noreply.github.com
as an unmapped commit-author email (introduced by the autocrlf fix commit
on this PR). Add the per-email mapping file as the gate instructs (the
legacy AUTHOR_MAP in scripts/release.py is frozen).

---------

Co-authored-by: HexLab98 <liruixinch@outlook.com>
Co-authored-by: austinpickett <austinpickett@users.noreply.github.com>
Co-authored-by: HexLab98 <8422520+HexLab98@users.noreply.github.com>
This commit is contained in:
Austin Pickett 2026-07-19 19:29:36 -04:00 committed by GitHub
parent 8142331616
commit bc6839aa37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 478 additions and 36 deletions

View file

@ -10,12 +10,16 @@ import {
buildPosixPinArgs,
cachedScriptPath,
hasExistingGitCheckout,
installRefForStamp,
installedAgentInstallScript,
isPinnedCommit,
resolveInstallScript,
resolveMarkerPinnedCommit,
runBootstrap
} from './bootstrap-runner'
const SCRIPT_NAME = process.platform === 'win32' ? 'install.ps1' : 'install.sh'
const ZERO_COMMIT = '0000000000000000000000000000000000000000'
function mkTmpHome() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-bootstrap-test-'))
@ -106,6 +110,83 @@ test('existing-checkout bootstrap args keep branch but skip the packaged commit
)
})
test('fallback install stamps use an unpinned branch ref', () => {
const stamp = { commit: ZERO_COMMIT, branch: 'main' }
assert.equal(isPinnedCommit(ZERO_COMMIT), false)
assert.deepEqual(installRefForStamp(stamp), {
ref: 'main',
cacheKey: 'fallback-main',
pinned: false
})
// Must NOT pass -Commit / --commit for the all-zero placeholder.
assert.deepEqual(buildPinArgs(stamp), ['-Branch', 'main'])
assert.deepEqual(
buildPosixPinArgs({
installStamp: stamp,
activeRoot: '/tmp/hermes',
hermesHome: '/tmp/home'
}),
['--dir', '/tmp/hermes', '--hermes-home', '/tmp/home', '--branch', 'main']
)
})
test('resolveMarkerPinnedCommit prefers real HEAD over fallback stamp zeros', () => {
const realHead = 'c'.repeat(40)
assert.equal(
resolveMarkerPinnedCommit({ commit: ZERO_COMMIT, branch: 'main' }, '/tmp/checkout', {
resolveHead: () => realHead
}),
realHead
)
assert.equal(
resolveMarkerPinnedCommit({ commit: 'd'.repeat(40), branch: 'main' }, '/tmp/checkout', {
resolveHead: () => realHead
}),
'd'.repeat(40),
'packaged real pin wins over checkout HEAD'
)
assert.equal(
resolveMarkerPinnedCommit({ commit: ZERO_COMMIT, branch: 'main' }, '/tmp/missing', {
resolveHead: () => null
}),
null
)
})
test('resolveInstallScript downloads fallback stamps by branch instead of zero commit', async () => {
const home = mkTmpHome()
try {
const logs = []
const refs = []
const result = await resolveInstallScript({
installStamp: { commit: ZERO_COMMIT, branch: 'main' },
sourceRepoRoot: null,
hermesHome: home,
emit: ev => logs.push(ev),
_download: async (ref, destPath) => {
refs.push(ref)
fs.mkdirSync(path.dirname(destPath), { recursive: true })
fs.writeFileSync(destPath, '#!/bin/sh\necho fallback branch\n')
return destPath
}
})
assert.deepEqual(refs, ['main'])
assert.equal(result.source, 'download')
assert.equal(result.commit, null)
assert.equal(result.path, cachedScriptPath(home, 'fallback-main'))
assert.ok(
logs.some(ev => /fallback, unpinned/.test(ev.line || '')),
'emits an unpinned fallback log line'
)
} finally {
fs.rmSync(home, { recursive: true, force: true })
}
})
test('resolveInstallScript prefers a cached script without touching the network', async () => {
const home = mkTmpHome()

View file

@ -32,7 +32,7 @@
* no UI consumes them yet)
*/
import { spawn } from 'node:child_process'
import { execFileSync, spawn } from 'node:child_process'
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import https from 'node:https'
@ -43,6 +43,117 @@ import { hiddenWindowsChildOptions } from './windows-child-options'
const IS_WINDOWS = process.platform === 'win32'
const STAMP_COMMIT_RE = /^[0-9a-f]{7,40}$/i
const FALLBACK_COMMIT_RE = /^0{7,40}$/
const FALLBACK_BRANCH = 'main'
function isPinnedCommit(commit) {
return typeof commit === 'string' && STAMP_COMMIT_RE.test(commit) && !FALLBACK_COMMIT_RE.test(commit)
}
type ExecGitFn = (args: string[], cwd: string) => string
type ResolveHeadFn = (activeRoot: string | null | undefined) => string | null
/**
* Read HEAD from a managed checkout. Used after bootstrap so fallback
* (all-zero) install stamps still produce a marker that
* isBootstrapComplete() accepts (pinnedCommit length >= 7).
*/
function resolveCheckoutHead(
activeRoot: string | null | undefined,
opts: { execGit?: ExecGitFn } = {}
): string | null {
if (!activeRoot) {
return null
}
const run: ExecGitFn =
opts.execGit ||
((args, cwd) =>
execFileSync('git', args, {
cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 15_000,
...hiddenWindowsChildOptions()
}).trim())
try {
const sha = run(['-c', 'windows.appendAtomically=false', 'rev-parse', 'HEAD'], activeRoot)
return isPinnedCommit(sha) ? sha : null
} catch {
return null
}
}
/** Prefer a real pin already written by install.ps1's bootstrap-marker stage. */
function readExistingPinnedCommit(activeRoot: string | null | undefined): string | null {
if (!activeRoot) {
return null
}
try {
const raw = fs.readFileSync(path.join(activeRoot, '.hermes-bootstrap-complete'), 'utf8')
const parsed = JSON.parse(raw)
return parsed && isPinnedCommit(parsed.pinnedCommit) ? parsed.pinnedCommit : null
} catch {
return null
}
}
/**
* Pick the commit to store on the bootstrap-complete marker.
* Packaged fallback stamps must NOT win (all-zero is not a real pin); after a
* successful install the checkout's HEAD (or install.ps1's marker) does.
*/
function resolveMarkerPinnedCommit(
installStamp: { commit?: string; branch?: string | null } | null | undefined,
activeRoot: string | null | undefined,
opts: { resolveHead?: ResolveHeadFn } = {}
): string | null {
const resolveHead = opts.resolveHead || resolveCheckoutHead
if (installStamp && isPinnedCommit(installStamp.commit)) {
return installStamp.commit
}
const head = resolveHead(activeRoot)
if (head) {
return head
}
return readExistingPinnedCommit(activeRoot)
}
/**
* Map an install stamp to the GitHub ref used to fetch install.ps1/sh.
* Real CI/git stamps pin an immutable SHA. Non-git fallback stamps carry an
* all-zero placeholder -- treat those as an unpinned branch ref so bootstrap
* never asks GitHub for commit 0000000... (#50823).
*/
function installRefForStamp(installStamp) {
if (installStamp && isPinnedCommit(installStamp.commit)) {
return {
ref: installStamp.commit,
cacheKey: installStamp.commit,
pinned: true
}
}
if (installStamp && typeof installStamp.commit === 'string' && FALLBACK_COMMIT_RE.test(installStamp.commit)) {
const ref = installStamp.branch || FALLBACK_BRANCH
return {
ref,
cacheKey: `fallback-${String(ref).replace(/[^0-9A-Za-z._-]/g, '_')}`,
pinned: false
}
}
return null
}
// Stages flagged needs_user_input=true in the manifest are skipped by the
// runner (passed -NonInteractive to install.ps1, which the install script
@ -119,12 +230,13 @@ function cachedScriptPath(hermesHome, commit) {
return path.join(bootstrapCacheDir(hermesHome), `install-${commit}.${process.platform === 'win32' ? 'ps1' : 'sh'}`)
}
function downloadInstallScript(commit, destPath) {
// Fetch from GitHub raw at the pinned commit. The raw URL with a SHA
// is immutable (unlike a branch ref), so we don't need integrity
// verification beyond "did the file we wrote pass a syntax probe."
function downloadInstallScript(ref, destPath) {
// Fetch from GitHub raw at the install ref. Normal production builds pass a
// pinned SHA (immutable). Non-git fallback builds pass an unpinned branch
// ref so local builds can still bootstrap without pretending the all-zero
// placeholder is a real GitHub commit.
const scriptName = installScriptName()
const url = `https://raw.githubusercontent.com/NousResearch/hermes-agent/${commit}/scripts/${scriptName}`
const url = `https://raw.githubusercontent.com/NousResearch/hermes-agent/${ref}/scripts/${scriptName}`
return new Promise((resolve, reject) => {
fs.mkdirSync(path.dirname(destPath), { recursive: true })
@ -223,38 +335,45 @@ async function resolveInstallScript({
return { path: localScript, source: 'local', kind: installScriptKind() }
}
// 2. Packaged path: download from GitHub at the pinned commit (1B's stamp).
if (!installStamp || !installStamp.commit || !STAMP_COMMIT_RE.test(installStamp.commit)) {
// 2. Packaged path: download from GitHub at the install stamp's ref.
// Non-git fallback builds carry an all-zero commit; treat that as an
// unpinned branch ref instead of trying to fetch a non-existent SHA.
const installRef = installRefForStamp(installStamp)
if (!installRef) {
throw new Error(
`Cannot resolve ${installScriptName()}: no SOURCE_REPO_ROOT and no install stamp. ` +
'This packaged build was produced without a valid build-time stamp.'
)
}
const cached = cachedScriptPath(hermesHome, installStamp.commit)
const cached = cachedScriptPath(hermesHome, installRef.cacheKey)
const resolvedCommit = installRef.pinned ? installRef.ref : null
try {
await fsp.access(cached, fs.constants.R_OK)
emit({
type: 'log',
line: `[bootstrap] using cached ${installScriptName()} for ${installStamp.commit.slice(0, 12)}`
line: `[bootstrap] using cached ${installScriptName()} for ${installRef.ref.slice(0, 12)}`
})
return { path: cached, source: 'cache', commit: installStamp.commit, kind: installScriptKind() }
return { path: cached, source: 'cache', commit: resolvedCommit, kind: installScriptKind() }
} catch {
// not cached; download
}
emit({
type: 'log',
line: `[bootstrap] fetching ${installScriptName()} for ${installStamp.commit.slice(0, 12)} from GitHub`
line:
`[bootstrap] fetching ${installScriptName()} for ${installRef.ref.slice(0, 12)} from GitHub` +
(installRef.pinned ? '' : ' (fallback, unpinned)')
})
try {
await _download(installStamp.commit, cached)
await _download(installRef.ref, cached)
emit({ type: 'log', line: `[bootstrap] saved to ${cached}` })
return { path: cached, source: 'download', commit: installStamp.commit, kind: installScriptKind() }
return { path: cached, source: 'download', commit: resolvedCommit, 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
@ -275,10 +394,10 @@ async function resolveInstallScript({
fs.mkdirSync(path.dirname(cached), { recursive: true })
fs.copyFileSync(installed, cached)
return { path: cached, source: 'installed-agent', commit: installStamp.commit, kind: installScriptKind() }
return { path: cached, source: 'installed-agent', commit: resolvedCommit, 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() }
return { path: installed, source: 'installed-agent', commit: resolvedCommit, kind: installScriptKind() }
}
}
@ -544,11 +663,12 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome
// Build the installer branch/pin args from the install stamp. The commit pin
// is fresh-install only: once a managed checkout already exists, bootstrap is
// a repair/update path and must not let an old packaged app detach the checkout
// back to the commit baked into that app.
// back to the commit baked into that app. All-zero fallback stamps are never
// passed as -Commit/--commit — only the branch is used (#50823 / #50864 review).
function buildPinArgs(installStamp, { pinCommit = true } = {}) {
const args = []
if (pinCommit && installStamp && installStamp.commit) {
if (pinCommit && installStamp && isPinnedCommit(installStamp.commit)) {
args.push('-Commit', installStamp.commit)
}
@ -566,7 +686,7 @@ function buildPosixPinArgs({ installStamp, activeRoot, hermesHome, pinCommit = t
args.push('--branch', installStamp.branch)
}
if (pinCommit && installStamp && installStamp.commit) {
if (pinCommit && installStamp && isPinnedCommit(installStamp.commit)) {
args.push('--commit', installStamp.commit)
}
@ -860,9 +980,28 @@ async function runBootstrap(opts) {
}
}
// 4. Write the bootstrap-complete marker.
// 4. Write the bootstrap-complete marker. Fallback (all-zero) stamps are
// not real pins -- resolve HEAD from the checkout we just installed so
// isBootstrapComplete() (pinnedCommit.length >= 7) accepts the marker
// instead of re-running bootstrap on every launch (#50823 review).
const pinnedCommit = resolveMarkerPinnedCommit(installStamp, activeRoot)
if (!pinnedCommit) {
emit({
type: 'log',
line:
'[bootstrap] WARNING: could not resolve a real pinnedCommit for the ' +
'bootstrap-complete marker; subsequent launches may re-run bootstrap'
})
} else if (installStamp && !isPinnedCommit(installStamp.commit)) {
emit({
type: 'log',
line: `[bootstrap] fallback stamp resolved marker pin to ${pinnedCommit.slice(0, 12)} from checkout`
})
}
const markerPayload = {
pinnedCommit: installStamp ? installStamp.commit : null,
pinnedCommit,
pinnedBranch: installStamp ? installStamp.branch : null
}
@ -888,10 +1027,14 @@ export {
buildPosixPinArgs,
cachedScriptPath,
hasExistingGitCheckout,
installRefForStamp,
installedAgentInstallScript,
isPinnedCommit,
// Exposed for testability
parseStageResult,
resolveCheckoutHead,
resolveInstallScript,
resolveLocalInstallScript,
resolveMarkerPinnedCommit,
runBootstrap
}

View file

@ -11,25 +11,33 @@
* "branch": "<branch name>",
* "builtAt": "<ISO 8601 UTC timestamp>",
* "dirty": true|false,
* "source": "ci" | "local"
* "source": "ci" | "local" | "fallback"
* }
*
* Source preference order:
* 1. CI env vars ($GITHUB_SHA / $GITHUB_REF_NAME) -- avoid edge cases with
* shallow clones, detached HEADs, etc. in CI.
* 2. Local `git rev-parse` against the parent repo (../..).
* 3. Fallback stamp for local/personal builds from non-git source trees
* (ZIP extract, interrupted clone with no HEAD, etc.).
*
* Dev / out-of-repo builds without git produce an explicit error rather than
* silently writing an unstamped manifest -- the packaged app refuses to
* bootstrap without a stamp.
* Dev / out-of-repo builds without git produce an explicit fallback stamp
* rather than aborting the whole build. Bootstrap treats the all-zero
* commit as unpinned and follows the branch instead of fetching a fake SHA.
*/
import { mkdirSync, writeFileSync } from "fs"
import { resolve, join, relative } from "path"
import { execSync } from "child_process"
import { isMain } from "./utils.mjs"
const STAMP_SCHEMA_VERSION = 1
/** All-zero placeholder used when no real commit can be resolved. */
export const FALLBACK_COMMIT = "0000000000000000000000000000000000000000"
export const FALLBACK_BRANCH = "main"
const DESKTOP_ROOT = resolve(import.meta.dirname, "..")
const REPO_ROOT = resolve(DESKTOP_ROOT, "..", "..")
const OUT_DIR = join(DESKTOP_ROOT, "build")
@ -43,10 +51,10 @@ function tryExec(cmd, opts) {
}
}
function fromCI() {
const sha = process.env.GITHUB_SHA
export function fromCI(env = process.env) {
const sha = env.GITHUB_SHA
if (!sha) return null
const branch = process.env.GITHUB_REF_NAME || process.env.GITHUB_HEAD_REF || null
const branch = env.GITHUB_REF_NAME || env.GITHUB_HEAD_REF || null
return {
commit: sha,
branch: branch,
@ -55,17 +63,17 @@ function fromCI() {
}
}
function fromLocalGit() {
const sha = tryExec("git rev-parse HEAD", { cwd: REPO_ROOT })
export function fromLocalGit(repoRoot = REPO_ROOT, execFn = tryExec) {
const sha = execFn("git rev-parse HEAD", { cwd: repoRoot })
if (!sha) return null
const branch = tryExec("git rev-parse --abbrev-ref HEAD", { cwd: REPO_ROOT })
const branch = execFn("git rev-parse --abbrev-ref HEAD", { cwd: repoRoot })
// `git status --porcelain -uno` is empty iff tracked files match HEAD.
// We exclude untracked files (-uno) intentionally: a developer who's
// checked out an installer scratch dir alongside the repo shouldn't
// poison every local build with a [DIRTY] stamp. We DO care about
// tracked-but-modified files because those mean the .exe content
// differs from the commit being pinned.
const status = tryExec("git status --porcelain -uno", { cwd: REPO_ROOT })
const status = execFn("git status --porcelain -uno", { cwd: repoRoot })
const dirty = status !== null && status.length > 0
return {
commit: sha,
@ -75,9 +83,41 @@ function fromLocalGit() {
}
}
export function fromFallback(branch = FALLBACK_BRANCH) {
// Non-git builds (ZIP download, bootstrap installer without a resolvable
// HEAD) cannot determine a real commit. Use a placeholder so local /
// personal builds can still complete. The desktop bootstrap treats the
// all-zero commit as "unknown" and falls back to an unpinned branch
// bootstrap instead of trying to fetch a non-existent GitHub commit.
return {
commit: FALLBACK_COMMIT,
branch: branch || FALLBACK_BRANCH,
dirty: false,
source: "fallback"
}
}
/**
* Resolve the install stamp without writing it. Pure enough for unit tests:
* inject env / execFn / repoRoot to simulate CI, local git, or no-git trees.
*/
export function resolveStamp({
env = process.env,
repoRoot = REPO_ROOT,
execFn = tryExec,
fallbackBranch = FALLBACK_BRANCH
} = {}) {
return fromCI(env) || fromLocalGit(repoRoot, execFn) || fromFallback(fallbackBranch)
}
export function isFallbackCommit(commit) {
return typeof commit === "string" && /^0{7,40}$/.test(commit)
}
function main() {
const stamp = fromCI() || fromLocalGit()
const stamp = resolveStamp()
if (!stamp || !stamp.commit) {
// Should not happen — fromFallback() always provides a commit.
console.error(
"[write-build-stamp] ERROR: could not determine git commit.\n" +
" - $GITHUB_SHA not set\n" +
@ -90,6 +130,15 @@ function main() {
process.exit(1)
}
if (isFallbackCommit(stamp.commit)) {
console.warn(
"[write-build-stamp] WARNING: no git commit found (non-git checkout?).\n" +
" Using placeholder commit — the packaged app will fall back to the\n" +
" default branch for first-launch bootstrap. For production builds,\n" +
" run from a git checkout or set $GITHUB_SHA."
)
}
if (stamp.dirty) {
console.warn(
"[write-build-stamp] WARNING: working tree is dirty.\n" +
@ -117,8 +166,11 @@ function main() {
" -> " +
stamp.commit.slice(0, 12) +
(stamp.branch ? " (" + stamp.branch + ")" : "") +
(stamp.dirty ? " [DIRTY]" : "")
(stamp.dirty ? " [DIRTY]" : "") +
(stamp.source === "fallback" ? " [FALLBACK]" : "")
)
}
main()
if (isMain(import.meta.url)) {
main()
}

View file

@ -0,0 +1,86 @@
import assert from 'node:assert/strict'
import { test } from 'vitest'
import {
FALLBACK_BRANCH,
FALLBACK_COMMIT,
fromCI,
fromFallback,
fromLocalGit,
isFallbackCommit,
resolveStamp
} from './write-build-stamp.mjs'
test('fromCI reads GITHUB_SHA / GITHUB_REF_NAME', () => {
assert.deepEqual(
fromCI({ GITHUB_SHA: 'a'.repeat(40), GITHUB_REF_NAME: 'release' }),
{ commit: 'a'.repeat(40), branch: 'release', dirty: false, source: 'ci' }
)
assert.equal(fromCI({}), null)
})
test('fromLocalGit returns null when git rev-parse fails', () => {
const stamp = fromLocalGit('/tmp/not-a-repo', () => null)
assert.equal(stamp, null)
})
test('fromLocalGit reads HEAD + branch + dirty status', () => {
const calls = []
const execFn = (cmd) => {
calls.push(cmd)
if (cmd === 'git rev-parse HEAD') return 'b'.repeat(40)
if (cmd === 'git rev-parse --abbrev-ref HEAD') return 'main'
if (cmd === 'git status --porcelain -uno') return ' M apps/desktop/package.json'
return null
}
assert.deepEqual(fromLocalGit('/repo', execFn), {
commit: 'b'.repeat(40),
branch: 'main',
dirty: true,
source: 'local'
})
assert.ok(calls.includes('git rev-parse HEAD'))
})
test('fromFallback uses the all-zero placeholder commit', () => {
assert.deepEqual(fromFallback(), {
commit: FALLBACK_COMMIT,
branch: FALLBACK_BRANCH,
dirty: false,
source: 'fallback'
})
assert.equal(isFallbackCommit(FALLBACK_COMMIT), true)
assert.equal(isFallbackCommit('a'.repeat(40)), false)
})
test('resolveStamp prefers CI over local git over fallback', () => {
const ci = resolveStamp({
env: { GITHUB_SHA: 'c'.repeat(40), GITHUB_REF_NAME: 'main' },
execFn: () => 'should-not-run'
})
assert.equal(ci.source, 'ci')
assert.equal(ci.commit, 'c'.repeat(40))
const local = resolveStamp({
env: {},
execFn: (cmd) => {
if (cmd === 'git rev-parse HEAD') return 'd'.repeat(40)
if (cmd === 'git rev-parse --abbrev-ref HEAD') return 'main'
if (cmd === 'git status --porcelain -uno') return ''
return null
}
})
assert.equal(local.source, 'local')
assert.equal(local.commit, 'd'.repeat(40))
assert.equal(local.dirty, false)
})
test('resolveStamp falls back when neither CI nor git is available', () => {
const stamp = resolveStamp({ env: {}, execFn: () => null })
assert.deepEqual(stamp, {
commit: FALLBACK_COMMIT,
branch: FALLBACK_BRANCH,
dirty: false,
source: 'fallback'
})
})

View file

@ -0,0 +1,2 @@
austinpickett
# PR #67730 salvage of #67643

View file

@ -1686,11 +1686,54 @@ function Install-Repository {
Move-Item $extractedDir.FullName $InstallDir -Force
Write-Success "Downloaded and extracted"
# Initialize git repo so updates work later
# Initialize git repo so updates work later. A bare
# `git init` leaves NO HEAD -- desktop's write-build-stamp
# then hard-fails with "could not determine git commit"
# (#50823 / #61657). Fetch the requested ref and force-check
# it out (-f) so untracked ZIP files cannot block checkout.
Push-Location $InstallDir
git -c windows.appendAtomically=false init 2>$null
git -c windows.appendAtomically=false config windows.appendAtomically false 2>$null
# Pin autocrlf=false BEFORE the checkout below. Git for Windows
# defaults to core.autocrlf=true, which would renormalize the
# repo's LF text files to CRLF in the working tree during
# `checkout -f FETCH_HEAD` -- leaving this freshly-created
# managed checkout dirty vs HEAD and aborting the next
# `hermes update` (see the notes at the shared clone-path
# config below and install.ps1:1461-1469). The later pin on
# the shared path is idempotent and still covers git clones.
git -c windows.appendAtomically=false config core.autocrlf false 2>$null
git remote add origin $RepoUrlHttps 2>$null
$fetchRef = if ($Commit) { $Commit } elseif ($Tag) { "refs/tags/$Tag" } else { $Branch }
Write-Info "Fetching $fetchRef so the ZIP checkout has a resolvable HEAD..."
$prevZipEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
git -c windows.appendAtomically=false fetch --depth 1 origin $fetchRef 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
if ($Commit -or $Tag) {
git -c windows.appendAtomically=false checkout -f --detach FETCH_HEAD 2>&1 | Out-Null
} else {
git -c windows.appendAtomically=false checkout -f -B $Branch FETCH_HEAD 2>&1 | Out-Null
}
if ($LASTEXITCODE -eq 0) {
Write-Success "ZIP checkout pinned to $fetchRef"
} else {
# Checkout blocked, but FETCH_HEAD still has a SHA we can stamp with.
$fetchSha = & git -c windows.appendAtomically=false rev-parse FETCH_HEAD 2>$null
if ($LASTEXITCODE -eq 0 -and $fetchSha) {
if (-not $env:GITHUB_SHA) { $env:GITHUB_SHA = ("$fetchSha").Trim() }
Write-Warn "ZIP checkout failed; seeded GITHUB_SHA from FETCH_HEAD for desktop stamp"
} else {
Write-Warn "ZIP extract succeeded but git checkout failed -- desktop build may need `$env:GITHUB_SHA"
}
}
} else {
Write-Warn "ZIP extract succeeded but git fetch of $fetchRef failed -- desktop build may need `$env:GITHUB_SHA"
}
} finally {
$ErrorActionPreference = $prevZipEAP
}
Pop-Location
Write-Success "Git repo initialized for future updates"
@ -2888,6 +2931,41 @@ function Install-Desktop {
# for some other tool, electron-builder would still try to sign.
Write-Info "Building desktop app (this takes 1-3 minutes)..."
$buildLog = "$env:TEMP\hermes-desktop-build-$(Get-Random).log"
# Seed GITHUB_SHA for write-build-stamp.mjs. The stamp prefers CI env vars
# over `git rev-parse`, so this covers: (1) node can't find git.exe on PATH
# even though this PowerShell session can, (2) ZIP/init trees that still
# lack a HEAD after a failed post-extract fetch. Without it the desktop
# pack dies with "could not determine git commit" (#50823).
if (-not $env:GITHUB_SHA) {
if ($Commit) {
$env:GITHUB_SHA = $Commit
} else {
Push-Location $InstallDir
try {
$global:LASTEXITCODE = 0
$resolvedSha = & git -c windows.appendAtomically=false rev-parse HEAD 2>$null
if ($LASTEXITCODE -ne 0 -or -not $resolvedSha) {
# ZIP path may have FETCH_HEAD after a fetch even when HEAD is unset.
$global:LASTEXITCODE = 0
$resolvedSha = & git -c windows.appendAtomically=false rev-parse FETCH_HEAD 2>$null
}
if ($LASTEXITCODE -eq 0 -and $resolvedSha) {
$env:GITHUB_SHA = ("$resolvedSha").Trim()
}
} catch { } finally {
Pop-Location
}
}
}
if (-not $env:GITHUB_REF_NAME) {
$env:GITHUB_REF_NAME = if ($Branch) { $Branch } else { "main" }
}
if ($env:GITHUB_SHA) {
$shaPreview = if ($env:GITHUB_SHA.Length -ge 12) { $env:GITHUB_SHA.Substring(0, 12) } else { $env:GITHUB_SHA }
Write-Info "Desktop build stamp: $shaPreview ($($env:GITHUB_REF_NAME))"
} else {
Write-Warn "Could not resolve a git commit for the desktop stamp -- write-build-stamp will use its non-git fallback"
}
Push-Location $desktopDir
$prevEAP = $ErrorActionPreference
$prevCSCAuto = $env:CSC_IDENTITY_AUTO_DISCOVERY