diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee135ac..2c5ef7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,9 @@ on: push: - # Sequence of patterns matched against refs/tags - tags: - - 'v*.*.*' # Push events to matching v*, i.e. v1.0, v20.15.10 + branches: + - main + - func + - lint name: Create Release @@ -10,20 +11,66 @@ jobs: build: name: Create Release runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout code - uses: actions/checkout@v2 - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.2" + + - name: Install dependencies + run: go get gopkg.in/yaml.v3 + + - name: Build binary + run: go build -o keploy-runner main.go + + - name: Extract commit information + id: commit_info + run: | + COMMIT_MSG=$(git log -1 --pretty=%s) + COMMIT_BODY=$(git log -1 --pretty=%b) + # Get the latest tag if it exists + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "No previous tags") + + echo "message<> $GITHUB_OUTPUT + echo "$COMMIT_MSG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "body<> $GITHUB_OUTPUT + echo "$COMMIT_BODY" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT + + - name: Create latest tag + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + # Create or update the 'latest' tag + git tag -fa latest -m "Latest release" + # Push the latest tag + git push origin latest --force + + - name: Create Latest Release + uses: softprops/action-gh-release@v2 with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} + tag_name: latest + name: Latest Release body: | - Changes in this Release - - First Change - - Second Change + # Latest Release + + This is an automatically updated release that always points to the most recent build. + + Latest commit: ${{ steps.commit_info.outputs.message }} + + This release includes the pre-built binary for the TestGPT GitHub Action. draft: false - prerelease: false \ No newline at end of file + prerelease: false + files: | + keploy-runner + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64ae7f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env \ No newline at end of file diff --git a/action.yml b/action.yml index 4262137..6bc9c7c 100644 --- a/action.yml +++ b/action.yml @@ -1,9 +1,9 @@ -name: 'Keploy TestGPT' +name: "Keploy TestGPT" description: "TestGPT is a GitHub Action designed to execute Keploy test cases and generate detailed test reports." author: Sonichigo branding: - icon: 'aperture' - color: 'orange' + icon: "aperture" + color: "orange" inputs: working-directory: @@ -19,12 +19,18 @@ inputs: delay: description: Time to start application required: true - default: 10 + default: "10" container-name: description: Name of the container in case of "docker compose" command build-delay: description: Time to wait for docker container build - default: 50s + default: "50" + runner-version: + description: Version of the runner to use + default: "latest" + github-token: + description: GitHub token for API access to fetch PR details + default: ${{ github.token }} runs: using: "composite" @@ -32,58 +38,143 @@ runs: - name: Setup GITHUB_PATH for script run: | echo "${{ github.action_path }}" >> $GITHUB_PATH - echo "${{ inputs.working-directory }}" + echo "Working directory: ${{ inputs.working-directory }}" + echo "Keploy path: ${{ inputs.keploy-path }}" shell: bash - - name: Grant permissions - run: chmod +x ${GITHUB_ACTION_PATH}/install.sh + + - name: Run MegaLinter shell: bash - - id: keploy-test-report - name: Run Script - run: | - ${GITHUB_ACTION_PATH}/install.sh > ${GITHUB_WORKSPACE}/${WORKDIR}/report.txt - cat ${GITHUB_WORKSPACE}/${WORKDIR}/report.txt + run: | + docker run -v $(pwd):/tmp/lint \ + -e GITHUB_TOKEN=${{ inputs.github-token }} \ + -e REPORT_OUTPUT_FOLDER=/tmp/lint/megalinter-reports \ + -e DISABLE_ERRORS=true \ + -e PARALLEL=true \ + -e JSON_REPORTER=true \ + -e MARKDOWN_SUMMARY_REPORTER=true \ + -e EXCLUDED_DIRECTORIES="keploy,megalinter-reports" \ + -e SHOW_SKIPPED_LINTERS=false \ + -e SHOW_ELAPSED_TIME=true \ + oxsecurity/megalinter-cupcake:v8.5.0 + working-directory: ${{ inputs.working-directory }} + + - name: Upload MegaLinter Report + uses: actions/upload-artifact@v4 + with: + name: megalinter-report + path: ${{ inputs.working-directory }}/megalinter-reports/ + retention-days: 1 + id: upload-megalinter + + - name: Set MegaLinter artifact link + shell: bash + run: | + echo "MEGALINTER_ARTIFACT_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" >> $GITHUB_ENV - # Search for the complete testrun summary and extract total tests, total test passed, and total test failed data - grep -oE "COMPLETE TESTRUN SUMMARY\.\s+Total tests: [0-9]+" ${GITHUB_WORKSPACE}/${WORKDIR}/report.txt | sed -r "s/\x1B\[[0-9;]*[mGK]//g" > ${GITHUB_WORKSPACE}/${WORKDIR}/final_total_tests.out - grep -oE "COMPLETE TESTRUN SUMMARY\.\s+Total test passed: [0-9]+" ${GITHUB_WORKSPACE}/${WORKDIR}/report.txt | sed -r "s/\x1B\[[0-9;]*[mGK]//g" > ${GITHUB_WORKSPACE}/${WORKDIR}/final_total_passed.out - grep -oE "COMPLETE TESTRUN SUMMARY\.\s+Total test failed: [0-9]+" ${GITHUB_WORKSPACE}/${WORKDIR}/report.txt | sed -r "s/\x1B\[[0-9;]*[mGK]//g" > ${GITHUB_WORKSPACE}/${WORKDIR}/final_total_failed.out + - name: Download pre-built runner + run: | + echo "Downloading from: https://github.com/rycerzes/testGPT/releases/download/latest/keploy-runner" + curl -L -o ${GITHUB_ACTION_PATH}/keploy-runner https://github.com/rycerzes/testGPT/releases/download/latest/keploy-runner + chmod +x ${GITHUB_ACTION_PATH}/keploy-runner + shell: bash - # Combine the results into a single file and prepare output - cat ${GITHUB_WORKSPACE}/${WORKDIR}/final_total_tests.out ${GITHUB_WORKSPACE}/${WORKDIR}/final_total_passed.out ${GITHUB_WORKSPACE}/${WORKDIR}/final_total_failed.out > ${GITHUB_WORKSPACE}/${WORKDIR}/final.out - echo 'KEPLOY_REPORT< $GITHUB_OUTPUT - cat ${GITHUB_WORKSPACE}/${WORKDIR}/final.out >> $GITHUB_OUTPUT - echo 'EOF' >> $GITHUB_OUTPUT - cat $GITHUB_OUTPUT + - name: Run Keploy TestGPT + run: | + cd ${GITHUB_ACTION_PATH} + ./keploy-runner > ${GITHUB_WORKSPACE}/${WORKDIR}/console-output.txt shell: bash env: WORKDIR: ${{ inputs.working-directory }} DELAY: ${{ inputs.delay }} - COMMAND : ${{ inputs.command }} - KEPLOY_PATH: ${{inputs.keploy-path}} + COMMAND: ${{ inputs.command }} + KEPLOY_PATH: ${{ inputs.keploy-path }} + CONTAINER_NAME: ${{ inputs.container-name }} + BUILD_DELAY: ${{ inputs.build-delay }} + GITHUB_TOKEN: ${{ inputs.github-token }} + MEGALINTER_ARTIFACT_URL: ${{ env.MEGALINTER_ARTIFACT_URL }} + + - id: keploy-test-report + name: Process Test Results + run: | + if [ -f "${GITHUB_WORKSPACE}/${WORKDIR}/github_output.txt" ]; then + cat "${GITHUB_WORKSPACE}/${WORKDIR}/github_output.txt" >> $GITHUB_OUTPUT + else + echo "Error: GitHub output file not found at: ${GITHUB_WORKSPACE}/${WORKDIR}/github_output.txt" >&2 + exit 1 + fi + shell: bash + env: + WORKDIR: ${{ inputs.working-directory }} + - name: Check if report is generated run: | - if [ -s ${GITHUB_WORKSPACE}/${WORKDIR}/final.out ]; then + if [ -f "${GITHUB_WORKSPACE}/${WORKDIR}/final.out" ]; then echo "Report generated successfully." else echo "Error: Report not generated." >&2 exit 1 fi shell: bash + env: + WORKDIR: ${{ inputs.working-directory }} + - name: Comment on PR - if: success() + if: success() && github.event_name == 'pull_request' uses: actions/github-script@v6 env: KEPLOY_REPORT: ${{ steps.keploy-test-report.outputs.KEPLOY_REPORT }} with: github-token: ${{ github.token }} script: | - if (!process.env.KEPLOY_REPORT) { - console.error('Error: KEPLOY_REPORT not found.'); - process.exit(1); + try { + if (!process.env.KEPLOY_REPORT) { + console.error('Error: KEPLOY_REPORT not found.'); + process.exit(1); + } + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: process.env.KEPLOY_REPORT + }); + console.log('Successfully commented on PR'); + } catch (error) { + console.error('Error commenting on PR:', error.message); + console.log('Report content (for reference):'); + console.log(process.env.KEPLOY_REPORT); + + // Don't fail the workflow if commenting fails + if (error.status === 403) { + console.log('Permission issue: Make sure your workflow has proper permissions.'); + console.log('Add the following to your workflow file:'); + console.log('permissions:'); + console.log(' contents: read'); + console.log(' issues: write'); + console.log(' pull-requests: write'); + } } - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: process.env.KEPLOY_REPORT - }) + + - name: Display Test Results + if: always() + run: | + echo "============ KEPLOY TEST RESULTS ============" + if [ -f "${GITHUB_WORKSPACE}/${WORKDIR}/final.out" ]; then + cat "${GITHUB_WORKSPACE}/${WORKDIR}/final.out" + echo "" + echo "============ SUMMARY ============" + if [ -f "${GITHUB_WORKSPACE}/${WORKDIR}/final_total_tests.out" ]; then + cat "${GITHUB_WORKSPACE}/${WORKDIR}/final_total_tests.out" + fi + if [ -f "${GITHUB_WORKSPACE}/${WORKDIR}/final_total_passed.out" ]; then + cat "${GITHUB_WORKSPACE}/${WORKDIR}/final_total_passed.out" + fi + if [ -f "${GITHUB_WORKSPACE}/${WORKDIR}/final_total_failed.out" ]; then + cat "${GITHUB_WORKSPACE}/${WORKDIR}/final_total_failed.out" + fi + else + echo "No test results found" + fi + shell: bash + env: + WORKDIR: ${{ inputs.working-directory }} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c10b79e --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module testGPT + +go 1.23.2 + +require ( + github.com/google/go-github/v70 v70.0.0 + golang.org/x/oauth2 v0.28.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require github.com/google/go-querystring v1.1.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0f3cccb --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHSQbo55o= +github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/install.sh b/install.sh deleted file mode 100644 index f998ddf..0000000 --- a/install.sh +++ /dev/null @@ -1,60 +0,0 @@ -# Install Keploy binary using curl command -curl --silent --location "https://github.com/keploy/keploy/releases/latest/download/keploy_linux_amd64.tar.gz" | tar xz -C /tmp -echo "curl --silent --location 'https://github.com/keploy/keploy/releases/latest/download/keploy_linux_amd64.tar.gz' | tar xz -C /tmp" - -sudo mv /tmp/keploy /usr/local/bin/keploy -chmod +x /usr/local/bin/keploy - -echo "Keploy installed successfully 🎉" - -cd ${GITHUB_WORKSPACE}/${WORKDIR} -echo "${GITHUB_WORKSPACE}/${WORKDIR}" -# Generate app binary -echo "ls" -ls - -if [[ "$COMMAND" =~ .*"go".* ]]; then - echo "go is present." - go mod download - go build -o application - echo 'Test Mode Starting 🎉' - echo sudo -E keploy test -c "./application" --delay ${DELAY} --path "${KEPLOY_PATH}" - sudo -E keploy test -c "./application" --delay ${DELAY} --path "${KEPLOY_PATH}" - -elif [[ "$COMMAND" =~ .*"node".* ]]; then - echo "Node is present." - npm install - echo 'Test Mode Starting 🎉' - echo sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" - sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" - -elif [[ "$COMMAND" =~ .*"java".* ]] || [[ "$COMMAND" =~ .*"mvn".* ]]; then - echo "Java is present." - mvn clean install - echo 'Test Mode Starting 🎉' - echo sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" - sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" - -elif [[ "$COMMAND" =~ .*"python".* ]] || [[ "$COMMAND" =~ .*"python3".* ]]; then - echo "Python is present." - pip install -r requirements.txt - echo 'Test Mode Starting 🎉' - echo sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" - sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" - -elif [[ "$COMMAND" =~ .*"docker-compose".* ]] || [[ "$COMMAND" =~ .*"docker compose".* ]]; then - echo "Docker compose is present." - echo 'Test Mode Starting 🎉' - echo sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" --containerName "${CONTAINER_NAME}" --buildDelay ${BUILD_DELAY} - sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" --containerName "${CONTAINER_NAME}" --buildDelay ${BUILD_DELAY} - -elif [[ "$COMMAND" =~ .*"docker".* ]]; then - echo "Docker is present." - echo 'Test Mode Starting 🎉' - echo sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" --buildDelay ${BUILD_DELAY} - sudo -E keploy test -c "${COMMAND}" --delay ${DELAY} --path "${KEPLOY_PATH}" --buildDelay ${BUILD_DELAY} - -else - echo "Language not found" - echo 'Test Mode Shutting 🎉' -fi \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..4016419 --- /dev/null +++ b/main.go @@ -0,0 +1,438 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" + + "testGPT/utils" +) + +// TestReport represents the structure of a Keploy test report +type TestReport struct { + Total int `yaml:"total"` + Success int `yaml:"success"` + Failure int `yaml:"failure"` + Status string `yaml:"status"` +} + +// TestSetReport represents details for a single test set +type TestSetReport struct { + ID string + PassedTests int + FailedTests int +} + +// AggregatedReport holds the final aggregated test results +type AggregatedReport struct { + TotalTests int `json:"total_tests"` + PassedTests int `json:"passed_tests"` + FailedTests int `json:"failed_tests"` + Status string `json:"status"` +} + +func main() { + // Get environment variables + githubWorkspace := os.Getenv("GITHUB_WORKSPACE") + workDir := os.Getenv("WORKDIR") + keployPath := os.Getenv("KEPLOY_PATH") + delay := os.Getenv("DELAY") + command := os.Getenv("COMMAND") + containerName := os.Getenv("CONTAINER_NAME") + buildDelay := os.Getenv("BUILD_DELAY") + + workingDir := filepath.Join(githubWorkspace, workDir) + + if err := installKeploy(); err != nil { + log.Fatalf("Failed to install Keploy: %v", err) + } + + if err := os.Chdir(workingDir); err != nil { + log.Fatalf("Failed to change to working directory %s: %v", workingDir, err) + } + fmt.Printf("Working directory: %s\n", workingDir) + fmt.Printf("Keploy path: %s\n", keployPath) + + // Debug: List directory contents + fmt.Println("Directory contents:") + dirEntries, err := os.ReadDir(".") + if err != nil { + log.Fatalf("Failed to read directory: %v", err) + } + for _, entry := range dirEntries { + info, _ := entry.Info() + var sizeStr string + if info != nil { + sizeStr = fmt.Sprintf("%d", info.Size()) + } else { + sizeStr = "-" + } + fmt.Printf("%s %s\n", entry.Name(), sizeStr) + } + + // Debug: Check for test sets + fmt.Printf("Checking for test-sets in %s\n", keployPath) + checkTestSets(keployPath) + + if strings.Contains(command, "go") { + fmt.Println("go is present.") + + runCommand("go", "mod", "download") + runCommand("go", "build", "-o", "application") + + fmt.Println("Test Mode Starting 🎉") + fmt.Printf("Running: sudo -E keploy test -c \"./application\" --delay %s --path %s\n", delay, keployPath) + + runKeployTest("./application", delay, keployPath, "", "") + + } else if strings.Contains(command, "node") { + fmt.Println("Node is present.") + + runCommand("npm", "install") + + fmt.Println("Test Mode Starting 🎉") + fmt.Printf("Running: sudo -E keploy test -c \"%s\" --delay %s --path %s\n", command, delay, keployPath) + + runKeployTest(command, delay, keployPath, "", "") + + } else if strings.Contains(command, "java") || strings.Contains(command, "mvn") { + fmt.Println("Java is present.") + + runCommand("mvn", "clean", "install") + + fmt.Println("Test Mode Starting 🎉") + fmt.Printf("Running: sudo -E keploy test -c \"%s\" --delay %s --path %s\n", command, delay, keployPath) + + runKeployTest(command, delay, keployPath, "", "") + + } else if strings.Contains(command, "python") || strings.Contains(command, "python3") { + fmt.Println("Python is present.") + + runCommand("pip", "install", "-r", "requirements.txt") + + fmt.Println("Test Mode Starting 🎉") + fmt.Printf("Running: sudo -E keploy test -c \"%s\" --delay %s --path %s\n", command, delay, keployPath) + + runKeployTest(command, delay, keployPath, "", "") + + } else if strings.Contains(command, "docker-compose") || strings.Contains(command, "docker compose") { + fmt.Println("Docker compose is present.") + + fmt.Println("Test Mode Starting 🎉") + fmt.Printf("Running: sudo -E keploy test -c \"%s\" --delay %s --path %s --containerName %s --buildDelay %s\n", + command, delay, keployPath, containerName, buildDelay) + + runKeployTest(command, delay, keployPath, containerName, buildDelay) + + } else if strings.Contains(command, "docker") { + fmt.Println("Docker is present.") + + fmt.Println("Test Mode Starting 🎉") + fmt.Printf("Running: sudo -E keploy test -c \"%s\" --delay %s --path %s --buildDelay %s\n", + command, delay, keployPath, buildDelay) + + runKeployTest(command, delay, keployPath, "", buildDelay) + + } else { + fmt.Println("Language not found") + fmt.Println("Test Mode Shutting 🎉") + } + + processTestReports(githubWorkspace, workDir) +} + +func installKeploy() error { + fmt.Println("Installing Keploy...") + + // Download the Keploy binary + resp, err := http.Get("https://github.com/keploy/keploy/releases/latest/download/keploy_linux_amd64.tar.gz") + if err != nil { + return fmt.Errorf("failed to download Keploy: %v", err) + } + defer resp.Body.Close() + + tarFile, err := os.CreateTemp("", "keploy_*.tar.gz") + if err != nil { + return fmt.Errorf("failed to create temp file: %v", err) + } + defer os.Remove(tarFile.Name()) + + _, err = io.Copy(tarFile, resp.Body) + if err != nil { + return fmt.Errorf("failed to save tarball: %v", err) + } + tarFile.Close() + + cmd := exec.Command("tar", "xz", "-C", "/tmp", "-f", tarFile.Name()) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to extract tarball: %v", err) + } + + cmd = exec.Command("sudo", "mv", "/tmp/keploy", "/usr/local/bin/keploy") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to move keploy to /usr/local/bin: %v", err) + } + + cmd = exec.Command("sudo", "chmod", "+x", "/usr/local/bin/keploy") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to make keploy executable: %v", err) + } + + fmt.Println("Keploy installed successfully 🎉") + return nil +} + +func checkTestSets(keployPath string) { + cmd := exec.Command("find", keployPath, "-type", "d", "-name", "test-sets", "-o", "-name", "test-set-*") + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Printf("Error checking for test sets: %v\n", err) + return + } + fmt.Println(string(output)) +} + +func runCommand(name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func runKeployTest(command, delay, keployPath, containerName, buildDelay string) { + args := []string{"-E", "keploy", "test", "-c", command, "--delay", delay, "--path", keployPath} + + if containerName != "" { + args = append(args, "--containerName", containerName) + } + + if buildDelay != "" { + args = append(args, "--buildDelay", buildDelay) + } + + cmd := exec.Command("sudo", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + fmt.Printf("Error running Keploy test: %v\n", err) + if exitErr, ok := err.(*exec.ExitError); ok { + fmt.Printf("Exit code: %d\n", exitErr.ExitCode()) + } + } else { + fmt.Println("Keploy test completed successfully") + } +} + +func processTestReports(githubWorkspace, workDir string) { + reportDir := filepath.Join(githubWorkspace, workDir, "keploy/reports/test-run-0") + + fmt.Printf("Looking for test reports in: %s\n", reportDir) + + if _, err := os.Stat(reportDir); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Error: Keploy test reports directory not found at: %s\n", reportDir) + listKeployFiles(filepath.Join(githubWorkspace, workDir, "keploy")) + os.Exit(1) + } + + var reportFiles []string + err := filepath.Walk(reportDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasPrefix(filepath.Base(path), "test-set-") && + strings.HasSuffix(filepath.Base(path), "-report.yaml") { + reportFiles = append(reportFiles, path) + } + return nil + }) + + if err != nil { + fmt.Fprintf(os.Stderr, "Error walking through report directory: %v\n", err) + os.Exit(1) + } + + if len(reportFiles) == 0 { + fmt.Fprintf(os.Stderr, "Error: No Keploy test reports found in directory: %s\n", reportDir) + os.Exit(1) + } + + totalTests := 0 + passedTests := 0 + failedTests := 0 + status := "UNKNOWN" + var testSets []TestSetReport + + fmt.Println("Processing test reports:") + + for _, reportPath := range reportFiles { + fmt.Printf("Processing report: %s\n", reportPath) + + data, err := os.ReadFile(reportPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", reportPath, err) + continue + } + + var report TestReport + if err := yaml.Unmarshal(data, &report); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing YAML in file %s: %v\n", reportPath, err) + continue + } + + // Extract the test set ID from the filename + base := filepath.Base(reportPath) + testSetID := strings.TrimSuffix(base, "-report.yaml") + + testSetReport := TestSetReport{ + ID: testSetID, + PassedTests: report.Success, + FailedTests: report.Failure, + } + testSets = append(testSets, testSetReport) + + fmt.Printf(" %s: Passed: %d, Failed: %d\n", + testSetID, report.Success, report.Failure) + + totalTests += report.Total + passedTests += report.Success + failedTests += report.Failure + + if report.Status == "FAILED" { + status = "FAILED" + } else if status != "FAILED" && report.Status == "PASSED" { + status = "PASSED" + } + } + + // Get PR details if we're in a PR context + var prDetailsMarkdown string + isPRContext := false + prNumber, isPR := utils.GetPRNumberFromEnv() + if isPR { + isPRContext = true + fmt.Printf("Running in PR context. PR number: %d\n", prNumber) + client, err := utils.NewClient() + if err != nil { + fmt.Printf("Warning: Failed to create GitHub client: %v\n", err) + } else { + prDetails, err := client.GetPRDetails(prNumber) + if err != nil { + fmt.Printf("Warning: Failed to fetch PR details: %v\n", err) + } else { + prDetailsMarkdown = utils.FormatPRDetailsForComment(prDetails) + fmt.Printf("Successfully fetched details for PR #%d\n", prNumber) + } + } + } else { + fmt.Println("Not running in a PR context, running in manual trigger mode") + } + + // Get MegaLinter report if available + var megaLinterMarkdown string + megaLinterSummary, err := utils.GetMegaLinterReport(githubWorkspace, workDir) + if err != nil { + fmt.Printf("Warning: Failed to read MegaLinter report: %v\n", err) + } else { + // Get the artifact link from environment variable + artifactLink := os.Getenv("MEGALINTER_ARTIFACT_URL") + megaLinterMarkdown = utils.FormatMegaLinterReport(megaLinterSummary, artifactLink) + fmt.Println("Successfully processed MegaLinter report") + } + + outputDir := filepath.Join(githubWorkspace, workDir) + if _, err := os.Stat(outputDir); os.IsNotExist(err) { + os.MkdirAll(outputDir, 0755) + } + + // Create a detailed report for all contexts + var detailedReport strings.Builder + detailedReport.WriteString("testrun summary\n") + for _, testSet := range testSets { + detailedReport.WriteString(fmt.Sprintf("id: %s\n", testSet.ID)) + detailedReport.WriteString(fmt.Sprintf("tests passed: %d\n", testSet.PassedTests)) + detailedReport.WriteString(fmt.Sprintf("test failed: %d\n\n", testSet.FailedTests)) + } + + detailedReportStr := detailedReport.String() + + os.WriteFile( + filepath.Join(outputDir, "final_total_tests.out"), + []byte(fmt.Sprintf("COMPLETE TESTRUN SUMMARY. Total tests: %d\n", totalTests)), + 0644, + ) + os.WriteFile( + filepath.Join(outputDir, "final_total_passed.out"), + []byte(fmt.Sprintf("COMPLETE TESTRUN SUMMARY. Total test passed: %d\n", passedTests)), + 0644, + ) + os.WriteFile( + filepath.Join(outputDir, "final_total_failed.out"), + []byte(fmt.Sprintf("COMPLETE TESTRUN SUMMARY. Total test failed: %d\n", failedTests)), + 0644, + ) + + os.WriteFile(filepath.Join(outputDir, "final.out"), []byte(detailedReportStr), 0644) + + aggregatedReport := AggregatedReport{ + TotalTests: totalTests, + PassedTests: passedTests, + FailedTests: failedTests, + Status: status, + } + + jsonData, err := json.MarshalIndent(aggregatedReport, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "Error generating JSON report: %v\n", err) + os.Exit(1) + } + + os.WriteFile(filepath.Join(outputDir, "keploy_report.json"), jsonData, 0644) + fmt.Println("Test report processing complete") + + // Create GitHub output for both PR and non-PR contexts + var githubOutputBuilder strings.Builder + githubOutputBuilder.WriteString("KEPLOY_REPORT<\n") + githubOutputBuilder.WriteString("

