From d7a27eb63a6bf4d22f4aef50b389eb400e07dd5f Mon Sep 17 00:00:00 2001 From: fcaffieri Date: Wed, 20 May 2026 17:22:46 -0300 Subject: [PATCH] Create WF for integration testing --- .../workflows/check_integration_tools.yaml | 472 ++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 .github/workflows/check_integration_tools.yaml diff --git a/.github/workflows/check_integration_tools.yaml b/.github/workflows/check_integration_tools.yaml new file mode 100644 index 00000000..eef80fc7 --- /dev/null +++ b/.github/workflows/check_integration_tools.yaml @@ -0,0 +1,472 @@ +run-name: >- + ${{ github.event_name == 'workflow_dispatch' + && format('Docker Integration Test - Manual {0} on {1}', inputs.deployment_type, inputs.pr_head_ref) + || format('Docker Integration Test - #{0} {1}', github.event.issue.number, github.event.issue.title) }} +name: PR Check - Docker Integration Tests + +on: + issue_comment: + types: [created] + workflow_dispatch: + inputs: + pr_head_ref: + description: 'Branch of wazuh-docker to test' + required: true + type: string + automation_reference: + description: 'Branch of wazuh-automation to use' + required: false + default: 'main' + type: string + deployment_type: + description: 'Deployment type to test' + required: true + type: choice + options: + - single-node + - multi-node + - both + +permissions: + id-token: write + contents: read + pull-requests: write + issues: write + checks: write + +env: + AUTOMATION_REFERENCE: ${{ inputs.automation_reference || 'main' }} + ALLOCATOR_PATH: /tmp/allocator_instance + REGION: us-east-1 + +jobs: + # ------------------------------------------------------------------------- + # Job 1: Parse PR info and determine which deployment(s) to test + # + # Available commands (checked longest-match first to avoid substring collision): + # /test-docker-single — test single-node deployment + # /test-docker-multi — test multi-node deployment + # /test-docker — test both single-node and multi-node + # ------------------------------------------------------------------------- + get_pr_info: + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.issue.state == 'open' && + !github.event.issue.draft && + (contains(github.event.comment.body, '/test-docker-single') || + contains(github.event.comment.body, '/test-docker-multi') || + contains(github.event.comment.body, '/test-docker')) + runs-on: ubuntu-latest + outputs: + pr_number: ${{ steps.pr_data.outputs.pr_number }} + pr_head_ref: ${{ steps.pr_data.outputs.pr_head_ref }} + pr_head_sha: ${{ steps.pr_data.outputs.pr_head_sha }} + check_run_id: ${{ steps.create_check.outputs.result }} + deployment_matrix: ${{ steps.parse_command.outputs.deployment_matrix }} + check_name: ${{ steps.parse_command.outputs.check_name }} + + steps: + - name: React to comment + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket' + }); + + - name: Extract PR data + id: pr_data + env: + GH_TOKEN: ${{ github.token }} + run: | + PR_NUMBER="${{ github.event.issue.number }}" + PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER}) + PR_HEAD_REF=$(echo "$PR_DATA" | jq -r '.head.ref') + PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head.sha') + echo "pr_number=${PR_NUMBER}" >> $GITHUB_OUTPUT + echo "pr_head_ref=${PR_HEAD_REF}" >> $GITHUB_OUTPUT + echo "pr_head_sha=${PR_HEAD_SHA}" >> $GITHUB_OUTPUT + + - name: Parse command and set deployment metadata + id: parse_command + run: | + COMMENT_BODY="${{ github.event.comment.body }}" + # Check longest match first to avoid /test-docker matching /test-docker-single + if echo "$COMMENT_BODY" | grep -q '/test-docker-single'; then + echo 'deployment_matrix=["single-node"]' >> $GITHUB_OUTPUT + echo 'check_name=Docker Integration Check (Single-Node)' >> $GITHUB_OUTPUT + elif echo "$COMMENT_BODY" | grep -q '/test-docker-multi'; then + echo 'deployment_matrix=["multi-node"]' >> $GITHUB_OUTPUT + echo 'check_name=Docker Integration Check (Multi-Node)' >> $GITHUB_OUTPUT + elif echo "$COMMENT_BODY" | grep -q '/test-docker'; then + echo 'deployment_matrix=["single-node","multi-node"]' >> $GITHUB_OUTPUT + echo 'check_name=Docker Integration Check' >> $GITHUB_OUTPUT + fi + + - name: Create check run + id: create_check + uses: actions/github-script@v7 + env: + HEAD_SHA: ${{ steps.pr_data.outputs.pr_head_sha }} + CHECK_NAME: ${{ steps.parse_command.outputs.check_name }} + COMMENT_BODY: ${{ github.event.comment.body }} + with: + script: | + const { data: check } = await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: process.env.CHECK_NAME, + head_sha: process.env.HEAD_SHA, + status: 'in_progress', + started_at: new Date().toISOString(), + details_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + output: { + title: `🔨 Running ${process.env.CHECK_NAME}...`, + summary: `Triggered by comment: \`${process.env.COMMENT_BODY}\``, + text: 'Allocating instance and running Docker integration tests' + } + }); + console.log('Check run created:', check.id); + return check.id; + + # ------------------------------------------------------------------------- + # Job 2: For each deployment type — provision VM, deploy Docker stack, test, + # collect results, and clean up. + # ------------------------------------------------------------------------- + docker_test: + needs: [get_pr_info] + if: | + always() && + (needs.get_pr_info.result == 'success' || github.event_name == 'workflow_dispatch') + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + deployment_type: ${{ fromJSON( + github.event_name == 'workflow_dispatch' + && (inputs.deployment_type == 'both' && '["single-node","multi-node"]' + || format('["{0}"]', inputs.deployment_type)) + || needs.get_pr_info.outputs.deployment_matrix + ) }} + + steps: + # ----------------------------------------------------------------------- + # Setup + # ----------------------------------------------------------------------- + - name: Resolve PR head ref + id: ctx + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "pr_head_ref=${{ inputs.pr_head_ref }}" >> $GITHUB_OUTPUT + else + echo "pr_head_ref=${{ needs.get_pr_info.outputs.pr_head_ref }}" >> $GITHUB_OUTPUT + fi + + - name: Checkout wazuh-automation + uses: actions/checkout@v4 + with: + repository: wazuh/wazuh-automation + ref: ${{ env.AUTOMATION_REFERENCE }} + token: ${{ secrets.GH_CLONE_TOKEN }} + path: wazuh-automation + + - name: Checkout wazuh-docker PR branch + uses: actions/checkout@v4 + with: + ref: ${{ steps.ctx.outputs.pr_head_ref }} + path: wazuh-docker + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install requirements + run: | + pip install -r wazuh-automation/deployability/deps/requirements.txt + pip install -r wazuh-automation/integration-test-module/requirements.txt + pip install -e wazuh-automation/integration-test-module/ + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + role-session-name: docker-test-${{ github.run_id }}-${{ matrix.deployment_type }} + aws-region: ${{ env.REGION }} + + # ----------------------------------------------------------------------- + # Provision: allocate VM and extract SSH credentials + # ----------------------------------------------------------------------- + - name: Allocate instance + id: allocate + run: | + mkdir -p ${{ env.ALLOCATOR_PATH }} + python3 wazuh-automation/deployability/modules/allocation/main.py \ + --action create \ + --provider aws \ + --size xlarge \ + --composite-name ubuntu-24-amd64 \ + --working-dir ${{ env.ALLOCATOR_PATH }} \ + --track-output ${{ env.ALLOCATOR_PATH }}/track.yml \ + --inventory-output ${{ env.ALLOCATOR_PATH }}/inventory.yml \ + --instance-name gha_${{ github.run_id }}_docker_${{ matrix.deployment_type }} \ + --label-team devops \ + --label-termination-date 1d + + sed -n '/hosts:/,/^[^ ]/p' ${{ env.ALLOCATOR_PATH }}/inventory.yml \ + | grep "ansible_" \ + | sed 's/^[ ]*//g' \ + > ${{ env.ALLOCATOR_PATH }}/inventory_vars_raw.yml + sed 's/: */=/g' ${{ env.ALLOCATOR_PATH }}/inventory_vars_raw.yml \ + > ${{ env.ALLOCATOR_PATH }}/inventory_vars.yml + sed -i 's/-o StrictHostKeyChecking=no/"-o StrictHostKeyChecking=no"/g' \ + ${{ env.ALLOCATOR_PATH }}/inventory_vars.yml + + - name: Set SSH credentials from inventory + run: | + find ${{ env.ALLOCATOR_PATH }} -name '*-key-*' -exec chmod 600 {} \; + source ${{ env.ALLOCATOR_PATH }}/inventory_vars.yml + echo "SSH_HOST=$ansible_host" >> $GITHUB_ENV + echo "SSH_PORT=$ansible_port" >> $GITHUB_ENV + echo "SSH_USER=$ansible_user" >> $GITHUB_ENV + echo "SSH_KEY=$ansible_ssh_private_key_file" >> $GITHUB_ENV + + - name: Set SSH/SCP helper env vars + run: | + echo "SSH_OPTS=-o StrictHostKeyChecking=no -o ServerAliveInterval=60 -o ServerAliveCountMax=20 -p ${{ env.SSH_PORT }} -i ${{ env.SSH_KEY }}" >> $GITHUB_ENV + echo "SCP_OPTS=-o StrictHostKeyChecking=no -P ${{ env.SSH_PORT }} -i ${{ env.SSH_KEY }}" >> $GITHUB_ENV + echo "REMOTE=${{ env.SSH_USER }}@${{ env.SSH_HOST }}" >> $GITHUB_ENV + + # ----------------------------------------------------------------------- + # Install Docker CE on the remote VM + # ----------------------------------------------------------------------- + - name: Install Docker CE + run: | + ssh ${{ env.SSH_OPTS }} "${{ env.REMOTE }}" " + curl -fsSL https://get.docker.com | sudo sh + sudo systemctl enable --now docker + " + + # ----------------------------------------------------------------------- + # Deploy: copy wazuh-docker and start the stack + # ----------------------------------------------------------------------- + - name: Copy wazuh-docker to VM + run: | + scp ${{ env.SCP_OPTS }} -r wazuh-docker "${{ env.REMOTE }}:/tmp/wazuh-docker" + + - name: Start Docker Compose + run: | + DEPLOYMENT="${{ matrix.deployment_type }}" + ssh ${{ env.SSH_OPTS }} "${{ env.REMOTE }}" " + cd /tmp/wazuh-docker/${DEPLOYMENT} + sudo docker compose up -d 2>&1 | tee /tmp/docker-compose-up.log + " + + - name: Wait for containers healthy + timeout-minutes: 15 + run: | + DEPLOYMENT="${{ matrix.deployment_type }}" + ssh ${{ env.SSH_OPTS }} "${{ env.REMOTE }}" " + echo 'Waiting for all containers to be healthy...' + for i in \$(seq 1 90); do + TOTAL=\$(sudo docker compose -f /tmp/wazuh-docker/${DEPLOYMENT}/docker-compose.yml ps -q 2>/dev/null | wc -l) + HEALTHY=\$(sudo docker compose -f /tmp/wazuh-docker/${DEPLOYMENT}/docker-compose.yml ps 2>/dev/null \ + | grep -E '(healthy|\(healthy\))' | wc -l) + NOT_HEALTHY=\$(sudo docker compose -f /tmp/wazuh-docker/${DEPLOYMENT}/docker-compose.yml ps 2>/dev/null \ + | grep -vE '(healthy|\(healthy\)|NAME|^$)' | grep -v 'nginx' | wc -l) + + if [ \"\$NOT_HEALTHY\" -eq 0 ] && [ \"\$TOTAL\" -gt 0 ]; then + echo \"All containers healthy after \${i} x 10s attempts (total: \$TOTAL)\" + exit 0 + fi + echo \" attempt \$i: \$HEALTHY healthy, \$NOT_HEALTHY not yet healthy (total: \$TOTAL)\" + sleep 10 + done + echo 'ERROR: containers not healthy after 15 minutes' + sudo docker compose -f /tmp/wazuh-docker/${DEPLOYMENT}/docker-compose.yml ps + exit 1 + " + + # ----------------------------------------------------------------------- + # Run integration tests + # ----------------------------------------------------------------------- + - name: Run tests + id: run_tests + continue-on-error: true + run: | + DEPLOYMENT="${{ matrix.deployment_type }}" + test_runner \ + --test-type "docker-${DEPLOYMENT}" \ + --deployment-type "docker-${DEPLOYMENT}" \ + --ssh-host "${{ env.SSH_HOST }}" \ + --ssh-port "${{ env.SSH_PORT }}" \ + --ssh-key-path "${{ env.SSH_KEY }}" \ + --ssh-username "${{ env.SSH_USER }}" \ + --log-level INFO \ + --output github \ + --output-file "test-results-docker-${DEPLOYMENT}.github" + + # ----------------------------------------------------------------------- + # Collect logs on failure + # ----------------------------------------------------------------------- + - name: Collect Docker logs on failure + if: failure() || steps.run_tests.outcome == 'failure' + run: | + DEPLOYMENT="${{ matrix.deployment_type }}" + ssh ${{ env.SSH_OPTS }} "${{ env.REMOTE }}" " + sudo docker compose -f /tmp/wazuh-docker/${DEPLOYMENT}/docker-compose.yml logs --no-color 2>&1 + " > docker-logs-${DEPLOYMENT}.txt || true + + - name: Upload Docker logs + if: failure() || steps.run_tests.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: docker-logs-${{ matrix.deployment_type }}-${{ github.run_id }} + path: docker-logs-*.txt + retention-days: 7 + + # ----------------------------------------------------------------------- + # Reporting + # ----------------------------------------------------------------------- + - name: Create step summary + if: always() + run: | + DEPLOYMENT="${{ matrix.deployment_type }}" + echo "## Docker Integration Test Results — ${DEPLOYMENT}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ -f "test-results-docker-${DEPLOYMENT}.github" ]; then + cat "test-results-docker-${DEPLOYMENT}.github" >> $GITHUB_STEP_SUMMARY + else + echo "No test results file found." >> $GITHUB_STEP_SUMMARY + fi + + - name: Post PR comment with results + if: always() && github.event_name == 'issue_comment' + uses: actions/github-script@v7 + env: + DEPLOYMENT: ${{ matrix.deployment_type }} + RUN_OUTCOME: ${{ steps.run_tests.outcome }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const deployment = process.env.DEPLOYMENT; + const outcome = process.env.RUN_OUTCOME; + const marker = ``; + + let body = `${marker}\n## Docker Integration Tests — \`${deployment}\`\n\n`; + body += outcome === 'success' + ? '✅ **All tests passed!**\n\n' + : '❌ **Some tests failed**\n\n'; + + const resultsFile = `test-results-docker-${deployment}.github`; + try { + if (fs.existsSync(resultsFile)) { + body += '### Results\n\n' + fs.readFileSync(resultsFile, 'utf8') + '\n\n'; + } + } catch (e) { + console.log('Could not read results file:', e.message); + } + body += `- **Workflow:** [View Details](${context.payload.repository.html_url}/actions/runs/${context.runId})\n`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => + c.user.type === 'Bot' && c.body.includes(marker) + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body, + }); + } + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-docker-${{ matrix.deployment_type }}-${{ github.run_id }} + path: test-results-docker-${{ matrix.deployment_type }}.github + retention-days: 7 + + # ----------------------------------------------------------------------- + # Cleanup: always stop stack and deallocate VM + # ----------------------------------------------------------------------- + - name: Stop Docker Compose + if: always() + run: | + DEPLOYMENT="${{ matrix.deployment_type }}" + ssh ${{ env.SSH_OPTS }} "${{ env.REMOTE }}" " + sudo docker compose -f /tmp/wazuh-docker/${DEPLOYMENT}/docker-compose.yml down -v || true + " || true + + - name: Configure AWS credentials for cleanup + if: always() + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + role-session-name: docker-cleanup-${{ github.run_id }}-${{ matrix.deployment_type }} + aws-region: ${{ env.REGION }} + + - name: Deallocate instance + if: always() + run: | + python3 wazuh-automation/deployability/modules/allocation/main.py \ + --action delete \ + --track-output ${{ env.ALLOCATOR_PATH }}/track.yml + + # ------------------------------------------------------------------------- + # Job 3: Update the GitHub check run (issue_comment trigger only) + # ------------------------------------------------------------------------- + update_check: + needs: [get_pr_info, docker_test] + if: always() && github.event_name == 'issue_comment' && needs.get_pr_info.result == 'success' + runs-on: ubuntu-latest + steps: + - name: Update check run + uses: actions/github-script@v7 + env: + DOCKER_RESULT: ${{ needs.docker_test.result }} + CHECK_NAME: ${{ needs.get_pr_info.outputs.check_name }} + CHECK_RUN_ID: ${{ needs.get_pr_info.outputs.check_run_id }} + with: + script: | + const result = process.env.DOCKER_RESULT; + const conclusionMap = { + success: { conclusion: 'success', icon: '✅', summary: 'All Docker integration tests passed.' }, + failure: { conclusion: 'failure', icon: '❌', summary: 'One or more Docker integration tests failed.' }, + cancelled: { conclusion: 'cancelled', icon: 'âšī¸', summary: 'Workflow was cancelled.' }, + }; + const { conclusion, icon, summary } = conclusionMap[result] ?? conclusionMap.failure; + const label = conclusion.charAt(0).toUpperCase() + conclusion.slice(1); + await github.rest.checks.update({ + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: parseInt(process.env.CHECK_RUN_ID), + status: 'completed', + conclusion, + completed_at: new Date().toISOString(), + output: { + title: `${icon} ${process.env.CHECK_NAME} — ${label}`, + summary, + text: `[View workflow run](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})` + } + });