Skip to content

Commit a5975a9

Browse files
Fix CI/CD problems
1 parent c97af5a commit a5975a9

File tree

5 files changed

+157
-81
lines changed

5 files changed

+157
-81
lines changed

src/app/__tests__/page.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,44 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
22
import { render, screen, fireEvent } from '@testing-library/react'
33
import Home from '../page'
44

5+
// Mock tRPC
6+
vi.mock('~/trpc/react', () => ({
7+
api: {
8+
scripts: {
9+
getRepoStatus: {
10+
useQuery: vi.fn(() => ({
11+
data: { isRepo: true, isBehind: false, branch: 'main', lastCommit: 'abc123' },
12+
refetch: vi.fn(),
13+
})),
14+
},
15+
getScriptCards: {
16+
useQuery: vi.fn(() => ({
17+
data: { success: true, cards: [] },
18+
isLoading: false,
19+
error: null,
20+
})),
21+
},
22+
getCtScripts: {
23+
useQuery: vi.fn(() => ({
24+
data: { scripts: [] },
25+
isLoading: false,
26+
error: null,
27+
})),
28+
},
29+
getScriptBySlug: {
30+
useQuery: vi.fn(() => ({
31+
data: null,
32+
})),
33+
},
34+
fullUpdateRepo: {
35+
useMutation: vi.fn(() => ({
36+
mutate: vi.fn(),
37+
})),
38+
},
39+
},
40+
},
41+
}))
42+
543
// Mock child components
644
vi.mock('../_components/ScriptsGrid', () => ({
745
ScriptsGrid: ({ onInstallScript }: { onInstallScript?: (path: string, name: string) => void }) => (

src/server/lib/git.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ const execAsync = promisify(exec);
99
export class GitManager {
1010
private git: SimpleGit;
1111
private repoPath: string;
12-
private scriptsDir: string;
12+
private scriptsDir: string | null = null;
1313

1414
constructor() {
1515
this.repoPath = process.cwd();
16-
this.scriptsDir = join(this.repoPath, env.SCRIPTS_DIRECTORY);
1716
this.git = simpleGit(this.repoPath);
1817
}
1918

19+
private initializeConfig() {
20+
this.scriptsDir ??= join(this.repoPath, env.SCRIPTS_DIRECTORY);
21+
}
22+
2023
/**
2124
* Check if the repository is behind the remote
2225
*/

src/server/lib/scripts.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,45 @@ export interface ScriptInfo {
1616
}
1717

1818
export class ScriptManager {
19-
private scriptsDir: string;
20-
private allowedExtensions: string[];
21-
private allowedPaths: string[];
22-
private maxExecutionTime: number;
19+
private scriptsDir: string | null = null;
20+
private allowedExtensions: string[] | null = null;
21+
private allowedPaths: string[] | null = null;
22+
private maxExecutionTime: number | null = null;
2323

2424
constructor() {
25-
// Handle both absolute and relative paths for testing
26-
this.scriptsDir = env.SCRIPTS_DIRECTORY.startsWith('/')
27-
? env.SCRIPTS_DIRECTORY
28-
: join(process.cwd(), env.SCRIPTS_DIRECTORY);
29-
this.allowedExtensions = env.ALLOWED_SCRIPT_EXTENSIONS.split(',').map(ext => ext.trim());
30-
this.allowedPaths = env.ALLOWED_SCRIPT_PATHS.split(',').map(path => path.trim());
31-
this.maxExecutionTime = parseInt(env.MAX_SCRIPT_EXECUTION_TIME, 10);
25+
// Initialize lazily to avoid accessing env vars during module load
26+
}
27+
28+
private initializeConfig() {
29+
if (this.scriptsDir === null) {
30+
// Handle both absolute and relative paths for testing
31+
this.scriptsDir = env.SCRIPTS_DIRECTORY.startsWith('/')
32+
? env.SCRIPTS_DIRECTORY
33+
: join(process.cwd(), env.SCRIPTS_DIRECTORY);
34+
this.allowedExtensions = env.ALLOWED_SCRIPT_EXTENSIONS.split(',').map(ext => ext.trim());
35+
this.allowedPaths = env.ALLOWED_SCRIPT_PATHS.split(',').map(path => path.trim());
36+
this.maxExecutionTime = parseInt(env.MAX_SCRIPT_EXECUTION_TIME, 10);
37+
}
3238
}
3339

3440
/**
3541
* Get all available scripts in the scripts directory
3642
*/
3743
async getScripts(): Promise<ScriptInfo[]> {
44+
this.initializeConfig();
3845
try {
39-
const files = await readdir(this.scriptsDir);
46+
const files = await readdir(this.scriptsDir!);
4047
const scripts: ScriptInfo[] = [];
4148

4249
for (const file of files) {
43-
const filePath = join(this.scriptsDir, file);
50+
const filePath = join(this.scriptsDir!, file);
4451
const stats = await stat(filePath);
4552

4653
if (stats.isFile()) {
4754
const extension = extname(file);
4855

4956
// Check if file extension is allowed
50-
if (this.allowedExtensions.includes(extension)) {
57+
if (this.allowedExtensions!.includes(extension)) {
5158
// Check if file is executable
5259
const executable = await this.isExecutable(filePath);
5360

@@ -74,8 +81,9 @@ export class ScriptManager {
7481
* Get all available scripts in the ct subdirectory
7582
*/
7683
async getCtScripts(): Promise<ScriptInfo[]> {
84+
this.initializeConfig();
7785
try {
78-
const ctDir = join(this.scriptsDir, 'ct');
86+
const ctDir = join(this.scriptsDir!, 'ct');
7987
const files = await readdir(ctDir);
8088
const scripts: ScriptInfo[] = [];
8189

@@ -87,7 +95,7 @@ export class ScriptManager {
8795
const extension = extname(file);
8896

8997
// Check if file extension is allowed
90-
if (this.allowedExtensions.includes(extension)) {
98+
if (this.allowedExtensions!.includes(extension)) {
9199
// Check if file is executable
92100
const executable = await this.isExecutable(filePath);
93101

@@ -143,8 +151,9 @@ export class ScriptManager {
143151
* Validate if a script path is allowed to be executed
144152
*/
145153
validateScriptPath(scriptPath: string): { valid: boolean; message?: string } {
154+
this.initializeConfig();
146155
const resolvedPath = resolve(scriptPath);
147-
const scriptsDirResolved = resolve(this.scriptsDir);
156+
const scriptsDirResolved = resolve(this.scriptsDir!);
148157

149158
// Check if the script is within the allowed directory
150159
if (!resolvedPath.startsWith(scriptsDirResolved)) {
@@ -158,7 +167,7 @@ export class ScriptManager {
158167
const relativePath = resolvedPath.replace(scriptsDirResolved, '').replace(/\\/g, '/');
159168
const normalizedRelativePath = relativePath.startsWith('/') ? relativePath : '/' + relativePath;
160169

161-
const isAllowed = this.allowedPaths.some(allowedPath => {
170+
const isAllowed = this.allowedPaths!.some(allowedPath => {
162171
const normalizedAllowedPath = allowedPath.startsWith('/') ? allowedPath : '/' + allowedPath;
163172
// For root path '/', allow files directly in the scripts directory (no subdirectories)
164173
if (normalizedAllowedPath === '/') {
@@ -177,10 +186,10 @@ export class ScriptManager {
177186

178187
// Check file extension
179188
const extension = extname(scriptPath);
180-
if (!this.allowedExtensions.includes(extension)) {
189+
if (!this.allowedExtensions!.includes(extension)) {
181190
return {
182191
valid: false,
183-
message: `File extension '${extension}' is not allowed. Allowed extensions: ${this.allowedExtensions.join(', ')}`
192+
message: `File extension '${extension}' is not allowed. Allowed extensions: ${this.allowedExtensions!.join(', ')}`
184193
};
185194
}
186195

@@ -227,7 +236,7 @@ export class ScriptManager {
227236

228237
// Spawn the process
229238
const childProcess = spawn(command, args, {
230-
cwd: this.scriptsDir,
239+
cwd: this.scriptsDir!,
231240
stdio: ['pipe', 'pipe', 'pipe'],
232241
shell: true
233242
});
@@ -237,7 +246,7 @@ export class ScriptManager {
237246
if (!childProcess.killed) {
238247
childProcess.kill('SIGTERM');
239248
}
240-
}, this.maxExecutionTime);
249+
}, this.maxExecutionTime!);
241250

242251
// Clean up timeout when process exits
243252
childProcess.on('exit', () => {
@@ -268,11 +277,12 @@ export class ScriptManager {
268277
allowedPaths: string[];
269278
maxExecutionTime: number;
270279
} {
280+
this.initializeConfig();
271281
return {
272-
path: this.scriptsDir,
273-
allowedExtensions: this.allowedExtensions,
274-
allowedPaths: this.allowedPaths,
275-
maxExecutionTime: this.maxExecutionTime
282+
path: this.scriptsDir!,
283+
allowedExtensions: this.allowedExtensions!,
284+
allowedPaths: this.allowedPaths!,
285+
maxExecutionTime: this.maxExecutionTime!
276286
};
277287
}
278288
}

src/server/services/githubJsonService.ts

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,44 @@ import { env } from '~/env.js';
44
import type { Script, ScriptCard, GitHubFile } from '~/types/script';
55

66
export class GitHubJsonService {
7-
private baseUrl: string;
8-
private repoUrl: string;
9-
private branch: string;
10-
private jsonFolder: string;
11-
private localJsonDirectory: string;
7+
private baseUrl: string | null = null;
8+
private repoUrl: string | null = null;
9+
private branch: string | null = null;
10+
private jsonFolder: string | null = null;
11+
private localJsonDirectory: string | null = null;
1212
private scriptCache: Map<string, Script> = new Map();
1313

1414
constructor() {
15-
this.repoUrl = env.REPO_URL ?? "";
16-
this.branch = env.REPO_BRANCH;
17-
this.jsonFolder = env.JSON_FOLDER;
18-
this.localJsonDirectory = join(process.cwd(), 'scripts', 'json');
19-
20-
// Only validate GitHub URL if it's provided
21-
if (this.repoUrl) {
22-
// Extract owner and repo from the URL
23-
const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl);
24-
if (!urlMatch) {
25-
throw new Error(`Invalid GitHub repository URL: ${this.repoUrl}`);
26-
}
15+
// Initialize lazily to avoid accessing env vars during module load
16+
}
17+
18+
private initializeConfig() {
19+
if (this.repoUrl === null) {
20+
this.repoUrl = env.REPO_URL ?? "";
21+
this.branch = env.REPO_BRANCH;
22+
this.jsonFolder = env.JSON_FOLDER;
23+
this.localJsonDirectory = join(process.cwd(), 'scripts', 'json');
2724

28-
const [, owner, repo] = urlMatch;
29-
this.baseUrl = `https://api.github.com/repos/${owner}/${repo}`;
30-
} else {
31-
// Set a dummy base URL if no REPO_URL is provided
32-
this.baseUrl = "";
25+
// Only validate GitHub URL if it's provided
26+
if (this.repoUrl) {
27+
// Extract owner and repo from the URL
28+
const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl);
29+
if (!urlMatch) {
30+
throw new Error(`Invalid GitHub repository URL: ${this.repoUrl}`);
31+
}
32+
33+
const [, owner, repo] = urlMatch;
34+
this.baseUrl = `https://api.github.com/repos/${owner}/${repo}`;
35+
} else {
36+
// Set a dummy base URL if no REPO_URL is provided
37+
this.baseUrl = "";
38+
}
3339
}
3440
}
3541

3642
private async fetchFromGitHub<T>(endpoint: string): Promise<T> {
37-
const response = await fetch(`${this.baseUrl}${endpoint}`, {
43+
this.initializeConfig();
44+
const response = await fetch(`${this.baseUrl!}${endpoint}`, {
3845
headers: {
3946
'Accept': 'application/vnd.github.v3+json',
4047
'User-Agent': 'PVEScripts-Local/1.0',
@@ -49,7 +56,8 @@ export class GitHubJsonService {
4956
}
5057

5158
private async downloadJsonFile(filePath: string): Promise<Script> {
52-
const rawUrl = `https://raw.githubusercontent.com/${this.extractRepoPath()}/${this.branch}/${filePath}`;
59+
this.initializeConfig();
60+
const rawUrl = `https://raw.githubusercontent.com/${this.extractRepoPath()}/${this.branch!}/${filePath}`;
5361

5462
const response = await fetch(rawUrl);
5563
if (!response.ok) {
@@ -61,21 +69,23 @@ export class GitHubJsonService {
6169
}
6270

6371
private extractRepoPath(): string {
64-
const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl);
72+
this.initializeConfig();
73+
const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl!);
6574
if (!match) {
6675
throw new Error('Invalid GitHub repository URL');
6776
}
6877
return `${match[1]}/${match[2]}`;
6978
}
7079

7180
async getJsonFiles(): Promise<GitHubFile[]> {
81+
this.initializeConfig();
7282
if (!this.repoUrl) {
7383
throw new Error('REPO_URL environment variable is not set. Cannot fetch from GitHub.');
7484
}
7585

7686
try {
7787
const files = await this.fetchFromGitHub<GitHubFile[]>(
78-
`/contents/${this.jsonFolder}?ref=${this.branch}`
88+
`/contents/${this.jsonFolder!}?ref=${this.branch!}`
7989
);
8090

8191
// Filter for JSON files only
@@ -139,7 +149,8 @@ export class GitHubJsonService {
139149

140150
// If not found locally, try to download just this specific script
141151
try {
142-
const script = await this.downloadJsonFile(`${this.jsonFolder}/${slug}.json`);
152+
this.initializeConfig();
153+
const script = await this.downloadJsonFile(`${this.jsonFolder!}/${slug}.json`);
143154
return script;
144155
} catch {
145156
console.log(`Script ${slug} not found in repository`);
@@ -161,7 +172,8 @@ export class GitHubJsonService {
161172
const { readFile } = await import('fs/promises');
162173
const { join } = await import('path');
163174

164-
const filePath = join(this.localJsonDirectory, `${slug}.json`);
175+
this.initializeConfig();
176+
const filePath = join(this.localJsonDirectory!, `${slug}.json`);
165177
const content = await readFile(filePath, 'utf-8');
166178
const script = JSON.parse(content) as Script;
167179

@@ -198,14 +210,15 @@ export class GitHubJsonService {
198210
}
199211

200212
private async saveScriptsLocally(scripts: Script[]): Promise<void> {
213+
this.initializeConfig();
201214
try {
202215
// Ensure the directory exists
203-
await mkdir(this.localJsonDirectory, { recursive: true });
216+
await mkdir(this.localJsonDirectory!, { recursive: true });
204217

205218
// Save each script as a JSON file
206219
for (const script of scripts) {
207220
const filename = `${script.slug}.json`;
208-
const filePath = join(this.localJsonDirectory, filename);
221+
const filePath = join(this.localJsonDirectory!, filename);
209222
const content = JSON.stringify(script, null, 2);
210223
await writeFile(filePath, content, 'utf-8');
211224
}

0 commit comments

Comments
 (0)