mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-02 14:56:16 -07:00
Compare commits
4 Commits
feature/bu
...
fix/secure
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f80e4f66d4 | ||
|
|
b2327008ed | ||
|
|
b3bd405399 | ||
|
|
8a41533779 |
34
action.yml
34
action.yml
@@ -105,6 +105,12 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
description: '[Orchestrator] Github private token to pull from github'
|
description: '[Orchestrator] Github private token to pull from github'
|
||||||
|
gitAuthMode:
|
||||||
|
required: false
|
||||||
|
default: 'header'
|
||||||
|
description:
|
||||||
|
'[Orchestrator] How git authentication is configured. "header" (default) uses http.extraHeader so the token
|
||||||
|
never appears in clone URLs or git config. "url" embeds the token in clone URLs (legacy behavior).'
|
||||||
githubOwner:
|
githubOwner:
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
@@ -182,8 +188,8 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
description:
|
description:
|
||||||
'[Orchestrator] Run a custom job instead of the standard build automation for orchestrator (in yaml format with
|
'[Orchestrator] Run a custom job instead of the standard build automation for orchestrator (in yaml format with the
|
||||||
the keys image, secrets (name, value object array), command line string)'
|
keys image, secrets (name, value object array), command line string)'
|
||||||
awsStackName:
|
awsStackName:
|
||||||
default: 'game-ci'
|
default: 'game-ci'
|
||||||
required: false
|
required: false
|
||||||
@@ -279,30 +285,6 @@ inputs:
|
|||||||
description:
|
description:
|
||||||
'[Orchestrator] Specifies the repo for the unity builder. Useful if you forked the repo for testing, features, or
|
'[Orchestrator] Specifies the repo for the unity builder. Useful if you forked the repo for testing, features, or
|
||||||
fixes.'
|
fixes.'
|
||||||
gitIntegrityCheck:
|
|
||||||
description: 'Run git integrity checks before build (fsck, lock cleanup, submodule validation)'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
gitAutoRecover:
|
|
||||||
description: 'Attempt automatic recovery if git corruption is detected'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
cleanReservedFilenames:
|
|
||||||
description: 'Remove Windows reserved filenames that cause Unity import loops'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
buildArchiveEnabled:
|
|
||||||
description: 'Archive build output after successful build'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
buildArchivePath:
|
|
||||||
description: 'Path to store build archives'
|
|
||||||
required: false
|
|
||||||
default: './build-archives'
|
|
||||||
buildArchiveRetention:
|
|
||||||
description: 'Days to retain build archives before cleanup'
|
|
||||||
required: false
|
|
||||||
default: '30'
|
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
volume:
|
volume:
|
||||||
|
|||||||
689
dist/index.js
generated
vendored
689
dist/index.js
generated
vendored
@@ -38,7 +38,6 @@ const model_1 = __nccwpck_require__(41359);
|
|||||||
const cli_1 = __nccwpck_require__(55651);
|
const cli_1 = __nccwpck_require__(55651);
|
||||||
const mac_builder_1 = __importDefault(__nccwpck_require__(39364));
|
const mac_builder_1 = __importDefault(__nccwpck_require__(39364));
|
||||||
const platform_setup_1 = __importDefault(__nccwpck_require__(64423));
|
const platform_setup_1 = __importDefault(__nccwpck_require__(64423));
|
||||||
const reliability_1 = __nccwpck_require__(9842);
|
|
||||||
async function runMain() {
|
async function runMain() {
|
||||||
try {
|
try {
|
||||||
if (cli_1.Cli.InitCliMode()) {
|
if (cli_1.Cli.InitCliMode()) {
|
||||||
@@ -47,32 +46,9 @@ async function runMain() {
|
|||||||
}
|
}
|
||||||
model_1.Action.checkCompatibility();
|
model_1.Action.checkCompatibility();
|
||||||
model_1.Cache.verify();
|
model_1.Cache.verify();
|
||||||
// Always configure git environment for CI reliability
|
|
||||||
reliability_1.BuildReliabilityService.configureGitEnvironment();
|
|
||||||
const { workspace, actionFolder } = model_1.Action;
|
const { workspace, actionFolder } = model_1.Action;
|
||||||
const buildParameters = await model_1.BuildParameters.create();
|
const buildParameters = await model_1.BuildParameters.create();
|
||||||
const baseImage = new model_1.ImageTag(buildParameters);
|
const baseImage = new model_1.ImageTag(buildParameters);
|
||||||
// Pre-build reliability checks
|
|
||||||
if (buildParameters.gitIntegrityCheck) {
|
|
||||||
core.info('Running git integrity checks...');
|
|
||||||
const isHealthy = reliability_1.BuildReliabilityService.checkGitIntegrity(workspace);
|
|
||||||
reliability_1.BuildReliabilityService.cleanStaleLockFiles(workspace);
|
|
||||||
reliability_1.BuildReliabilityService.validateSubmoduleBackingStores(workspace);
|
|
||||||
if (buildParameters.cleanReservedFilenames) {
|
|
||||||
reliability_1.BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
|
||||||
}
|
|
||||||
if (!isHealthy && buildParameters.gitAutoRecover) {
|
|
||||||
core.info('Git corruption detected, attempting automatic recovery...');
|
|
||||||
const recovered = reliability_1.BuildReliabilityService.recoverCorruptedRepo(workspace);
|
|
||||||
if (!recovered) {
|
|
||||||
core.warning('Automatic recovery failed. Build may encounter issues.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (buildParameters.cleanReservedFilenames) {
|
|
||||||
// cleanReservedFilenames can run independently of gitIntegrityCheck
|
|
||||||
reliability_1.BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
|
||||||
}
|
|
||||||
let exitCode = -1;
|
let exitCode = -1;
|
||||||
if (buildParameters.providerStrategy === 'local') {
|
if (buildParameters.providerStrategy === 'local') {
|
||||||
core.info('Building locally');
|
core.info('Building locally');
|
||||||
@@ -90,12 +66,6 @@ async function runMain() {
|
|||||||
await model_1.Orchestrator.run(buildParameters, baseImage.toString());
|
await model_1.Orchestrator.run(buildParameters, baseImage.toString());
|
||||||
exitCode = 0;
|
exitCode = 0;
|
||||||
}
|
}
|
||||||
// Post-build: archive and enforce retention
|
|
||||||
if (buildParameters.buildArchiveEnabled && exitCode === 0) {
|
|
||||||
core.info('Archiving build output...');
|
|
||||||
reliability_1.BuildReliabilityService.archiveBuildOutput(buildParameters.buildPath, buildParameters.buildArchivePath);
|
|
||||||
reliability_1.BuildReliabilityService.enforceRetention(buildParameters.buildArchivePath, buildParameters.buildArchiveRetention);
|
|
||||||
}
|
|
||||||
// Set output
|
// Set output
|
||||||
await model_1.Output.setBuildVersion(buildParameters.buildVersion);
|
await model_1.Output.setBuildVersion(buildParameters.buildVersion);
|
||||||
await model_1.Output.setAndroidVersionCode(buildParameters.androidVersionCode);
|
await model_1.Output.setAndroidVersionCode(buildParameters.androidVersionCode);
|
||||||
@@ -357,6 +327,7 @@ class BuildParameters {
|
|||||||
containerRegistryRepository: input_1.default.containerRegistryRepository,
|
containerRegistryRepository: input_1.default.containerRegistryRepository,
|
||||||
containerRegistryImageVersion: input_1.default.containerRegistryImageVersion,
|
containerRegistryImageVersion: input_1.default.containerRegistryImageVersion,
|
||||||
providerStrategy: orchestrator_options_1.default.providerStrategy,
|
providerStrategy: orchestrator_options_1.default.providerStrategy,
|
||||||
|
gitAuthMode: orchestrator_options_1.default.gitAuthMode,
|
||||||
buildPlatform: orchestrator_options_1.default.buildPlatform,
|
buildPlatform: orchestrator_options_1.default.buildPlatform,
|
||||||
kubeConfig: orchestrator_options_1.default.kubeConfig,
|
kubeConfig: orchestrator_options_1.default.kubeConfig,
|
||||||
containerMemory: orchestrator_options_1.default.containerMemory,
|
containerMemory: orchestrator_options_1.default.containerMemory,
|
||||||
@@ -405,12 +376,6 @@ class BuildParameters {
|
|||||||
cacheUnityInstallationOnMac: input_1.default.cacheUnityInstallationOnMac,
|
cacheUnityInstallationOnMac: input_1.default.cacheUnityInstallationOnMac,
|
||||||
unityHubVersionOnMac: input_1.default.unityHubVersionOnMac,
|
unityHubVersionOnMac: input_1.default.unityHubVersionOnMac,
|
||||||
dockerWorkspacePath: input_1.default.dockerWorkspacePath,
|
dockerWorkspacePath: input_1.default.dockerWorkspacePath,
|
||||||
gitIntegrityCheck: input_1.default.gitIntegrityCheck,
|
|
||||||
gitAutoRecover: input_1.default.gitAutoRecover,
|
|
||||||
cleanReservedFilenames: input_1.default.cleanReservedFilenames,
|
|
||||||
buildArchiveEnabled: input_1.default.buildArchiveEnabled,
|
|
||||||
buildArchivePath: input_1.default.buildArchivePath,
|
|
||||||
buildArchiveRetention: input_1.default.buildArchiveRetention,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
static parseBuildFile(filename, platform, androidExportType) {
|
static parseBuildFile(filename, platform, androidExportType) {
|
||||||
@@ -1862,28 +1827,6 @@ class Input {
|
|||||||
static get skipActivation() {
|
static get skipActivation() {
|
||||||
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
||||||
}
|
}
|
||||||
static get gitIntegrityCheck() {
|
|
||||||
const input = Input.getInput('gitIntegrityCheck') ?? 'false';
|
|
||||||
return input === 'true';
|
|
||||||
}
|
|
||||||
static get gitAutoRecover() {
|
|
||||||
const input = Input.getInput('gitAutoRecover') ?? 'false';
|
|
||||||
return input === 'true';
|
|
||||||
}
|
|
||||||
static get cleanReservedFilenames() {
|
|
||||||
const input = Input.getInput('cleanReservedFilenames') ?? 'false';
|
|
||||||
return input === 'true';
|
|
||||||
}
|
|
||||||
static get buildArchiveEnabled() {
|
|
||||||
const input = Input.getInput('buildArchiveEnabled') ?? 'false';
|
|
||||||
return input === 'true';
|
|
||||||
}
|
|
||||||
static get buildArchivePath() {
|
|
||||||
return Input.getInput('buildArchivePath') ?? './build-archives';
|
|
||||||
}
|
|
||||||
static get buildArchiveRetention() {
|
|
||||||
return Number.parseInt(Input.getInput('buildArchiveRetention') ?? '30', 10);
|
|
||||||
}
|
|
||||||
static ToEnvVarFormat(input) {
|
static ToEnvVarFormat(input) {
|
||||||
if (input.toUpperCase() === input) {
|
if (input.toUpperCase() === input) {
|
||||||
return input;
|
return input;
|
||||||
@@ -2002,6 +1945,29 @@ exports["default"] = OrchestratorEnvironmentVariable;
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
@@ -2056,12 +2022,57 @@ class OrchestratorFolders {
|
|||||||
static get libraryCacheFolderFull() {
|
static get libraryCacheFolderFull() {
|
||||||
return node_path_1.default.join(OrchestratorFolders.cacheFolderForCacheKeyFull, `Library`);
|
return node_path_1.default.join(OrchestratorFolders.cacheFolderForCacheKeyFull, `Library`);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Whether to use http.extraHeader for git authentication (secure, default)
|
||||||
|
* instead of embedding the token in clone URLs (legacy).
|
||||||
|
*/
|
||||||
|
static get useHeaderAuth() {
|
||||||
|
return orchestrator_1.default.buildParameters.gitAuthMode !== 'url';
|
||||||
|
}
|
||||||
static get unityBuilderRepoUrl() {
|
static get unityBuilderRepoUrl() {
|
||||||
|
if (OrchestratorFolders.useHeaderAuth) {
|
||||||
|
return `https://github.com/${orchestrator_1.default.buildParameters.orchestratorRepoName}.git`;
|
||||||
|
}
|
||||||
return `https://${orchestrator_1.default.buildParameters.gitPrivateToken}@github.com/${orchestrator_1.default.buildParameters.orchestratorRepoName}.git`;
|
return `https://${orchestrator_1.default.buildParameters.gitPrivateToken}@github.com/${orchestrator_1.default.buildParameters.orchestratorRepoName}.git`;
|
||||||
}
|
}
|
||||||
static get targetBuildRepoUrl() {
|
static get targetBuildRepoUrl() {
|
||||||
|
if (OrchestratorFolders.useHeaderAuth) {
|
||||||
|
return `https://github.com/${orchestrator_1.default.buildParameters.githubRepo}.git`;
|
||||||
|
}
|
||||||
return `https://${orchestrator_1.default.buildParameters.gitPrivateToken}@github.com/${orchestrator_1.default.buildParameters.githubRepo}.git`;
|
return `https://${orchestrator_1.default.buildParameters.gitPrivateToken}@github.com/${orchestrator_1.default.buildParameters.githubRepo}.git`;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Shell commands to configure git authentication via http.extraHeader.
|
||||||
|
* Uses GIT_PRIVATE_TOKEN env var so the token never appears in clone URLs or git config output.
|
||||||
|
* This is the same mechanism used by actions/checkout.
|
||||||
|
*
|
||||||
|
* Only emits commands when gitAuthMode is 'header' (default). In 'url' mode,
|
||||||
|
* returns a no-op comment since the token is already in the URL.
|
||||||
|
*/
|
||||||
|
static get gitAuthConfigScript() {
|
||||||
|
if (!OrchestratorFolders.useHeaderAuth) {
|
||||||
|
return `# git auth: using token-in-URL mode (legacy)`;
|
||||||
|
}
|
||||||
|
return `# git auth: configuring http.extraHeader (secure mode)
|
||||||
|
if [ -n "$GIT_PRIVATE_TOKEN" ]; then
|
||||||
|
git config --global http.https://github.com/.extraHeader "Authorization: Basic $(printf '%s' "x-access-token:$GIT_PRIVATE_TOKEN" | base64 -w 0)"
|
||||||
|
fi`;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Configure git authentication via http.extraHeader in the current Node process.
|
||||||
|
* For use in the remote-client where shell scripts aren't used.
|
||||||
|
* Only configures when gitAuthMode is 'header' (default).
|
||||||
|
*/
|
||||||
|
static async configureGitAuth() {
|
||||||
|
if (!OrchestratorFolders.useHeaderAuth)
|
||||||
|
return;
|
||||||
|
const token = orchestrator_1.default.buildParameters.gitPrivateToken || process.env.GIT_PRIVATE_TOKEN || '';
|
||||||
|
if (!token)
|
||||||
|
return;
|
||||||
|
const encoded = Buffer.from(`x-access-token:${token}`).toString('base64');
|
||||||
|
const { OrchestratorSystem } = await Promise.resolve().then(() => __importStar(__nccwpck_require__(9744)));
|
||||||
|
await OrchestratorSystem.Run(`git config --global http.https://github.com/.extraHeader "Authorization: Basic ${encoded}"`);
|
||||||
|
}
|
||||||
static get buildVolumeFolder() {
|
static get buildVolumeFolder() {
|
||||||
return 'data';
|
return 'data';
|
||||||
}
|
}
|
||||||
@@ -2262,6 +2273,9 @@ class OrchestratorOptions {
|
|||||||
}
|
}
|
||||||
return provider || 'local';
|
return provider || 'local';
|
||||||
}
|
}
|
||||||
|
static get gitAuthMode() {
|
||||||
|
return OrchestratorOptions.getInput('gitAuthMode') || 'header';
|
||||||
|
}
|
||||||
static get containerCpu() {
|
static get containerCpu() {
|
||||||
return OrchestratorOptions.getInput('containerCpu') || `1024`;
|
return OrchestratorOptions.getInput('containerCpu') || `1024`;
|
||||||
}
|
}
|
||||||
@@ -8043,6 +8057,7 @@ class RemoteClient {
|
|||||||
}
|
}
|
||||||
remote_client_logger_1.RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`);
|
remote_client_logger_1.RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`);
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global advice.detachedHead false`);
|
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global advice.detachedHead false`);
|
||||||
|
await orchestrator_folders_1.OrchestratorFolders.configureGitAuth();
|
||||||
remote_client_logger_1.RemoteClientLogger.log(`Cloning the repository being built:`);
|
remote_client_logger_1.RemoteClientLogger.log(`Cloning the repository being built:`);
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`);
|
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`);
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`);
|
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`);
|
||||||
@@ -8147,10 +8162,7 @@ class RemoteClient {
|
|||||||
const gitPrivateToken = process.env.GIT_PRIVATE_TOKEN;
|
const gitPrivateToken = process.env.GIT_PRIVATE_TOKEN;
|
||||||
if (gitPrivateToken) {
|
if (gitPrivateToken) {
|
||||||
remote_client_logger_1.RemoteClientLogger.log(`Attempting to pull LFS files with GIT_PRIVATE_TOKEN...`);
|
remote_client_logger_1.RemoteClientLogger.log(`Attempting to pull LFS files with GIT_PRIVATE_TOKEN...`);
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global --unset-all url."https://github.com/".insteadOf || true`);
|
await RemoteClient.configureTokenAuth(gitPrivateToken);
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global --unset-all url."ssh://git@github.com/".insteadOf || true`);
|
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global --unset-all url."git@github.com".insteadOf || true`);
|
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global url."https://${gitPrivateToken}@github.com/".insteadOf "https://github.com/"`);
|
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git lfs pull`, true);
|
await orchestrator_system_1.OrchestratorSystem.Run(`git lfs pull`, true);
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git lfs checkout || true`, true);
|
await orchestrator_system_1.OrchestratorSystem.Run(`git lfs checkout || true`, true);
|
||||||
remote_client_logger_1.RemoteClientLogger.log(`Successfully pulled LFS files with GIT_PRIVATE_TOKEN`);
|
remote_client_logger_1.RemoteClientLogger.log(`Successfully pulled LFS files with GIT_PRIVATE_TOKEN`);
|
||||||
@@ -8165,10 +8177,7 @@ class RemoteClient {
|
|||||||
const githubToken = process.env.GITHUB_TOKEN;
|
const githubToken = process.env.GITHUB_TOKEN;
|
||||||
if (githubToken) {
|
if (githubToken) {
|
||||||
remote_client_logger_1.RemoteClientLogger.log(`Attempting to pull LFS files with GITHUB_TOKEN fallback...`);
|
remote_client_logger_1.RemoteClientLogger.log(`Attempting to pull LFS files with GITHUB_TOKEN fallback...`);
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global --unset-all url."https://github.com/".insteadOf || true`);
|
await RemoteClient.configureTokenAuth(githubToken);
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global --unset-all url."ssh://git@github.com/".insteadOf || true`);
|
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global --unset-all url."git@github.com".insteadOf || true`);
|
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global url."https://${githubToken}@github.com/".insteadOf "https://github.com/"`);
|
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git lfs pull`, true);
|
await orchestrator_system_1.OrchestratorSystem.Run(`git lfs pull`, true);
|
||||||
await orchestrator_system_1.OrchestratorSystem.Run(`git lfs checkout || true`, true);
|
await orchestrator_system_1.OrchestratorSystem.Run(`git lfs checkout || true`, true);
|
||||||
remote_client_logger_1.RemoteClientLogger.log(`Successfully pulled LFS files with GITHUB_TOKEN`);
|
remote_client_logger_1.RemoteClientLogger.log(`Successfully pulled LFS files with GITHUB_TOKEN`);
|
||||||
@@ -8225,6 +8234,23 @@ class RemoteClient {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Configure git authentication for a token. In header mode (default), uses
|
||||||
|
* http.extraHeader so the token never appears in URLs or git config output.
|
||||||
|
* In url mode (legacy), uses url.insteadOf to embed the token in URLs.
|
||||||
|
*/
|
||||||
|
static async configureTokenAuth(token) {
|
||||||
|
if (orchestrator_folders_1.OrchestratorFolders.useHeaderAuth) {
|
||||||
|
const encoded = Buffer.from(`x-access-token:${token}`).toString('base64');
|
||||||
|
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global http.https://github.com/.extraHeader "Authorization: Basic ${encoded}"`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global --unset-all url."https://github.com/".insteadOf || true`);
|
||||||
|
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global --unset-all url."ssh://git@github.com/".insteadOf || true`);
|
||||||
|
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global --unset-all url."git@github.com".insteadOf || true`);
|
||||||
|
await orchestrator_system_1.OrchestratorSystem.Run(`git config --global url."https://${token}@github.com/".insteadOf "https://github.com/"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, cli_functions_repository_1.CliFunction)(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`)
|
(0, cli_functions_repository_1.CliFunction)(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`)
|
||||||
@@ -9690,535 +9716,6 @@ class ContainerHookService {
|
|||||||
exports.ContainerHookService = ContainerHookService;
|
exports.ContainerHookService = ContainerHookService;
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
|
||||||
|
|
||||||
/***/ 37347:
|
|
||||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
||||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
||||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
||||||
}
|
|
||||||
Object.defineProperty(o, k2, desc);
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
||||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
||||||
}) : function(o, v) {
|
|
||||||
o["default"] = v;
|
|
||||||
});
|
|
||||||
var __importStar = (this && this.__importStar) || function (mod) {
|
|
||||||
if (mod && mod.__esModule) return mod;
|
|
||||||
var result = {};
|
|
||||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
||||||
__setModuleDefault(result, mod);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
||||||
exports.BuildReliabilityService = void 0;
|
|
||||||
const node_child_process_1 = __nccwpck_require__(17718);
|
|
||||||
const node_fs_1 = __importDefault(__nccwpck_require__(87561));
|
|
||||||
const node_path_1 = __importDefault(__nccwpck_require__(49411));
|
|
||||||
const core = __importStar(__nccwpck_require__(42186));
|
|
||||||
/**
|
|
||||||
* Build reliability features for hardening CI pipelines.
|
|
||||||
* Provides git integrity checks, stale lock cleanup, submodule validation,
|
|
||||||
* reserved filename removal, build archival, and git environment configuration.
|
|
||||||
* All features are opt-in and fail gracefully (warnings only).
|
|
||||||
*/
|
|
||||||
class BuildReliabilityService {
|
|
||||||
/**
|
|
||||||
* Run git fsck to check repository integrity.
|
|
||||||
* Returns true if the repo is healthy, false if corruption detected.
|
|
||||||
*/
|
|
||||||
static checkGitIntegrity(repoPath = '.') {
|
|
||||||
core.info(`[Reliability] Checking git integrity in ${repoPath}`);
|
|
||||||
try {
|
|
||||||
const output = (0, node_child_process_1.execSync)(`git -C "${repoPath}" fsck --no-dangling`, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 120000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
// Parse output for corruption indicators
|
|
||||||
const corruptionPatterns = [
|
|
||||||
/broken link/i,
|
|
||||||
/missing (blob|tree|commit|tag)/i,
|
|
||||||
/dangling/i,
|
|
||||||
/corrupt/i,
|
|
||||||
/error in /i,
|
|
||||||
];
|
|
||||||
for (const pattern of corruptionPatterns) {
|
|
||||||
if (pattern.test(output)) {
|
|
||||||
core.warning(`[Reliability] Git integrity check found issues: ${output.trim()}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
core.info('[Reliability] Git integrity check passed');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// execSync throws on non-zero exit code
|
|
||||||
const stderr = error.stderr?.toString() ?? error.message;
|
|
||||||
core.warning(`[Reliability] Git integrity check failed: ${stderr}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Remove stale .lock files from the .git directory.
|
|
||||||
* Only removes lock files older than 10 minutes to avoid interfering with active operations.
|
|
||||||
* Returns the number of lock files removed.
|
|
||||||
*/
|
|
||||||
static cleanStaleLockFiles(repoPath = '.') {
|
|
||||||
const gitDir = node_path_1.default.join(repoPath, '.git');
|
|
||||||
if (!node_fs_1.default.existsSync(gitDir) || !node_fs_1.default.statSync(gitDir).isDirectory()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
core.info(`[Reliability] Scanning for stale lock files in ${gitDir}`);
|
|
||||||
const now = Date.now();
|
|
||||||
let removed = 0;
|
|
||||||
const cleanDirectory = (directory) => {
|
|
||||||
if (!node_fs_1.default.existsSync(directory))
|
|
||||||
return;
|
|
||||||
try {
|
|
||||||
const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = node_path_1.default.join(directory, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
cleanDirectory(fullPath);
|
|
||||||
}
|
|
||||||
else if (entry.name.endsWith('.lock')) {
|
|
||||||
// Check if it is a known lock file location OR under refs/
|
|
||||||
const relativePath = node_path_1.default.relative(gitDir, fullPath);
|
|
||||||
const isKnownLock = BuildReliabilityService.LOCK_FILE_NAMES.has(entry.name);
|
|
||||||
const isRefsLock = relativePath.startsWith('refs' + node_path_1.default.sep);
|
|
||||||
if (isKnownLock || isRefsLock) {
|
|
||||||
try {
|
|
||||||
const stat = node_fs_1.default.statSync(fullPath);
|
|
||||||
const ageMs = now - stat.mtimeMs;
|
|
||||||
if (ageMs > BuildReliabilityService.LOCK_FILE_MAX_AGE_MS) {
|
|
||||||
node_fs_1.default.unlinkSync(fullPath);
|
|
||||||
removed++;
|
|
||||||
core.info(`[Reliability] Removed stale lock file (age: ${Math.round(ageMs / 1000)}s): ${relativePath}`);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
core.info(`[Reliability] Lock file is recent (age: ${Math.round(ageMs / 1000)}s), skipping: ${relativePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
core.warning(`[Reliability] Could not remove lock file: ${fullPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Directory not accessible
|
|
||||||
}
|
|
||||||
};
|
|
||||||
cleanDirectory(gitDir);
|
|
||||||
if (removed > 0) {
|
|
||||||
core.info(`[Reliability] Cleaned ${removed} stale lock file(s)`);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
core.info('[Reliability] No stale lock files found');
|
|
||||||
}
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Validate that submodule .git files point to existing backing stores
|
|
||||||
* under .git/modules/. Returns list of submodule paths with broken backing stores.
|
|
||||||
*/
|
|
||||||
static validateSubmoduleBackingStores(repoPath = '.') {
|
|
||||||
const broken = [];
|
|
||||||
const gitmodulesPath = node_path_1.default.join(repoPath, '.gitmodules');
|
|
||||||
if (!node_fs_1.default.existsSync(gitmodulesPath)) {
|
|
||||||
core.info('[Reliability] No .gitmodules found, skipping submodule validation');
|
|
||||||
return broken;
|
|
||||||
}
|
|
||||||
core.info(`[Reliability] Validating submodule backing stores in ${repoPath}`);
|
|
||||||
try {
|
|
||||||
const content = node_fs_1.default.readFileSync(gitmodulesPath, 'utf8');
|
|
||||||
const pathMatches = content.matchAll(/path\s*=\s*(.+)/g);
|
|
||||||
for (const match of pathMatches) {
|
|
||||||
const submodulePath = match[1].trim();
|
|
||||||
const gitFile = node_path_1.default.join(repoPath, submodulePath, '.git');
|
|
||||||
if (!node_fs_1.default.existsSync(gitFile)) {
|
|
||||||
// Submodule not initialized -- not necessarily broken
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const stat = node_fs_1.default.statSync(gitFile);
|
|
||||||
if (stat.isFile()) {
|
|
||||||
// .git is a file -- should contain "gitdir: <path>"
|
|
||||||
const gitFileContent = node_fs_1.default.readFileSync(gitFile, 'utf8').trim();
|
|
||||||
const gitdirMatch = gitFileContent.match(/^gitdir:\s*(.+)$/);
|
|
||||||
if (gitdirMatch) {
|
|
||||||
const backingStore = node_path_1.default.resolve(node_path_1.default.join(repoPath, submodulePath), gitdirMatch[1]);
|
|
||||||
if (!node_fs_1.default.existsSync(backingStore)) {
|
|
||||||
broken.push(submodulePath);
|
|
||||||
core.warning(`[Reliability] Submodule ${submodulePath} has broken backing store: ${backingStore}`);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
core.info(`[Reliability] Submodule ${submodulePath} backing store OK`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
broken.push(submodulePath);
|
|
||||||
core.warning(`[Reliability] Submodule ${submodulePath} .git file has invalid format`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Can't read .git file
|
|
||||||
core.warning(`[Reliability] Could not read .git file for submodule: ${submodulePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
core.warning(`[Reliability] Could not read .gitmodules: ${error.message}`);
|
|
||||||
}
|
|
||||||
if (broken.length > 0) {
|
|
||||||
core.warning(`[Reliability] ${broken.length} submodule(s) have broken backing stores`);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
core.info('[Reliability] All submodule backing stores are valid');
|
|
||||||
}
|
|
||||||
return broken;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Orchestrate recovery of a corrupted repository.
|
|
||||||
* Sequence: fsck -> clean locks -> re-fetch -> retry fsck.
|
|
||||||
* Returns true if recovery succeeded.
|
|
||||||
*/
|
|
||||||
static recoverCorruptedRepo(repoPath = '.') {
|
|
||||||
core.warning(`[Reliability] Attempting automatic recovery for ${repoPath}`);
|
|
||||||
// Step 1: Clean stale lock files that may be preventing operations
|
|
||||||
const locksRemoved = BuildReliabilityService.cleanStaleLockFiles(repoPath);
|
|
||||||
if (locksRemoved > 0) {
|
|
||||||
core.info(`[Reliability] Recovery: cleaned ${locksRemoved} lock file(s)`);
|
|
||||||
}
|
|
||||||
// Step 2: Re-fetch to restore missing objects
|
|
||||||
try {
|
|
||||||
core.info('[Reliability] Recovery: re-fetching from remote');
|
|
||||||
(0, node_child_process_1.execSync)(`git -C "${repoPath}" fetch --all`, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 300000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
core.info('[Reliability] Recovery: fetch completed');
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
core.warning(`[Reliability] Recovery: fetch failed: ${error.stderr?.toString() ?? error.message}`);
|
|
||||||
}
|
|
||||||
// Step 3: Retry fsck
|
|
||||||
const healthy = BuildReliabilityService.checkGitIntegrity(repoPath);
|
|
||||||
if (healthy) {
|
|
||||||
core.info('[Reliability] Recovery succeeded -- repository is healthy');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
core.warning('[Reliability] Recovery failed -- repository still has integrity issues');
|
|
||||||
}
|
|
||||||
return healthy;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Scan a directory tree for files/directories with Windows reserved names.
|
|
||||||
* These names (con, prn, aux, nul, com1-9, lpt1-9) with any extension
|
|
||||||
* cause Unity asset importer infinite loops on Windows.
|
|
||||||
* Returns list of paths that were removed.
|
|
||||||
*/
|
|
||||||
static cleanReservedFilenames(projectPath) {
|
|
||||||
const assetsPath = node_path_1.default.join(projectPath, 'Assets');
|
|
||||||
if (!node_fs_1.default.existsSync(assetsPath)) {
|
|
||||||
core.info(`[Reliability] No Assets directory found at ${assetsPath}, skipping reserved filename scan`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
core.info(`[Reliability] Scanning for reserved filenames in ${assetsPath}`);
|
|
||||||
const cleaned = [];
|
|
||||||
const scanDirectory = (directory) => {
|
|
||||||
try {
|
|
||||||
const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const nameWithoutExtension = entry.name.split('.')[0].toLowerCase();
|
|
||||||
const fullPath = node_path_1.default.join(directory, entry.name);
|
|
||||||
if (BuildReliabilityService.RESERVED_NAMES.has(nameWithoutExtension)) {
|
|
||||||
try {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
node_fs_1.default.rmSync(fullPath, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
node_fs_1.default.unlinkSync(fullPath);
|
|
||||||
}
|
|
||||||
cleaned.push(fullPath);
|
|
||||||
core.warning(`[Reliability] Removed reserved filename: ${fullPath}`);
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
core.warning(`[Reliability] Could not remove reserved filename: ${fullPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (entry.isDirectory()) {
|
|
||||||
scanDirectory(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Directory not accessible
|
|
||||||
}
|
|
||||||
};
|
|
||||||
scanDirectory(assetsPath);
|
|
||||||
if (cleaned.length > 0) {
|
|
||||||
core.warning(`[Reliability] Cleaned ${cleaned.length} reserved filename(s)`);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
core.info('[Reliability] No reserved filenames found');
|
|
||||||
}
|
|
||||||
return cleaned;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Get available disk space in megabytes for a given directory.
|
|
||||||
* Returns -1 if the check fails (unknown space).
|
|
||||||
*
|
|
||||||
* Cross-platform: uses wmic on Windows, df on Unix.
|
|
||||||
*/
|
|
||||||
static getAvailableSpaceMB(directoryPath) {
|
|
||||||
try {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
const drive = node_path_1.default.parse(directoryPath).root;
|
|
||||||
const driveLetter = drive.replace(/[:\\\/]/g, '');
|
|
||||||
const output = (0, node_child_process_1.execFileSync)('wmic', ['logicaldisk', 'where', `DeviceID='${driveLetter}:'`, 'get', 'FreeSpace', '/value'], { encoding: 'utf8', timeout: 10000 });
|
|
||||||
const match = output.match(/FreeSpace=(\d+)/);
|
|
||||||
return match ? Number.parseInt(match[1], 10) / (1024 * 1024) : -1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const output = (0, node_child_process_1.execFileSync)('df', ['-BM', '--output=avail', directoryPath], {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
const lines = output.trim().split('\n');
|
|
||||||
return Number.parseInt(lines[lines.length - 1], 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return -1; // Unknown, caller should proceed with warning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Calculate the total size of a directory in megabytes.
|
|
||||||
* Returns -1 if the calculation fails.
|
|
||||||
*/
|
|
||||||
static getDirectorySizeMB(directoryPath) {
|
|
||||||
try {
|
|
||||||
const stat = node_fs_1.default.statSync(directoryPath);
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
return stat.size / (1024 * 1024);
|
|
||||||
}
|
|
||||||
let totalBytes = 0;
|
|
||||||
const walkDirectory = (dir) => {
|
|
||||||
const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = node_path_1.default.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
walkDirectory(fullPath);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
totalBytes += node_fs_1.default.statSync(fullPath).size;
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Skip inaccessible files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
walkDirectory(directoryPath);
|
|
||||||
return totalBytes / (1024 * 1024);
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Create a tar.gz archive of build output.
|
|
||||||
*
|
|
||||||
* Validates disk space before archiving. Skips archival with a warning
|
|
||||||
* if insufficient space is detected, preventing partial writes on full disks.
|
|
||||||
*/
|
|
||||||
static archiveBuildOutput(sourcePath, archivePath) {
|
|
||||||
if (!node_fs_1.default.existsSync(sourcePath)) {
|
|
||||||
core.info(`[Reliability] No build output to archive at ${sourcePath}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
node_fs_1.default.mkdirSync(archivePath, { recursive: true });
|
|
||||||
// Check available disk space before archiving
|
|
||||||
const sourceSizeMB = BuildReliabilityService.getDirectorySizeMB(sourcePath);
|
|
||||||
const availableSpaceMB = BuildReliabilityService.getAvailableSpaceMB(archivePath);
|
|
||||||
if (sourceSizeMB >= 0 && availableSpaceMB >= 0) {
|
|
||||||
const neededMB = Math.ceil(sourceSizeMB * 1.1); // 10% safety margin
|
|
||||||
if (availableSpaceMB < neededMB) {
|
|
||||||
core.warning(`[Reliability] Insufficient disk space for archive. ` +
|
|
||||||
`Need ~${neededMB}MB, available: ${Math.floor(availableSpaceMB)}MB. Skipping archive.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
core.info(`[Reliability] Disk space check passed: need ~${neededMB}MB, available: ${Math.floor(availableSpaceMB)}MB`);
|
|
||||||
}
|
|
||||||
else if (availableSpaceMB < 0) {
|
|
||||||
core.warning('[Reliability] Could not determine available disk space. Proceeding with archive cautiously.');
|
|
||||||
}
|
|
||||||
const timestamp = new Date().toISOString().replace(/[.:]/g, '-');
|
|
||||||
const archiveFile = node_path_1.default.join(archivePath, `build-${timestamp}.tar.gz`);
|
|
||||||
try {
|
|
||||||
(0, node_child_process_1.execSync)(`tar -czf "${archiveFile}" -C "${node_path_1.default.dirname(sourcePath)}" "${node_path_1.default.basename(sourcePath)}"`, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 600000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
core.info(`[Reliability] Build output archived to ${archiveFile}`);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
core.warning(`[Reliability] Failed to archive build output: ${error.stderr?.toString() ?? error.message}`);
|
|
||||||
// Clean up partial archive if it exists to avoid leaving corrupted files
|
|
||||||
try {
|
|
||||||
if (node_fs_1.default.existsSync(archiveFile)) {
|
|
||||||
node_fs_1.default.unlinkSync(archiveFile);
|
|
||||||
core.info(`[Reliability] Cleaned up partial archive: ${archiveFile}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Best-effort cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Enforce retention policy -- delete archives older than the retention period.
|
|
||||||
* Returns the number of old archives removed.
|
|
||||||
*/
|
|
||||||
static enforceRetention(archivePath, retentionDays) {
|
|
||||||
if (!node_fs_1.default.existsSync(archivePath)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const now = Date.now();
|
|
||||||
const retentionMs = retentionDays * 24 * 60 * 60 * 1000;
|
|
||||||
let removed = 0;
|
|
||||||
try {
|
|
||||||
const entries = node_fs_1.default.readdirSync(archivePath, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = node_path_1.default.join(archivePath, entry.name);
|
|
||||||
try {
|
|
||||||
const stat = node_fs_1.default.statSync(fullPath);
|
|
||||||
const ageMs = now - stat.mtimeMs;
|
|
||||||
if (ageMs > retentionMs) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
node_fs_1.default.rmSync(fullPath, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
node_fs_1.default.unlinkSync(fullPath);
|
|
||||||
}
|
|
||||||
removed++;
|
|
||||||
core.info(`[Reliability] Removed old archive: ${entry.name} (age: ${Math.round(ageMs / (24 * 60 * 60 * 1000))} days)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
core.warning(`[Reliability] Could not process archive entry: ${fullPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
core.warning(`[Reliability] Could not read archive directory: ${archivePath}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (removed > 0) {
|
|
||||||
core.info(`[Reliability] Retention enforced: removed ${removed} old archive(s), retention: ${retentionDays} days`);
|
|
||||||
}
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Configure git environment variables for CI reliability.
|
|
||||||
* Sets GIT_TERMINAL_PROMPT=0, increases http.postBuffer, enables core.longpaths.
|
|
||||||
*/
|
|
||||||
static configureGitEnvironment() {
|
|
||||||
core.info('[Reliability] Configuring git environment for CI');
|
|
||||||
// Prevent git from prompting for credentials (hangs in CI)
|
|
||||||
process.env.GIT_TERMINAL_PROMPT = '0';
|
|
||||||
core.info('[Reliability] Set GIT_TERMINAL_PROMPT=0');
|
|
||||||
try {
|
|
||||||
// Increase http.postBuffer to 500MB for large pushes
|
|
||||||
(0, node_child_process_1.execSync)('git config --global http.postBuffer 524288000', {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 10000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
core.info('[Reliability] Set http.postBuffer=524288000 (500MB)');
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
core.warning(`[Reliability] Could not set http.postBuffer: ${error.message}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Enable long paths on Windows
|
|
||||||
(0, node_child_process_1.execSync)('git config --global core.longpaths true', {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 10000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
core.info('[Reliability] Set core.longpaths=true');
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
core.warning(`[Reliability] Could not set core.longpaths: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.BuildReliabilityService = BuildReliabilityService;
|
|
||||||
// Windows reserved device names that cause Unity asset importer infinite loops
|
|
||||||
BuildReliabilityService.RESERVED_NAMES = new Set([
|
|
||||||
'con',
|
|
||||||
'prn',
|
|
||||||
'aux',
|
|
||||||
'nul',
|
|
||||||
'com1',
|
|
||||||
'com2',
|
|
||||||
'com3',
|
|
||||||
'com4',
|
|
||||||
'com5',
|
|
||||||
'com6',
|
|
||||||
'com7',
|
|
||||||
'com8',
|
|
||||||
'com9',
|
|
||||||
'lpt1',
|
|
||||||
'lpt2',
|
|
||||||
'lpt3',
|
|
||||||
'lpt4',
|
|
||||||
'lpt5',
|
|
||||||
'lpt6',
|
|
||||||
'lpt7',
|
|
||||||
'lpt8',
|
|
||||||
'lpt9',
|
|
||||||
]);
|
|
||||||
// Lock files to look for in the .git directory
|
|
||||||
BuildReliabilityService.LOCK_FILE_NAMES = new Set(['index.lock', 'shallow.lock', 'config.lock', 'HEAD.lock']);
|
|
||||||
// Maximum age in milliseconds before a lock file is considered stale (10 minutes)
|
|
||||||
BuildReliabilityService.LOCK_FILE_MAX_AGE_MS = 10 * 60 * 1000;
|
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
|
||||||
|
|
||||||
/***/ 9842:
|
|
||||||
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
||||||
exports.BuildReliabilityService = void 0;
|
|
||||||
var build_reliability_service_1 = __nccwpck_require__(37347);
|
|
||||||
Object.defineProperty(exports, "BuildReliabilityService", ({ enumerable: true, get: function () { return build_reliability_service_1.BuildReliabilityService; } }));
|
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 23451:
|
/***/ 23451:
|
||||||
@@ -10312,6 +9809,7 @@ printenv
|
|||||||
git config --global advice.detachedHead false
|
git config --global advice.detachedHead false
|
||||||
git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"
|
git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"
|
||||||
git config --global filter.lfs.process "git-lfs filter-process --skip"
|
git config --global filter.lfs.process "git-lfs filter-process --skip"
|
||||||
|
${orchestrator_folders_1.OrchestratorFolders.gitAuthConfigScript}
|
||||||
BRANCH="${orchestrator_1.default.buildParameters.orchestratorBranch}"
|
BRANCH="${orchestrator_1.default.buildParameters.orchestratorBranch}"
|
||||||
REPO="${orchestrator_folders_1.OrchestratorFolders.unityBuilderRepoUrl}"
|
REPO="${orchestrator_folders_1.OrchestratorFolders.unityBuilderRepoUrl}"
|
||||||
if [ -n "$(git ls-remote --heads "$REPO" "$BRANCH" 2>/dev/null)" ]; then
|
if [ -n "$(git ls-remote --heads "$REPO" "$BRANCH" 2>/dev/null)" ]; then
|
||||||
@@ -10428,6 +9926,7 @@ class BuildAutomationWorkflow {
|
|||||||
static setupCommands(builderPath, isContainerized) {
|
static setupCommands(builderPath, isContainerized) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const commands = `mkdir -p ${orchestrator_folders_1.OrchestratorFolders.ToLinuxFolder(orchestrator_folders_1.OrchestratorFolders.builderPathAbsolute)}
|
const commands = `mkdir -p ${orchestrator_folders_1.OrchestratorFolders.ToLinuxFolder(orchestrator_folders_1.OrchestratorFolders.builderPathAbsolute)}
|
||||||
|
${orchestrator_folders_1.OrchestratorFolders.gitAuthConfigScript}
|
||||||
BRANCH="${orchestrator_1.default.buildParameters.orchestratorBranch}"
|
BRANCH="${orchestrator_1.default.buildParameters.orchestratorBranch}"
|
||||||
REPO="${orchestrator_folders_1.OrchestratorFolders.unityBuilderRepoUrl}"
|
REPO="${orchestrator_folders_1.OrchestratorFolders.unityBuilderRepoUrl}"
|
||||||
DEST="${orchestrator_folders_1.OrchestratorFolders.ToLinuxFolder(orchestrator_folders_1.OrchestratorFolders.builderPathAbsolute)}"
|
DEST="${orchestrator_folders_1.OrchestratorFolders.ToLinuxFolder(orchestrator_folders_1.OrchestratorFolders.builderPathAbsolute)}"
|
||||||
|
|||||||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
35
src/index.ts
35
src/index.ts
@@ -3,7 +3,6 @@ import { Action, BuildParameters, Cache, Orchestrator, Docker, ImageTag, Output
|
|||||||
import { Cli } from './model/cli/cli';
|
import { Cli } from './model/cli/cli';
|
||||||
import MacBuilder from './model/mac-builder';
|
import MacBuilder from './model/mac-builder';
|
||||||
import PlatformSetup from './model/platform-setup';
|
import PlatformSetup from './model/platform-setup';
|
||||||
import { BuildReliabilityService } from './model/orchestrator/services/reliability';
|
|
||||||
|
|
||||||
async function runMain() {
|
async function runMain() {
|
||||||
try {
|
try {
|
||||||
@@ -15,38 +14,11 @@ async function runMain() {
|
|||||||
Action.checkCompatibility();
|
Action.checkCompatibility();
|
||||||
Cache.verify();
|
Cache.verify();
|
||||||
|
|
||||||
// Always configure git environment for CI reliability
|
|
||||||
BuildReliabilityService.configureGitEnvironment();
|
|
||||||
|
|
||||||
const { workspace, actionFolder } = Action;
|
const { workspace, actionFolder } = Action;
|
||||||
|
|
||||||
const buildParameters = await BuildParameters.create();
|
const buildParameters = await BuildParameters.create();
|
||||||
const baseImage = new ImageTag(buildParameters);
|
const baseImage = new ImageTag(buildParameters);
|
||||||
|
|
||||||
// Pre-build reliability checks
|
|
||||||
if (buildParameters.gitIntegrityCheck) {
|
|
||||||
core.info('Running git integrity checks...');
|
|
||||||
|
|
||||||
const isHealthy = BuildReliabilityService.checkGitIntegrity(workspace);
|
|
||||||
BuildReliabilityService.cleanStaleLockFiles(workspace);
|
|
||||||
BuildReliabilityService.validateSubmoduleBackingStores(workspace);
|
|
||||||
|
|
||||||
if (buildParameters.cleanReservedFilenames) {
|
|
||||||
BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isHealthy && buildParameters.gitAutoRecover) {
|
|
||||||
core.info('Git corruption detected, attempting automatic recovery...');
|
|
||||||
const recovered = BuildReliabilityService.recoverCorruptedRepo(workspace);
|
|
||||||
if (!recovered) {
|
|
||||||
core.warning('Automatic recovery failed. Build may encounter issues.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (buildParameters.cleanReservedFilenames) {
|
|
||||||
// cleanReservedFilenames can run independently of gitIntegrityCheck
|
|
||||||
BuildReliabilityService.cleanReservedFilenames(buildParameters.projectPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
let exitCode = -1;
|
let exitCode = -1;
|
||||||
|
|
||||||
if (buildParameters.providerStrategy === 'local') {
|
if (buildParameters.providerStrategy === 'local') {
|
||||||
@@ -65,13 +37,6 @@ async function runMain() {
|
|||||||
exitCode = 0;
|
exitCode = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post-build: archive and enforce retention
|
|
||||||
if (buildParameters.buildArchiveEnabled && exitCode === 0) {
|
|
||||||
core.info('Archiving build output...');
|
|
||||||
BuildReliabilityService.archiveBuildOutput(buildParameters.buildPath, buildParameters.buildArchivePath);
|
|
||||||
BuildReliabilityService.enforceRetention(buildParameters.buildArchivePath, buildParameters.buildArchiveRetention);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set output
|
// Set output
|
||||||
await Output.setBuildVersion(buildParameters.buildVersion);
|
await Output.setBuildVersion(buildParameters.buildVersion);
|
||||||
await Output.setAndroidVersionCode(buildParameters.androidVersionCode);
|
await Output.setAndroidVersionCode(buildParameters.androidVersionCode);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class BuildParameters {
|
|||||||
public sshAgent!: string;
|
public sshAgent!: string;
|
||||||
public sshPublicKeysDirectoryPath!: string;
|
public sshPublicKeysDirectoryPath!: string;
|
||||||
public providerStrategy!: string;
|
public providerStrategy!: string;
|
||||||
|
public gitAuthMode!: string;
|
||||||
public gitPrivateToken!: string;
|
public gitPrivateToken!: string;
|
||||||
public awsStackName!: string;
|
public awsStackName!: string;
|
||||||
public awsEndpoint?: string;
|
public awsEndpoint?: string;
|
||||||
@@ -106,12 +107,6 @@ class BuildParameters {
|
|||||||
public cacheUnityInstallationOnMac!: boolean;
|
public cacheUnityInstallationOnMac!: boolean;
|
||||||
public unityHubVersionOnMac!: string;
|
public unityHubVersionOnMac!: string;
|
||||||
public dockerWorkspacePath!: string;
|
public dockerWorkspacePath!: string;
|
||||||
public gitIntegrityCheck!: boolean;
|
|
||||||
public gitAutoRecover!: boolean;
|
|
||||||
public cleanReservedFilenames!: boolean;
|
|
||||||
public buildArchiveEnabled!: boolean;
|
|
||||||
public buildArchivePath!: string;
|
|
||||||
public buildArchiveRetention!: number;
|
|
||||||
|
|
||||||
public static shouldUseRetainedWorkspaceMode(buildParameters: BuildParameters) {
|
public static shouldUseRetainedWorkspaceMode(buildParameters: BuildParameters) {
|
||||||
return buildParameters.maxRetainedWorkspaces > 0 && Orchestrator.lockedWorkspace !== ``;
|
return buildParameters.maxRetainedWorkspaces > 0 && Orchestrator.lockedWorkspace !== ``;
|
||||||
@@ -200,6 +195,7 @@ class BuildParameters {
|
|||||||
containerRegistryRepository: Input.containerRegistryRepository,
|
containerRegistryRepository: Input.containerRegistryRepository,
|
||||||
containerRegistryImageVersion: Input.containerRegistryImageVersion,
|
containerRegistryImageVersion: Input.containerRegistryImageVersion,
|
||||||
providerStrategy: OrchestratorOptions.providerStrategy,
|
providerStrategy: OrchestratorOptions.providerStrategy,
|
||||||
|
gitAuthMode: OrchestratorOptions.gitAuthMode,
|
||||||
buildPlatform: OrchestratorOptions.buildPlatform,
|
buildPlatform: OrchestratorOptions.buildPlatform,
|
||||||
kubeConfig: OrchestratorOptions.kubeConfig,
|
kubeConfig: OrchestratorOptions.kubeConfig,
|
||||||
containerMemory: OrchestratorOptions.containerMemory,
|
containerMemory: OrchestratorOptions.containerMemory,
|
||||||
@@ -248,12 +244,6 @@ class BuildParameters {
|
|||||||
cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac,
|
cacheUnityInstallationOnMac: Input.cacheUnityInstallationOnMac,
|
||||||
unityHubVersionOnMac: Input.unityHubVersionOnMac,
|
unityHubVersionOnMac: Input.unityHubVersionOnMac,
|
||||||
dockerWorkspacePath: Input.dockerWorkspacePath,
|
dockerWorkspacePath: Input.dockerWorkspacePath,
|
||||||
gitIntegrityCheck: Input.gitIntegrityCheck,
|
|
||||||
gitAutoRecover: Input.gitAutoRecover,
|
|
||||||
cleanReservedFilenames: Input.cleanReservedFilenames,
|
|
||||||
buildArchiveEnabled: Input.buildArchiveEnabled,
|
|
||||||
buildArchivePath: Input.buildArchivePath,
|
|
||||||
buildArchiveRetention: Input.buildArchiveRetention,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -282,38 +282,6 @@ class Input {
|
|||||||
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get gitIntegrityCheck(): boolean {
|
|
||||||
const input = Input.getInput('gitIntegrityCheck') ?? 'false';
|
|
||||||
|
|
||||||
return input === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get gitAutoRecover(): boolean {
|
|
||||||
const input = Input.getInput('gitAutoRecover') ?? 'false';
|
|
||||||
|
|
||||||
return input === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get cleanReservedFilenames(): boolean {
|
|
||||||
const input = Input.getInput('cleanReservedFilenames') ?? 'false';
|
|
||||||
|
|
||||||
return input === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get buildArchiveEnabled(): boolean {
|
|
||||||
const input = Input.getInput('buildArchiveEnabled') ?? 'false';
|
|
||||||
|
|
||||||
return input === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get buildArchivePath(): string {
|
|
||||||
return Input.getInput('buildArchivePath') ?? './build-archives';
|
|
||||||
}
|
|
||||||
|
|
||||||
static get buildArchiveRetention(): number {
|
|
||||||
return Number.parseInt(Input.getInput('buildArchiveRetention') ?? '30', 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ToEnvVarFormat(input: string) {
|
public static ToEnvVarFormat(input: string) {
|
||||||
if (input.toUpperCase() === input) {
|
if (input.toUpperCase() === input) {
|
||||||
return input;
|
return input;
|
||||||
|
|||||||
140
src/model/orchestrator/options/orchestrator-folders-auth.test.ts
Normal file
140
src/model/orchestrator/options/orchestrator-folders-auth.test.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { OrchestratorFolders } from './orchestrator-folders';
|
||||||
|
|
||||||
|
jest.mock('../orchestrator', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
buildParameters: {
|
||||||
|
orchestratorRepoName: 'game-ci/unity-builder',
|
||||||
|
githubRepo: 'myorg/myrepo',
|
||||||
|
gitPrivateToken: 'ghp_test123',
|
||||||
|
gitAuthMode: 'header',
|
||||||
|
buildGuid: 'test-guid',
|
||||||
|
projectPath: '',
|
||||||
|
buildPath: 'Builds',
|
||||||
|
cacheKey: 'test-cache',
|
||||||
|
},
|
||||||
|
lockedWorkspace: '',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./orchestrator-options', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
useSharedBuilder: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../services/core/orchestrator-system', () => ({
|
||||||
|
OrchestratorSystem: {
|
||||||
|
Run: jest.fn().mockResolvedValue(''),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockOrchestrator = require('../orchestrator').default;
|
||||||
|
|
||||||
|
describe('OrchestratorFolders git auth', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useHeaderAuth', () => {
|
||||||
|
it('should return true when gitAuthMode is header', () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'header';
|
||||||
|
expect(OrchestratorFolders.useHeaderAuth).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when gitAuthMode is undefined (default)', () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = undefined;
|
||||||
|
expect(OrchestratorFolders.useHeaderAuth).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when gitAuthMode is url', () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'url';
|
||||||
|
expect(OrchestratorFolders.useHeaderAuth).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unityBuilderRepoUrl', () => {
|
||||||
|
it('should not include token in URL when using header auth', () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'header';
|
||||||
|
const url = OrchestratorFolders.unityBuilderRepoUrl;
|
||||||
|
expect(url).toBe('https://github.com/game-ci/unity-builder.git');
|
||||||
|
expect(url).not.toContain('ghp_test123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include token in URL when using url auth (legacy)', () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'url';
|
||||||
|
const url = OrchestratorFolders.unityBuilderRepoUrl;
|
||||||
|
expect(url).toBe('https://ghp_test123@github.com/game-ci/unity-builder.git');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('targetBuildRepoUrl', () => {
|
||||||
|
it('should not include token in URL when using header auth', () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'header';
|
||||||
|
const url = OrchestratorFolders.targetBuildRepoUrl;
|
||||||
|
expect(url).toBe('https://github.com/myorg/myrepo.git');
|
||||||
|
expect(url).not.toContain('ghp_test123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include token in URL when using url auth (legacy)', () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'url';
|
||||||
|
const url = OrchestratorFolders.targetBuildRepoUrl;
|
||||||
|
expect(url).toBe('https://ghp_test123@github.com/myorg/myrepo.git');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('gitAuthConfigScript', () => {
|
||||||
|
it('should emit http.extraHeader commands in header mode', () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'header';
|
||||||
|
const script = OrchestratorFolders.gitAuthConfigScript;
|
||||||
|
expect(script).toContain('http.extraHeader');
|
||||||
|
expect(script).toContain('GIT_PRIVATE_TOKEN');
|
||||||
|
expect(script).toContain('Authorization: Basic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit no-op comment in url mode', () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'url';
|
||||||
|
const script = OrchestratorFolders.gitAuthConfigScript;
|
||||||
|
expect(script).toContain('legacy');
|
||||||
|
expect(script).not.toContain('http.extraHeader');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('configureGitAuth', () => {
|
||||||
|
it('should run git config with http.extraHeader in header mode', async () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'header';
|
||||||
|
mockOrchestrator.buildParameters.gitPrivateToken = 'ghp_test123';
|
||||||
|
const { OrchestratorSystem } = require('../services/core/orchestrator-system');
|
||||||
|
|
||||||
|
await OrchestratorFolders.configureGitAuth();
|
||||||
|
|
||||||
|
// Verify the base64 encoding and extraHeader config are correct
|
||||||
|
const expectedEncoded = Buffer.from('x-access-token:ghp_test123').toString('base64');
|
||||||
|
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(expect.stringContaining(expectedEncoded));
|
||||||
|
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(expect.stringContaining('.extraHeader'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run git config in url mode', async () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'url';
|
||||||
|
const { OrchestratorSystem } = require('../services/core/orchestrator-system');
|
||||||
|
|
||||||
|
await OrchestratorFolders.configureGitAuth();
|
||||||
|
|
||||||
|
expect(OrchestratorSystem.Run).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run git config when no token is available', async () => {
|
||||||
|
mockOrchestrator.buildParameters.gitAuthMode = 'header';
|
||||||
|
mockOrchestrator.buildParameters.gitPrivateToken = '';
|
||||||
|
const originalEnv = process.env.GIT_PRIVATE_TOKEN;
|
||||||
|
delete process.env.GIT_PRIVATE_TOKEN;
|
||||||
|
const { OrchestratorSystem } = require('../services/core/orchestrator-system');
|
||||||
|
|
||||||
|
await OrchestratorFolders.configureGitAuth();
|
||||||
|
|
||||||
|
expect(OrchestratorSystem.Run).not.toHaveBeenCalled();
|
||||||
|
if (originalEnv !== undefined) process.env.GIT_PRIVATE_TOKEN = originalEnv;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -72,14 +72,67 @@ export class OrchestratorFolders {
|
|||||||
return path.join(OrchestratorFolders.cacheFolderForCacheKeyFull, `Library`);
|
return path.join(OrchestratorFolders.cacheFolderForCacheKeyFull, `Library`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to use http.extraHeader for git authentication (secure, default)
|
||||||
|
* instead of embedding the token in clone URLs (legacy).
|
||||||
|
*/
|
||||||
|
public static get useHeaderAuth(): boolean {
|
||||||
|
return Orchestrator.buildParameters.gitAuthMode !== 'url';
|
||||||
|
}
|
||||||
|
|
||||||
public static get unityBuilderRepoUrl(): string {
|
public static get unityBuilderRepoUrl(): string {
|
||||||
|
if (OrchestratorFolders.useHeaderAuth) {
|
||||||
|
return `https://github.com/${Orchestrator.buildParameters.orchestratorRepoName}.git`;
|
||||||
|
}
|
||||||
|
|
||||||
return `https://${Orchestrator.buildParameters.gitPrivateToken}@github.com/${Orchestrator.buildParameters.orchestratorRepoName}.git`;
|
return `https://${Orchestrator.buildParameters.gitPrivateToken}@github.com/${Orchestrator.buildParameters.orchestratorRepoName}.git`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get targetBuildRepoUrl(): string {
|
public static get targetBuildRepoUrl(): string {
|
||||||
|
if (OrchestratorFolders.useHeaderAuth) {
|
||||||
|
return `https://github.com/${Orchestrator.buildParameters.githubRepo}.git`;
|
||||||
|
}
|
||||||
|
|
||||||
return `https://${Orchestrator.buildParameters.gitPrivateToken}@github.com/${Orchestrator.buildParameters.githubRepo}.git`;
|
return `https://${Orchestrator.buildParameters.gitPrivateToken}@github.com/${Orchestrator.buildParameters.githubRepo}.git`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shell commands to configure git authentication via http.extraHeader.
|
||||||
|
* Uses GIT_PRIVATE_TOKEN env var so the token never appears in clone URLs or git config output.
|
||||||
|
* This is the same mechanism used by actions/checkout.
|
||||||
|
*
|
||||||
|
* Only emits commands when gitAuthMode is 'header' (default). In 'url' mode,
|
||||||
|
* returns a no-op comment since the token is already in the URL.
|
||||||
|
*/
|
||||||
|
public static get gitAuthConfigScript(): string {
|
||||||
|
if (!OrchestratorFolders.useHeaderAuth) {
|
||||||
|
return `# git auth: using token-in-URL mode (legacy)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `# git auth: configuring http.extraHeader (secure mode)
|
||||||
|
if [ -n "$GIT_PRIVATE_TOKEN" ]; then
|
||||||
|
git config --global http.https://github.com/.extraHeader "Authorization: Basic $(printf '%s' "x-access-token:$GIT_PRIVATE_TOKEN" | base64 -w 0)"
|
||||||
|
fi`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure git authentication via http.extraHeader in the current Node process.
|
||||||
|
* For use in the remote-client where shell scripts aren't used.
|
||||||
|
* Only configures when gitAuthMode is 'header' (default).
|
||||||
|
*/
|
||||||
|
public static async configureGitAuth(): Promise<void> {
|
||||||
|
if (!OrchestratorFolders.useHeaderAuth) return;
|
||||||
|
|
||||||
|
const token = Orchestrator.buildParameters.gitPrivateToken || process.env.GIT_PRIVATE_TOKEN || '';
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
const encoded = Buffer.from(`x-access-token:${token}`).toString('base64');
|
||||||
|
const { OrchestratorSystem } = await import('../services/core/orchestrator-system');
|
||||||
|
await OrchestratorSystem.Run(
|
||||||
|
`git config --global http.https://github.com/.extraHeader "Authorization: Basic ${encoded}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static get buildVolumeFolder() {
|
public static get buildVolumeFolder() {
|
||||||
return 'data';
|
return 'data';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ class OrchestratorOptions {
|
|||||||
return provider || 'local';
|
return provider || 'local';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get gitAuthMode(): string {
|
||||||
|
return OrchestratorOptions.getInput('gitAuthMode') || 'header';
|
||||||
|
}
|
||||||
|
|
||||||
static get containerCpu(): string {
|
static get containerCpu(): string {
|
||||||
return OrchestratorOptions.getInput('containerCpu') || `1024`;
|
return OrchestratorOptions.getInput('containerCpu') || `1024`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ export class RemoteClient {
|
|||||||
|
|
||||||
RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`);
|
RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`);
|
||||||
await OrchestratorSystem.Run(`git config --global advice.detachedHead false`);
|
await OrchestratorSystem.Run(`git config --global advice.detachedHead false`);
|
||||||
|
await OrchestratorFolders.configureGitAuth();
|
||||||
RemoteClientLogger.log(`Cloning the repository being built:`);
|
RemoteClientLogger.log(`Cloning the repository being built:`);
|
||||||
await OrchestratorSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`);
|
await OrchestratorSystem.Run(`git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"`);
|
||||||
await OrchestratorSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`);
|
await OrchestratorSystem.Run(`git config --global filter.lfs.process "git-lfs filter-process --skip"`);
|
||||||
@@ -411,12 +412,7 @@ export class RemoteClient {
|
|||||||
const gitPrivateToken = process.env.GIT_PRIVATE_TOKEN;
|
const gitPrivateToken = process.env.GIT_PRIVATE_TOKEN;
|
||||||
if (gitPrivateToken) {
|
if (gitPrivateToken) {
|
||||||
RemoteClientLogger.log(`Attempting to pull LFS files with GIT_PRIVATE_TOKEN...`);
|
RemoteClientLogger.log(`Attempting to pull LFS files with GIT_PRIVATE_TOKEN...`);
|
||||||
await OrchestratorSystem.Run(`git config --global --unset-all url."https://github.com/".insteadOf || true`);
|
await RemoteClient.configureTokenAuth(gitPrivateToken);
|
||||||
await OrchestratorSystem.Run(`git config --global --unset-all url."ssh://git@github.com/".insteadOf || true`);
|
|
||||||
await OrchestratorSystem.Run(`git config --global --unset-all url."git@github.com".insteadOf || true`);
|
|
||||||
await OrchestratorSystem.Run(
|
|
||||||
`git config --global url."https://${gitPrivateToken}@github.com/".insteadOf "https://github.com/"`,
|
|
||||||
);
|
|
||||||
await OrchestratorSystem.Run(`git lfs pull`, true);
|
await OrchestratorSystem.Run(`git lfs pull`, true);
|
||||||
await OrchestratorSystem.Run(`git lfs checkout || true`, true);
|
await OrchestratorSystem.Run(`git lfs checkout || true`, true);
|
||||||
RemoteClientLogger.log(`Successfully pulled LFS files with GIT_PRIVATE_TOKEN`);
|
RemoteClientLogger.log(`Successfully pulled LFS files with GIT_PRIVATE_TOKEN`);
|
||||||
@@ -432,12 +428,7 @@ export class RemoteClient {
|
|||||||
const githubToken = process.env.GITHUB_TOKEN;
|
const githubToken = process.env.GITHUB_TOKEN;
|
||||||
if (githubToken) {
|
if (githubToken) {
|
||||||
RemoteClientLogger.log(`Attempting to pull LFS files with GITHUB_TOKEN fallback...`);
|
RemoteClientLogger.log(`Attempting to pull LFS files with GITHUB_TOKEN fallback...`);
|
||||||
await OrchestratorSystem.Run(`git config --global --unset-all url."https://github.com/".insteadOf || true`);
|
await RemoteClient.configureTokenAuth(githubToken);
|
||||||
await OrchestratorSystem.Run(`git config --global --unset-all url."ssh://git@github.com/".insteadOf || true`);
|
|
||||||
await OrchestratorSystem.Run(`git config --global --unset-all url."git@github.com".insteadOf || true`);
|
|
||||||
await OrchestratorSystem.Run(
|
|
||||||
`git config --global url."https://${githubToken}@github.com/".insteadOf "https://github.com/"`,
|
|
||||||
);
|
|
||||||
await OrchestratorSystem.Run(`git lfs pull`, true);
|
await OrchestratorSystem.Run(`git lfs pull`, true);
|
||||||
await OrchestratorSystem.Run(`git lfs checkout || true`, true);
|
await OrchestratorSystem.Run(`git lfs checkout || true`, true);
|
||||||
RemoteClientLogger.log(`Successfully pulled LFS files with GITHUB_TOKEN`);
|
RemoteClientLogger.log(`Successfully pulled LFS files with GITHUB_TOKEN`);
|
||||||
@@ -501,4 +492,25 @@ export class RemoteClient {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure git authentication for a token. In header mode (default), uses
|
||||||
|
* http.extraHeader so the token never appears in URLs or git config output.
|
||||||
|
* In url mode (legacy), uses url.insteadOf to embed the token in URLs.
|
||||||
|
*/
|
||||||
|
private static async configureTokenAuth(token: string): Promise<void> {
|
||||||
|
if (OrchestratorFolders.useHeaderAuth) {
|
||||||
|
const encoded = Buffer.from(`x-access-token:${token}`).toString('base64');
|
||||||
|
await OrchestratorSystem.Run(
|
||||||
|
`git config --global http.https://github.com/.extraHeader "Authorization: Basic ${encoded}"`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await OrchestratorSystem.Run(`git config --global --unset-all url."https://github.com/".insteadOf || true`);
|
||||||
|
await OrchestratorSystem.Run(`git config --global --unset-all url."ssh://git@github.com/".insteadOf || true`);
|
||||||
|
await OrchestratorSystem.Run(`git config --global --unset-all url."git@github.com".insteadOf || true`);
|
||||||
|
await OrchestratorSystem.Run(
|
||||||
|
`git config --global url."https://${token}@github.com/".insteadOf "https://github.com/"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,638 +0,0 @@
|
|||||||
import { execSync, execFileSync } from 'node:child_process';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { BuildReliabilityService } from './build-reliability-service';
|
|
||||||
|
|
||||||
// Mock dependencies
|
|
||||||
jest.mock('node:child_process');
|
|
||||||
jest.mock('node:fs');
|
|
||||||
jest.mock('@actions/core', () => ({
|
|
||||||
info: jest.fn(),
|
|
||||||
warning: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockExecSync = execSync as jest.MockedFunction<typeof execSync>;
|
|
||||||
const mockExecFileSync = execFileSync as jest.MockedFunction<typeof execFileSync>;
|
|
||||||
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
||||||
|
|
||||||
describe('BuildReliabilityService', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// checkGitIntegrity
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('checkGitIntegrity', () => {
|
|
||||||
it('should return true when fsck succeeds with clean output', () => {
|
|
||||||
mockExecSync.mockReturnValue('');
|
|
||||||
const result = BuildReliabilityService.checkGitIntegrity('/repo');
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(mockExecSync).toHaveBeenCalledWith(
|
|
||||||
'git -C "/repo" fsck --no-dangling',
|
|
||||||
expect.objectContaining({ encoding: 'utf8' }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when fsck output contains corruption indicators', () => {
|
|
||||||
mockExecSync.mockReturnValue('broken link from tree abc123');
|
|
||||||
const result = BuildReliabilityService.checkGitIntegrity('/repo');
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when fsck output contains missing objects', () => {
|
|
||||||
mockExecSync.mockReturnValue('missing blob abc123');
|
|
||||||
const result = BuildReliabilityService.checkGitIntegrity('/repo');
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when execSync throws (non-zero exit code)', () => {
|
|
||||||
mockExecSync.mockImplementation(() => {
|
|
||||||
const error: any = new Error('fsck failed');
|
|
||||||
error.stderr = Buffer.from('error: bad object HEAD');
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
const result = BuildReliabilityService.checkGitIntegrity('/repo');
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use current directory when no repoPath provided', () => {
|
|
||||||
mockExecSync.mockReturnValue('');
|
|
||||||
BuildReliabilityService.checkGitIntegrity();
|
|
||||||
expect(mockExecSync).toHaveBeenCalledWith('git -C "." fsck --no-dangling', expect.anything());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// cleanStaleLockFiles
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('cleanStaleLockFiles', () => {
|
|
||||||
it('should return 0 when .git directory does not exist', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
const result = BuildReliabilityService.cleanStaleLockFiles('/repo');
|
|
||||||
expect(result).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove lock files older than 10 minutes', () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const oldTime = now - 15 * 60 * 1000; // 15 minutes ago
|
|
||||||
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.statSync.mockImplementation((filePath: any) => {
|
|
||||||
if (filePath === path.join('/repo', '.git')) {
|
|
||||||
return { isDirectory: () => true } as fs.Stats;
|
|
||||||
}
|
|
||||||
return { mtimeMs: oldTime } as fs.Stats;
|
|
||||||
});
|
|
||||||
mockFs.readdirSync.mockImplementation((dir: any) => {
|
|
||||||
if (dir === path.join('/repo', '.git')) {
|
|
||||||
return [
|
|
||||||
{ name: 'index.lock', isDirectory: () => false },
|
|
||||||
{ name: 'HEAD.lock', isDirectory: () => false },
|
|
||||||
] as any;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
mockFs.unlinkSync.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.cleanStaleLockFiles('/repo');
|
|
||||||
expect(result).toBe(2);
|
|
||||||
expect(mockFs.unlinkSync).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should NOT remove lock files younger than 10 minutes', () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const recentTime = now - 2 * 60 * 1000; // 2 minutes ago
|
|
||||||
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.statSync.mockImplementation((filePath: any) => {
|
|
||||||
if (filePath === path.join('/repo', '.git')) {
|
|
||||||
return { isDirectory: () => true } as fs.Stats;
|
|
||||||
}
|
|
||||||
return { mtimeMs: recentTime } as fs.Stats;
|
|
||||||
});
|
|
||||||
mockFs.readdirSync.mockImplementation((dir: any) => {
|
|
||||||
if (dir === path.join('/repo', '.git')) {
|
|
||||||
return [{ name: 'index.lock', isDirectory: () => false }] as any;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.cleanStaleLockFiles('/repo');
|
|
||||||
expect(result).toBe(0);
|
|
||||||
expect(mockFs.unlinkSync).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should recursively scan refs directory for lock files', () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const oldTime = now - 15 * 60 * 1000;
|
|
||||||
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.statSync.mockImplementation((filePath: any) => {
|
|
||||||
if (filePath === path.join('/repo', '.git')) {
|
|
||||||
return { isDirectory: () => true } as fs.Stats;
|
|
||||||
}
|
|
||||||
return { mtimeMs: oldTime } as fs.Stats;
|
|
||||||
});
|
|
||||||
mockFs.readdirSync.mockImplementation((dir: any) => {
|
|
||||||
const gitDir = path.join('/repo', '.git');
|
|
||||||
if (dir === gitDir) {
|
|
||||||
return [{ name: 'refs', isDirectory: () => true }] as any;
|
|
||||||
}
|
|
||||||
if (dir === path.join(gitDir, 'refs')) {
|
|
||||||
return [{ name: 'heads', isDirectory: () => true }] as any;
|
|
||||||
}
|
|
||||||
if (dir === path.join(gitDir, 'refs', 'heads')) {
|
|
||||||
return [{ name: 'main.lock', isDirectory: () => false }] as any;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
mockFs.unlinkSync.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.cleanStaleLockFiles('/repo');
|
|
||||||
expect(result).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// validateSubmoduleBackingStores
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('validateSubmoduleBackingStores', () => {
|
|
||||||
it('should return empty array when .gitmodules does not exist', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
const result = BuildReliabilityService.validateSubmoduleBackingStores('/repo');
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect broken backing store for submodule', () => {
|
|
||||||
mockFs.existsSync.mockImplementation((p: any) => {
|
|
||||||
if (p === path.join('/repo', '.gitmodules')) return true;
|
|
||||||
if (p === path.join('/repo', 'lib/sub', '.git')) return true;
|
|
||||||
// Backing store does not exist
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
mockFs.readFileSync.mockImplementation((p: any) => {
|
|
||||||
if (p === path.join('/repo', '.gitmodules')) {
|
|
||||||
return '[submodule "sub"]\n\tpath = lib/sub\n\turl = https://example.com/sub.git';
|
|
||||||
}
|
|
||||||
if (p === path.join('/repo', 'lib/sub', '.git')) {
|
|
||||||
return 'gitdir: ../../.git/modules/lib/sub';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
mockFs.statSync.mockReturnValue({ isFile: () => true } as fs.Stats);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.validateSubmoduleBackingStores('/repo');
|
|
||||||
expect(result).toContain('lib/sub');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty array when all submodule backing stores are valid', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.readFileSync.mockImplementation((p: any) => {
|
|
||||||
if (p === path.join('/repo', '.gitmodules')) {
|
|
||||||
return '[submodule "sub"]\n\tpath = lib/sub\n\turl = https://example.com/sub.git';
|
|
||||||
}
|
|
||||||
if (p === path.join('/repo', 'lib/sub', '.git')) {
|
|
||||||
return 'gitdir: ../../.git/modules/lib/sub';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
mockFs.statSync.mockReturnValue({ isFile: () => true } as fs.Stats);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.validateSubmoduleBackingStores('/repo');
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// recoverCorruptedRepo
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('recoverCorruptedRepo', () => {
|
|
||||||
it('should orchestrate fsck cleanup and re-fetch, returning true on success', () => {
|
|
||||||
// cleanStaleLockFiles: no .git dir
|
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
mockFs.statSync.mockReturnValue({ isDirectory: () => false } as fs.Stats);
|
|
||||||
|
|
||||||
// fetch succeeds, then fsck succeeds
|
|
||||||
mockExecSync.mockReturnValue('');
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.recoverCorruptedRepo('/repo');
|
|
||||||
expect(result).toBe(true);
|
|
||||||
// Should have called fetch
|
|
||||||
expect(mockExecSync).toHaveBeenCalledWith('git -C "/repo" fetch --all', expect.anything());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when recovery fails to restore integrity', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
mockFs.statSync.mockReturnValue({ isDirectory: () => false } as fs.Stats);
|
|
||||||
|
|
||||||
// fetch succeeds, but fsck fails
|
|
||||||
mockExecSync.mockImplementation((cmd: any) => {
|
|
||||||
if (typeof cmd === 'string' && cmd.includes('fetch')) return '';
|
|
||||||
if (typeof cmd === 'string' && cmd.includes('fsck')) {
|
|
||||||
return 'missing blob abc123';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.recoverCorruptedRepo('/repo');
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should continue recovery even when fetch fails', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
mockFs.statSync.mockReturnValue({ isDirectory: () => false } as fs.Stats);
|
|
||||||
|
|
||||||
let callCount = 0;
|
|
||||||
mockExecSync.mockImplementation((cmd: any) => {
|
|
||||||
callCount++;
|
|
||||||
if (typeof cmd === 'string' && cmd.includes('fetch')) {
|
|
||||||
throw new Error('network error');
|
|
||||||
}
|
|
||||||
// fsck call
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.recoverCorruptedRepo('/repo');
|
|
||||||
// Should still attempt fsck after failed fetch
|
|
||||||
expect(callCount).toBeGreaterThanOrEqual(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// cleanReservedFilenames
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('cleanReservedFilenames', () => {
|
|
||||||
it('should return empty array when Assets directory does not exist', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
const result = BuildReliabilityService.cleanReservedFilenames('/project');
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove files with reserved names (con, prn, aux, nul)', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.readdirSync.mockImplementation((dir: any) => {
|
|
||||||
if (dir === path.join('/project', 'Assets')) {
|
|
||||||
return [
|
|
||||||
{ name: 'con.txt', isDirectory: () => false },
|
|
||||||
{ name: 'PRN.meta', isDirectory: () => false },
|
|
||||||
{ name: 'aux.shader', isDirectory: () => false },
|
|
||||||
{ name: 'nul.png', isDirectory: () => false },
|
|
||||||
{ name: 'valid-file.cs', isDirectory: () => false },
|
|
||||||
] as any;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
mockFs.unlinkSync.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.cleanReservedFilenames('/project');
|
|
||||||
expect(result).toHaveLength(4);
|
|
||||||
expect(mockFs.unlinkSync).toHaveBeenCalledTimes(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove directories with reserved names', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.readdirSync.mockImplementation((dir: any) => {
|
|
||||||
if (dir === path.join('/project', 'Assets')) {
|
|
||||||
return [{ name: 'com1', isDirectory: () => true }] as any;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
mockFs.rmSync.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.cleanReservedFilenames('/project');
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(mockFs.rmSync).toHaveBeenCalledWith(path.join('/project', 'Assets', 'com1'), {
|
|
||||||
recursive: true,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect COM1 through COM9 and LPT1 through LPT9', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.readdirSync.mockImplementation((dir: any) => {
|
|
||||||
if (dir === path.join('/project', 'Assets')) {
|
|
||||||
return [
|
|
||||||
{ name: 'com1.txt', isDirectory: () => false },
|
|
||||||
{ name: 'COM9.meta', isDirectory: () => false },
|
|
||||||
{ name: 'lpt1.dat', isDirectory: () => false },
|
|
||||||
{ name: 'LPT9.log', isDirectory: () => false },
|
|
||||||
] as any;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
mockFs.unlinkSync.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.cleanReservedFilenames('/project');
|
|
||||||
expect(result).toHaveLength(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not remove files that merely contain reserved names as substrings', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.readdirSync.mockImplementation((dir: any) => {
|
|
||||||
if (dir === path.join('/project', 'Assets')) {
|
|
||||||
return [
|
|
||||||
{ name: 'controller.cs', isDirectory: () => false },
|
|
||||||
{ name: 'printer-utils.cs', isDirectory: () => false },
|
|
||||||
{ name: 'auxiliary.shader', isDirectory: () => false },
|
|
||||||
] as any;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.cleanReservedFilenames('/project');
|
|
||||||
expect(result).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// getAvailableSpaceMB
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('getAvailableSpaceMB', () => {
|
|
||||||
it('should return -1 when the check fails', () => {
|
|
||||||
mockExecFileSync.mockImplementation(() => {
|
|
||||||
throw new Error('Command failed');
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.getAvailableSpaceMB('/some/path');
|
|
||||||
expect(result).toBe(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse wmic output on Windows', () => {
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
||||||
|
|
||||||
// 10 GB in bytes
|
|
||||||
mockExecFileSync.mockReturnValue('\r\nFreeSpace=10737418240\r\n' as any);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.getAvailableSpaceMB('C:\\builds');
|
|
||||||
// 10737418240 / (1024 * 1024) = 10240 MB
|
|
||||||
expect(result).toBeCloseTo(10240, 0);
|
|
||||||
|
|
||||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse df output on Unix', () => {
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
||||||
|
|
||||||
mockExecFileSync.mockReturnValue(' Avail\n 5120M\n' as any);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.getAvailableSpaceMB('/builds');
|
|
||||||
expect(result).toBe(5120);
|
|
||||||
|
|
||||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// getDirectorySizeMB
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('getDirectorySizeMB', () => {
|
|
||||||
it('should return file size for a single file', () => {
|
|
||||||
// 5 MB in bytes
|
|
||||||
mockFs.statSync.mockReturnValue({ isDirectory: () => false, size: 5 * 1024 * 1024 } as any);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.getDirectorySizeMB('/path/to/file.zip');
|
|
||||||
expect(result).toBeCloseTo(5, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return total size for a directory tree', () => {
|
|
||||||
const subDir = path.join('/build', 'sub');
|
|
||||||
|
|
||||||
mockFs.statSync.mockImplementation((p: any) => {
|
|
||||||
const pathStr = typeof p === 'string' ? p : p.toString();
|
|
||||||
if (pathStr === '/build' || pathStr === subDir) {
|
|
||||||
return { isDirectory: () => true, size: 0 } as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isDirectory: () => false, size: 1024 * 1024 } as any; // 1 MB each
|
|
||||||
});
|
|
||||||
|
|
||||||
mockFs.readdirSync.mockImplementation((dirPath: any, _options?: any) => {
|
|
||||||
const dirStr = typeof dirPath === 'string' ? dirPath : dirPath.toString();
|
|
||||||
if (dirStr === '/build') {
|
|
||||||
return [
|
|
||||||
{ name: 'file1.bin', isDirectory: () => false },
|
|
||||||
{ name: 'sub', isDirectory: () => true },
|
|
||||||
] as any;
|
|
||||||
}
|
|
||||||
if (dirStr === subDir) {
|
|
||||||
return [{ name: 'file2.bin', isDirectory: () => false }] as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [] as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.getDirectorySizeMB('/build');
|
|
||||||
expect(result).toBeCloseTo(2, 0); // 2 files * 1 MB each
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return -1 when calculation fails', () => {
|
|
||||||
mockFs.statSync.mockImplementation(() => {
|
|
||||||
throw new Error('Access denied');
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.getDirectorySizeMB('/inaccessible');
|
|
||||||
expect(result).toBe(-1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// archiveBuildOutput
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('archiveBuildOutput', () => {
|
|
||||||
it('should skip archiving when source path does not exist', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
BuildReliabilityService.archiveBuildOutput('/builds/output', '/archives');
|
|
||||||
expect(mockExecSync).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create archive directory and tar.gz output', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.mkdirSync.mockReturnValue(undefined as any);
|
|
||||||
mockExecSync.mockReturnValue('');
|
|
||||||
// Make disk space check return unknown so we proceed
|
|
||||||
mockExecFileSync.mockImplementation(() => {
|
|
||||||
throw new Error('Command not found');
|
|
||||||
});
|
|
||||||
mockFs.statSync.mockImplementation(() => {
|
|
||||||
throw new Error('Not mocked');
|
|
||||||
});
|
|
||||||
|
|
||||||
BuildReliabilityService.archiveBuildOutput('/builds/output', '/archives');
|
|
||||||
|
|
||||||
expect(mockFs.mkdirSync).toHaveBeenCalledWith('/archives', { recursive: true });
|
|
||||||
expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining('tar -czf'), expect.anything());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip archival when insufficient disk space', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.mkdirSync.mockReturnValue(undefined as any);
|
|
||||||
|
|
||||||
// Source is 1000 MB
|
|
||||||
mockFs.statSync.mockImplementation((p: any) => {
|
|
||||||
const pathStr = typeof p === 'string' ? p : p.toString();
|
|
||||||
if (pathStr.endsWith('big-file.bin')) {
|
|
||||||
return { isDirectory: () => false, size: 1000 * 1024 * 1024 } as any;
|
|
||||||
}
|
|
||||||
return { isDirectory: () => true, size: 0 } as any;
|
|
||||||
});
|
|
||||||
mockFs.readdirSync.mockImplementation(() => {
|
|
||||||
return [{ name: 'big-file.bin', isDirectory: () => false }] as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only 500 MB available
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
||||||
mockExecFileSync.mockReturnValue(' Avail\n 500M\n' as any);
|
|
||||||
|
|
||||||
BuildReliabilityService.archiveBuildOutput('/builds/output', '/archives');
|
|
||||||
|
|
||||||
// Should NOT have attempted the tar command
|
|
||||||
expect(mockExecSync).not.toHaveBeenCalledWith(expect.stringContaining('tar'), expect.anything());
|
|
||||||
|
|
||||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clean up partial archive on tar failure', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.mkdirSync.mockReturnValue(undefined as any);
|
|
||||||
mockFs.unlinkSync.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
// Make disk space check return unknown so we proceed
|
|
||||||
mockExecFileSync.mockImplementation(() => {
|
|
||||||
throw new Error('Command not found');
|
|
||||||
});
|
|
||||||
mockFs.statSync.mockImplementation(() => {
|
|
||||||
throw new Error('Not mocked');
|
|
||||||
});
|
|
||||||
|
|
||||||
// tar command fails
|
|
||||||
mockExecSync.mockImplementation(() => {
|
|
||||||
const error: any = new Error('tar failed');
|
|
||||||
error.stderr = Buffer.from('No space left on device');
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
BuildReliabilityService.archiveBuildOutput('/builds/output', '/archives');
|
|
||||||
|
|
||||||
// Should have attempted to clean up the partial archive
|
|
||||||
// (existsSync returns true for the partial file)
|
|
||||||
expect(mockFs.unlinkSync).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should proceed with warning when disk space check fails', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.mkdirSync.mockReturnValue(undefined as any);
|
|
||||||
mockExecSync.mockReturnValue('');
|
|
||||||
|
|
||||||
// Disk space check fails
|
|
||||||
mockExecFileSync.mockImplementation(() => {
|
|
||||||
throw new Error('Command not found');
|
|
||||||
});
|
|
||||||
// Directory size check also fails
|
|
||||||
mockFs.statSync.mockImplementation(() => {
|
|
||||||
throw new Error('Not mocked');
|
|
||||||
});
|
|
||||||
|
|
||||||
BuildReliabilityService.archiveBuildOutput('/builds/output', '/archives');
|
|
||||||
|
|
||||||
// Should still proceed with tar
|
|
||||||
expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining('tar -czf'), expect.anything());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// enforceRetention
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('enforceRetention', () => {
|
|
||||||
it('should return 0 when archive path does not exist', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
const result = BuildReliabilityService.enforceRetention('/archive', 30);
|
|
||||||
expect(result).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove archives older than retention period', () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const oldTime = now - 45 * 24 * 60 * 60 * 1000; // 45 days ago
|
|
||||||
const recentTime = now - 5 * 24 * 60 * 60 * 1000; // 5 days ago
|
|
||||||
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.readdirSync.mockReturnValue([
|
|
||||||
{ name: 'build-old.tar.gz', isDirectory: () => false },
|
|
||||||
{ name: 'build-recent.tar.gz', isDirectory: () => false },
|
|
||||||
] as any);
|
|
||||||
mockFs.statSync.mockImplementation((p: any) => {
|
|
||||||
if ((p as string).includes('old')) {
|
|
||||||
return { mtimeMs: oldTime } as fs.Stats;
|
|
||||||
}
|
|
||||||
return { mtimeMs: recentTime } as fs.Stats;
|
|
||||||
});
|
|
||||||
mockFs.unlinkSync.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.enforceRetention('/archive', 30);
|
|
||||||
expect(result).toBe(1);
|
|
||||||
expect(mockFs.unlinkSync).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should keep all archives within retention period', () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const recentTime = now - 5 * 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
mockFs.readdirSync.mockReturnValue([
|
|
||||||
{ name: 'build-1.tar.gz', isDirectory: () => false },
|
|
||||||
{ name: 'build-2.tar.gz', isDirectory: () => false },
|
|
||||||
] as any);
|
|
||||||
mockFs.statSync.mockReturnValue({ mtimeMs: recentTime } as fs.Stats);
|
|
||||||
|
|
||||||
const result = BuildReliabilityService.enforceRetention('/archive', 30);
|
|
||||||
expect(result).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// configureGitEnvironment
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
describe('configureGitEnvironment', () => {
|
|
||||||
it('should set GIT_TERMINAL_PROMPT=0 in process.env', () => {
|
|
||||||
mockExecSync.mockReturnValue('');
|
|
||||||
BuildReliabilityService.configureGitEnvironment();
|
|
||||||
expect(process.env.GIT_TERMINAL_PROMPT).toBe('0');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should configure http.postBuffer via git config', () => {
|
|
||||||
mockExecSync.mockReturnValue('');
|
|
||||||
BuildReliabilityService.configureGitEnvironment();
|
|
||||||
expect(mockExecSync).toHaveBeenCalledWith('git config --global http.postBuffer 524288000', expect.anything());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should configure core.longpaths via git config', () => {
|
|
||||||
mockExecSync.mockReturnValue('');
|
|
||||||
BuildReliabilityService.configureGitEnvironment();
|
|
||||||
expect(mockExecSync).toHaveBeenCalledWith('git config --global core.longpaths true', expect.anything());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should warn but not throw when git config commands fail', () => {
|
|
||||||
const core = require('@actions/core');
|
|
||||||
mockExecSync.mockImplementation(() => {
|
|
||||||
throw new Error('git config failed');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should not throw
|
|
||||||
expect(() => BuildReliabilityService.configureGitEnvironment()).not.toThrow();
|
|
||||||
expect(core.warning).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,527 +0,0 @@
|
|||||||
import { execSync, execFileSync } from 'node:child_process';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build reliability features for hardening CI pipelines.
|
|
||||||
* Provides git integrity checks, stale lock cleanup, submodule validation,
|
|
||||||
* reserved filename removal, build archival, and git environment configuration.
|
|
||||||
* All features are opt-in and fail gracefully (warnings only).
|
|
||||||
*/
|
|
||||||
export class BuildReliabilityService {
|
|
||||||
// Windows reserved device names that cause Unity asset importer infinite loops
|
|
||||||
private static readonly RESERVED_NAMES = new Set([
|
|
||||||
'con',
|
|
||||||
'prn',
|
|
||||||
'aux',
|
|
||||||
'nul',
|
|
||||||
'com1',
|
|
||||||
'com2',
|
|
||||||
'com3',
|
|
||||||
'com4',
|
|
||||||
'com5',
|
|
||||||
'com6',
|
|
||||||
'com7',
|
|
||||||
'com8',
|
|
||||||
'com9',
|
|
||||||
'lpt1',
|
|
||||||
'lpt2',
|
|
||||||
'lpt3',
|
|
||||||
'lpt4',
|
|
||||||
'lpt5',
|
|
||||||
'lpt6',
|
|
||||||
'lpt7',
|
|
||||||
'lpt8',
|
|
||||||
'lpt9',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Lock files to look for in the .git directory
|
|
||||||
private static readonly LOCK_FILE_NAMES = new Set(['index.lock', 'shallow.lock', 'config.lock', 'HEAD.lock']);
|
|
||||||
|
|
||||||
// Maximum age in milliseconds before a lock file is considered stale (10 minutes)
|
|
||||||
private static readonly LOCK_FILE_MAX_AGE_MS = 10 * 60 * 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run git fsck to check repository integrity.
|
|
||||||
* Returns true if the repo is healthy, false if corruption detected.
|
|
||||||
*/
|
|
||||||
static checkGitIntegrity(repoPath: string = '.'): boolean {
|
|
||||||
core.info(`[Reliability] Checking git integrity in ${repoPath}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const output = execSync(`git -C "${repoPath}" fsck --no-dangling`, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 120_000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse output for corruption indicators
|
|
||||||
const corruptionPatterns = [
|
|
||||||
/broken link/i,
|
|
||||||
/missing (blob|tree|commit|tag)/i,
|
|
||||||
/dangling/i,
|
|
||||||
/corrupt/i,
|
|
||||||
/error in /i,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const pattern of corruptionPatterns) {
|
|
||||||
if (pattern.test(output)) {
|
|
||||||
core.warning(`[Reliability] Git integrity check found issues: ${output.trim()}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info('[Reliability] Git integrity check passed');
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
// execSync throws on non-zero exit code
|
|
||||||
const stderr = error.stderr?.toString() ?? error.message;
|
|
||||||
core.warning(`[Reliability] Git integrity check failed: ${stderr}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove stale .lock files from the .git directory.
|
|
||||||
* Only removes lock files older than 10 minutes to avoid interfering with active operations.
|
|
||||||
* Returns the number of lock files removed.
|
|
||||||
*/
|
|
||||||
static cleanStaleLockFiles(repoPath: string = '.'): number {
|
|
||||||
const gitDir = path.join(repoPath, '.git');
|
|
||||||
if (!fs.existsSync(gitDir) || !fs.statSync(gitDir).isDirectory()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`[Reliability] Scanning for stale lock files in ${gitDir}`);
|
|
||||||
const now = Date.now();
|
|
||||||
let removed = 0;
|
|
||||||
|
|
||||||
const cleanDirectory = (directory: string): void => {
|
|
||||||
if (!fs.existsSync(directory)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(directory, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
cleanDirectory(fullPath);
|
|
||||||
} else if (entry.name.endsWith('.lock')) {
|
|
||||||
// Check if it is a known lock file location OR under refs/
|
|
||||||
const relativePath = path.relative(gitDir, fullPath);
|
|
||||||
const isKnownLock = BuildReliabilityService.LOCK_FILE_NAMES.has(entry.name);
|
|
||||||
const isRefsLock = relativePath.startsWith('refs' + path.sep);
|
|
||||||
|
|
||||||
if (isKnownLock || isRefsLock) {
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
const ageMs = now - stat.mtimeMs;
|
|
||||||
|
|
||||||
if (ageMs > BuildReliabilityService.LOCK_FILE_MAX_AGE_MS) {
|
|
||||||
fs.unlinkSync(fullPath);
|
|
||||||
removed++;
|
|
||||||
core.info(
|
|
||||||
`[Reliability] Removed stale lock file (age: ${Math.round(ageMs / 1000)}s): ${relativePath}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
core.info(
|
|
||||||
`[Reliability] Lock file is recent (age: ${Math.round(ageMs / 1000)}s), skipping: ${relativePath}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
core.warning(`[Reliability] Could not remove lock file: ${fullPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Directory not accessible
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cleanDirectory(gitDir);
|
|
||||||
|
|
||||||
if (removed > 0) {
|
|
||||||
core.info(`[Reliability] Cleaned ${removed} stale lock file(s)`);
|
|
||||||
} else {
|
|
||||||
core.info('[Reliability] No stale lock files found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that submodule .git files point to existing backing stores
|
|
||||||
* under .git/modules/. Returns list of submodule paths with broken backing stores.
|
|
||||||
*/
|
|
||||||
static validateSubmoduleBackingStores(repoPath: string = '.'): string[] {
|
|
||||||
const broken: string[] = [];
|
|
||||||
const gitmodulesPath = path.join(repoPath, '.gitmodules');
|
|
||||||
|
|
||||||
if (!fs.existsSync(gitmodulesPath)) {
|
|
||||||
core.info('[Reliability] No .gitmodules found, skipping submodule validation');
|
|
||||||
return broken;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`[Reliability] Validating submodule backing stores in ${repoPath}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(gitmodulesPath, 'utf8');
|
|
||||||
const pathMatches = content.matchAll(/path\s*=\s*(.+)/g);
|
|
||||||
|
|
||||||
for (const match of pathMatches) {
|
|
||||||
const submodulePath = match[1].trim();
|
|
||||||
const gitFile = path.join(repoPath, submodulePath, '.git');
|
|
||||||
|
|
||||||
if (!fs.existsSync(gitFile)) {
|
|
||||||
// Submodule not initialized -- not necessarily broken
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(gitFile);
|
|
||||||
if (stat.isFile()) {
|
|
||||||
// .git is a file -- should contain "gitdir: <path>"
|
|
||||||
const gitFileContent = fs.readFileSync(gitFile, 'utf8').trim();
|
|
||||||
const gitdirMatch = gitFileContent.match(/^gitdir:\s*(.+)$/);
|
|
||||||
|
|
||||||
if (gitdirMatch) {
|
|
||||||
const backingStore = path.resolve(path.join(repoPath, submodulePath), gitdirMatch[1]);
|
|
||||||
if (!fs.existsSync(backingStore)) {
|
|
||||||
broken.push(submodulePath);
|
|
||||||
core.warning(`[Reliability] Submodule ${submodulePath} has broken backing store: ${backingStore}`);
|
|
||||||
} else {
|
|
||||||
core.info(`[Reliability] Submodule ${submodulePath} backing store OK`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
broken.push(submodulePath);
|
|
||||||
core.warning(`[Reliability] Submodule ${submodulePath} .git file has invalid format`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Can't read .git file
|
|
||||||
core.warning(`[Reliability] Could not read .git file for submodule: ${submodulePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
core.warning(`[Reliability] Could not read .gitmodules: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (broken.length > 0) {
|
|
||||||
core.warning(`[Reliability] ${broken.length} submodule(s) have broken backing stores`);
|
|
||||||
} else {
|
|
||||||
core.info('[Reliability] All submodule backing stores are valid');
|
|
||||||
}
|
|
||||||
|
|
||||||
return broken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Orchestrate recovery of a corrupted repository.
|
|
||||||
* Sequence: fsck -> clean locks -> re-fetch -> retry fsck.
|
|
||||||
* Returns true if recovery succeeded.
|
|
||||||
*/
|
|
||||||
static recoverCorruptedRepo(repoPath: string = '.'): boolean {
|
|
||||||
core.warning(`[Reliability] Attempting automatic recovery for ${repoPath}`);
|
|
||||||
|
|
||||||
// Step 1: Clean stale lock files that may be preventing operations
|
|
||||||
const locksRemoved = BuildReliabilityService.cleanStaleLockFiles(repoPath);
|
|
||||||
if (locksRemoved > 0) {
|
|
||||||
core.info(`[Reliability] Recovery: cleaned ${locksRemoved} lock file(s)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Re-fetch to restore missing objects
|
|
||||||
try {
|
|
||||||
core.info('[Reliability] Recovery: re-fetching from remote');
|
|
||||||
execSync(`git -C "${repoPath}" fetch --all`, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 300_000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
core.info('[Reliability] Recovery: fetch completed');
|
|
||||||
} catch (error: any) {
|
|
||||||
core.warning(`[Reliability] Recovery: fetch failed: ${error.stderr?.toString() ?? error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Retry fsck
|
|
||||||
const healthy = BuildReliabilityService.checkGitIntegrity(repoPath);
|
|
||||||
if (healthy) {
|
|
||||||
core.info('[Reliability] Recovery succeeded -- repository is healthy');
|
|
||||||
} else {
|
|
||||||
core.warning('[Reliability] Recovery failed -- repository still has integrity issues');
|
|
||||||
}
|
|
||||||
|
|
||||||
return healthy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scan a directory tree for files/directories with Windows reserved names.
|
|
||||||
* These names (con, prn, aux, nul, com1-9, lpt1-9) with any extension
|
|
||||||
* cause Unity asset importer infinite loops on Windows.
|
|
||||||
* Returns list of paths that were removed.
|
|
||||||
*/
|
|
||||||
static cleanReservedFilenames(projectPath: string): string[] {
|
|
||||||
const assetsPath = path.join(projectPath, 'Assets');
|
|
||||||
if (!fs.existsSync(assetsPath)) {
|
|
||||||
core.info(`[Reliability] No Assets directory found at ${assetsPath}, skipping reserved filename scan`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`[Reliability] Scanning for reserved filenames in ${assetsPath}`);
|
|
||||||
const cleaned: string[] = [];
|
|
||||||
|
|
||||||
const scanDirectory = (directory: string): void => {
|
|
||||||
try {
|
|
||||||
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const nameWithoutExtension = entry.name.split('.')[0].toLowerCase();
|
|
||||||
const fullPath = path.join(directory, entry.name);
|
|
||||||
|
|
||||||
if (BuildReliabilityService.RESERVED_NAMES.has(nameWithoutExtension)) {
|
|
||||||
try {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
||||||
} else {
|
|
||||||
fs.unlinkSync(fullPath);
|
|
||||||
}
|
|
||||||
cleaned.push(fullPath);
|
|
||||||
core.warning(`[Reliability] Removed reserved filename: ${fullPath}`);
|
|
||||||
} catch {
|
|
||||||
core.warning(`[Reliability] Could not remove reserved filename: ${fullPath}`);
|
|
||||||
}
|
|
||||||
} else if (entry.isDirectory()) {
|
|
||||||
scanDirectory(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Directory not accessible
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
scanDirectory(assetsPath);
|
|
||||||
|
|
||||||
if (cleaned.length > 0) {
|
|
||||||
core.warning(`[Reliability] Cleaned ${cleaned.length} reserved filename(s)`);
|
|
||||||
} else {
|
|
||||||
core.info('[Reliability] No reserved filenames found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get available disk space in megabytes for a given directory.
|
|
||||||
* Returns -1 if the check fails (unknown space).
|
|
||||||
*
|
|
||||||
* Cross-platform: uses wmic on Windows, df on Unix.
|
|
||||||
*/
|
|
||||||
static getAvailableSpaceMB(directoryPath: string): number {
|
|
||||||
try {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
const drive = path.parse(directoryPath).root;
|
|
||||||
const driveLetter = drive.replace(/[:\\\/]/g, '');
|
|
||||||
const output = execFileSync(
|
|
||||||
'wmic',
|
|
||||||
['logicaldisk', 'where', `DeviceID='${driveLetter}:'`, 'get', 'FreeSpace', '/value'],
|
|
||||||
{ encoding: 'utf8', timeout: 10_000 },
|
|
||||||
);
|
|
||||||
const match = output.match(/FreeSpace=(\d+)/);
|
|
||||||
|
|
||||||
return match ? Number.parseInt(match[1], 10) / (1024 * 1024) : -1;
|
|
||||||
} else {
|
|
||||||
const output = execFileSync('df', ['-BM', '--output=avail', directoryPath], {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 10_000,
|
|
||||||
});
|
|
||||||
const lines = output.trim().split('\n');
|
|
||||||
|
|
||||||
return Number.parseInt(lines[lines.length - 1], 10);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return -1; // Unknown, caller should proceed with warning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the total size of a directory in megabytes.
|
|
||||||
* Returns -1 if the calculation fails.
|
|
||||||
*/
|
|
||||||
static getDirectorySizeMB(directoryPath: string): number {
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(directoryPath);
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
return stat.size / (1024 * 1024);
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalBytes = 0;
|
|
||||||
const walkDirectory = (dir: string): void => {
|
|
||||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
walkDirectory(fullPath);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
totalBytes += fs.statSync(fullPath).size;
|
|
||||||
} catch {
|
|
||||||
// Skip inaccessible files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
walkDirectory(directoryPath);
|
|
||||||
|
|
||||||
return totalBytes / (1024 * 1024);
|
|
||||||
} catch {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a tar.gz archive of build output.
|
|
||||||
*
|
|
||||||
* Validates disk space before archiving. Skips archival with a warning
|
|
||||||
* if insufficient space is detected, preventing partial writes on full disks.
|
|
||||||
*/
|
|
||||||
static archiveBuildOutput(sourcePath: string, archivePath: string): void {
|
|
||||||
if (!fs.existsSync(sourcePath)) {
|
|
||||||
core.info(`[Reliability] No build output to archive at ${sourcePath}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.mkdirSync(archivePath, { recursive: true });
|
|
||||||
|
|
||||||
// Check available disk space before archiving
|
|
||||||
const sourceSizeMB = BuildReliabilityService.getDirectorySizeMB(sourcePath);
|
|
||||||
const availableSpaceMB = BuildReliabilityService.getAvailableSpaceMB(archivePath);
|
|
||||||
|
|
||||||
if (sourceSizeMB >= 0 && availableSpaceMB >= 0) {
|
|
||||||
const neededMB = Math.ceil(sourceSizeMB * 1.1); // 10% safety margin
|
|
||||||
if (availableSpaceMB < neededMB) {
|
|
||||||
core.warning(
|
|
||||||
`[Reliability] Insufficient disk space for archive. ` +
|
|
||||||
`Need ~${neededMB}MB, available: ${Math.floor(availableSpaceMB)}MB. Skipping archive.`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
core.info(
|
|
||||||
`[Reliability] Disk space check passed: need ~${neededMB}MB, available: ${Math.floor(availableSpaceMB)}MB`,
|
|
||||||
);
|
|
||||||
} else if (availableSpaceMB < 0) {
|
|
||||||
core.warning('[Reliability] Could not determine available disk space. Proceeding with archive cautiously.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[.:]/g, '-');
|
|
||||||
const archiveFile = path.join(archivePath, `build-${timestamp}.tar.gz`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
execSync(`tar -czf "${archiveFile}" -C "${path.dirname(sourcePath)}" "${path.basename(sourcePath)}"`, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 600_000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
core.info(`[Reliability] Build output archived to ${archiveFile}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
core.warning(`[Reliability] Failed to archive build output: ${error.stderr?.toString() ?? error.message}`);
|
|
||||||
|
|
||||||
// Clean up partial archive if it exists to avoid leaving corrupted files
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(archiveFile)) {
|
|
||||||
fs.unlinkSync(archiveFile);
|
|
||||||
core.info(`[Reliability] Cleaned up partial archive: ${archiveFile}`);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Best-effort cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enforce retention policy -- delete archives older than the retention period.
|
|
||||||
* Returns the number of old archives removed.
|
|
||||||
*/
|
|
||||||
static enforceRetention(archivePath: string, retentionDays: number): number {
|
|
||||||
if (!fs.existsSync(archivePath)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const retentionMs = retentionDays * 24 * 60 * 60 * 1000;
|
|
||||||
let removed = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const entries = fs.readdirSync(archivePath, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(archivePath, entry.name);
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
const ageMs = now - stat.mtimeMs;
|
|
||||||
|
|
||||||
if (ageMs > retentionMs) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
||||||
} else {
|
|
||||||
fs.unlinkSync(fullPath);
|
|
||||||
}
|
|
||||||
removed++;
|
|
||||||
core.info(
|
|
||||||
`[Reliability] Removed old archive: ${entry.name} (age: ${Math.round(
|
|
||||||
ageMs / (24 * 60 * 60 * 1000),
|
|
||||||
)} days)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
core.warning(`[Reliability] Could not process archive entry: ${fullPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
core.warning(`[Reliability] Could not read archive directory: ${archivePath}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removed > 0) {
|
|
||||||
core.info(
|
|
||||||
`[Reliability] Retention enforced: removed ${removed} old archive(s), retention: ${retentionDays} days`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure git environment variables for CI reliability.
|
|
||||||
* Sets GIT_TERMINAL_PROMPT=0, increases http.postBuffer, enables core.longpaths.
|
|
||||||
*/
|
|
||||||
static configureGitEnvironment(): void {
|
|
||||||
core.info('[Reliability] Configuring git environment for CI');
|
|
||||||
|
|
||||||
// Prevent git from prompting for credentials (hangs in CI)
|
|
||||||
process.env.GIT_TERMINAL_PROMPT = '0';
|
|
||||||
core.info('[Reliability] Set GIT_TERMINAL_PROMPT=0');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Increase http.postBuffer to 500MB for large pushes
|
|
||||||
execSync('git config --global http.postBuffer 524288000', {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 10_000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
core.info('[Reliability] Set http.postBuffer=524288000 (500MB)');
|
|
||||||
} catch (error: any) {
|
|
||||||
core.warning(`[Reliability] Could not set http.postBuffer: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Enable long paths on Windows
|
|
||||||
execSync('git config --global core.longpaths true', {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 10_000,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
});
|
|
||||||
core.info('[Reliability] Set core.longpaths=true');
|
|
||||||
} catch (error: any) {
|
|
||||||
core.warning(`[Reliability] Could not set core.longpaths: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { BuildReliabilityService } from './build-reliability-service';
|
|
||||||
@@ -27,6 +27,7 @@ printenv
|
|||||||
git config --global advice.detachedHead false
|
git config --global advice.detachedHead false
|
||||||
git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"
|
git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"
|
||||||
git config --global filter.lfs.process "git-lfs filter-process --skip"
|
git config --global filter.lfs.process "git-lfs filter-process --skip"
|
||||||
|
${OrchestratorFolders.gitAuthConfigScript}
|
||||||
BRANCH="${Orchestrator.buildParameters.orchestratorBranch}"
|
BRANCH="${Orchestrator.buildParameters.orchestratorBranch}"
|
||||||
REPO="${OrchestratorFolders.unityBuilderRepoUrl}"
|
REPO="${OrchestratorFolders.unityBuilderRepoUrl}"
|
||||||
if [ -n "$(git ls-remote --heads "$REPO" "$BRANCH" 2>/dev/null)" ]; then
|
if [ -n "$(git ls-remote --heads "$REPO" "$BRANCH" 2>/dev/null)" ]; then
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export class BuildAutomationWorkflow implements WorkflowInterface {
|
|||||||
const commands = `mkdir -p ${OrchestratorFolders.ToLinuxFolder(
|
const commands = `mkdir -p ${OrchestratorFolders.ToLinuxFolder(
|
||||||
OrchestratorFolders.builderPathAbsolute,
|
OrchestratorFolders.builderPathAbsolute,
|
||||||
)}
|
)}
|
||||||
|
${OrchestratorFolders.gitAuthConfigScript}
|
||||||
BRANCH="${Orchestrator.buildParameters.orchestratorBranch}"
|
BRANCH="${Orchestrator.buildParameters.orchestratorBranch}"
|
||||||
REPO="${OrchestratorFolders.unityBuilderRepoUrl}"
|
REPO="${OrchestratorFolders.unityBuilderRepoUrl}"
|
||||||
DEST="${OrchestratorFolders.ToLinuxFolder(OrchestratorFolders.builderPathAbsolute)}"
|
DEST="${OrchestratorFolders.ToLinuxFolder(OrchestratorFolders.builderPathAbsolute)}"
|
||||||
|
|||||||
Reference in New Issue
Block a user