name: Quality gate on: # push: # branches: # - main pull_request: branches: - main types: [ opened, synchronize, reopened ] jobs: sonar: name: SonarQube Analysis runs-on: ubuntu-latest permissions: write-all steps: - name: ⬇️ Checkout Code uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - name: Calculate Version id: calculate-version if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); // Função para calcular nova versão baseada no branch function calculateVersion(lastVersion, branchName) { const parseVersion = (v) => { const match = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-rc(\d+))?$/); if (!match) throw new Error(`Invalid version format: ${v}`); return { major: parseInt(match[1]), minor: parseInt(match[2]), patch: parseInt(match[3]), rc: match[4] ? parseInt(match[4]) : null }; }; const current = parseVersion(lastVersion); if (branchName.startsWith('release/')) { return `${current.major + 1}.0.0`; } else if (branchName.startsWith('feature/')) { return `${current.major}.${current.minor + 1}.0`; } else if (branchName.startsWith('fix/')) { return `${current.major}.${current.minor}.${current.patch + 1}`; } else if (branchName.startsWith('rc/')) { if (current.rc !== null) { return `${current.major}.${current.minor}.${current.patch}-rc${current.rc + 1}`; } else { return `${current.major}.${current.minor}.${current.patch}-rc1`; } } return null; // Não sugerir para outros tipos de branch } try { const branchName = context.payload.pull_request.head.ref; console.log(`Branch name: ${branchName}`); // Validar se o branch segue os padrões aceitos const validPrefixes = ['release/', 'feature/', 'fix/', 'rc/']; const isValidBranch = validPrefixes.some(prefix => branchName.startsWith(prefix)); if (!isValidBranch) { const errorMessage = `## 🚨 Erro: Nome do Branch Inválido\n\n` + `O branch \`${branchName}\` não segue os padrões de nomenclatura aceitos.\n\n` + `### 📝 Padrões Aceitos:\n` + `- \`release/*\`: Para releases de major version (ex: release/v2.0.0)\n` + `- \`feature/*\`: Para novas funcionalidades (ex: feature/nova-funcionalidade)\n` + `- \`fix/*\`: Para correções de bugs (ex: fix/correcao-bug)\n` + `- \`rc/*\`: Para release candidates (ex: rc/v1.2.0-rc1)\n\n` + `### 🔧 Como corrigir:\n` + `1. Renomeie o branch para seguir um dos padrões acima\n` + `2. Ou crie um novo branch com o nome correto\n`; const prNumber = context.issue.number; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: errorMessage }); core.setFailed(`Invalid branch name: ${branchName}. Must start with release/, feature/, fix/, or rc/`); return; } // Obter a última tag de release console.log('Fetching latest release tag...'); const { data: releases } = await github.rest.repos.listReleases({ owner: context.repo.owner, repo: context.repo.repo, per_page: 1 }); let lastReleaseVersion = null; let newVersion = null; if (releases.length > 0) { lastReleaseVersion = releases[0].tag_name.replace(/^v/, ''); console.log(`Latest release version: ${lastReleaseVersion}`); // Calcular a nova versão baseada na última release newVersion = calculateVersion(lastReleaseVersion, branchName); } else { console.log('No releases found, starting from 0.0.0'); lastReleaseVersion = 'N/A'; // Primeira release: começar com 0.0.0 independente do tipo de branch newVersion = '0.0.0'; } console.log(`Calculated version: ${newVersion}`); // Exportar a versão como output core.setOutput('version', newVersion); // Adicionar comentário informativo no PR const prNumber = context.issue.number; const infoMessage = `## ✅ Versão Calculada Automaticamente\n\n` + `**Branch:** \`${branchName}\`\n` + `**Última release:** \`${lastReleaseVersion}\`\n` + `**Nova versão:** \`${newVersion}\`\n\n` + `### 📝 Regras Aplicadas:\n` + `- \`release/*\`: Aumenta major, zera minor e patch (ex: 2.0.0)\n` + `- \`feature/*\`: Mantém major, aumenta minor, zera patch (ex: 1.2.0)\n` + `- \`fix/*\`: Mantém major e minor, aumenta patch (ex: 1.1.3)\n` + `- \`rc/*\`: Mantém versão base, aumenta RC (ex: 1.1.2-rc2)\n`; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: infoMessage }); } catch (error) { console.error('Error during version calculation:', error); const prNumber = context.issue.number; const errorMessage = `## 🚨 Erro no Cálculo de Versão\n\n` + `Ocorreu um erro durante o cálculo da versão:\n\n\`\`\`\n${error.message}\n\`\`\`\n\n` + `Por favor, verifique se o nome do branch está correto e tente novamente.`; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: errorMessage }); core.setFailed(`Version calculation error: ${error.message}`); } - name: Generate App Token id: generate-app-token uses: actions/create-github-app-token@v1 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} owner: 'Aignosi' repositories: 'sientia-dataops-library,sientia-mlops-library' - name: Prepare requirements.txt id: prepare-requirements run: | sed -e "s|git+ssh://git@github.com/|git+https://github.com/|g" \ -e "s|git@github.com:|git+https://github.com/|g" \ requirements.txt > requirements_prepared.txt echo "PROCESSED_REQUIREMENTS_FILE=requirements_prepared.txt" >> $GITHUB_OUTPUT - name: Configure Git to use App Token env: GH_APP_TOKEN: ${{ steps.generate-app-token.outputs.token }} run: | git config --global url."https://oauth2:${GH_APP_TOKEN}@github.com/".insteadOf "https://github.com/" - name: 🧹 Free Disk Space run: | echo "Disk space before cleanup:" df -h sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/local/lib/android sudo rm -rf /opt/ghc sudo rm -rf /opt/hostedtoolcache/CodeQL sudo docker image prune --all --force echo "Disk space after cleanup:" df -h - name: 🔧 Setup Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: 💾 Cache pip packages uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt', 'requirements-dev.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: 📦 Install Dependencies run: | python -m pip install --upgrade pip pip install -r ${{ steps.prepare-requirements.outputs.PROCESSED_REQUIREMENTS_FILE }} pip install -r requirements-dev.txt - name: 📝 Code Formatting Check (Ruff) run: | echo "Checking code formatting..." ruff format --check model_manager/ tests/ continue-on-error: false - name: 🔎 Code Linting (Ruff) run: | echo "Running linting checks..." ruff check model_manager/ tests/ continue-on-error: false - name: 🏷️ Type Checking (mypy) run: | echo "Running type checks..." mypy model_manager/ continue-on-error: true - name: 🔒 Security Analysis (Bandit) run: | echo "Running security analysis..." bandit -r model_manager/ -ll -q continue-on-error: true - name: 🧪 Run Tests with Pytest run: | pytest tests --junitxml=pytest.xml --cov=model_manager --cov-report=xml --cov-report=term - name: Run SonarQube Analysis uses: SonarSource/sonarqube-scan-action@v6 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} with: args: > -Dsonar.projectVersion=${{ steps.calculate-version.outputs.version || '0.0.0' }}