mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
* 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>
176 lines
6 KiB
JavaScript
176 lines
6 KiB
JavaScript
/**
|
|
* Writes apps/desktop/build/install-stamp.json with the git ref the desktop
|
|
* .exe should pin to at first-launch bootstrap time. This file ships inside
|
|
* the packaged app via electron-builder's extraResources entry and is read
|
|
* by electron/main.ts to drive the install.ps1 stage bootstrap flow.
|
|
*
|
|
* Schema (subject to bump via STAMP_SCHEMA_VERSION):
|
|
* {
|
|
* "schemaVersion": 1,
|
|
* "commit": "<40-char SHA>",
|
|
* "branch": "<branch name>",
|
|
* "builtAt": "<ISO 8601 UTC timestamp>",
|
|
* "dirty": true|false,
|
|
* "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 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")
|
|
const OUT_FILE = join(OUT_DIR, "install-stamp.json")
|
|
|
|
function tryExec(cmd, opts) {
|
|
try {
|
|
return execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], ...opts }).trim()
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function fromCI(env = process.env) {
|
|
const sha = env.GITHUB_SHA
|
|
if (!sha) return null
|
|
const branch = env.GITHUB_REF_NAME || env.GITHUB_HEAD_REF || null
|
|
return {
|
|
commit: sha,
|
|
branch: branch,
|
|
dirty: false, // CI builds from a checkout-of-ref by definition
|
|
source: "ci"
|
|
}
|
|
}
|
|
|
|
export function fromLocalGit(repoRoot = REPO_ROOT, execFn = tryExec) {
|
|
const sha = execFn("git rev-parse HEAD", { cwd: repoRoot })
|
|
if (!sha) return null
|
|
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 = execFn("git status --porcelain -uno", { cwd: repoRoot })
|
|
const dirty = status !== null && status.length > 0
|
|
return {
|
|
commit: sha,
|
|
branch: branch === "HEAD" ? null : branch, // detached HEAD -> null
|
|
dirty: dirty,
|
|
source: "local"
|
|
}
|
|
}
|
|
|
|
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 = 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" +
|
|
" - `git rev-parse HEAD` failed at " +
|
|
REPO_ROOT +
|
|
"\n" +
|
|
"Packaged builds require a git ref to pin first-launch install.ps1\n" +
|
|
"against. Run from a git checkout or set $GITHUB_SHA explicitly."
|
|
)
|
|
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" +
|
|
" Pinning to " +
|
|
stamp.commit.slice(0, 12) +
|
|
" but the packaged code may differ from that commit.\n" +
|
|
" Commit your changes before publishing this build."
|
|
)
|
|
}
|
|
|
|
const payload = {
|
|
schemaVersion: STAMP_SCHEMA_VERSION,
|
|
commit: stamp.commit,
|
|
branch: stamp.branch,
|
|
builtAt: new Date().toISOString(),
|
|
dirty: stamp.dirty,
|
|
source: stamp.source
|
|
}
|
|
|
|
mkdirSync(OUT_DIR, { recursive: true })
|
|
writeFileSync(OUT_FILE, JSON.stringify(payload, null, 2) + "\n", "utf8")
|
|
console.log(
|
|
"[write-build-stamp] wrote " +
|
|
relative(REPO_ROOT, OUT_FILE) +
|
|
" -> " +
|
|
stamp.commit.slice(0, 12) +
|
|
(stamp.branch ? " (" + stamp.branch + ")" : "") +
|
|
(stamp.dirty ? " [DIRTY]" : "") +
|
|
(stamp.source === "fallback" ? " [FALLBACK]" : "")
|
|
)
|
|
}
|
|
|
|
if (isMain(import.meta.url)) {
|
|
main()
|
|
}
|