🔍 PR Analysis

\n\n") + githubOutputBuilder.WriteString(prDetailsMarkdown) + githubOutputBuilder.WriteString("\n\n") + } + + // Add MegaLinter report if available + if megaLinterMarkdown != "" { + githubOutputBuilder.WriteString("
\n") + githubOutputBuilder.WriteString("

🔍 MegaLinter Analysis

\n\n") + githubOutputBuilder.WriteString(megaLinterMarkdown) + githubOutputBuilder.WriteString("
\n\n") + } + + githubOutputBuilder.WriteString("EOF\n") + + os.WriteFile(filepath.Join(outputDir, "github_output.txt"), []byte(githubOutputBuilder.String()), 0644) +} + +func listKeployFiles(keployDir string) { + fmt.Println("Contents of keploy directory:") + cmd := exec.Command("find", keployDir, "-type", "f") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() +} diff --git a/utils/megalinter_report.go b/utils/megalinter_report.go new file mode 100644 index 0000000..6a8307d --- /dev/null +++ b/utils/megalinter_report.go @@ -0,0 +1,89 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +type MegaLinterSummary struct { + Status string + Table string + ReportID string +} + +func GetMegaLinterReport(githubWorkspace, workDir string) (*MegaLinterSummary, error) { + reportPath := filepath.Join(githubWorkspace, workDir, "megalinter-reports", "megalinter-report.md") + + data, err := os.ReadFile(reportPath) + if err != nil { + return nil, fmt.Errorf("failed to read MegaLinter report: %v", err) + } + + content := string(data) + + status := "UNKNOWN" + if strings.Contains(content, "⚠️ WARNING") { + status = "WARNING" + } else if strings.Contains(content, "❌ ERROR") { + status = "ERROR" + } else if strings.Contains(content, "✅ SUCCESS") { + status = "SUCCESS" + } + + lines := strings.Split(content, "\n") + var tableLines []string + inTable := false + + for _, line := range lines { + if strings.HasPrefix(line, "|") { + inTable = true + tableLines = append(tableLines, line) + } else if inTable && len(line) == 0 { + break + } + } + + table := strings.Join(tableLines, "\n") + + reportID := fmt.Sprintf("megalinter-report-%d", os.Getpid()) + + return &MegaLinterSummary{ + Status: status, + Table: table, + ReportID: reportID, + }, nil +} + +func FormatMegaLinterReport(summary *MegaLinterSummary, artifactLink string) string { + var sb strings.Builder + + statusEmoji := "⚠️" + if summary.Status == "SUCCESS" { + statusEmoji = "✅" + } else if summary.Status == "ERROR" { + statusEmoji = "❌" + } + + sb.WriteString(fmt.Sprintf("### %s **MegaLinter Results**\n\n", statusEmoji)) + + sb.WriteString(summary.Table) + + actionsLink := fmt.Sprintf("https://github.com/%s/actions/runs/%s", + os.Getenv("GITHUB_REPOSITORY"), + os.Getenv("GITHUB_RUN_ID")) + + sb.WriteString(fmt.Sprintf("\n- [MegaLinter Full Report](%s)\n", actionsLink)) + + // Use the provided artifact link instead of constructing one + if artifactLink != "" { + sb.WriteString(fmt.Sprintf("- [Download MegaLinter Report](%s)\n", artifactLink)) + } + + return sb.String() +} + +func GetGitHubRunID() string { + return os.Getenv("GITHUB_RUN_ID") +} diff --git a/utils/pr_analysis.go b/utils/pr_analysis.go new file mode 100644 index 0000000..e77e5f5 --- /dev/null +++ b/utils/pr_analysis.go @@ -0,0 +1,145 @@ +package utils + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + + "github.com/google/go-github/v70/github" + "golang.org/x/oauth2" +) + +type PRDetails struct { + Number int + Title string + State string + Author string + CreatedAt string + UpdatedAt string + ChangedFiles []FileDetails +} + +type FileDetails struct { + Filename string + Status string + Additions int + Deletions int + Patch string +} + +type Client struct { + client *github.Client + owner string + repo string +} + +func NewClient() (*Client, error) { + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + return nil, fmt.Errorf("GITHUB_TOKEN environment variable is not set") + } + + repoFullName := os.Getenv("GITHUB_REPOSITORY") + if repoFullName == "" { + return nil, fmt.Errorf("GITHUB_REPOSITORY environment variable is not set") + } + + parts := strings.Split(repoFullName, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid repository format: %s", repoFullName) + } + + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + client := github.NewClient(tc) + + return &Client{ + client: client, + owner: parts[0], + repo: parts[1], + }, nil +} + +func (c *Client) GetPRDetails(prNumber int) (*PRDetails, error) { + ctx := context.Background() + + pr, _, err := c.client.PullRequests.Get(ctx, c.owner, c.repo, prNumber) + if err != nil { + return nil, fmt.Errorf("failed to get PR #%d: %v", prNumber, err) + } + + files, _, err := c.client.PullRequests.ListFiles(ctx, c.owner, c.repo, prNumber, nil) + if err != nil { + return nil, fmt.Errorf("failed to get files for PR #%d: %v", prNumber, err) + } + + fileDetails := make([]FileDetails, 0, len(files)) + for _, file := range files { + fileDetails = append(fileDetails, FileDetails{ + Filename: file.GetFilename(), + Status: file.GetStatus(), + Additions: file.GetAdditions(), + Deletions: file.GetDeletions(), + Patch: file.GetPatch(), + }) + } + + return &PRDetails{ + Number: pr.GetNumber(), + Title: pr.GetTitle(), + State: pr.GetState(), + Author: pr.GetUser().GetLogin(), + CreatedAt: pr.GetCreatedAt().Format("2006-01-02 15:04:05"), + UpdatedAt: pr.GetUpdatedAt().Format("2006-01-02 15:04:05"), + ChangedFiles: fileDetails, + }, nil +} + +func FormatPRDetailsForComment(pr *PRDetails) string { + var sb strings.Builder + + sb.WriteString("### **Test Results Summary**\n\n") + sb.WriteString(fmt.Sprintf("**PR Number**: #%d\n", pr.Number)) + sb.WriteString(fmt.Sprintf("**Title**: %s\n", pr.Title)) + sb.WriteString(fmt.Sprintf("**State**: %s\n", pr.State)) + sb.WriteString(fmt.Sprintf("**Author**: %s\n", pr.Author)) + sb.WriteString(fmt.Sprintf("**Created At**: %s\n", pr.CreatedAt)) + sb.WriteString(fmt.Sprintf("**Updated At**: %s\n\n", pr.UpdatedAt)) + + sb.WriteString("
\n") + sb.WriteString("

🔍 PR Analysis Details

\n\n") + sb.WriteString("#### **Changed Files**\n\n") + + for _, file := range pr.ChangedFiles { + sb.WriteString(fmt.Sprintf("- **File**: `%s` (%s)\n", file.Filename, file.Status)) + sb.WriteString(fmt.Sprintf(" - **Additions**: %d\n", file.Additions)) + sb.WriteString(fmt.Sprintf(" - **Deletions**: %d\n\n", file.Deletions)) + } + + sb.WriteString("
\n\n") + return sb.String() +} + +func GetPRNumberFromEnv() (int, bool) { + ref := os.Getenv("GITHUB_REF") + if ref == "" || !strings.HasPrefix(ref, "refs/pull/") { + return 0, false + } + + parts := strings.Split(ref, "/") + if len(parts) < 3 { + return 0, false + } + + prNum, err := strconv.Atoi(parts[2]) + if err != nil { + return 0, false + } + + return prNum, true +}