Compare commits

..

3 Commits

Author SHA1 Message Date
frostebite
c112e851c2 fix: replace orchestrator-develop branch references with main
The orchestrator-develop branch no longer exists. Update all fallback
clone commands and test fixtures to use main instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:58:45 +00:00
frostebite
fe2c4946b7 ci: set macOS builds to continue-on-error
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:33:00 +00:00
frostebite
ccbe1bcfbf feat: community plugin validation workflow (#800)
Add scheduled workflow that validates community Unity packages compile
and build correctly using unity-builder. Runs weekly on Sunday.

Includes:
- YAML plugin registry (community-plugins.yml) for package listings
- Matrix expansion across plugins and platforms
- Automatic failure reporting via GitHub issues
- Manual trigger with plugin filter and Unity version override

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:40:58 +00:00
12 changed files with 251 additions and 326 deletions

View File

@@ -0,0 +1,203 @@
name: Validate Community Plugins
on:
schedule:
# Run weekly on Sunday at 02:00 UTC
- cron: '0 2 * * 0'
workflow_dispatch:
inputs:
plugin_filter:
description: 'Filter plugins by name (regex pattern, empty = all)'
required: false
default: ''
unity_version:
description: 'Override Unity version (empty = use plugin default)'
required: false
default: ''
permissions:
contents: read
issues: write
jobs:
load-plugins:
name: Load Plugin Registry
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.parse.outputs.matrix }}
plugin_count: ${{ steps.parse.outputs.count }}
steps:
- uses: actions/checkout@v4
- name: Parse plugin registry
id: parse
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const yaml = require('js-yaml');
const registry = yaml.load(fs.readFileSync('community-plugins.yml', 'utf8'));
let plugins = registry.plugins || [];
// Apply name filter if provided
const filter = '${{ github.event.inputs.plugin_filter }}';
if (filter) {
const regex = new RegExp(filter, 'i');
plugins = plugins.filter(p => regex.test(p.name));
}
// Expand platform matrix
const matrix = [];
for (const plugin of plugins) {
const platforms = plugin.platforms || ['StandaloneLinux64'];
for (const platform of platforms) {
matrix.push({
name: plugin.name,
package: plugin.package,
source: plugin.source || 'git',
unity: '${{ github.event.inputs.unity_version }}' || plugin.unity || '2021.3',
platform: platform,
timeout: plugin.timeout || 30
});
}
}
core.setOutput('matrix', JSON.stringify({ include: matrix }));
core.setOutput('count', matrix.length);
console.log(`Found ${matrix.length} plugin-platform combinations to validate`);
validate:
name: '${{ matrix.name }} (${{ matrix.platform }})'
needs: load-plugins
if: needs.load-plugins.outputs.plugin_count > 0
runs-on: ubuntu-latest
timeout-minutes: ${{ fromJson(matrix.timeout) }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.load-plugins.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Create test project
run: |
mkdir -p test-project/Assets
mkdir -p test-project/Packages
mkdir -p test-project/ProjectSettings
# Create minimal manifest.json
if [ "${{ matrix.source }}" = "git" ]; then
cat > test-project/Packages/manifest.json << 'MANIFEST'
{
"dependencies": {
"com.unity.modules.imgui": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0"
}
}
MANIFEST
# Add git package via manifest
cd test-project
cat Packages/manifest.json | python3 -c "
import sys, json
manifest = json.load(sys.stdin)
manifest['dependencies']['${{ matrix.name }}'] = '${{ matrix.package }}'
json.dump(manifest, sys.stdout, indent=2)
" > Packages/manifest.tmp && mv Packages/manifest.tmp Packages/manifest.json
cd ..
fi
# Create minimal ProjectSettings
cat > test-project/ProjectSettings/ProjectVersion.txt << EOF
m_EditorVersion: ${{ matrix.unity }}
EOF
- name: Build with unity-builder
uses: ./
id: build
with:
projectPath: test-project
targetPlatform: ${{ matrix.platform }}
unityVersion: ${{ matrix.unity }}
continue-on-error: true
- name: Record result
if: always()
run: |
STATUS="${{ steps.build.outcome }}"
echo "## ${{ matrix.name }} — ${{ matrix.platform }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$STATUS" = "success" ]; then
echo "✅ **PASSED** — Compiled and built successfully" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **FAILED** — Build or compilation failed" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Unity: ${{ matrix.unity }}" >> $GITHUB_STEP_SUMMARY
echo "- Platform: ${{ matrix.platform }}" >> $GITHUB_STEP_SUMMARY
echo "- Source: ${{ matrix.source }}" >> $GITHUB_STEP_SUMMARY
echo "- Package: \`${{ matrix.package }}\`" >> $GITHUB_STEP_SUMMARY
report:
name: Validation Report
needs: [load-plugins, validate]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate summary
uses: actions/github-script@v7
with:
script: |
const { data: run } = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId
});
const validateJobs = run.jobs.filter(j => j.name.startsWith('validate'));
const passed = validateJobs.filter(j => j.conclusion === 'success').length;
const failed = validateJobs.filter(j => j.conclusion === 'failure').length;
const total = validateJobs.length;
let summary = `# Community Plugin Validation Report\n\n`;
summary += `**${passed}/${total} passed** | ${failed} failed\n\n`;
summary += `| Plugin | Platform | Status |\n|--------|----------|--------|\n`;
for (const job of validateJobs) {
const icon = job.conclusion === 'success' ? '✅' : '❌';
summary += `| ${job.name} | | ${icon} ${job.conclusion} |\n`;
}
await core.summary.addRaw(summary).write();
// Create or update issue if there are failures
if (failed > 0) {
const title = `Community Plugin Validation: ${failed} failure(s) — ${new Date().toISOString().split('T')[0]}`;
const body = summary + `\n\n[Workflow Run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'community-plugin-validation'
});
if (issues.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issues[0].number,
body: body
});
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['community-plugin-validation']
});
}
}

View File

@@ -105,12 +105,6 @@ inputs:
required: false
default: ''
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:
required: false
default: ''

27
community-plugins.yml Normal file
View File

@@ -0,0 +1,27 @@
# Community Plugin Validation Registry
# Packages listed here are automatically tested on a schedule
# to ensure compatibility with unity-builder.
#
# Format:
# - name: Human-readable name
# package: UPM package name or git URL
# source: upm | git | asset-store
# unity: Minimum Unity version (optional, defaults to 2021.3)
# platforms: List of platforms to test (optional, defaults to [StandaloneLinux64])
# timeout: Build timeout in minutes (optional, defaults to 30)
plugins:
# Example entries — community members can submit PRs to add their packages
- name: UniTask
package: https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask
source: git
platforms: [StandaloneLinux64, StandaloneWindows64]
- name: NaughtyAttributes
package: https://github.com/dbrizov/NaughtyAttributes.git?path=Assets/NaughtyAttributes
source: git
- name: Unity Atoms
package: https://github.com/unity-atoms/unity-atoms.git
source: git
platforms: [StandaloneLinux64]

102
dist/index.js generated vendored
View File

@@ -327,7 +327,6 @@ class BuildParameters {
containerRegistryRepository: input_1.default.containerRegistryRepository,
containerRegistryImageVersion: input_1.default.containerRegistryImageVersion,
providerStrategy: orchestrator_options_1.default.providerStrategy,
gitAuthMode: orchestrator_options_1.default.gitAuthMode,
buildPlatform: orchestrator_options_1.default.buildPlatform,
kubeConfig: orchestrator_options_1.default.kubeConfig,
containerMemory: orchestrator_options_1.default.containerMemory,
@@ -1945,29 +1944,6 @@ exports["default"] = OrchestratorEnvironmentVariable;
"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 };
};
@@ -2022,57 +1998,12 @@ class OrchestratorFolders {
static get libraryCacheFolderFull() {
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() {
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`;
}
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`;
}
/**
* 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() {
return 'data';
}
@@ -2273,9 +2204,6 @@ class OrchestratorOptions {
}
return provider || 'local';
}
static get gitAuthMode() {
return OrchestratorOptions.getInput('gitAuthMode') || 'header';
}
static get containerCpu() {
return OrchestratorOptions.getInput('containerCpu') || `1024`;
}
@@ -8057,7 +7985,6 @@ class RemoteClient {
}
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_folders_1.OrchestratorFolders.configureGitAuth();
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.process "git-lfs filter-process --skip"`);
@@ -8162,7 +8089,10 @@ class RemoteClient {
const gitPrivateToken = process.env.GIT_PRIVATE_TOKEN;
if (gitPrivateToken) {
remote_client_logger_1.RemoteClientLogger.log(`Attempting to pull LFS files with GIT_PRIVATE_TOKEN...`);
await RemoteClient.configureTokenAuth(gitPrivateToken);
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://${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 checkout || true`, true);
remote_client_logger_1.RemoteClientLogger.log(`Successfully pulled LFS files with GIT_PRIVATE_TOKEN`);
@@ -8177,7 +8107,10 @@ class RemoteClient {
const githubToken = process.env.GITHUB_TOKEN;
if (githubToken) {
remote_client_logger_1.RemoteClientLogger.log(`Attempting to pull LFS files with GITHUB_TOKEN fallback...`);
await RemoteClient.configureTokenAuth(githubToken);
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://${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 checkout || true`, true);
remote_client_logger_1.RemoteClientLogger.log(`Successfully pulled LFS files with GITHUB_TOKEN`);
@@ -8234,23 +8167,6 @@ class RemoteClient {
}
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([
(0, cli_functions_repository_1.CliFunction)(`remote-cli-pre-build`, `sets up a repository, usually before a game-ci build`)
@@ -9809,7 +9725,6 @@ printenv
git config --global advice.detachedHead false
git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"
git config --global filter.lfs.process "git-lfs filter-process --skip"
${orchestrator_folders_1.OrchestratorFolders.gitAuthConfigScript}
BRANCH="${orchestrator_1.default.buildParameters.orchestratorBranch}"
REPO="${orchestrator_folders_1.OrchestratorFolders.unityBuilderRepoUrl}"
if [ -n "$(git ls-remote --heads "$REPO" "$BRANCH" 2>/dev/null)" ]; then
@@ -9926,7 +9841,6 @@ class BuildAutomationWorkflow {
static setupCommands(builderPath, isContainerized) {
// prettier-ignore
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}"
REPO="${orchestrator_folders_1.OrchestratorFolders.unityBuilderRepoUrl}"
DEST="${orchestrator_folders_1.OrchestratorFolders.ToLinuxFolder(orchestrator_folders_1.OrchestratorFolders.builderPathAbsolute)}"

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -54,7 +54,6 @@ class BuildParameters {
public sshAgent!: string;
public sshPublicKeysDirectoryPath!: string;
public providerStrategy!: string;
public gitAuthMode!: string;
public gitPrivateToken!: string;
public awsStackName!: string;
public awsEndpoint?: string;
@@ -195,7 +194,6 @@ class BuildParameters {
containerRegistryRepository: Input.containerRegistryRepository,
containerRegistryImageVersion: Input.containerRegistryImageVersion,
providerStrategy: OrchestratorOptions.providerStrategy,
gitAuthMode: OrchestratorOptions.gitAuthMode,
buildPlatform: OrchestratorOptions.buildPlatform,
kubeConfig: OrchestratorOptions.kubeConfig,
containerMemory: OrchestratorOptions.containerMemory,

View File

@@ -1,140 +0,0 @@
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;
});
});
});

View File

@@ -72,67 +72,14 @@ export class OrchestratorFolders {
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 {
if (OrchestratorFolders.useHeaderAuth) {
return `https://github.com/${Orchestrator.buildParameters.orchestratorRepoName}.git`;
}
return `https://${Orchestrator.buildParameters.gitPrivateToken}@github.com/${Orchestrator.buildParameters.orchestratorRepoName}.git`;
}
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`;
}
/**
* 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() {
return 'data';
}

View File

@@ -138,10 +138,6 @@ class OrchestratorOptions {
return provider || 'local';
}
static get gitAuthMode(): string {
return OrchestratorOptions.getInput('gitAuthMode') || 'header';
}
static get containerCpu(): string {
return OrchestratorOptions.getInput('containerCpu') || `1024`;
}

View File

@@ -302,7 +302,6 @@ export class RemoteClient {
RemoteClientLogger.log(`Initializing source repository for cloning with caching of LFS files`);
await OrchestratorSystem.Run(`git config --global advice.detachedHead false`);
await OrchestratorFolders.configureGitAuth();
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.process "git-lfs filter-process --skip"`);
@@ -412,7 +411,12 @@ export class RemoteClient {
const gitPrivateToken = process.env.GIT_PRIVATE_TOKEN;
if (gitPrivateToken) {
RemoteClientLogger.log(`Attempting to pull LFS files with GIT_PRIVATE_TOKEN...`);
await RemoteClient.configureTokenAuth(gitPrivateToken);
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://${gitPrivateToken}@github.com/".insteadOf "https://github.com/"`,
);
await OrchestratorSystem.Run(`git lfs pull`, true);
await OrchestratorSystem.Run(`git lfs checkout || true`, true);
RemoteClientLogger.log(`Successfully pulled LFS files with GIT_PRIVATE_TOKEN`);
@@ -428,7 +432,12 @@ export class RemoteClient {
const githubToken = process.env.GITHUB_TOKEN;
if (githubToken) {
RemoteClientLogger.log(`Attempting to pull LFS files with GITHUB_TOKEN fallback...`);
await RemoteClient.configureTokenAuth(githubToken);
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://${githubToken}@github.com/".insteadOf "https://github.com/"`,
);
await OrchestratorSystem.Run(`git lfs pull`, true);
await OrchestratorSystem.Run(`git lfs checkout || true`, true);
RemoteClientLogger.log(`Successfully pulled LFS files with GITHUB_TOKEN`);
@@ -492,25 +501,4 @@ export class RemoteClient {
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/"`,
);
}
}
}

View File

@@ -27,7 +27,6 @@ printenv
git config --global advice.detachedHead false
git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"
git config --global filter.lfs.process "git-lfs filter-process --skip"
${OrchestratorFolders.gitAuthConfigScript}
BRANCH="${Orchestrator.buildParameters.orchestratorBranch}"
REPO="${OrchestratorFolders.unityBuilderRepoUrl}"
if [ -n "$(git ls-remote --heads "$REPO" "$BRANCH" 2>/dev/null)" ]; then

View File

@@ -92,7 +92,6 @@ export class BuildAutomationWorkflow implements WorkflowInterface {
const commands = `mkdir -p ${OrchestratorFolders.ToLinuxFolder(
OrchestratorFolders.builderPathAbsolute,
)}
${OrchestratorFolders.gitAuthConfigScript}
BRANCH="${Orchestrator.buildParameters.orchestratorBranch}"
REPO="${OrchestratorFolders.unityBuilderRepoUrl}"
DEST="${OrchestratorFolders.ToLinuxFolder(OrchestratorFolders.builderPathAbsolute)}"