mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-05-31 22:06:16 -07:00
feat(lfs): add built-in elastic-git-storage support with auto-install
First-class support for elastic-git-storage as a custom LFS transfer agent. When lfsTransferAgent is set to "elastic-git-storage" (or "elastic-git-storage@v1.0.0" for a specific version), the service automatically finds or installs the agent from GitHub releases, then configures it via git config. Supports version pinning via @version suffix in the agent value, eliminating the need for a separate version parameter. Platform and architecture detection handles linux/darwin/windows on amd64/arm64. 37 unit tests covering detection, PATH lookup, installation, version parsing, and configuration delegation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -320,7 +320,9 @@ inputs:
|
||||
required: false
|
||||
default: ''
|
||||
description:
|
||||
'Path to a custom Git LFS transfer agent executable (e.g. elastic-git-storage). When set, the agent is registered
|
||||
'Custom Git LFS transfer agent. Set to "elastic-git-storage" for built-in support (auto-installs
|
||||
from GitHub releases). Append @version for a specific release (e.g. "elastic-git-storage@v1.0.0").
|
||||
Or provide a path to any custom transfer agent executable. When set, the agent is registered
|
||||
via git config before LFS operations.'
|
||||
lfsTransferAgentArgs:
|
||||
required: false
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import { ElasticGitStorageService } from './elastic-git-storage-service';
|
||||
|
||||
jest.mock('node:fs');
|
||||
jest.mock('node:os');
|
||||
jest.mock('../core/orchestrator-system', () => ({
|
||||
OrchestratorSystem: {
|
||||
Run: jest.fn().mockResolvedValue(''),
|
||||
},
|
||||
}));
|
||||
jest.mock('../core/orchestrator-logger', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
log: jest.fn(),
|
||||
logWarning: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./lfs-agent-service', () => ({
|
||||
LfsAgentService: {
|
||||
configure: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockFs = fs as jest.Mocked<typeof fs>;
|
||||
const mockOs = os as jest.Mocked<typeof os>;
|
||||
|
||||
describe('ElasticGitStorageService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('parseAgentValue', () => {
|
||||
it('should parse name without version', () => {
|
||||
const result = ElasticGitStorageService.parseAgentValue('elastic-git-storage');
|
||||
expect(result.name).toBe('elastic-git-storage');
|
||||
expect(result.version).toBe('latest');
|
||||
});
|
||||
|
||||
it('should parse name@version', () => {
|
||||
const result = ElasticGitStorageService.parseAgentValue('elastic-git-storage@v1.0.0');
|
||||
expect(result.name).toBe('elastic-git-storage');
|
||||
expect(result.version).toBe('v1.0.0');
|
||||
});
|
||||
|
||||
it('should parse name@latest', () => {
|
||||
const result = ElasticGitStorageService.parseAgentValue('elastic-git-storage@latest');
|
||||
expect(result.name).toBe('elastic-git-storage');
|
||||
expect(result.version).toBe('latest');
|
||||
});
|
||||
|
||||
it('should handle trailing @ as latest', () => {
|
||||
const result = ElasticGitStorageService.parseAgentValue('elastic-git-storage@');
|
||||
expect(result.name).toBe('elastic-git-storage');
|
||||
expect(result.version).toBe('latest');
|
||||
});
|
||||
|
||||
it('should handle whitespace', () => {
|
||||
const result = ElasticGitStorageService.parseAgentValue(' elastic-git-storage@v2.0.0 ');
|
||||
expect(result.name).toBe('elastic-git-storage');
|
||||
expect(result.version).toBe('v2.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isElasticGitStorage', () => {
|
||||
it('should match exact name', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('elastic-git-storage')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match with .exe extension', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('elastic-git-storage.exe')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match with @version suffix', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('elastic-git-storage@v1.0.0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match with @latest suffix', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('elastic-git-storage@latest')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match forward-slash path', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('/usr/local/bin/elastic-git-storage')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match backslash path', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('C:\\tools\\elastic-git-storage')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match path with .exe', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('C:\\tools\\elastic-git-storage.exe')).toBe(true);
|
||||
});
|
||||
|
||||
it('should be case-insensitive', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('Elastic-Git-Storage')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle whitespace', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage(' elastic-git-storage ')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match other agent names', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('lfs-folderstore')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty string', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not match partial names', () => {
|
||||
expect(ElasticGitStorageService.isElasticGitStorage('my-elastic-git-storage-v2')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findInstalled', () => {
|
||||
it('should find on PATH via which/where', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
OrchestratorSystem.Run.mockResolvedValue('/usr/local/bin/elastic-git-storage\n');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await ElasticGitStorageService.findInstalled();
|
||||
expect(result).toBe('/usr/local/bin/elastic-git-storage');
|
||||
});
|
||||
|
||||
it('should use where on windows', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('win32');
|
||||
OrchestratorSystem.Run.mockResolvedValue('C:\\tools\\elastic-git-storage.exe\n');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await ElasticGitStorageService.findInstalled();
|
||||
expect(result).toBe('C:\\tools\\elastic-git-storage.exe');
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('where'),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should check common install locations when not on PATH', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.homedir.mockReturnValue('/home/runner');
|
||||
OrchestratorSystem.Run.mockRejectedValue(new Error('not found'));
|
||||
(mockFs.existsSync as jest.Mock)
|
||||
.mockReturnValueOnce(false) // RUNNER_TOOL_CACHE
|
||||
.mockReturnValueOnce(true); // /usr/local/bin
|
||||
|
||||
const result = await ElasticGitStorageService.findInstalled();
|
||||
expect(result).toBe('/usr/local/bin/elastic-git-storage');
|
||||
});
|
||||
|
||||
it('should return empty string when not found anywhere', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.homedir.mockReturnValue('/home/runner');
|
||||
OrchestratorSystem.Run.mockRejectedValue(new Error('not found'));
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await ElasticGitStorageService.findInstalled();
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should check windows-specific locations on win32', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('win32');
|
||||
mockOs.homedir.mockReturnValue('C:\\Users\\runner');
|
||||
OrchestratorSystem.Run.mockRejectedValue(new Error('not found'));
|
||||
|
||||
const originalEnv = { ...process.env };
|
||||
process.env.LOCALAPPDATA = 'C:\\Users\\runner\\AppData\\Local';
|
||||
process.env.RUNNER_TOOL_CACHE = '';
|
||||
|
||||
(mockFs.existsSync as jest.Mock).mockImplementation((p: string) => {
|
||||
return p.includes('AppData');
|
||||
});
|
||||
|
||||
const result = await ElasticGitStorageService.findInstalled();
|
||||
expect(result).toContain('elastic-git-storage.exe');
|
||||
|
||||
process.env = originalEnv;
|
||||
});
|
||||
});
|
||||
|
||||
describe('install', () => {
|
||||
it('should download correct binary for linux amd64', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
mockOs.tmpdir.mockReturnValue('/tmp');
|
||||
OrchestratorSystem.Run.mockResolvedValue('');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await ElasticGitStorageService.install('latest');
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('elastic-git-storage_linux_amd64'),
|
||||
);
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('chmod +x'),
|
||||
);
|
||||
expect(result).toContain('elastic-git-storage');
|
||||
});
|
||||
|
||||
it('should download correct binary for darwin arm64', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('darwin');
|
||||
mockOs.arch.mockReturnValue('arm64');
|
||||
mockOs.tmpdir.mockReturnValue('/tmp');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
await ElasticGitStorageService.install('v1.2.0');
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('elastic-git-storage_darwin_arm64'),
|
||||
);
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('v1.2.0'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should download .exe for windows', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('win32');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
mockOs.tmpdir.mockReturnValue('C:\\temp');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
await ElasticGitStorageService.install('latest');
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('elastic-git-storage_windows_amd64.exe'),
|
||||
);
|
||||
// Should NOT chmod on windows
|
||||
expect(OrchestratorSystem.Run).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('chmod'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use RUNNER_TOOL_CACHE for install dir when available', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
OrchestratorSystem.Run.mockResolvedValue('');
|
||||
|
||||
const originalCache = process.env.RUNNER_TOOL_CACHE;
|
||||
process.env.RUNNER_TOOL_CACHE = '/opt/hostedtoolcache';
|
||||
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await ElasticGitStorageService.install('latest');
|
||||
expect(result).toContain('hostedtoolcache');
|
||||
|
||||
if (originalCache === undefined) {
|
||||
delete process.env.RUNNER_TOOL_CACHE;
|
||||
} else {
|
||||
process.env.RUNNER_TOOL_CACHE = originalCache;
|
||||
}
|
||||
});
|
||||
|
||||
it('should use latest release URL when version is latest', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
mockOs.tmpdir.mockReturnValue('/tmp');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
await ElasticGitStorageService.install('latest');
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/releases/latest/download/'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use tagged release URL when version is specified', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
mockOs.tmpdir.mockReturnValue('/tmp');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
await ElasticGitStorageService.install('v2.0.0');
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/releases/download/v2.0.0/'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty string on download failure', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
mockOs.tmpdir.mockReturnValue('/tmp');
|
||||
OrchestratorSystem.Run.mockRejectedValue(new Error('curl failed'));
|
||||
|
||||
const result = await ElasticGitStorageService.install('latest');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string if binary not found after download', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
mockOs.tmpdir.mockReturnValue('/tmp');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await ElasticGitStorageService.install('latest');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureAndConfigure', () => {
|
||||
it('should use existing installation if found', async () => {
|
||||
const { LfsAgentService } = require('./lfs-agent-service');
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.homedir.mockReturnValue('/home/runner');
|
||||
// findInstalled returns a result
|
||||
OrchestratorSystem.Run.mockResolvedValue('/usr/local/bin/elastic-git-storage\n');
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const result = await ElasticGitStorageService.ensureAndConfigure(
|
||||
'latest',
|
||||
'--verbose',
|
||||
['/mnt/lfs'],
|
||||
'/repo',
|
||||
);
|
||||
|
||||
expect(result).toBe('/usr/local/bin/elastic-git-storage');
|
||||
expect(LfsAgentService.configure).toHaveBeenCalledWith(
|
||||
'/usr/local/bin/elastic-git-storage',
|
||||
'--verbose',
|
||||
['/mnt/lfs'],
|
||||
'/repo',
|
||||
);
|
||||
});
|
||||
|
||||
it('should install when not found and configure', async () => {
|
||||
const { LfsAgentService } = require('./lfs-agent-service');
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
mockOs.homedir.mockReturnValue('/home/runner');
|
||||
mockOs.tmpdir.mockReturnValue('/tmp');
|
||||
|
||||
// findInstalled finds nothing
|
||||
OrchestratorSystem.Run
|
||||
.mockRejectedValueOnce(new Error('not found')) // which
|
||||
.mockResolvedValueOnce('') // curl download
|
||||
.mockResolvedValueOnce(''); // chmod
|
||||
|
||||
(mockFs.existsSync as jest.Mock)
|
||||
.mockReturnValueOnce(false) // RUNNER_TOOL_CACHE
|
||||
.mockReturnValueOnce(false) // /usr/local/bin
|
||||
.mockReturnValueOnce(false) // ~/.local/bin
|
||||
.mockReturnValueOnce(true); // after install
|
||||
|
||||
const result = await ElasticGitStorageService.ensureAndConfigure(
|
||||
'v1.0.0',
|
||||
'',
|
||||
[],
|
||||
'/repo',
|
||||
);
|
||||
|
||||
expect(result).toContain('elastic-git-storage');
|
||||
expect(LfsAgentService.configure).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty string when install fails', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
mockOs.homedir.mockReturnValue('/home/runner');
|
||||
mockOs.tmpdir.mockReturnValue('/tmp');
|
||||
|
||||
// findInstalled finds nothing
|
||||
OrchestratorSystem.Run.mockRejectedValue(new Error('not found'));
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const result = await ElasticGitStorageService.ensureAndConfigure(
|
||||
'latest',
|
||||
'',
|
||||
[],
|
||||
'/repo',
|
||||
);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should use default version when empty string passed', async () => {
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
mockOs.platform.mockReturnValue('linux');
|
||||
mockOs.arch.mockReturnValue('x64');
|
||||
mockOs.homedir.mockReturnValue('/home/runner');
|
||||
mockOs.tmpdir.mockReturnValue('/tmp');
|
||||
|
||||
// findInstalled finds nothing
|
||||
OrchestratorSystem.Run
|
||||
.mockRejectedValueOnce(new Error('not found'))
|
||||
.mockResolvedValueOnce('')
|
||||
.mockResolvedValueOnce('');
|
||||
|
||||
(mockFs.existsSync as jest.Mock)
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce(true);
|
||||
|
||||
await ElasticGitStorageService.ensureAndConfigure('', '', [], '/repo');
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/releases/latest/download/'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('constants', () => {
|
||||
it('should have correct repo owner', () => {
|
||||
expect(ElasticGitStorageService.REPO_OWNER).toBe('frostebite');
|
||||
});
|
||||
|
||||
it('should have correct repo name', () => {
|
||||
expect(ElasticGitStorageService.REPO_NAME).toBe('elastic-git-storage');
|
||||
});
|
||||
|
||||
it('should have correct agent name', () => {
|
||||
expect(ElasticGitStorageService.AGENT_NAME).toBe('elastic-git-storage');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { OrchestratorSystem } from '../core/orchestrator-system';
|
||||
import OrchestratorLogger from '../core/orchestrator-logger';
|
||||
import { LfsAgentService } from './lfs-agent-service';
|
||||
|
||||
/**
|
||||
* Built-in support for elastic-git-storage, a custom Git LFS transfer agent
|
||||
* that supports multiple storage backends (local filesystem, WebDAV, rclone remotes).
|
||||
*
|
||||
* When `lfsTransferAgent` is set to 'elastic-git-storage', this service:
|
||||
* 1. Checks if the agent is already installed on the system
|
||||
* 2. If not, downloads it from the GitHub release page
|
||||
* 3. Makes it executable
|
||||
* 4. Delegates to LfsAgentService for git config setup
|
||||
*
|
||||
* @see https://github.com/frostebite/elastic-git-storage
|
||||
*/
|
||||
export class ElasticGitStorageService {
|
||||
static readonly REPO_OWNER = 'frostebite';
|
||||
static readonly REPO_NAME = 'elastic-git-storage';
|
||||
static readonly AGENT_NAME = 'elastic-git-storage';
|
||||
|
||||
/**
|
||||
* Parse an agent value that may include a version suffix.
|
||||
* Supports formats like:
|
||||
* - 'elastic-git-storage' → { name: 'elastic-git-storage', version: 'latest' }
|
||||
* - 'elastic-git-storage@v1.0.0' → { name: 'elastic-git-storage', version: 'v1.0.0' }
|
||||
* - 'elastic-git-storage@latest' → { name: 'elastic-git-storage', version: 'latest' }
|
||||
*/
|
||||
static parseAgentValue(agentValue: string): { name: string; version: string } {
|
||||
const trimmed = agentValue.trim();
|
||||
const atIndex = trimmed.indexOf('@');
|
||||
if (atIndex > 0) {
|
||||
return {
|
||||
name: trimmed.substring(0, atIndex),
|
||||
version: trimmed.substring(atIndex + 1) || 'latest',
|
||||
};
|
||||
}
|
||||
|
||||
return { name: trimmed, version: 'latest' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given lfsTransferAgent value refers to elastic-git-storage.
|
||||
* Matches the exact name (without path) or a path ending in the agent name.
|
||||
* Also matches 'elastic-git-storage@version' format.
|
||||
*/
|
||||
static isElasticGitStorage(agentValue: string): boolean {
|
||||
if (!agentValue) return false;
|
||||
|
||||
const { name } = ElasticGitStorageService.parseAgentValue(agentValue);
|
||||
const normalized = name.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
normalized === 'elastic-git-storage' ||
|
||||
normalized === 'elastic-git-storage.exe' ||
|
||||
normalized.endsWith('/elastic-git-storage') ||
|
||||
normalized.endsWith('\\elastic-git-storage') ||
|
||||
normalized.endsWith('/elastic-git-storage.exe') ||
|
||||
normalized.endsWith('\\elastic-git-storage.exe')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the full path to the elastic-git-storage executable.
|
||||
*
|
||||
* Search order:
|
||||
* 1. The provided path (if it's a full path and exists)
|
||||
* 2. $PATH lookup via `which` / `where`
|
||||
* 3. Known install locations ($RUNNER_TOOL_CACHE, /usr/local/bin, ~/.local/bin)
|
||||
*
|
||||
* @returns Full path to the executable, or empty string if not found
|
||||
*/
|
||||
static async findInstalled(): Promise<string> {
|
||||
// Check PATH
|
||||
try {
|
||||
const whichCmd = os.platform() === 'win32' ? 'where elastic-git-storage' : 'which elastic-git-storage';
|
||||
const result = await OrchestratorSystem.Run(whichCmd, false, true);
|
||||
const foundPath = result.trim().split('\n')[0].trim();
|
||||
if (foundPath && fs.existsSync(foundPath)) {
|
||||
return foundPath;
|
||||
}
|
||||
} catch {
|
||||
// Not on PATH
|
||||
}
|
||||
|
||||
// Check common install locations
|
||||
const candidates = [
|
||||
path.join(process.env.RUNNER_TOOL_CACHE || '', 'elastic-git-storage', 'elastic-git-storage'),
|
||||
'/usr/local/bin/elastic-git-storage',
|
||||
path.join(os.homedir(), '.local', 'bin', 'elastic-git-storage'),
|
||||
];
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
candidates.push(
|
||||
path.join(process.env.RUNNER_TOOL_CACHE || '', 'elastic-git-storage', 'elastic-git-storage.exe'),
|
||||
path.join(process.env.LOCALAPPDATA || '', 'elastic-git-storage', 'elastic-git-storage.exe'),
|
||||
);
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (candidate && fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Install elastic-git-storage from GitHub releases.
|
||||
*
|
||||
* @param version - Version to install (e.g., 'v1.0.0', 'latest')
|
||||
* @returns Path to the installed executable
|
||||
*/
|
||||
static async install(version: string = 'latest'): Promise<string> {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
|
||||
const osName = platform === 'win32' ? 'windows' : platform === 'darwin' ? 'darwin' : 'linux';
|
||||
const archName = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : 'amd64';
|
||||
const ext = platform === 'win32' ? '.exe' : '';
|
||||
|
||||
const installDir = process.env.RUNNER_TOOL_CACHE
|
||||
? path.join(process.env.RUNNER_TOOL_CACHE, 'elastic-git-storage')
|
||||
: path.join(os.tmpdir(), 'elastic-git-storage');
|
||||
|
||||
const binaryName = `elastic-git-storage${ext}`;
|
||||
const installPath = path.join(installDir, binaryName);
|
||||
|
||||
OrchestratorLogger.log(`[ElasticGitStorage] Installing to ${installPath}`);
|
||||
|
||||
// Create install directory
|
||||
fs.mkdirSync(installDir, { recursive: true });
|
||||
|
||||
// Build download URL
|
||||
const releaseTag = version === 'latest' ? 'latest' : version;
|
||||
const assetName = `elastic-git-storage_${osName}_${archName}${ext}`;
|
||||
|
||||
let downloadUrl: string;
|
||||
if (releaseTag === 'latest') {
|
||||
downloadUrl = `https://github.com/${ElasticGitStorageService.REPO_OWNER}/${ElasticGitStorageService.REPO_NAME}/releases/latest/download/${assetName}`;
|
||||
} else {
|
||||
downloadUrl = `https://github.com/${ElasticGitStorageService.REPO_OWNER}/${ElasticGitStorageService.REPO_NAME}/releases/download/${releaseTag}/${assetName}`;
|
||||
}
|
||||
|
||||
OrchestratorLogger.log(`[ElasticGitStorage] Downloading from ${downloadUrl}`);
|
||||
|
||||
try {
|
||||
await OrchestratorSystem.Run(`curl -fsSL -o "${installPath}" "${downloadUrl}"`);
|
||||
|
||||
if (platform !== 'win32') {
|
||||
await OrchestratorSystem.Run(`chmod +x "${installPath}"`);
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
if (!fs.existsSync(installPath)) {
|
||||
throw new Error(`Binary not found after download at ${installPath}`);
|
||||
}
|
||||
|
||||
OrchestratorLogger.log(`[ElasticGitStorage] Successfully installed ${releaseTag} to ${installPath}`);
|
||||
|
||||
return installPath;
|
||||
} catch (error: any) {
|
||||
OrchestratorLogger.logWarning(
|
||||
`[ElasticGitStorage] Failed to install: ${error.message}. Continuing without elastic-git-storage.`,
|
||||
);
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure elastic-git-storage is available (find or install) and configure it.
|
||||
*
|
||||
* @param version - Version to install if not found ('latest' or a tag like 'v1.0.0')
|
||||
* @param agentArgs - Additional arguments to pass to the agent
|
||||
* @param storagePaths - Storage paths for the agent
|
||||
* @param repoPath - Path to the git repository
|
||||
* @returns Path to the configured executable, or empty string if setup failed
|
||||
*/
|
||||
static async ensureAndConfigure(
|
||||
version: string,
|
||||
agentArgs: string,
|
||||
storagePaths: string[],
|
||||
repoPath: string,
|
||||
): Promise<string> {
|
||||
OrchestratorLogger.log(`[ElasticGitStorage] Setting up elastic-git-storage (version: ${version || 'latest'})`);
|
||||
|
||||
// Try to find existing installation
|
||||
let agentPath = await ElasticGitStorageService.findInstalled();
|
||||
|
||||
if (agentPath) {
|
||||
OrchestratorLogger.log(`[ElasticGitStorage] Found existing installation at ${agentPath}`);
|
||||
} else {
|
||||
OrchestratorLogger.log(`[ElasticGitStorage] Not found on system, installing...`);
|
||||
agentPath = await ElasticGitStorageService.install(version || 'latest');
|
||||
|
||||
if (!agentPath) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate to LfsAgentService for git config setup
|
||||
await LfsAgentService.configure(agentPath, agentArgs, storagePaths, repoPath);
|
||||
|
||||
return agentPath;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user