mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 15:11:42 +00:00
CLI: include commit hash in --version output (#39712)
* CLI: include commit hash in --version output * fix(version): harden commit SHA resolution and keep output consistent * CLI: keep install checks compatible with commit-tagged version output * fix(cli): include commit hash in root version fast path * test(cli): allow null commit-hash mocks * Installer: share version parser across install scripts * Installer: avoid sourcing helpers from stdin cwd * CLI: note commit-tagged version output * CLI: anchor commit hash resolution to module root * CLI: harden commit hash resolution * CLI: fix commit hash lookup edge cases * CLI: prefer live git metadata in dev builds * CLI: keep git lookup inside package root * Infra: tolerate invalid moduleUrl hints * CLI: cache baked commit metadata fallbacks * CLI: align changelog attribution with prep gate * CLI: restore changelog contributor credit --------- Co-authored-by: echoVic <echovic@163.com> Co-authored-by: echoVic <echoVic@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveGitHeadPath } from "./git-root.js";
|
||||
import { resolveOpenClawPackageRootSync } from "./openclaw-root.js";
|
||||
|
||||
const formatCommit = (value?: string | null) => {
|
||||
if (!value) {
|
||||
@@ -11,10 +13,127 @@ const formatCommit = (value?: string | null) => {
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
return trimmed.length > 7 ? trimmed.slice(0, 7) : trimmed;
|
||||
const match = trimmed.match(/[0-9a-fA-F]{7,40}/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return match[0].slice(0, 7).toLowerCase();
|
||||
};
|
||||
|
||||
let cachedCommit: string | null | undefined;
|
||||
const cachedGitCommitBySearchDir = new Map<string, string | null>();
|
||||
|
||||
function isMissingPathError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
return code === "ENOENT" || code === "ENOTDIR";
|
||||
}
|
||||
|
||||
const resolveCommitSearchDir = (options: { cwd?: string; moduleUrl?: string }) => {
|
||||
if (options.cwd) {
|
||||
return path.resolve(options.cwd);
|
||||
}
|
||||
if (options.moduleUrl) {
|
||||
try {
|
||||
return path.dirname(fileURLToPath(options.moduleUrl));
|
||||
} catch {
|
||||
// moduleUrl is not a valid file:// URL; fall back to process.cwd().
|
||||
}
|
||||
}
|
||||
return process.cwd();
|
||||
};
|
||||
|
||||
/** Read at most `limit` bytes from a file to avoid unbounded reads. */
|
||||
const safeReadFilePrefix = (filePath: string, limit = 256) => {
|
||||
const fd = fs.openSync(filePath, "r");
|
||||
try {
|
||||
const buf = Buffer.alloc(limit);
|
||||
const bytesRead = fs.readSync(fd, buf, 0, limit, 0);
|
||||
return buf.subarray(0, bytesRead).toString("utf-8");
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
};
|
||||
|
||||
const cacheGitCommit = (searchDir: string, commit: string | null) => {
|
||||
cachedGitCommitBySearchDir.set(searchDir, commit);
|
||||
return commit;
|
||||
};
|
||||
|
||||
const resolveGitLookupDepth = (searchDir: string, packageRoot: string | null) => {
|
||||
if (!packageRoot) {
|
||||
return undefined;
|
||||
}
|
||||
const relative = path.relative(packageRoot, searchDir);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
return undefined;
|
||||
}
|
||||
const depth = relative ? relative.split(path.sep).filter(Boolean).length : 0;
|
||||
return depth + 1;
|
||||
};
|
||||
|
||||
const readCommitFromGit = (
|
||||
searchDir: string,
|
||||
packageRoot: string | null,
|
||||
): string | null | undefined => {
|
||||
const headPath = resolveGitHeadPath(searchDir, {
|
||||
maxDepth: resolveGitLookupDepth(searchDir, packageRoot),
|
||||
});
|
||||
if (!headPath) {
|
||||
return undefined;
|
||||
}
|
||||
const head = fs.readFileSync(headPath, "utf-8").trim();
|
||||
if (!head) {
|
||||
return null;
|
||||
}
|
||||
if (head.startsWith("ref:")) {
|
||||
const ref = head.replace(/^ref:\s*/i, "").trim();
|
||||
const refPath = resolveRefPath(headPath, ref);
|
||||
if (!refPath) {
|
||||
return null;
|
||||
}
|
||||
const refHash = safeReadFilePrefix(refPath).trim();
|
||||
return formatCommit(refHash);
|
||||
}
|
||||
return formatCommit(head);
|
||||
};
|
||||
|
||||
const resolveGitRefsBase = (headPath: string) => {
|
||||
const gitDir = path.dirname(headPath);
|
||||
try {
|
||||
const commonDir = safeReadFilePrefix(path.join(gitDir, "commondir")).trim();
|
||||
if (commonDir) {
|
||||
return path.resolve(gitDir, commonDir);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isMissingPathError(error)) {
|
||||
throw error;
|
||||
}
|
||||
// Plain repo git dirs do not have commondir.
|
||||
}
|
||||
return gitDir;
|
||||
};
|
||||
|
||||
/** Safely resolve a git ref path, rejecting traversal attacks from a crafted HEAD file. */
|
||||
const resolveRefPath = (headPath: string, ref: string) => {
|
||||
if (!ref.startsWith("refs/")) {
|
||||
return null;
|
||||
}
|
||||
if (path.isAbsolute(ref)) {
|
||||
return null;
|
||||
}
|
||||
if (ref.split(/[/]/).includes("..")) {
|
||||
return null;
|
||||
}
|
||||
const refsBase = resolveGitRefsBase(headPath);
|
||||
const resolved = path.resolve(refsBase, ref);
|
||||
const rel = path.relative(refsBase, resolved);
|
||||
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
|
||||
return null;
|
||||
}
|
||||
return resolved;
|
||||
};
|
||||
|
||||
const readCommitFromPackageJson = () => {
|
||||
try {
|
||||
@@ -52,49 +171,46 @@ const readCommitFromBuildInfo = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const resolveCommitHash = (options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}) => {
|
||||
if (cachedCommit !== undefined) {
|
||||
return cachedCommit;
|
||||
}
|
||||
export const resolveCommitHash = (
|
||||
options: {
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
moduleUrl?: string;
|
||||
} = {},
|
||||
) => {
|
||||
const env = options.env ?? process.env;
|
||||
const envCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim();
|
||||
const normalized = formatCommit(envCommit);
|
||||
if (normalized) {
|
||||
cachedCommit = normalized;
|
||||
return cachedCommit;
|
||||
return normalized;
|
||||
}
|
||||
const searchDir = resolveCommitSearchDir(options);
|
||||
if (cachedGitCommitBySearchDir.has(searchDir)) {
|
||||
return cachedGitCommitBySearchDir.get(searchDir) ?? null;
|
||||
}
|
||||
const packageRoot = resolveOpenClawPackageRootSync({
|
||||
cwd: options.cwd,
|
||||
moduleUrl: options.moduleUrl,
|
||||
});
|
||||
try {
|
||||
const gitCommit = readCommitFromGit(searchDir, packageRoot);
|
||||
if (gitCommit !== undefined) {
|
||||
return cacheGitCommit(searchDir, gitCommit);
|
||||
}
|
||||
} catch {
|
||||
// Fall through to baked metadata for packaged installs that are not in a live checkout.
|
||||
}
|
||||
const buildInfoCommit = readCommitFromBuildInfo();
|
||||
if (buildInfoCommit) {
|
||||
cachedCommit = buildInfoCommit;
|
||||
return cachedCommit;
|
||||
return cacheGitCommit(searchDir, buildInfoCommit);
|
||||
}
|
||||
const pkgCommit = readCommitFromPackageJson();
|
||||
if (pkgCommit) {
|
||||
cachedCommit = pkgCommit;
|
||||
return cachedCommit;
|
||||
return cacheGitCommit(searchDir, pkgCommit);
|
||||
}
|
||||
try {
|
||||
const headPath = resolveGitHeadPath(options.cwd ?? process.cwd());
|
||||
if (!headPath) {
|
||||
cachedCommit = null;
|
||||
return cachedCommit;
|
||||
}
|
||||
const head = fs.readFileSync(headPath, "utf-8").trim();
|
||||
if (!head) {
|
||||
cachedCommit = null;
|
||||
return cachedCommit;
|
||||
}
|
||||
if (head.startsWith("ref:")) {
|
||||
const ref = head.replace(/^ref:\s*/i, "").trim();
|
||||
const refPath = path.resolve(path.dirname(headPath), ref);
|
||||
const refHash = fs.readFileSync(refPath, "utf-8").trim();
|
||||
cachedCommit = formatCommit(refHash);
|
||||
return cachedCommit;
|
||||
}
|
||||
cachedCommit = formatCommit(head);
|
||||
return cachedCommit;
|
||||
return cacheGitCommit(searchDir, readCommitFromGit(searchDir, packageRoot) ?? null);
|
||||
} catch {
|
||||
cachedCommit = null;
|
||||
return cachedCommit;
|
||||
return cacheGitCommit(searchDir, null);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user