diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..4a26e51 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,300 @@ +name: 'Code Quality & Security Checks' + +on: + pull_request: + branches: ['main', 'develop'] + push: + branches: ['main'] + workflow_dispatch: + inputs: + coverage_threshold: + description: 'Minimum coverage threshold (%)' + required: false + default: '80' + type: string + audit_level: + description: 'Audit severity level (low, moderate, high, critical)' + required: false + default: 'moderate' + type: choice + options: + - low + - moderate + - high + - critical + +# Prevent concurrent runs on the same PR +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + code-quality: + name: 'Lint, Audit & Coverage' + runs-on: ubuntu-latest + timeout-minutes: 10 + + permissions: + contents: read + pull-requests: write + + steps: + # ============================================================================= + # SETUP + # ============================================================================= + - name: 'Checkout Repository' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 'Setup Node.js' + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: 'Install Dependencies' + run: npm ci + + # ============================================================================= + # LINTING & FORMATTING + # ============================================================================= + - name: 'Run Biome Linter' + id: biome_lint + run: | + echo "๐Ÿ” Running Biome lint checks..." + npm run lint 2>&1 | tee biome-lint.log + + if [ ${PIPESTATUS[0]} -ne 0 ]; then + echo "โŒ Biome lint found errors" + echo "lint_status=failed" >> $GITHUB_OUTPUT + exit 1 + else + echo "โœ… Biome lint passed" + echo "lint_status=passed" >> $GITHUB_OUTPUT + fi + + - name: 'Run Biome Format Check' + id: biome_format + run: | + echo "๐ŸŽจ Checking code formatting..." + npm run format 2>&1 | tee biome-format.log + + if [ ${PIPESTATUS[0]} -ne 0 ]; then + echo "โŒ Code formatting issues detected" + echo "format_status=failed" >> $GITHUB_OUTPUT + exit 1 + else + echo "โœ… Code formatting is correct" + echo "format_status=passed" >> $GITHUB_OUTPUT + fi + + # ============================================================================= + # DEPENDENCY AUDITING + # ============================================================================= + - name: 'Run Dependency Audit' + id: audit + continue-on-error: true + run: | + echo "๐Ÿ”’ Running dependency security audit..." + + # Determine audit level (from workflow input or default) + AUDIT_LEVEL="${{ github.event.inputs.audit_level || 'moderate' }}" + echo "๐Ÿ“Š Audit level: $AUDIT_LEVEL" + + # Run audit-ci with configured level + npx audit-ci --$AUDIT_LEVEL 2>&1 | tee audit-report.log + + AUDIT_EXIT_CODE=${PIPESTATUS[0]} + + if [ $AUDIT_EXIT_CODE -eq 0 ]; then + echo "โœ… No vulnerabilities found at $AUDIT_LEVEL level or above" + echo "audit_status=passed" >> $GITHUB_OUTPUT + echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT + else + echo "โš ๏ธ Vulnerabilities found at $AUDIT_LEVEL level or above" + echo "audit_status=failed" >> $GITHUB_OUTPUT + echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT + + # Extract vulnerability summary + VULN_COUNT=$(grep -oP '\d+(?= vulnerabilities found)' audit-report.log || echo "0") + echo "vulnerability_count=$VULN_COUNT" >> $GITHUB_OUTPUT + fi + + - name: 'Upload Audit Report' + if: always() + uses: actions/upload-artifact@v4 + with: + name: audit-report + path: audit-report.log + retention-days: 30 + + # ============================================================================= + # TEST COVERAGE + # ============================================================================= + - name: 'Run Tests with Coverage' + id: coverage + run: | + echo "๐Ÿงช Running tests with coverage analysis..." + + # Determine coverage threshold (from workflow input, secret, or default) + COVERAGE_THRESHOLD="${{ github.event.inputs.coverage_threshold || secrets.COVERAGE_THRESHOLD || '80' }}" + echo "๐Ÿ“Š Coverage threshold: $COVERAGE_THRESHOLD%" + + # Run tests with coverage + npm run test:coverage 2>&1 | tee test-coverage.log + + TEST_EXIT_CODE=${PIPESTATUS[0]} + + if [ $TEST_EXIT_CODE -ne 0 ]; then + echo "โŒ Tests failed or coverage below threshold" + echo "coverage_status=failed" >> $GITHUB_OUTPUT + exit 1 + else + echo "โœ… Tests passed with sufficient coverage" + echo "coverage_status=passed" >> $GITHUB_OUTPUT + fi + + # Extract coverage percentages from Jest output + if [ -f coverage/coverage-summary.json ]; then + COVERAGE_PCT=$(node -pe "JSON.parse(require('fs').readFileSync('coverage/coverage-summary.json')).total.lines.pct") + echo "coverage_percentage=$COVERAGE_PCT" >> $GITHUB_OUTPUT + echo "๐Ÿ“Š Overall coverage: $COVERAGE_PCT%" + fi + + - name: 'Upload Coverage Reports' + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: | + coverage/ + test-coverage.log + retention-days: 30 + + - name: 'Upload Coverage to Codecov (Optional)' + if: always() && steps.coverage.outputs.coverage_status == 'passed' + continue-on-error: true + uses: codecov/codecov-action@v4 + with: + files: ./coverage/lcov.info + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + # ============================================================================= + # DOCUMENTATION VALIDATION + # ============================================================================= + - name: 'Validate Endpoint Documentation' + id: docs_validation + run: | + echo "๐Ÿ“š Validating endpoint documentation consistency..." + npm run lint:docs-endpoints 2>&1 | tee docs-validation.log + + if [ ${PIPESTATUS[0]} -ne 0 ]; then + echo "โŒ Documentation validation failed" + echo "docs_status=failed" >> $GITHUB_OUTPUT + exit 1 + else + echo "โœ… Documentation validation passed" + echo "docs_status=passed" >> $GITHUB_OUTPUT + fi + + - name: 'Upload Documentation Validation Report' + if: always() + uses: actions/upload-artifact@v4 + with: + name: docs-validation-report + path: docs-validation.log + retention-days: 7 + + # ============================================================================= + # PR COMMENT SUMMARY + # ============================================================================= + - name: 'Post PR Comment with Results' + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const lintStatus = '${{ steps.biome_lint.outputs.lint_status }}'; + const formatStatus = '${{ steps.biome_format.outputs.format_status }}'; + const auditStatus = '${{ steps.audit.outputs.audit_status }}'; + const coverageStatus = '${{ steps.coverage.outputs.coverage_status }}'; + const docsStatus = '${{ steps.docs_validation.outputs.docs_status }}'; + const coveragePct = '${{ steps.coverage.outputs.coverage_percentage }}' || 'N/A'; + const vulnCount = '${{ steps.audit.outputs.vulnerability_count }}' || '0'; + + const statusIcon = (status) => status === 'passed' ? 'โœ…' : 'โŒ'; + + const body = `## ๐Ÿ” Code Quality Report + + | Check | Status | Details | + |-------|--------|---------| + | **Biome Lint** | ${statusIcon(lintStatus)} ${lintStatus} | Static code analysis | + | **Code Formatting** | ${statusIcon(formatStatus)} ${formatStatus} | Style consistency | + | **Security Audit** | ${statusIcon(auditStatus)} ${auditStatus} | ${vulnCount} vulnerabilities (if any) | + | **Test Coverage** | ${statusIcon(coverageStatus)} ${coverageStatus} | ${coveragePct}% overall coverage | + | **Documentation** | ${statusIcon(docsStatus)} ${docsStatus} | API docs validation | + + --- + + ${auditStatus === 'failed' ? 'โš ๏ธ **Security Alert**: Vulnerabilities detected. Review the audit report artifact.' : ''} + ${coverageStatus === 'failed' ? 'โš ๏ธ **Coverage Warning**: Tests failed or coverage below threshold.' : ''} + + ๐Ÿ“ฆ **Artifacts**: Download detailed reports from the workflow run page. + + Workflow: \`${context.workflow}\` | Run: [#${context.runNumber}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Code Quality Report') + ); + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + # ============================================================================= + # FINAL STATUS CHECK + # ============================================================================= + - name: 'Verify All Checks Passed' + if: always() + run: | + echo "๐Ÿ“‹ Final Status Summary:" + echo " Lint: ${{ steps.biome_lint.outputs.lint_status }}" + echo " Format: ${{ steps.biome_format.outputs.format_status }}" + echo " Audit: ${{ steps.audit.outputs.audit_status }}" + echo " Coverage: ${{ steps.coverage.outputs.coverage_status }}" + echo " Docs: ${{ steps.docs_validation.outputs.docs_status }}" + + if [ "${{ steps.biome_lint.outputs.lint_status }}" != "passed" ] || \ + [ "${{ steps.biome_format.outputs.format_status }}" != "passed" ] || \ + [ "${{ steps.audit.outputs.audit_status }}" != "passed" ] || \ + [ "${{ steps.coverage.outputs.coverage_status }}" != "passed" ] || \ + [ "${{ steps.docs_validation.outputs.docs_status }}" != "passed" ]; then + echo "โŒ One or more quality checks failed" + exit 1 + fi + + echo "โœ… All quality checks passed successfully"