forked from wazuh/wazuh-docker
Create WF for integration testing
This commit is contained in:
@@ -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 = `<!-- docker-integration-check-${deployment} -->`;
|
||||
|
||||
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})`
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user