Add test coverage validation step to CI workflow(AST-22222) #587
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Continuous Integration Tests | |
| on: | |
| pull_request: | |
| jobs: | |
| unit-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout the repository | |
| uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 #v4.0.0 | |
| - name: Set up Go version | |
| uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 #v4 | |
| with: | |
| go-version-file: go.mod | |
| - run: go version | |
| - name: go test with coverage | |
| run: | | |
| sudo chmod +x ./internal/commands/.scripts/up.sh | |
| ./internal/commands/.scripts/up.sh | |
| - name: Check if total coverage is greater then 77.7 | |
| shell: bash | |
| run: | | |
| CODE_COV=$(go tool cover -func cover.out | grep total | awk '{print substr($3, 1, length($3)-1)}') | |
| EXPECTED_CODE_COV=77.7 | |
| var=$(awk 'BEGIN{ print "'$CODE_COV'"<"'$EXPECTED_CODE_COV'" }') | |
| if [ "$var" -eq 1 ];then | |
| echo "Your code coverage is too low. Coverage precentage is: $CODE_COV" | |
| exit 1 | |
| else | |
| echo "Your code coverage test passed! Coverage precentage is: $CODE_COV" | |
| exit 0 | |
| fi | |
| # Validate that all integration tests are covered by the matrix patterns | |
| # and generate a list of uncovered tests for the catch-all group | |
| validate-test-coverage: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| uncovered_tests: ${{ steps.validate.outputs.uncovered_tests }} | |
| has_uncovered: ${{ steps.validate.outputs.has_uncovered }} | |
| steps: | |
| - name: Checkout the repository | |
| uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 #v4.0.0 | |
| - name: Validate all tests are covered by CI patterns | |
| id: validate | |
| shell: bash | |
| run: | | |
| echo "Validating that all integration tests are covered by CI matrix patterns..." | |
| # Extract all test function names | |
| all_tests=$(grep -rh "^func Test" test/integration/*_test.go | sed 's/func \(Test[^(]*\).*/\1/' | sort -u) | |
| # Define all patterns from the matrix (must match the patterns below) | |
| patterns=( | |
| "^Test(CreateScan|CreateAsyncScan|CreateQueryDescriptionLink|ScanCreate[^I]|ScanTypeApi|ScanTypesValidation|ValidateScanTypes|ScanGenerating|ScanWith|ScanList|ScanTimeout|ScanASCA|ExecuteASCAScan|ScansAPISecThreshold)" | |
| "^Test(ScansE2E|ScansUpdate|FastScan|LightQueries|RecommendedExclusions|IncrementalScan|BranchPrimary|CancelScan|ScanCreateInclude|ScanCreateIgnore|ScanWorkflow|ScanWorkFlow|ScanLogs|InvalidSource|ScanShow|RequiredScan|ScaResolver|BrokenLink|PartialScan|FailedScan|RunKics|RunSca|ScanGLReport|ContainerEngineScan)" | |
| "^Test(Result|CodeBashing|RiskManagement)" | |
| "^TestPR" | |
| "^Test(Project|CreateEmpty|CreateAlready|CreateWith|CreateProject|GetProjectByTagsFilter)" | |
| "^Test(Predicate|Bfl|RunGetBfl|SastUpdate|GetAndUpdate|Triage|ScaUpdate)" | |
| "^Test(ContainerScan|ContainerImage|EmptyFolder)" | |
| "^Test(IacRealtime|OssRealtime|Secrets_Realtime|ContainersRealtime|EngineNameResolution|ScaRealtime)" | |
| "^Test(Auth|LoadConfiguration|SetConfigProperty|GetTenant|FailProxy)" | |
| "^Test(RootVersion|SetLogOutput|_DownloadScan|_HandleFeatureFlags|Main)" | |
| "^Test(GitHub|GitLab|Azure|Bitbucket|BitBucket)(RateLimit|UserCount|Count)" | |
| "^Test(Chat|Import|GetLearnMore|GetProjectName|Telemetry|Mask|FailedMask|ScaRemediation|KicsRemediation|HooksPreCommit|PreReceive|Pre_Receive)" | |
| ) | |
| uncovered_list="" | |
| uncovered_display="" | |
| covered_count=0 | |
| total_count=0 | |
| for test in $all_tests; do | |
| total_count=$((total_count + 1)) | |
| matched=false | |
| for pattern in "${patterns[@]}"; do | |
| if echo "$test" | grep -qE "$pattern"; then | |
| matched=true | |
| covered_count=$((covered_count + 1)) | |
| break | |
| fi | |
| done | |
| if [ "$matched" = false ]; then | |
| # Build pipe-separated list for Go test -run pattern | |
| if [ -n "$uncovered_list" ]; then | |
| uncovered_list="$uncovered_list|$test" | |
| else | |
| uncovered_list="$test" | |
| fi | |
| uncovered_display="$uncovered_display\n - $test" | |
| fi | |
| done | |
| echo "Total tests found: $total_count" | |
| echo "Tests covered by patterns: $covered_count" | |
| if [ -n "$uncovered_list" ]; then | |
| uncovered_count=$((total_count - covered_count)) | |
| echo "" | |
| echo "WARNING: The following $uncovered_count tests are NOT covered by any CI matrix pattern:" | |
| echo -e "$uncovered_display" | |
| echo "" | |
| echo "These tests will run in the 'Uncovered Tests' catch-all group." | |
| echo "Please consider updating the matrix patterns or renaming the test functions." | |
| # Output for the catch-all group - create a regex pattern | |
| echo "uncovered_tests=^($uncovered_list)$" >> $GITHUB_OUTPUT | |
| echo "has_uncovered=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "SUCCESS: All $total_count tests are covered by CI matrix patterns!" | |
| echo "uncovered_tests=" >> $GITHUB_OUTPUT | |
| echo "has_uncovered=false" >> $GITHUB_OUTPUT | |
| fi | |
| integration-tests: | |
| runs-on: ubuntu-latest | |
| needs: validate-test-coverage | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Group 1: Scan creation tests - basic scan creation and configuration | |
| # Covers: TestCreateScan_*, TestCreateAsyncScan_*, TestCreateQueryDescriptionLink*, | |
| # TestScanCreate*, TestScanTypeApi*, TestScanTypesValidation*, TestValidateScanTypes*, | |
| # TestScanGenerating*, TestScanWith*, TestScanList*, TestScanTimeout*, | |
| # TestScanASCA*, TestExecuteASCAScan*, TestScansAPISecThresholdShouldBlock* | |
| - group: scan-create | |
| name: "Scan Creation" | |
| pattern: "^Test(CreateScan|CreateAsyncScan|CreateQueryDescriptionLink|ScanCreate[^I]|ScanTypeApi|ScanTypesValidation|ValidateScanTypes|ScanGenerating|ScanWith|ScanList|ScanTimeout|ScanASCA|ExecuteASCAScan|ScansAPISecThreshold)" | |
| # Group 2: Scan operations - E2E, workflow, incremental, cancel, filters | |
| # Covers: TestScansE2E*, TestScansUpdate*, TestFastScan*, TestLightQueries*, | |
| # TestRecommendedExclusions*, TestIncrementalScan*, TestBranchPrimary*, | |
| # TestCancelScan*, TestScanCreateInclude*, TestScanCreateIgnore*, | |
| # TestScanWorkflow*, TestScanWorkFlow*, TestScanLogs*, TestInvalidSource*, TestScanShow*, | |
| # TestRequiredScan*, TestScaResolver*, TestBrokenLink*, TestPartialScan*, | |
| # TestFailedScan*, TestRunKics*, TestRunSca*, TestScanGLReport*, TestContainerEngineScan* | |
| - group: scan-ops | |
| name: "Scan Operations" | |
| pattern: "^Test(ScansE2E|ScansUpdate|FastScan|LightQueries|RecommendedExclusions|IncrementalScan|BranchPrimary|CancelScan|ScanCreateInclude|ScanCreateIgnore|ScanWorkflow|ScanWorkFlow|ScanLogs|InvalidSource|ScanShow|RequiredScan|ScaResolver|BrokenLink|PartialScan|FailedScan|RunKics|RunSca|ScanGLReport|ContainerEngineScan)" | |
| # Group 3: Results and reports tests | |
| # Covers: TestResult*, TestCodeBashing*, TestRiskManagement* | |
| - group: results | |
| name: "Results & Reports" | |
| pattern: "^Test(Result|CodeBashing|RiskManagement)" | |
| # Group 4: PR decoration tests | |
| # Covers: TestPR* | |
| - group: pr-decoration | |
| name: "PR Decoration" | |
| pattern: "^TestPR" | |
| # Group 5: Project management tests | |
| # Covers: TestProject*, TestCreateEmpty*, TestCreateAlready*, TestCreateWith*, | |
| # TestCreateProjectWhen*, TestCreateProjectWith*, TestGetProjectByTagsFilter* | |
| - group: projects | |
| name: "Projects" | |
| pattern: "^Test(Project|CreateEmpty|CreateAlready|CreateWith|CreateProject|GetProjectByTagsFilter)" | |
| # Group 6: Predicates, BFL, and Triage tests | |
| # Covers: TestPredicate*, TestBfl*, TestRunGetBfl*, TestSastUpdate*, | |
| # TestGetAndUpdate*, TestTriage*, TestScaUpdate* | |
| - group: predicates | |
| name: "Predicates & BFL" | |
| pattern: "^Test(Predicate|Bfl|RunGetBfl|SastUpdate|GetAndUpdate|Triage|ScaUpdate)" | |
| # Group 7: Container-specific tests | |
| # Covers: TestContainerScan*, TestContainerImage*, TestEmptyFolder* | |
| - group: containers | |
| name: "Container Tests" | |
| pattern: "^Test(ContainerScan|ContainerImage|EmptyFolder)" | |
| # Group 8: Realtime scanning tests (IaC, OSS, Secrets, Containers) | |
| # Covers: TestIacRealtime*, TestOssRealtime*, TestSecrets_Realtime*, | |
| # TestContainersRealtime*, TestEngineNameResolution*, TestScaRealtime* | |
| - group: realtime | |
| name: "Realtime Scanning" | |
| pattern: "^Test(IacRealtime|OssRealtime|Secrets_Realtime|ContainersRealtime|EngineNameResolution|ScaRealtime)" | |
| # Group 9: Auth and configuration tests | |
| # Covers: TestAuth*, TestLoadConfiguration*, TestSetConfigProperty*, | |
| # TestGetTenant*, TestFailProxy* | |
| - group: auth-config | |
| name: "Auth & Config" | |
| pattern: "^Test(Auth|LoadConfiguration|SetConfigProperty|GetTenant|FailProxy)" | |
| # Group 10: Root, logs, and feature flags tests | |
| # Covers: TestRootVersion*, TestSetLogOutput*, Test_DownloadScan*, | |
| # Test_HandleFeatureFlags*, TestMain* | |
| - group: root-logs | |
| name: "Root & Logs" | |
| pattern: "^Test(RootVersion|SetLogOutput|_DownloadScan|_HandleFeatureFlags|Main)" | |
| # Group 11: SCM rate limiting and user count tests | |
| # Covers: TestGitHub*, TestGitLab*, TestAzure*, TestBitbucket*, TestBitBucket* | |
| # (with RateLimit, UserCount, or Count suffix) | |
| - group: scm-tests | |
| name: "SCM Rate Limit & User Count" | |
| pattern: "^Test(GitHub|GitLab|Azure|Bitbucket|BitBucket)(RateLimit|UserCount|Count)" | |
| # Group 12: Miscellaneous tests - chat, import, telemetry, remediation, hooks, masking | |
| # Covers: TestChat*, TestImport*, TestGetLearnMore*, TestGetProjectName*, | |
| # TestTelemetry*, TestMask*, TestFailedMaskSecrets*, TestScaRemediation*, | |
| # TestKicsRemediation*, TestHooksPreCommit*, TestPreReceive*, TestPre_Receive* | |
| - group: misc | |
| name: "Miscellaneous" | |
| pattern: "^Test(Chat|Import|GetLearnMore|GetProjectName|Telemetry|Mask|FailedMask|ScaRemediation|KicsRemediation|HooksPreCommit|PreReceive|Pre_Receive)" | |
| # Group 13: Catch-all for uncovered tests | |
| # This group runs tests that don't match any of the above patterns. | |
| # The pattern is dynamically set from the validate-test-coverage job output. | |
| # If no uncovered tests exist, this group will be skipped. | |
| - group: uncovered | |
| name: "Uncovered Tests (Catch-All)" | |
| pattern: "" # Will be overridden by job output or skipped if empty | |
| name: Integration - ${{ matrix.name }} | |
| steps: | |
| - name: Checkout the repository | |
| uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 #v4.0.0 | |
| - name: Set up Go version | |
| uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 #v4 | |
| with: | |
| go-version-file: go.mod | |
| - run: go version | |
| - name: Go Build | |
| run: go build -o ./bin/cx ./cmd | |
| - name: Start Squid proxy | |
| run: | | |
| docker run \ | |
| --name squid \ | |
| -d \ | |
| -p 3128:3128 \ | |
| -v $(pwd)/internal/commands/.scripts/squid/squid.conf:/etc/squid/squid.conf \ | |
| -v $(pwd)/internal/commands/.scripts/squid/passwords:/etc/squid/passwords \ | |
| ubuntu/squid:5.2-22.04_beta | |
| - name: Download ScaResolver | |
| run: | | |
| wget https://sca-downloads.s3.amazonaws.com/cli/latest/ScaResolver-linux64.tar.gz | |
| tar -xzvf ScaResolver-linux64.tar.gz -C /tmp | |
| rm -rf ScaResolver-linux64.tar.gz | |
| - name: Install pre-commit (for pre-commit tests) | |
| if: matrix.group == 'misc' | |
| run: | | |
| pip install pre-commit | |
| pre-commit install | |
| - name: Pre-test cleanup (delete stale test projects) | |
| if: matrix.group == 'projects' || matrix.group == 'scan-create' || matrix.group == 'scan-ops' | |
| env: | |
| CX_BASE_URI: ${{ secrets.CX_BASE_URI }} | |
| CX_CLIENT_ID: ${{ secrets.CX_CLIENT_ID }} | |
| CX_CLIENT_SECRET: ${{ secrets.CX_CLIENT_SECRET }} | |
| CX_BASE_AUTH_URI: ${{ secrets.CX_BASE_AUTH_URI }} | |
| CX_APIKEY: ${{ secrets.CX_APIKEY }} | |
| CX_TENANT: ${{ secrets.CX_TENANT }} | |
| run: | | |
| echo "Running pre-test cleanup for ${{ matrix.group }}..." | |
| go test -v -timeout 5m github.com/checkmarx/ast-cli/test/cleandata || true | |
| echo "Cleanup complete" | |
| - name: Warn about uncovered tests | |
| if: matrix.group == 'uncovered' && needs.validate-test-coverage.outputs.has_uncovered == 'true' | |
| run: | | |
| echo "::warning::Some tests are not covered by any CI matrix pattern and will run in this catch-all group." | |
| echo "::warning::Please consider updating the matrix patterns or renaming the test functions to match existing patterns." | |
| echo "Tests being run in catch-all group:" | |
| echo "${{ needs.validate-test-coverage.outputs.uncovered_tests }}" | sed 's/\^(//;s/)$//;s/|/\n/g' | while read test; do | |
| echo " - $test" | |
| done | |
| - name: Run ${{ matrix.name }} tests | |
| # Skip the uncovered group if there are no uncovered tests | |
| if: matrix.group != 'uncovered' || needs.validate-test-coverage.outputs.has_uncovered == 'true' | |
| shell: bash | |
| env: | |
| CX_BASE_URI: ${{ secrets.CX_BASE_URI }} | |
| CX_CLIENT_ID: ${{ secrets.CX_CLIENT_ID }} | |
| CX_CLIENT_SECRET: ${{ secrets.CX_CLIENT_SECRET }} | |
| CX_BASE_AUTH_URI: ${{ secrets.CX_BASE_AUTH_URI }} | |
| CX_AST_USERNAME: ${{ secrets.CX_AST_USERNAME }} | |
| CX_AST_PASSWORD: ${{ secrets.CX_AST_PASSWORD }} | |
| CX_APIKEY: ${{ secrets.CX_APIKEY }} | |
| CX_TENANT: ${{ secrets.CX_TENANT }} | |
| CX_SCAN_SSH_KEY: ${{ secrets.CX_SCAN_SSH_KEY }} | |
| CX_ORIGIN: "cli-tests" | |
| PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| PROXY_HOST: localhost | |
| PROXY_PORT: 3128 | |
| PROXY_USERNAME: ${{ secrets.PROXY_USER }} | |
| PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }} | |
| PR_GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| PR_GITHUB_NAMESPACE: "checkmarx" | |
| PR_GITHUB_REPO_NAME: "ast-cli" | |
| PR_GITHUB_NUMBER: 983 | |
| PR_GITLAB_TOKEN: ${{ secrets.PR_GITLAB_TOKEN }} | |
| PR_GITLAB_NAMESPACE: ${{ secrets.PR_GITLAB_NAMESPACE }} | |
| PR_GITLAB_REPO_NAME: ${{ secrets.PR_GITLAB_REPO_NAME }} | |
| PR_GITLAB_PROJECT_ID: ${{ secrets.PR_GITLAB_PROJECT_ID }} | |
| PR_GITLAB_IID: ${{ secrets.PR_GITLAB_IID }} | |
| AZURE_ORG: ${{ secrets.AZURE_ORG }} | |
| AZURE_PROJECT: ${{ secrets.AZURE_PROJECT }} | |
| AZURE_REPOS: ${{ secrets.AZURE_REPOS }} | |
| AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} | |
| AZURE_PR_NUMBER: 1 | |
| BITBUCKET_WORKSPACE: ${{ secrets.BITBUCKET_WORKSPACE }} | |
| BITBUCKET_REPOS: ${{ secrets.BITBUCKET_REPOS }} | |
| BITBUCKET_USERNAME: ${{ secrets.BITBUCKET_USERNAME }} | |
| BITBUCKET_PASSWORD: ${{ secrets.BITBUCKET_PASSWORD }} | |
| GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }} | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| PR_BITBUCKET_TOKEN: ${{ secrets.PR_BITBUCKET_TOKEN }} | |
| PR_BITBUCKET_NAMESPACE: "AstSystemTest" | |
| PR_BITBUCKET_REPO_NAME: "cliIntegrationTest" | |
| PR_BITBUCKET_ID: 1 | |
| run: | | |
| echo "Running test group: ${{ matrix.name }}" | |
| # Determine the pattern to use | |
| # For the uncovered group, use the dynamically generated pattern from validate-test-coverage | |
| if [ "${{ matrix.group }}" = "uncovered" ]; then | |
| TEST_PATTERN="${{ needs.validate-test-coverage.outputs.uncovered_tests }}" | |
| echo "Using dynamically generated pattern for uncovered tests" | |
| else | |
| TEST_PATTERN="${{ matrix.pattern }}" | |
| fi | |
| echo "Pattern: $TEST_PATTERN" | |
| # Skip if pattern is empty (shouldn't happen, but safety check) | |
| if [ -z "$TEST_PATTERN" ]; then | |
| echo "No tests to run for this group (empty pattern)" | |
| exit 0 | |
| fi | |
| # Run tests matching the pattern with coverage | |
| # Use -p 1 to run tests sequentially within the group to avoid race conditions | |
| set +e # Don't exit on error so we can capture and retry | |
| go test \ | |
| -tags integration \ | |
| -v \ | |
| -p 1 \ | |
| -timeout 90m \ | |
| -run "$TEST_PATTERN" \ | |
| -coverpkg github.com/checkmarx/ast-cli/internal/commands,github.com/checkmarx/ast-cli/internal/services,github.com/checkmarx/ast-cli/internal/wrappers \ | |
| -coverprofile cover-${{ matrix.group }}.out \ | |
| github.com/checkmarx/ast-cli/test/integration 2>&1 | tee test_output_${{ matrix.group }}.log | |
| FIRST_RUN_EXIT_CODE=${PIPESTATUS[0]} | |
| set -e # Re-enable exit on error | |
| echo "First run exit code: $FIRST_RUN_EXIT_CODE" | |
| # ============================================================ | |
| # BULLETPROOF RETRY LOGIC - Handles ALL failure scenarios | |
| # ============================================================ | |
| # Retries: Up to 2 retries (3 total attempts) | |
| # Detects: FAIL, panic, API errors, auth failures, timeouts | |
| # ============================================================ | |
| extract_failed_tests() { | |
| local LOG_FILE="$1" | |
| local FAILED_TESTS="" | |
| echo "=== Analyzing log file for failures ===" | |
| # Method 1: Standard "--- FAIL: TestName" pattern | |
| local STANDARD_FAILS=$(grep -E "^--- FAIL:" "$LOG_FILE" 2>/dev/null | \ | |
| grep -oE "Test[A-Za-z0-9_]+" | sort -u | tr '\n' ' ' || true) | |
| if [ -n "$STANDARD_FAILS" ]; then | |
| echo " [Method 1] Found via --- FAIL: $STANDARD_FAILS" | |
| FAILED_TESTS="$STANDARD_FAILS" | |
| fi | |
| # Method 2: Find tests that panicked (look for === RUN before each panic) | |
| if grep -q "^panic:" "$LOG_FILE" 2>/dev/null; then | |
| echo " [Method 2] Panic detected, finding affected tests..." | |
| # Get all panic line numbers | |
| local PANIC_LINES=$(grep -n "^panic:" "$LOG_FILE" | cut -d: -f1) | |
| for PANIC_LINE in $PANIC_LINES; do | |
| local PANIC_TEST=$(head -n "$PANIC_LINE" "$LOG_FILE" | grep -E "^=== RUN" | tail -1 | grep -oE "Test[A-Za-z0-9_]+" | head -1 || true) | |
| if [ -n "$PANIC_TEST" ]; then | |
| echo " Panic in: $PANIC_TEST" | |
| FAILED_TESTS="$FAILED_TESTS $PANIC_TEST" | |
| fi | |
| done | |
| fi | |
| # Method 3: Find tests with error messages (API errors, auth failures, etc.) | |
| local ERROR_PATTERNS="Authorization failed|Failed showing|Failed creating|Failed getting|error getting|API error|status code: 5[0-9][0-9]" | |
| if grep -qE "$ERROR_PATTERNS" "$LOG_FILE" 2>/dev/null; then | |
| echo " [Method 3] API/Auth errors detected, finding affected tests..." | |
| local ERROR_LINES=$(grep -nE "$ERROR_PATTERNS" "$LOG_FILE" | cut -d: -f1 | head -5) | |
| for ERROR_LINE in $ERROR_LINES; do | |
| local ERROR_TEST=$(head -n "$ERROR_LINE" "$LOG_FILE" | grep -E "^=== RUN" | tail -1 | grep -oE "Test[A-Za-z0-9_]+" | head -1 || true) | |
| if [ -n "$ERROR_TEST" ]; then | |
| echo " Error in: $ERROR_TEST" | |
| FAILED_TESTS="$FAILED_TESTS $ERROR_TEST" | |
| fi | |
| done | |
| fi | |
| # Method 4: Last resort - get the last running test before FAIL | |
| if [ -z "$FAILED_TESTS" ]; then | |
| echo " [Method 4] Using last running test as fallback..." | |
| local LAST_TEST=$(grep -E "^=== RUN" "$LOG_FILE" | tail -1 | grep -oE "Test[A-Za-z0-9_]+" | head -1 || true) | |
| if [ -n "$LAST_TEST" ]; then | |
| echo " Last running: $LAST_TEST" | |
| FAILED_TESTS="$LAST_TEST" | |
| fi | |
| fi | |
| # Clean up: deduplicate and format as pipe-separated for -run flag | |
| if [ -n "$FAILED_TESTS" ]; then | |
| # Also extract parent test names (for subtests like TestFoo/SubTest -> TestFoo) | |
| local ALL_TESTS="" | |
| for TEST in $FAILED_TESTS; do | |
| ALL_TESTS="$ALL_TESTS $TEST" | |
| # Extract parent test name if this looks like a subtest | |
| local PARENT=$(echo "$TEST" | sed 's/_[^_]*$//' | grep -E "^Test" || true) | |
| if [ -n "$PARENT" ] && [ "$PARENT" != "$TEST" ]; then | |
| ALL_TESTS="$ALL_TESTS $PARENT" | |
| fi | |
| done | |
| FAILED_TESTS=$(echo "$ALL_TESTS" | tr ' ' '\n' | grep -E "^Test" | sort -u | tr '\n' '|' | sed 's/|$//') | |
| fi | |
| echo "$FAILED_TESTS" | |
| } | |
| run_tests_with_retry() { | |
| local PATTERN="$1" | |
| local ATTEMPT="$2" | |
| local MAX_ATTEMPTS="$3" | |
| local LOG_SUFFIX="$4" | |
| echo "" | |
| echo "==========================================" | |
| echo " RETRY ATTEMPT $ATTEMPT of $MAX_ATTEMPTS" | |
| echo " Pattern: $PATTERN" | |
| echo "==========================================" | |
| echo "" | |
| # Wait before retry to allow cleanup and server recovery | |
| if [ "$ATTEMPT" -gt 1 ]; then | |
| local WAIT_TIME=$((ATTEMPT * 15)) | |
| echo "Waiting ${WAIT_TIME}s before retry..." | |
| sleep $WAIT_TIME | |
| fi | |
| set +e | |
| go test \ | |
| -tags integration \ | |
| -v \ | |
| -p 1 \ | |
| -timeout 60m \ | |
| -run "$PATTERN" \ | |
| -coverpkg github.com/checkmarx/ast-cli/internal/commands,github.com/checkmarx/ast-cli/internal/services,github.com/checkmarx/ast-cli/internal/wrappers \ | |
| -coverprofile cover-${{ matrix.group }}-${LOG_SUFFIX}.out \ | |
| github.com/checkmarx/ast-cli/test/integration 2>&1 | tee test_output_${{ matrix.group }}_${LOG_SUFFIX}.log | |
| local EXIT_CODE=${PIPESTATUS[0]} | |
| set -e | |
| return $EXIT_CODE | |
| } | |
| if [ "$FIRST_RUN_EXIT_CODE" -ne 0 ]; then | |
| echo "" | |
| echo "============================================" | |
| echo " FIRST RUN FAILED - Starting retry logic" | |
| echo "============================================" | |
| # Check for hard infrastructure failures that shouldn't be retried | |
| if grep -qE "Could not reach provided Checkmarx server|connection refused|no such host" test_output_${{ matrix.group }}.log; then | |
| echo "::error::Infrastructure failure detected - Checkmarx server unreachable" | |
| echo "This is a server connectivity issue, not a test failure." | |
| exit 1 | |
| fi | |
| # Extract failed tests | |
| FAILED_TESTS=$(extract_failed_tests "test_output_${{ matrix.group }}.log") | |
| if [ -z "$FAILED_TESTS" ]; then | |
| echo "::error::Could not identify which tests failed" | |
| echo "Check the log file for details" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "Tests to retry: $FAILED_TESTS" | |
| # Retry loop - up to 2 more attempts | |
| MAX_RETRIES=2 | |
| CURRENT_RETRY=1 | |
| RETRY_SUCCESS=false | |
| while [ $CURRENT_RETRY -le $MAX_RETRIES ]; do | |
| run_tests_with_retry "^($FAILED_TESTS)$" "$CURRENT_RETRY" "$MAX_RETRIES" "retry${CURRENT_RETRY}" | |
| RETRY_EXIT_CODE=$? | |
| if [ $RETRY_EXIT_CODE -eq 0 ]; then | |
| echo "" | |
| echo "==========================================" | |
| echo " ✅ TESTS PASSED ON RETRY $CURRENT_RETRY" | |
| echo "==========================================" | |
| RETRY_SUCCESS=true | |
| break | |
| else | |
| echo "" | |
| echo "Retry $CURRENT_RETRY failed with exit code: $RETRY_EXIT_CODE" | |
| # Check if we should continue retrying | |
| if [ $CURRENT_RETRY -lt $MAX_RETRIES ]; then | |
| # Extract any new failures from this retry | |
| NEW_FAILURES=$(extract_failed_tests "test_output_${{ matrix.group }}_retry${CURRENT_RETRY}.log") | |
| if [ -n "$NEW_FAILURES" ]; then | |
| FAILED_TESTS="$NEW_FAILURES" | |
| echo "Updated failed tests for next retry: $FAILED_TESTS" | |
| fi | |
| fi | |
| fi | |
| CURRENT_RETRY=$((CURRENT_RETRY + 1)) | |
| done | |
| if [ "$RETRY_SUCCESS" = false ]; then | |
| echo "" | |
| echo "==========================================" | |
| echo " ❌ TESTS FAILED AFTER $MAX_RETRIES RETRIES" | |
| echo "==========================================" | |
| exit 1 | |
| fi | |
| else | |
| echo "" | |
| echo "==========================================" | |
| echo " ✅ ALL TESTS PASSED ON FIRST RUN" | |
| echo "==========================================" | |
| fi | |
| - name: Skip notification (no uncovered tests) | |
| if: matrix.group == 'uncovered' && needs.validate-test-coverage.outputs.has_uncovered != 'true' | |
| run: | | |
| echo "::notice::No uncovered tests found - all tests are properly categorized!" | |
| echo "The 'Uncovered Tests (Catch-All)' group was skipped because all tests match existing patterns." | |
| - name: Stop Squid proxy | |
| if: always() | |
| run: docker stop squid || true && docker rm squid || true | |
| - name: Upload coverage artifact | |
| if: matrix.group != 'uncovered' || needs.validate-test-coverage.outputs.has_uncovered == 'true' | |
| uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 #v4 | |
| with: | |
| name: coverage-${{ matrix.group }} | |
| path: cover-*.out | |
| - name: Upload test logs | |
| if: always() && (matrix.group != 'uncovered' || needs.validate-test-coverage.outputs.has_uncovered == 'true') | |
| uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 #v4 | |
| with: | |
| name: test-logs-${{ matrix.group }} | |
| path: test_output_*.log | |
| merge-coverage: | |
| runs-on: ubuntu-latest | |
| needs: integration-tests | |
| steps: | |
| - name: Checkout the repository | |
| uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 #v4.0.0 | |
| - name: Set up Go version | |
| uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 #v4 | |
| with: | |
| go-version-file: go.mod | |
| - name: Install gocovmerge | |
| run: go install github.com/wadey/gocovmerge@latest | |
| - name: Download all coverage artifacts | |
| uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4 | |
| with: | |
| pattern: coverage-* | |
| path: coverage-files | |
| merge-multiple: true | |
| - name: Merge coverage files | |
| run: | | |
| echo "Merging coverage files..." | |
| ls -la coverage-files/ | |
| gocovmerge coverage-files/cover-*.out > cover.out | |
| go tool cover -html=cover.out -o coverage.html | |
| - name: Coverage report | |
| uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 #v4 | |
| with: | |
| name: ${{ runner.os }}-coverage-latest | |
| path: coverage.html | |
| - name: Check if total coverage is greater then 75 | |
| shell: bash | |
| run: | | |
| CODE_COV=$(go tool cover -func cover.out | grep total | awk '{print substr($3, 1, length($3)-1)}') | |
| EXPECTED_CODE_COV=75 | |
| var=$(awk 'BEGIN{ print "'$CODE_COV'"<"'$EXPECTED_CODE_COV'" }') | |
| if [ "$var" -eq 1 ];then | |
| echo "Your code coverage is too low. Coverage precentage is: $CODE_COV" | |
| exit 1 | |
| else | |
| echo "Your code coverage test passed! Coverage precentage is: $CODE_COV" | |
| exit 0 | |
| fi | |
| - name: Run cleandata to clean up projects | |
| env: | |
| CX_BASE_URI: ${{ secrets.CX_BASE_URI }} | |
| CX_CLIENT_ID: ${{ secrets.CX_CLIENT_ID }} | |
| CX_CLIENT_SECRET: ${{ secrets.CX_CLIENT_SECRET }} | |
| CX_BASE_AUTH_URI: ${{ secrets.CX_BASE_AUTH_URI }} | |
| CX_APIKEY: ${{ secrets.CX_APIKEY }} | |
| CX_TENANT: ${{ secrets.CX_TENANT }} | |
| run: | | |
| go test -v github.com/checkmarx/ast-cli/test/cleandata || true | |
| lint: | |
| name: lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 #v4.0.0 | |
| - name: Set up Go version | |
| uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 #v4 | |
| with: | |
| go-version-file: go.mod | |
| - run: go version | |
| - run: go mod tidy | |
| - name: golangci-lint | |
| uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc #v3 | |
| with: | |
| skip-pkg-cache: true | |
| version: v1.64.2 | |
| args: -c .golangci.yml | |
| --timeout 5m | |
| only-new-issues: true | |
| govulncheck: | |
| runs-on: ubuntu-latest | |
| name: govulncheck | |
| steps: | |
| - id: govulncheck | |
| uses: golang/govulncheck-action@7da72f730e37eeaad891fcff0a532d27ed737cd4 #v1 | |
| continue-on-error: true | |
| with: | |
| go-version-file: go.mod | |
| go-package: ./... | |
| checkDockerImage: | |
| runs-on: ubuntu-latest | |
| name: scan Docker Image with Trivy | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598 #2.0.0 | |
| - name: Set up Docker | |
| uses: docker/setup-buildx-action@cf09c5c41b299b55c366aff30022701412eb6ab0 #v1.0.0 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@49ed152c8eca782a232dede0303416e8f356c37b #v2 | |
| with: | |
| username: ${{ secrets.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Build the project | |
| run: go build -o ./cx ./cmd | |
| - name: Build Docker image | |
| run: docker build -t ast-cli:${{ github.sha }} . | |
| - name: Run Trivy scanner without downloading DBs | |
| uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 #v0.28.0 | |
| with: | |
| scan-type: 'image' | |
| image-ref: ast-cli:${{ github.sha }} | |
| format: 'table' | |
| exit-code: '1' | |
| ignore-unfixed: true | |
| vuln-type: 'os,library' | |
| output: './trivy-image-results.txt' | |
| env: | |
| TRIVY_SKIP_JAVA_DB_UPDATE: true | |
| - name: Inspect action report | |
| if: always() | |
| shell: bash | |
| run: cat ./trivy-image-results.txt |