mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-05-31 13:56:13 -07:00
feat(hooks): add Unity Git Hooks integration and runHookGroups
Built-in support for Unity Git Hooks (com.frostebite.unitygithooks): - Auto-detect UPM package in Packages/manifest.json - Run init-unity-lefthook.js before hook installation - Set CI-friendly env vars (disable background project mode) New gitHooksRunBeforeBuild input runs specific lefthook groups before the Unity build, allowing CI to trigger pre-commit or pre-push checks that normally only fire on git events. 35 unit tests covering detection, init, CI env, group execution, and failure handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -345,6 +345,14 @@ inputs:
|
||||
default: ''
|
||||
description:
|
||||
'Comma-separated list of hook names to skip even when gitHooksEnabled is true. Example: pre-push,post-merge'
|
||||
gitHooksRunBeforeBuild:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Comma-separated list of lefthook hook groups to run before the Unity build. Allows CI to
|
||||
trigger checks that normally only run on git events. Example: pre-commit,pre-push. Requires
|
||||
lefthook. Works with Unity Git Hooks (com.frostebite.unitygithooks) when installed as a UPM
|
||||
package — the init script runs automatically.'
|
||||
providerExecutable:
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
@@ -118,6 +118,7 @@ class BuildParameters {
|
||||
public lfsStoragePaths!: string;
|
||||
public gitHooksEnabled!: boolean;
|
||||
public gitHooksSkipList!: string;
|
||||
public gitHooksRunBeforeBuild!: string;
|
||||
public providerExecutable!: string;
|
||||
|
||||
public static shouldUseRetainedWorkspaceMode(buildParameters: BuildParameters) {
|
||||
@@ -267,6 +268,7 @@ class BuildParameters {
|
||||
lfsStoragePaths: Input.lfsStoragePaths,
|
||||
gitHooksEnabled: Input.gitHooksEnabled,
|
||||
gitHooksSkipList: Input.gitHooksSkipList,
|
||||
gitHooksRunBeforeBuild: Input.gitHooksRunBeforeBuild,
|
||||
providerExecutable: Input.providerExecutable,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -330,6 +330,10 @@ class Input {
|
||||
return Input.getInput('gitHooksSkipList') ?? '';
|
||||
}
|
||||
|
||||
static get gitHooksRunBeforeBuild(): string {
|
||||
return Input.getInput('gitHooksRunBeforeBuild') ?? '';
|
||||
}
|
||||
|
||||
static get providerExecutable(): string {
|
||||
return Input.getInput('providerExecutable') ?? '';
|
||||
}
|
||||
|
||||
@@ -61,36 +61,149 @@ describe('GitHooksService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureSkipList', () => {
|
||||
it('should return empty object for empty skip list', () => {
|
||||
const result = GitHooksService.configureSkipList([]);
|
||||
expect(result).toEqual({});
|
||||
describe('detectUnityGitHooks', () => {
|
||||
it('should return true when package is in manifest.json', () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify({
|
||||
dependencies: {
|
||||
'com.frostebite.unitygithooks': 'https://github.com/frostebite/UnityGitHooks.git#1.0.5',
|
||||
},
|
||||
}));
|
||||
|
||||
expect(GitHooksService.detectUnityGitHooks('/repo')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return LEFTHOOK_EXCLUDE with comma-separated hooks', () => {
|
||||
const result = GitHooksService.configureSkipList(['pre-commit', 'pre-push']);
|
||||
expect(result.LEFTHOOK_EXCLUDE).toBe('pre-commit,pre-push');
|
||||
it('should return false when package is not in manifest.json', () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify({
|
||||
dependencies: {
|
||||
'com.unity.textmeshpro': '3.0.6',
|
||||
},
|
||||
}));
|
||||
|
||||
expect(GitHooksService.detectUnityGitHooks('/repo')).toBe(false);
|
||||
});
|
||||
|
||||
it('should set HUSKY=0 when hooks are skipped', () => {
|
||||
const result = GitHooksService.configureSkipList(['pre-commit']);
|
||||
expect(result.HUSKY).toBe('0');
|
||||
it('should return false when manifest.json does not exist', () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
expect(GitHooksService.detectUnityGitHooks('/repo')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false on read error', () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readFileSync as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Permission denied');
|
||||
});
|
||||
|
||||
expect(GitHooksService.detectUnityGitHooks('/repo')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableHooks', () => {
|
||||
it('should set core.hooksPath to an empty directory', async () => {
|
||||
(mockFs.mkdirSync as jest.Mock).mockReturnValue(undefined);
|
||||
describe('findUnityGitHooksPackagePath', () => {
|
||||
it('should find versioned package directory', () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readdirSync as jest.Mock).mockReturnValue([
|
||||
'com.unity.textmeshpro@3.0.6',
|
||||
'com.frostebite.unitygithooks@1.0.5',
|
||||
'com.unity.ugui@1.0.0',
|
||||
]);
|
||||
|
||||
const result = GitHooksService.findUnityGitHooksPackagePath('/repo');
|
||||
expect(result).toContain('com.frostebite.unitygithooks@1.0.5');
|
||||
});
|
||||
|
||||
it('should return empty string when package not in cache', () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readdirSync as jest.Mock).mockReturnValue([
|
||||
'com.unity.textmeshpro@3.0.6',
|
||||
]);
|
||||
|
||||
const result = GitHooksService.findUnityGitHooksPackagePath('/repo');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when PackageCache does not exist', () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = GitHooksService.findUnityGitHooksPackagePath('/repo');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initUnityGitHooks', () => {
|
||||
it('should run the init script when found', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
|
||||
await GitHooksService.disableHooks('/repo');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readdirSync as jest.Mock).mockReturnValue([
|
||||
'com.frostebite.unitygithooks@1.0.5',
|
||||
]);
|
||||
|
||||
await GitHooksService.initUnityGitHooks('/repo');
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('git -C "/repo" config core.hooksPath'),
|
||||
expect.stringContaining('init-unity-lefthook.js'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip when package not found in cache', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
await GitHooksService.initUnityGitHooks('/repo');
|
||||
|
||||
expect(OrchestratorSystem.Run).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should warn when init script does not exist', async () => {
|
||||
const OrchestratorLogger = require('../core/orchestrator-logger').default;
|
||||
|
||||
(mockFs.existsSync as jest.Mock).mockImplementation((p: string) => {
|
||||
// PackageCache dir exists, but init script doesn't
|
||||
return !String(p).includes('init-unity-lefthook');
|
||||
});
|
||||
(mockFs.readdirSync as jest.Mock).mockReturnValue([
|
||||
'com.frostebite.unitygithooks@1.0.5',
|
||||
]);
|
||||
|
||||
await GitHooksService.initUnityGitHooks('/repo');
|
||||
|
||||
expect(OrchestratorLogger.logWarning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('init script not found'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should log warning on init failure', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
const OrchestratorLogger = require('../core/orchestrator-logger').default;
|
||||
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readdirSync as jest.Mock).mockReturnValue([
|
||||
'com.frostebite.unitygithooks@1.0.5',
|
||||
]);
|
||||
OrchestratorSystem.Run.mockRejectedValue(new Error('node not found'));
|
||||
|
||||
await GitHooksService.initUnityGitHooks('/repo');
|
||||
|
||||
expect(OrchestratorLogger.logWarning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('init failed'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureUnityGitHooksCIEnv', () => {
|
||||
it('should disable background project mode', () => {
|
||||
const env = GitHooksService.configureUnityGitHooksCIEnv();
|
||||
expect(env.UNITY_GITHOOKS_BACKGROUND_PROJECT_ENABLED).toBe('false');
|
||||
});
|
||||
|
||||
it('should set CI=true', () => {
|
||||
const env = GitHooksService.configureUnityGitHooksCIEnv();
|
||||
expect(env.CI).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('installHooks', () => {
|
||||
@@ -106,6 +219,52 @@ describe('GitHooksService', () => {
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(`cd "/repo" && npx lefthook install`, true);
|
||||
});
|
||||
|
||||
it('should init Unity Git Hooks before installing lefthook when detected', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
const OrchestratorLogger = require('../core/orchestrator-logger').default;
|
||||
const callOrder: string[] = [];
|
||||
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readFileSync as jest.Mock).mockReturnValue(
|
||||
`{"dependencies":{"com.frostebite.unitygithooks":"https://github.com/frostebite/UnityGitHooks.git"}}`,
|
||||
);
|
||||
(mockFs.readdirSync as jest.Mock).mockReturnValue([
|
||||
'com.frostebite.unitygithooks@1.0.5',
|
||||
]);
|
||||
|
||||
OrchestratorSystem.Run.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes('init-unity-lefthook')) {
|
||||
callOrder.push('init');
|
||||
} else if (cmd.includes('lefthook install')) {
|
||||
callOrder.push('install');
|
||||
}
|
||||
|
||||
return Promise.resolve('');
|
||||
});
|
||||
|
||||
await GitHooksService.installHooks('/repo');
|
||||
|
||||
// Init should happen before install
|
||||
expect(callOrder).toEqual(['init', 'install']);
|
||||
expect(OrchestratorLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Unity Git Hooks (UPM) detected'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set CI env vars when Unity Git Hooks detected', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readFileSync as jest.Mock).mockReturnValue(
|
||||
`{"dependencies":{"com.frostebite.unitygithooks":"1.0.5"}}`,
|
||||
);
|
||||
(mockFs.readdirSync as jest.Mock).mockReturnValue([
|
||||
'com.frostebite.unitygithooks@1.0.5',
|
||||
]);
|
||||
|
||||
await GitHooksService.installHooks('/repo');
|
||||
|
||||
expect(process.env.UNITY_GITHOOKS_BACKGROUND_PROJECT_ENABLED).toBe('false');
|
||||
});
|
||||
|
||||
it('should run npx husky install when husky is detected', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockImplementation((filePath: string) => {
|
||||
return String(filePath).endsWith('.husky');
|
||||
@@ -147,7 +306,120 @@ describe('GitHooksService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('runHookGroups', () => {
|
||||
it('should run each group via lefthook run', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
(mockFs.existsSync as jest.Mock).mockImplementation((filePath: string) => {
|
||||
return String(filePath).includes('lefthook.yml') && !String(filePath).startsWith('.');
|
||||
});
|
||||
OrchestratorSystem.Run.mockResolvedValue('');
|
||||
|
||||
const results = await GitHooksService.runHookGroups('/repo', ['pre-commit', 'pre-push']);
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
`cd "/repo" && npx lefthook run pre-commit`,
|
||||
true,
|
||||
);
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
`cd "/repo" && npx lefthook run pre-push`,
|
||||
true,
|
||||
);
|
||||
expect(results['pre-commit']).toBe(true);
|
||||
expect(results['pre-push']).toBe(true);
|
||||
});
|
||||
|
||||
it('should return empty results for empty groups', async () => {
|
||||
const results = await GitHooksService.runHookGroups('/repo', []);
|
||||
expect(results).toEqual({});
|
||||
});
|
||||
|
||||
it('should warn and return empty if not using lefthook', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
const OrchestratorLogger = require('../core/orchestrator-logger').default;
|
||||
|
||||
const results = await GitHooksService.runHookGroups('/repo', ['pre-commit']);
|
||||
|
||||
expect(results).toEqual({});
|
||||
expect(OrchestratorLogger.logWarning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('requires lefthook'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should mark failed groups as false', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
(mockFs.existsSync as jest.Mock).mockImplementation((filePath: string) => {
|
||||
return String(filePath).includes('lefthook.yml') && !String(filePath).startsWith('.');
|
||||
});
|
||||
|
||||
OrchestratorSystem.Run
|
||||
.mockResolvedValueOnce('') // pre-commit passes
|
||||
.mockRejectedValueOnce(new Error('tests failed')); // pre-push fails
|
||||
|
||||
const results = await GitHooksService.runHookGroups('/repo', ['pre-commit', 'pre-push']);
|
||||
|
||||
expect(results['pre-commit']).toBe(true);
|
||||
expect(results['pre-push']).toBe(false);
|
||||
});
|
||||
|
||||
it('should log each group result', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
const OrchestratorLogger = require('../core/orchestrator-logger').default;
|
||||
(mockFs.existsSync as jest.Mock).mockImplementation((filePath: string) => {
|
||||
return String(filePath).includes('lefthook.yml') && !String(filePath).startsWith('.');
|
||||
});
|
||||
|
||||
OrchestratorSystem.Run
|
||||
.mockResolvedValueOnce('')
|
||||
.mockRejectedValueOnce(new Error('check failed'));
|
||||
|
||||
await GitHooksService.runHookGroups('/repo', ['pre-commit', 'commit-msg']);
|
||||
|
||||
expect(OrchestratorLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("'pre-commit' passed"),
|
||||
);
|
||||
expect(OrchestratorLogger.logWarning).toHaveBeenCalledWith(
|
||||
expect.stringContaining("'commit-msg' failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureSkipList', () => {
|
||||
it('should return empty object for empty skip list', () => {
|
||||
const result = GitHooksService.configureSkipList([]);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should return LEFTHOOK_EXCLUDE with comma-separated hooks', () => {
|
||||
const result = GitHooksService.configureSkipList(['pre-commit', 'pre-push']);
|
||||
expect(result.LEFTHOOK_EXCLUDE).toBe('pre-commit,pre-push');
|
||||
});
|
||||
|
||||
it('should set HUSKY=0 when hooks are skipped', () => {
|
||||
const result = GitHooksService.configureSkipList(['pre-commit']);
|
||||
expect(result.HUSKY).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle single hook in skip list', () => {
|
||||
const result = GitHooksService.configureSkipList(['commit-msg']);
|
||||
expect(result.LEFTHOOK_EXCLUDE).toBe('commit-msg');
|
||||
expect(result.HUSKY).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableHooks', () => {
|
||||
it('should set core.hooksPath to an empty directory', async () => {
|
||||
(mockFs.mkdirSync as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
|
||||
await GitHooksService.disableHooks('/repo');
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('git -C "/repo" config core.hooksPath'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should log warning on failure to disable hooks', async () => {
|
||||
(mockFs.mkdirSync as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
@@ -163,11 +435,9 @@ describe('GitHooksService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureSkipList edge cases', () => {
|
||||
it('should handle single hook in skip list', () => {
|
||||
const result = GitHooksService.configureSkipList(['commit-msg']);
|
||||
expect(result.LEFTHOOK_EXCLUDE).toBe('commit-msg');
|
||||
expect(result.HUSKY).toBe('0');
|
||||
describe('constants', () => {
|
||||
it('should have correct package name', () => {
|
||||
expect(GitHooksService.UNITY_GIT_HOOKS_PACKAGE).toBe('com.frostebite.unitygithooks');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import { OrchestratorSystem } from '../core/orchestrator-system';
|
||||
import OrchestratorLogger from '../core/orchestrator-logger';
|
||||
|
||||
export class GitHooksService {
|
||||
static readonly UNITY_GIT_HOOKS_PACKAGE = 'com.frostebite.unitygithooks';
|
||||
|
||||
/**
|
||||
* Detect which git hook framework is configured in the repository.
|
||||
* Checks for lefthook and husky configuration files.
|
||||
@@ -23,8 +25,93 @@ export class GitHooksService {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if Unity Git Hooks (com.frostebite.unitygithooks) is installed as a UPM package.
|
||||
* Checks Packages/manifest.json for the package dependency.
|
||||
*/
|
||||
static detectUnityGitHooks(repoPath: string): boolean {
|
||||
const manifestPath = path.join(repoPath, 'Packages', 'manifest.json');
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(manifestPath, 'utf8');
|
||||
|
||||
return content.includes(GitHooksService.UNITY_GIT_HOOKS_PACKAGE);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Unity Git Hooks package directory in the Library/PackageCache.
|
||||
* Returns the path to the package directory, or empty string if not found.
|
||||
*/
|
||||
static findUnityGitHooksPackagePath(repoPath: string): string {
|
||||
const packageCacheDir = path.join(repoPath, 'Library', 'PackageCache');
|
||||
if (!fs.existsSync(packageCacheDir)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(packageCacheDir);
|
||||
const match = entries.find((entry) => entry.startsWith(GitHooksService.UNITY_GIT_HOOKS_PACKAGE));
|
||||
if (match) {
|
||||
return path.join(packageCacheDir, match);
|
||||
}
|
||||
} catch {
|
||||
// PackageCache not available
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Unity Git Hooks by running its init script.
|
||||
* This installs the required npm modules that the hooks depend on.
|
||||
* Should be called before installHooks() when Unity Git Hooks is detected.
|
||||
*/
|
||||
static async initUnityGitHooks(repoPath: string): Promise<void> {
|
||||
const packagePath = GitHooksService.findUnityGitHooksPackagePath(repoPath);
|
||||
if (!packagePath) {
|
||||
OrchestratorLogger.log(`[GitHooks] Unity Git Hooks package not found in Library/PackageCache, skipping init`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const initScript = path.join(packagePath, '~js', 'init-unity-lefthook.js');
|
||||
if (!fs.existsSync(initScript)) {
|
||||
OrchestratorLogger.logWarning(`[GitHooks] Unity Git Hooks init script not found at ${initScript}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
OrchestratorLogger.log(`[GitHooks] Initializing Unity Git Hooks from ${packagePath}`);
|
||||
|
||||
try {
|
||||
await OrchestratorSystem.Run(`cd "${repoPath}" && node "${initScript}"`, true);
|
||||
OrchestratorLogger.log(`[GitHooks] Unity Git Hooks initialized successfully`);
|
||||
} catch (error: any) {
|
||||
OrchestratorLogger.logWarning(`[GitHooks] Unity Git Hooks init failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure CI-friendly environment variables for Unity Git Hooks.
|
||||
* Disables background project mode (CI already has an isolated workspace)
|
||||
* and sets other env vars appropriate for headless CI environments.
|
||||
*/
|
||||
static configureUnityGitHooksCIEnv(): Record<string, string> {
|
||||
return {
|
||||
UNITY_GITHOOKS_BACKGROUND_PROJECT_ENABLED: 'false',
|
||||
CI: 'true',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install git hooks using the detected framework.
|
||||
* If Unity Git Hooks is detected, initializes it first.
|
||||
* Errors are caught and logged as warnings - hook installation should not fail the build.
|
||||
*/
|
||||
static async installHooks(repoPath: string): Promise<void> {
|
||||
@@ -38,6 +125,19 @@ export class GitHooksService {
|
||||
|
||||
OrchestratorLogger.log(`[GitHooks] Detected hook framework: ${framework}`);
|
||||
|
||||
// If Unity Git Hooks is present, initialize it before installing hooks
|
||||
if (framework === 'lefthook' && GitHooksService.detectUnityGitHooks(repoPath)) {
|
||||
OrchestratorLogger.log(`[GitHooks] Unity Git Hooks (UPM) detected, running init`);
|
||||
|
||||
// Set CI-friendly env vars
|
||||
const ciEnv = GitHooksService.configureUnityGitHooksCIEnv();
|
||||
for (const [key, value] of Object.entries(ciEnv)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
await GitHooksService.initUnityGitHooks(repoPath);
|
||||
}
|
||||
|
||||
try {
|
||||
if (framework === 'lefthook') {
|
||||
await OrchestratorSystem.Run(`cd "${repoPath}" && npx lefthook install`, true);
|
||||
@@ -51,6 +151,47 @@ export class GitHooksService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly run specific lefthook hook groups before the build.
|
||||
* This allows CI to trigger pre-commit, pre-push, or other checks
|
||||
* that would normally only run on git events.
|
||||
*
|
||||
* @param repoPath - Path to the repository
|
||||
* @param hookGroups - Lefthook group names to run (e.g., ['pre-commit', 'pre-push'])
|
||||
* @returns Map of group name to success/failure
|
||||
*/
|
||||
static async runHookGroups(repoPath: string, hookGroups: string[]): Promise<Record<string, boolean>> {
|
||||
const results: Record<string, boolean> = {};
|
||||
|
||||
if (hookGroups.length === 0) {
|
||||
return results;
|
||||
}
|
||||
|
||||
const framework = GitHooksService.detectHookFramework(repoPath);
|
||||
if (framework !== 'lefthook') {
|
||||
OrchestratorLogger.logWarning(
|
||||
`[GitHooks] runHookGroups requires lefthook, but detected: ${framework}`,
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
OrchestratorLogger.log(`[GitHooks] Running ${hookGroups.length} hook group(s): ${hookGroups.join(', ')}`);
|
||||
|
||||
for (const group of hookGroups) {
|
||||
try {
|
||||
await OrchestratorSystem.Run(`cd "${repoPath}" && npx lefthook run ${group}`, true);
|
||||
OrchestratorLogger.log(`[GitHooks] Hook group '${group}' passed`);
|
||||
results[group] = true;
|
||||
} catch (error: any) {
|
||||
OrchestratorLogger.logWarning(`[GitHooks] Hook group '${group}' failed: ${error.message}`);
|
||||
results[group] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return environment variables that will skip the listed hooks.
|
||||
* For lefthook: sets LEFTHOOK_EXCLUDE to a comma-separated list.
|
||||
|
||||
Reference in New Issue
Block a user