feat: support private git repositories with token auth
- Entity: added gitToken and gitBranch columns to Application
- DTO: added gitToken and gitBranch fields to Create/Update DTOs
- Build pipeline: git-clone init container now injects token into
HTTPS URL for private repo authentication (GitHub PAT, GitLab token, etc.)
- Build pipeline: supports cloning specific branch (default: main)
- Frontend deploy page: added Access Token (password field) and Branch
inputs when Git Repository source is selected
- Frontend deploy page: Review step shows branch and token status
- Frontend app detail: shows branch badge and 🔑 Private indicator
when git credentials are configured
- Works with: GitHub, GitLab, Bitbucket, any HTTPS-based git host
This commit is contained in:
@@ -37,6 +37,16 @@ export class CreateApplicationDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
gitUrl?: string;
|
gitUrl?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'ghp_xxxxxxxxxxxxxxxxxxxx', description: 'Personal access token for private git repos' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
gitToken?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'main', description: 'Git branch to clone (default: main)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
gitBranch?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: { NODE_ENV: 'production', PORT: '3000' } })
|
@ApiPropertyOptional({ example: { NODE_ENV: 'production', PORT: '3000' } })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@@ -86,6 +96,21 @@ export class UpdateApplicationDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
gitUrl?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Personal access token for private repos' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
gitToken?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Git branch to clone' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
gitBranch?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ export class Application {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
gitUrl: string;
|
gitUrl: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
gitToken: string; // Personal access token for private repos
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
gitBranch: string; // Branch to clone (default: main)
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
codePath: string; // Path to uploaded zip
|
codePath: string; // Path to uploaded zip
|
||||||
|
|
||||||
|
|||||||
@@ -149,13 +149,30 @@ export class BuildService {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
} else if (hasGitUrl) {
|
} else if (hasGitUrl) {
|
||||||
|
// Build the git clone URL — inject token for private repos
|
||||||
|
let cloneUrl = app.gitUrl!;
|
||||||
|
if (app.gitToken) {
|
||||||
|
// Convert https://github.com/user/repo.git → https://<token>@github.com/user/repo.git
|
||||||
|
// Also works for GitLab, Bitbucket, etc.
|
||||||
|
try {
|
||||||
|
const url = new URL(cloneUrl);
|
||||||
|
url.username = app.gitToken;
|
||||||
|
url.password = ''; // Some providers use token as username, others as password
|
||||||
|
cloneUrl = url.toString();
|
||||||
|
} catch {
|
||||||
|
// If URL parsing fails, try simple injection after protocol
|
||||||
|
cloneUrl = cloneUrl.replace('https://', `https://${app.gitToken}@`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const branch = app.gitBranch || 'main';
|
||||||
|
|
||||||
// Clone git repo into /workspace/source, then copy our generated Dockerfile
|
// Clone git repo into /workspace/source, then copy our generated Dockerfile
|
||||||
initContainers.push({
|
initContainers.push({
|
||||||
name: 'git-clone',
|
name: 'git-clone',
|
||||||
image: 'alpine/git:2.43.0',
|
image: 'alpine/git:2.43.0',
|
||||||
command: ['sh', '-c', `
|
command: ['sh', '-c', `
|
||||||
echo ">>> Cloning ${app.gitUrl}" &&
|
echo ">>> Cloning branch '${branch}' from ${app.gitUrl}" &&
|
||||||
git clone --depth 1 ${app.gitUrl} /workspace-out/source &&
|
git clone --depth 1 --branch ${branch} ${cloneUrl} /workspace-out/source &&
|
||||||
cp /dockerfile/Dockerfile /workspace-out/Dockerfile &&
|
cp /dockerfile/Dockerfile /workspace-out/Dockerfile &&
|
||||||
echo ">>> Workspace contents:" &&
|
echo ">>> Workspace contents:" &&
|
||||||
ls -la /workspace-out/source/
|
ls -la /workspace-out/source/
|
||||||
|
|||||||
@@ -453,6 +453,18 @@ export default function AppDetailPage() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-blue-800">Git repository connected</p>
|
<p className="text-sm font-medium text-blue-800">Git repository connected</p>
|
||||||
<p className="text-xs text-blue-600 font-mono">{app.gitUrl}</p>
|
<p className="text-xs text-blue-600 font-mono">{app.gitUrl}</p>
|
||||||
|
<div className="flex items-center space-x-3 mt-1">
|
||||||
|
{app.gitBranch && (
|
||||||
|
<span className="text-xs text-blue-500">
|
||||||
|
🌿 {app.gitBranch}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{app.gitToken && (
|
||||||
|
<span className="text-xs text-green-600">
|
||||||
|
🔑 Private
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export default function DeployPage() {
|
|||||||
runtime: 'nodejs',
|
runtime: 'nodejs',
|
||||||
databaseType: 'none',
|
databaseType: 'none',
|
||||||
gitUrl: '',
|
gitUrl: '',
|
||||||
|
gitToken: '',
|
||||||
|
gitBranch: '',
|
||||||
envVars: {},
|
envVars: {},
|
||||||
cpuRequest: '100m',
|
cpuRequest: '100m',
|
||||||
cpuLimit: '500m',
|
cpuLimit: '500m',
|
||||||
@@ -207,13 +209,42 @@ export default function DeployPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sourceMethod === 'git' ? (
|
{sourceMethod === 'git' ? (
|
||||||
<div>
|
<div className="space-y-3">
|
||||||
<input
|
<div>
|
||||||
className="input-field"
|
<label className="block text-xs text-gray-500 mb-1">Repository URL</label>
|
||||||
placeholder="https://github.com/user/repo.git"
|
<input
|
||||||
value={form.gitUrl}
|
className="input-field"
|
||||||
onChange={(e) => setForm({ ...form, gitUrl: e.target.value })}
|
placeholder="https://github.com/user/repo.git"
|
||||||
/>
|
value={form.gitUrl}
|
||||||
|
onChange={(e) => setForm({ ...form, gitUrl: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-500 mb-1">
|
||||||
|
Access Token <span className="text-gray-400">(for private repos)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="input-field"
|
||||||
|
type="password"
|
||||||
|
placeholder="ghp_xxxxxxxxxxxx or glpat-xxxxxxxxxxxx"
|
||||||
|
value={form.gitToken || ''}
|
||||||
|
onChange={(e) => setForm({ ...form, gitToken: e.target.value })}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-400">
|
||||||
|
GitHub: Personal Access Token · GitLab: Project Access Token · Leave empty for public repos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-500 mb-1">
|
||||||
|
Branch <span className="text-gray-400">(default: main)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="input-field"
|
||||||
|
placeholder="main"
|
||||||
|
value={form.gitBranch || ''}
|
||||||
|
onChange={(e) => setForm({ ...form, gitBranch: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
@@ -454,6 +485,18 @@ export default function DeployPage() {
|
|||||||
: form.gitUrl || '—'}
|
: form.gitUrl || '—'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{sourceMethod === 'git' && form.gitBranch && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Branch</span>
|
||||||
|
<span className="text-sm font-medium">{form.gitBranch}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sourceMethod === 'git' && form.gitToken && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Auth</span>
|
||||||
|
<span className="text-sm font-medium text-green-600">🔑 Token provided</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm text-gray-500">CPU</span>
|
<span className="text-sm text-gray-500">CPU</span>
|
||||||
<span className="text-sm font-medium">{form.cpuRequest} / {form.cpuLimit}</span>
|
<span className="text-sm font-medium">{form.cpuRequest} / {form.cpuLimit}</span>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export interface Application {
|
|||||||
runtime: 'nodejs' | 'laravel';
|
runtime: 'nodejs' | 'laravel';
|
||||||
databaseType: 'mysql' | 'postgresql' | 'none';
|
databaseType: 'mysql' | 'postgresql' | 'none';
|
||||||
gitUrl?: string;
|
gitUrl?: string;
|
||||||
|
gitToken?: string;
|
||||||
|
gitBranch?: string;
|
||||||
codePath?: string;
|
codePath?: string;
|
||||||
envVars?: Record<string, string>;
|
envVars?: Record<string, string>;
|
||||||
cpuRequest: string;
|
cpuRequest: string;
|
||||||
@@ -84,6 +86,8 @@ export interface CreateApplicationDto {
|
|||||||
runtime: 'nodejs' | 'laravel';
|
runtime: 'nodejs' | 'laravel';
|
||||||
databaseType: 'mysql' | 'postgresql' | 'none';
|
databaseType: 'mysql' | 'postgresql' | 'none';
|
||||||
gitUrl?: string;
|
gitUrl?: string;
|
||||||
|
gitToken?: string;
|
||||||
|
gitBranch?: string;
|
||||||
envVars?: Record<string, string>;
|
envVars?: Record<string, string>;
|
||||||
cpuRequest?: string;
|
cpuRequest?: string;
|
||||||
cpuLimit?: string;
|
cpuLimit?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user