From 051ae4050326903a0aab17d31ca1797a31107c81 Mon Sep 17 00:00:00 2001 From: nully0x <40327060+nully0x@users.noreply.github.com> Date: Sat, 7 Dec 2024 11:31:41 +0100 Subject: [PATCH] Refactor runner for rust condition (#15) * chore(feat): impl case for rust without test * chore(feat): add prod base image build --- .github/workflows/prod-build-base-image.yml | 49 +++++++++++++++++++ src/utils/runScriptGenerator.ts | 40 ++++++++++++++++ src/utils/runner.ts | 53 ++++++++------------- src/utils/testLoader.ts | 23 ++++++--- src/utils/testRepoManager.ts | 8 ++-- 5 files changed, 129 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/prod-build-base-image.yml create mode 100644 src/utils/runScriptGenerator.ts diff --git a/.github/workflows/prod-build-base-image.yml b/.github/workflows/prod-build-base-image.yml new file mode 100644 index 0000000..768c7cb --- /dev/null +++ b/.github/workflows/prod-build-base-image.yml @@ -0,0 +1,49 @@ +name: Build Base Images + +on: + workflow_dispatch: # Manual trigger + push: + paths: + - "baseImages/**" + branches: + - main + +jobs: + build-base-images: + name: Build Base Images on Droplet + runs-on: ubuntu-latest + strategy: + matrix: + image: + - { name: "rust", tag: "rs-base", path: "baseImages/rust" } + - { + name: "typescript", + tag: "ts-base", + path: "baseImages/typescript", + } + - { name: "python", tag: "py-base", path: "baseImages/python" } + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.PROD_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.PROD_DROPLET_IP }} >> ~/.ssh/known_hosts + + # Create directory and copy Dockerfile + - name: Copy Dockerfile to Droplet + run: | + ssh ${{ secrets.PROD_DROPLET_USER }}@${{ secrets.PROD_DROPLET_IP }} "mkdir -p ~/base-images/${{ matrix.image.name }}" + scp ${{ matrix.image.path }}/Dockerfile ${{ secrets.PROD_DROPLET_USER }}@${{ secrets.PROD_DROPLET_IP }}:~/base-images/${{ matrix.image.name }}/Dockerfile + + # Build the image on the droplet + - name: Build Base Image + run: | + ssh ${{ secrets.PROD_DROPLET_USER }}@${{ secrets.PROD_DROPLET_IP }} << 'EOF' + cd ~/base-images/${{ matrix.image.name }} + docker build -t ${{ matrix.image.tag }}:latest . + EOF diff --git a/src/utils/runScriptGenerator.ts b/src/utils/runScriptGenerator.ts new file mode 100644 index 0000000..b916889 --- /dev/null +++ b/src/utils/runScriptGenerator.ts @@ -0,0 +1,40 @@ +import { TestRepoManager } from "./testRepoManager"; +import path from "path"; +import fs from "fs/promises"; + +export async function generateRunScript( + repoDir: string, + language: string, + currentStep: number, + testContent: string | null, +): Promise { + const runScript = `#!/bin/bash + set -e # Exit on any error + + if [ -f "requirements.txt" ]; then + # For Python projects + ${ + testContent + ? `pytest ./app/stage${currentStep}${TestRepoManager.getTestExtension(language)} -v` + : "python ./app/main.py" + } + elif [ -f "Cargo.toml" ]; then + # For Rust projects + cargo build ${testContent ? "--quiet" : ""} + ${ + testContent ? `cargo test --test stage${currentStep}_test` : "cargo run" + } + else + # For TypeScript projects + ${ + testContent + ? `bun test ./app/stage${currentStep}${TestRepoManager.getTestExtension(language)}` + : "bun run start" + } + fi + `; + + const runScriptPath = path.join(repoDir, ".hxckr", "run.sh"); + await fs.writeFile(runScriptPath, runScript); + await fs.chmod(runScriptPath, 0o755); // Making it executable +} diff --git a/src/utils/runner.ts b/src/utils/runner.ts index 77163eb..a37a1b3 100644 --- a/src/utils/runner.ts +++ b/src/utils/runner.ts @@ -13,6 +13,7 @@ import { ProgressResponse } from "../models/types"; import { TestRepoManager } from "./testRepoManager"; import SSELogger from "./sseLogger"; import { SSEManager } from "./sseManager"; +import { generateRunScript } from "./runScriptGenerator"; export async function runTestProcess(request: TestRunRequest): Promise { const { repoUrl, branch, commitSha } = request; @@ -55,47 +56,31 @@ export async function runTestProcess(request: TestRunRequest): Promise { const languageConfig = getLanguageConfig(language); imageName = `test-image-${commitSha}`; - // Load and write test file + // Load test file (might be null) const testContent = await loadTestFile( challengeId, language, progress.progress_details.current_step, ); - if (language === "rust") { - const testsDir = path.join(repoDir, "tests"); - await fs.mkdir(testsDir, { recursive: true }); + // Write test file if it exists + if (testContent) { const testFileName = `stage${progress.progress_details.current_step}${TestRepoManager.getTestExtension(language)}`; - await fs.writeFile(path.join(testsDir, testFileName), testContent); - } else { - const appDir = path.join(repoDir, "app"); - await fs.mkdir(appDir, { recursive: true }); - const testFileName = `stage${progress.progress_details.current_step}${TestRepoManager.getTestExtension(language)}`; - await fs.writeFile(path.join(appDir, testFileName), testContent); + const testFilePath = + language === "rust" + ? path.join(repoDir, "tests", testFileName) + : path.join(repoDir, "app", testFileName); + await fs.mkdir(path.dirname(testFilePath), { recursive: true }); + await fs.writeFile(testFilePath, testContent); } - // Create run.sh with modified commands - const runScript = `#!/bin/bash - set -e # Exit on any error - - if [ -f "requirements.txt" ]; then - # For Python projects - pytest ./app/stage${progress.progress_details.current_step}${TestRepoManager.getTestExtension(language)} -v - elif [ -f "Cargo.toml" ]; then - # For Rust projects - # Build first (quietly) - cargo build > /dev/null 2>&1 - # Run tests and show only test output - cargo test --test stage${progress.progress_details.current_step}_test - else - # For TypeScript projects - bun test ./app/stage${progress.progress_details.current_step}${TestRepoManager.getTestExtension(language)} - fi - `; - - const runScriptPath = path.join(repoDir, ".hxckr", "run.sh"); - await fs.writeFile(runScriptPath, runScript); - await fs.chmod(runScriptPath, 0o755); // Make executable + // Generate run script + await generateRunScript( + repoDir, + language, + progress.progress_details.current_step, + testContent, + ); // Build Docker image await buildDockerImage(repoDir, imageName, languageConfig.dockerfilePath); @@ -112,7 +97,9 @@ export async function runTestProcess(request: TestRunRequest): Promise { if (testResult.stdout) { SSELogger.log(commitSha, `Test output:\n${testResult.stdout}`); } - if (testResult.stderr) { + + // Only log stderr if it contains actual errors + if (testResult.stderr && testResult.exitCode !== 0) { SSELogger.log(commitSha, `Test errors:\n${testResult.stderr}`); } diff --git a/src/utils/testLoader.ts b/src/utils/testLoader.ts index 80f856f..3fb2e2a 100644 --- a/src/utils/testLoader.ts +++ b/src/utils/testLoader.ts @@ -5,7 +5,8 @@ export async function loadTestFile( challengeId: string, language: string, stage: number, -): Promise { +): Promise { + // Changed return type try { const testRepo = TestRepoManager.getInstance(); const testContent = await testRepo.getTestContent( @@ -14,11 +15,19 @@ export async function loadTestFile( stage, ); - logger.info("Test file loaded successfully", { - challengeId, - language, - stage, - }); + if (testContent) { + logger.info("Test file loaded successfully", { + challengeId, + language, + stage, + }); + } else { + logger.info("No test file found, will run code directly", { + challengeId, + language, + stage, + }); + } return testContent; } catch (error) { @@ -28,6 +37,6 @@ export async function loadTestFile( language, stage, }); - throw new Error(`Test file not found for stage ${stage}`); + return null; // Return null instead of throwing error } } diff --git a/src/utils/testRepoManager.ts b/src/utils/testRepoManager.ts index e3d535f..0fd3e04 100644 --- a/src/utils/testRepoManager.ts +++ b/src/utils/testRepoManager.ts @@ -70,7 +70,8 @@ export class TestRepoManager { challengeId: string, language: string, stage: number, - ): Promise { + ): Promise { + // changed return type to allow null in case of no test file(need to review this flow) const repoDir = await this.ensureRepoUpdated(); const testPath = path.join( repoDir, @@ -85,14 +86,13 @@ export class TestRepoManager { logger.info("Test content retrieved successfully"); return content; } catch (error) { - logger.error("Error reading test file", { + logger.info("No test file found, will run code directly", { testPath, - error, challengeId, language, stage, }); - throw new Error(`Test file not found: ${testPath}`); + return null; // Return null instead of throwing error } }