diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..a33bba7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,14 @@ +# CODEOWNERS file +# This file defines code owners for the repository +# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# DevOps team owns all files +* @dhwani-ris/dhwani-devops + +# GitHub workflows - DevOps team +/.github/workflows/ @dhwani-ris/dhwani-devops + +# Configuration files - DevOps team +/.github/ @dhwani-ris/dhwani-devops +/.releaserc @dhwani-ris/dhwani-devops + diff --git a/.github/workflows/add-pr-to-devops.yml b/.github/workflows/add-pr-to-devops.yml new file mode 100644 index 0000000..325ba3f --- /dev/null +++ b/.github/workflows/add-pr-to-devops.yml @@ -0,0 +1,294 @@ +name: Add PR to DevOps Board + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + branches: [main, master, develop, development] + +jobs: + add_to_project: + runs-on: ubuntu-latest + if: | + github.event.pull_request.base.ref == 'main' || + github.event.pull_request.base.ref == 'master' || + github.event.pull_request.base.ref == 'develop' || + github.event.pull_request.base.ref == 'development' + permissions: + contents: read + pull-requests: write + repository-projects: write + organization-projects: write + issues: read + steps: + - name: Add PR to DevOps Release Board + uses: actions/github-script@v8 + # Note: GITHUB_TOKEN is automatically provided by GitHub Actions + # No need to add it as a secret - it's available in all workflows + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNodeId = context.payload.pull_request.node_id; + const prNumber = context.payload.pull_request.number; + const prUrl = context.payload.pull_request.html_url; + + console.log(`Processing PR #${prNumber} (Node ID: ${prNodeId})`); + + try { + // Try GraphQL API first (more reliable for projects) + let projects = []; + try { + const graphqlQuery = ` + query { + organization(login: "dhwani-ris") { + projectsV2(first: 50, states: OPEN) { + nodes { + id + title + number + } + } + } + } + `; + const graphqlResponse = await github.graphql(graphqlQuery); + if (graphqlResponse?.organization?.projectsV2?.nodes) { + projects = graphqlResponse.organization.projectsV2.nodes.map(p => ({ + id: p.id, + name: p.title, + number: p.number + })); + console.log(`Found ${projects.length} projects via GraphQL`); + } + } catch (graphqlError) { + console.log('GraphQL query failed, falling back to REST API:', graphqlError.message); + } + + // Fallback to REST API if GraphQL didn't work + if (projects.length === 0) { + const { data: restProjects } = await github.rest.projects.listForOrg({ + org: 'dhwani-ris', + state: 'open' + }); + projects = restProjects; + console.log(`Found ${projects.length} projects via REST API`); + } + + console.log(`Found ${projects.length} organization projects`); + + if (projects.length === 0) { + console.log('⚠️ No organization projects found. Check permissions.'); + return; + } + + // Log all project names for debugging + console.log('Available projects:', projects.map(p => `"${p.name}" (ID: ${p.id})`).join(', ')); + + // Find the DevOps Release & QC Board project - try exact match first + // Project name from UI: "Dhwani – DevOps Release & QC Board" (with en-dash) + let devopsProject = null; + + // Try multiple matching strategies + const searchTerms = [ + 'dhwani - devops release & qc board', + 'dhwani – devops release & qc board', // en-dash (U+2013) + 'dhwani — devops release & qc board', // em-dash (U+2014) + 'dhwani - devops release and qc board', + 'dhwani – devops release and qc board', + 'dhwani devops release qc board', + 'devops release qc board' + ]; + + for (const term of searchTerms) { + devopsProject = projects.find(p => + p.name.toLowerCase().trim() === term + ); + if (devopsProject) { + console.log(`✅ Found exact match: "${devopsProject.name}"`); + break; + } + } + + // If no exact match, try partial matches + if (!devopsProject) { + devopsProject = projects.find(p => { + const name = p.name.toLowerCase(); + return (name.includes('dhwani') && name.includes('devops') && name.includes('release') && name.includes('qc')) || + (name.includes('dhwani') && name.includes('devops') && name.includes('release') && name.includes('board')) || + (name.includes('devops') && name.includes('release') && name.includes('qc')) || + (name.includes('devops') && name.includes('release') && name.includes('board')); + }); + if (devopsProject) { + console.log(`✅ Found partial match: "${devopsProject.name}"`); + } + } + + // If still not found, try just "devops" and "release" + if (!devopsProject) { + devopsProject = projects.find(p => { + const name = p.name.toLowerCase(); + return (name.includes('devops') && name.includes('release')) || + (name.includes('devops') && name.includes('qc')); + }); + if (devopsProject) { + console.log(`✅ Found loose match: "${devopsProject.name}"`); + } + } + + // Last resort: any project with "devops" + if (!devopsProject) { + devopsProject = projects.find(p => + p.name.toLowerCase().includes('devops') + ); + if (devopsProject) { + console.log(`✅ Found devops project: "${devopsProject.name}"`); + } + } + + if (!devopsProject) { + console.log('❌ DevOps Release & QC Board project not found'); + console.log('Available project names:', projects.map(p => `"${p.name}"`).join(', ')); + console.log('Searched for: "Dhwani – DevOps Release & QC Board" (with en-dash)'); + return; + } + + console.log(`✅ Found project: "${devopsProject.name}" (ID: ${devopsProject.id})`); + + // Check if this is a Projects V2 (new) or Projects V1 (old) + const isV2Project = devopsProject.id && typeof devopsProject.id === 'string' && devopsProject.id.startsWith('PVT_'); + + if (isV2Project) { + // Use GraphQL for Projects V2 + console.log('Using Projects V2 (GraphQL)'); + + // Get project fields/columns + const projectQuery = ` + query($projectId: ID!) { + node(id: $projectId) { + ... on ProjectV2 { + id + title + fields(first: 20) { + nodes { + ... on ProjectV2Field { + id + name + } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + views(first: 10) { + nodes { + id + name + } + } + } + } + } + `; + + const projectData = await github.graphql(projectQuery, { + projectId: devopsProject.id + }); + + // Add item to project using GraphQL + const addItemMutation = ` + mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: { + projectId: $projectId + contentId: $contentId + }) { + item { + id + } + } + } + `; + + try { + await github.graphql(addItemMutation, { + projectId: devopsProject.id, + contentId: prNodeId + }); + console.log(`✅ Successfully added PR #${prNumber} to DevOps Release & QC Board (Projects V2)`); + console.log(` Project: "${devopsProject.name}"`); + console.log(` PR URL: ${prUrl}`); + } catch (addError) { + if (addError.message && addError.message.includes('already exists')) { + console.log(`✅ PR #${prNumber} is already in the project`); + } else { + throw addError; + } + } + } else { + // Use REST API for Projects V1 + console.log('Using Projects V1 (REST API)'); + + // Get project columns + const { data: columns } = await github.rest.projects.listColumns({ + project_id: devopsProject.id + }); + + if (columns.length === 0) { + console.log('❌ No columns found in project'); + return; + } + + console.log(`Found ${columns.length} columns:`, columns.map(c => `"${c.name}"`).join(', ')); + + // Find the first column (usually "To do", "In progress", or "Backlog") + const firstColumn = columns[0]; + console.log(`Adding PR to column: "${firstColumn.name}"`); + + // Check if PR is already in the project (check all columns) + let alreadyAdded = false; + for (const column of columns) { + const { data: cards } = await github.rest.projects.listCards({ + column_id: column.id + }); + alreadyAdded = cards.some(card => + card.content_url && card.content_url.includes(`/pulls/${prNumber}`) + ); + if (alreadyAdded) { + console.log(`✅ PR #${prNumber} is already in the project (column: ${column.name})`); + break; + } + } + + if (!alreadyAdded) { + // Add PR to the first column + await github.rest.projects.createCard({ + column_id: firstColumn.id, + content_id: prNodeId, + content_type: 'PullRequest' + }); + + console.log(`✅ Successfully added PR #${prNumber} to DevOps Release & QC Board`); + console.log(` Project: "${devopsProject.name}"`); + console.log(` Column: "${firstColumn.name}"`); + console.log(` PR URL: ${prUrl}`); + } + } + } catch (error) { + console.log('❌ Error adding PR to project:', error.message); + console.log('Error details:', JSON.stringify(error, null, 2)); + + if (error.status === 403) { + console.log('⚠️ Permission denied (403). Check repository permissions.'); + } else if (error.status === 404) { + console.log('⚠️ Not found (404). Check project name.'); + } else if (error.status === 401) { + console.log('⚠️ Unauthorized (401). Check authentication.'); + } + + // Don't fail the workflow, just log the error + console.log('⚠️ Continuing workflow despite error...'); + } + diff --git a/.github/workflows/auto-reviewer.yml b/.github/workflows/auto-reviewer.yml index 5ba1e01..7eea6e4 100644 --- a/.github/workflows/auto-reviewer.yml +++ b/.github/workflows/auto-reviewer.yml @@ -2,7 +2,8 @@ name: Auto Request Review on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review, closed] + branches: [main, master, develop, development] permissions: pull-requests: write @@ -13,8 +14,7 @@ jobs: name: Request Review from Default Reviewer runs-on: ubuntu-latest if: | - github.event.pull_request.base.ref == 'main' || - github.event.pull_request.base.ref == 'master' + (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review') steps: - name: Request review from default reviewer @@ -46,27 +46,81 @@ jobs: console.log('Current reviewers:', JSON.stringify(currentReviews, null, 2)); - // Check if reviewer is already requested - const isAlreadyRequested = currentReviews.users?.some( + // Check if DevOps team is already requested + const devopsTeam = 'dhwani-ris/dhwani-devops'; + const isTeamRequested = currentReviews.teams?.some( + team => `${team.organization.login}/${team.slug}` === devopsTeam + ); + + // Check if individual reviewer is already requested + const isUserRequested = currentReviews.users?.some( user => user.login.toLowerCase() === reviewer.toLowerCase() - ) || currentReviews.teams?.some( - team => team.slug.toLowerCase() === reviewer.toLowerCase() ); - if (isAlreadyRequested) { - console.log(`✅ Review already requested from ${reviewer}`); - return; + // Request review from DevOps team (from CODEOWNERS) + if (!isTeamRequested) { + try { + // First, verify team exists + const { data: teams } = await github.rest.teams.list({ + org: 'dhwani-ris', + }); + console.log(`Available teams:`, teams.map(t => t.slug).join(', ')); + + const devopsTeamData = teams.find(t => + t.slug === 'dhwani-devops' || + t.slug === 'devops' || + t.name.toLowerCase().includes('devops') + ); + + if (!devopsTeamData) { + console.log(`⚠️ Team "dhwani-devops" not found in organization`); + console.log(` Available teams:`, teams.map(t => `${t.slug} (${t.name})`).join(', ')); + } else { + console.log(`✅ Found team: ${devopsTeamData.slug} (ID: ${devopsTeamData.id})`); + + // Request review using team slug + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + team_reviewers: [devopsTeamData.slug], + }); + console.log(`✅ Successfully requested review from ${devopsTeam}`); + } + } catch (teamError) { + console.log(`⚠️ Could not request review from team ${devopsTeam}:`, teamError.message); + console.log(` Error status:`, teamError.status); + console.log(` Error details:`, JSON.stringify(teamError, null, 2)); + + // Try with just the slug as fallback + try { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + team_reviewers: ['dhwani-devops'], + }); + console.log(`✅ Successfully requested review using fallback method`); + } catch (fallbackError) { + console.log(`❌ Fallback also failed:`, fallbackError.message); + } + } + } else { + console.log(`✅ Review already requested from ${devopsTeam}`); } // Request review from default reviewer - await github.rest.pulls.requestReviewers({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pr.number, - reviewers: [reviewer], - }); - - console.log(`✅ Successfully requested review from ${reviewer}`); + if (!isUserRequested) { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + reviewers: [reviewer], + }); + console.log(`✅ Successfully requested review from ${reviewer}`); + } else { + console.log(`✅ Review already requested from ${reviewer}`); + } } catch (error) { console.error(`❌ Error requesting review from ${reviewer}:`, error); console.error('Error details:', JSON.stringify(error, null, 2)); diff --git a/.github/workflows/bot-handler.yml b/.github/workflows/bot-handler.yml index 8b0476a..050c0ed 100644 --- a/.github/workflows/bot-handler.yml +++ b/.github/workflows/bot-handler.yml @@ -4,7 +4,8 @@ on: issue_comment: types: [created, edited] pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened, closed] + branches: [master] permissions: contents: write diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12775b6..7cc0890 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,9 +30,10 @@ jobs: - name: Cache pip uses: actions/cache@v4 + continue-on-error: true with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') || 'no-deps' }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- @@ -58,7 +59,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: pip + # Cache disabled - no requirements.txt or pyproject.toml found + # cache: pip - name: Install dependencies run: | @@ -101,149 +103,8 @@ jobs: run: | echo "Codecov upload completed. Check https://codecov.io/gh/${{ github.repository }} for coverage reports." - frappe-bench-test: - name: 'Frappe Bench App Tests' - runs-on: ubuntu-latest - if: hashFiles('**/__init__.py') != '' || hashFiles('**/hooks.py') != '' - - services: - mariadb: - image: mariadb:10.11 - env: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: test_frappe - ports: - - 3306:3306 - options: >- - --health-cmd="mysqladmin ping" - --health-interval=10s - --health-timeout=5s - --health-retries=3 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 18 - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - mariadb-client \ - redis-server \ - curl \ - git \ - wget \ - xvfb \ - libfontconfig1 \ - libfreetype6 \ - libxrender1 \ - libjpeg-turbo8 \ - xfonts-75dpi \ - xfonts-base - - - name: Install Frappe Bench CLI - run: | - pip install frappe-bench - - - name: Initialize Frappe Bench - run: | - bench init --skip-redis-config-check --skip-assets --frappe-branch version-15 frappe-bench - cd frappe-bench - - - name: Detect app name - id: app-name - working-directory: frappe-bench - run: | - # Try to detect app name from current directory structure - if [ -d "../apps" ]; then - APP_NAME=$(ls -d ../apps/*/ 2>/dev/null | head -1 | xargs basename) - elif [ -d "apps" ]; then - APP_NAME=$(ls -d apps/*/ 2>/dev/null | head -1 | xargs basename) - else - # Check if current repo is an app - if [ -f "../__init__.py" ] || [ -f "../hooks.py" ]; then - APP_NAME=$(basename $(cd .. && pwd)) - else - # Default: use repo name - APP_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) - fi - fi - - if [ -z "$APP_NAME" ] || [ "$APP_NAME" = "." ]; then - APP_NAME="gatepass_check" - fi - - echo "name=$APP_NAME" >> $GITHUB_OUTPUT - echo "Detected app name: $APP_NAME" - - - name: Get app into bench - working-directory: frappe-bench - run: | - APP_NAME="${{ steps.app-name.outputs.name }}" - # If app is in parent directory, create symlink or copy - if [ -d "../$APP_NAME" ] && [ -f "../$APP_NAME/__init__.py" ]; then - bench get-app $APP_NAME ../$APP_NAME - elif [ -d "../apps/$APP_NAME" ]; then - bench get-app $APP_NAME ../apps/$APP_NAME - else - # Clone or use current repo as app - bench get-app $APP_NAME ${{ github.repository_url }} || \ - bench get-app $APP_NAME file://$(cd .. && pwd) || \ - echo "App $APP_NAME not found, skipping app-specific tests" - fi - - - name: Create test site - working-directory: frappe-bench - env: - DB_HOST: 127.0.0.1 - DB_PORT: 3306 - DB_ROOT_USER: root - DB_ROOT_PASSWORD: root - run: | - bench new-site test_site \ - --db-type mariadb \ - --admin-password admin \ - --no-mariadb-socket \ - --mariadb-host 127.0.0.1 \ - --mariadb-port 3306 \ - --mariadb-root-password root \ - --install-app ${{ steps.app-name.outputs.name }} || \ - echo "Site creation or app installation failed, will try to run tests anyway" - - - name: Run app-specific tests - working-directory: frappe-bench - env: - DB_HOST: 127.0.0.1 - DB_PORT: 3306 - run: | - APP_NAME="${{ steps.app-name.outputs.name }}" - echo "Running tests for app: $APP_NAME" - - # Try to run app-specific tests - bench --site test_site run-tests --app $APP_NAME || \ - bench --site test_site run-tests --app $APP_NAME --coverage || \ - echo "No tests found for app $APP_NAME or test execution failed" - continue-on-error: true - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: frappe-test-results - path: | - frappe-bench/sites/test_site/logs/*.log - if-no-files-found: ignore + # Frappe Bench tests are now in a separate workflow: frappe-bench-tests.yml + # This keeps the CI workflow focused and allows bench tests to be optional security: name: 'Security Check' diff --git a/.github/workflows/devops-checklist.yml b/.github/workflows/devops-checklist.yml index 1bc8956..ba7ea48 100644 --- a/.github/workflows/devops-checklist.yml +++ b/.github/workflows/devops-checklist.yml @@ -137,22 +137,22 @@ jobs: const fixes = commits.filter(c => c.message.startsWith('fix')).map(c => c.message.replace(/^fix(\(.+?\))?:\s*/i, '')); const other = commits.filter(c => !c.message.startsWith('feat') && !c.message.startsWith('fix') && !c.message.startsWith('chore') && !c.message.startsWith('ci')); - // Build feature details + // Build feature details (without numbering - will be added in formatFeatureDetails) let featureDetails = []; if (features.length > 0) { - featureDetails.push(...features.map(f => `1) ${f}`)); + featureDetails.push(...features); } if (fixes.length > 0) { - featureDetails.push(...fixes.map(f => `2) ${f}`)); + featureDetails.push(...fixes); } if (other.length > 0) { - featureDetails.push(...other.slice(0, 5).map((o, i) => `${i + 3}) ${o.message}`)); + featureDetails.push(...other.slice(0, 5).map(o => o.message)); } const today = new Date().toISOString().split('T')[0]; const releaseDate = today.split('-').reverse().join('-'); // Format: DD-MM-YYYY - // Format feature details better + // Format feature details with sequential numbering const formatFeatureDetails = (details) => { if (details.length === 0) return 'See commits above'; return details.map((f, i) => `${i + 1}) ${f}`).join('
'); @@ -181,87 +181,6 @@ jobs: |-------|-----------------|----------------|-----------------| | 1. | \`${context.repo.repo}\` | \`${pr.base.ref}-release-${version}\` | ${formatFeatureDetails(featureDetails)} | - **Dependencies:** - - Dependencies updated: \`TBD\` *(Please review and update)* - \`\`\` - - \`\`\` - - **Database Changes (Queries to run):** - - Database changes required: \`TBD\` *(Please review and update)* - \`\`\` - - \`\`\` - - **Testing:** - - [ ] Unit tests passed - - [ ] Integration tests passed - - [ ] E2E tests passed - - [ ] Manual testing completed - \`\`\` - - \`\`\` - - **Known Issues:** - - Known issues: \`TBD\` *(Please review and update)* - \`\`\` - - \`\`\` - - **Contact Information:** - - Support Team Email: \`\`\`\`\`\` - - Support Team Phone: \`\`\`\`\`\` - - **Attachments:** - - Deployment files attached/committed: \`TBD\` *(Please review and update)* - \`\`\` - - \`\`\` - - --- - - ### For DevOps Team Use Only - *(To be filled by the DevOps team after deploying the release)* - - **Deployment Details:** - - Date and time of deployment: \`\`\`\`\`\` - - Deployed by: \`\`\`\`\`\` - - Deployment Status: \`\`\`\`\`\` - - **Deployment Instructions:** - - [ ] Pre-deployment tasks completed (backups, etc.) - - [ ] Production environment accessed securely - - [ ] Latest release pulled from version control - - [ ] Dependencies installed/updated - - [ ] Database migrations run (if applicable) - - [ ] Application services restarted - - [ ] Deployment monitored and verified - - **Rollback Plan:** - - [ ] Rollback procedure documented - - [ ] Previous version tag identified: \`\`\`\`\`\` - - [ ] Database rollback scripts prepared (if applicable) - - [ ] Rollback tested in staging environment - - **Post-Deployment Checklist:** - - [ ] Service availability and response times verified - - [ ] System resources monitored - - [ ] Critical user scenarios tested - - [ ] Data integrity confirmed - - [ ] Error logs reviewed - - [ ] Security scans completed - - [ ] Server and infrastructure health checked - - [ ] Backup and disaster recovery procedures validated - - **Notes:** - \`\`\` - - \`\`\` - - **Acknowledgment:** - - [ ] Deployment acknowledged and system ready for production use - - --- **Note:** This deployment document was **automatically generated** from PR commits and information. Please review and update the TBD sections before merging.`; // Check if comment already exists @@ -320,35 +239,6 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const pr = context.payload.pull_request; - const checklist = `## 🔧 DevOps Checklist - Workflow Review - - **Please review all workflows and checks before merging:** - - ### Workflow Status Review - - [ ] All CI/CD workflows are passing - - [ ] Quality Checks workflow passed - - [ ] Security Scan workflow passed - - [ ] Code quality checks passed - - [ ] Test coverage meets requirements - - ### Review Status - - [ ] All required reviewers have approved - - [ ] Code review completed - - [ ] Security review completed (if applicable) - - ### Pre-Merge Verification - - [ ] Deployment Notes document reviewed (see Deployment Notes comment above) - - [ ] All commits reviewed - - [ ] Breaking changes identified (if any) - - [ ] Version number verified (if applicable) - - ### Final Checks - - [ ] No blocking issues or errors - - [ ] Ready for production deployment - - [ ] Rollback plan understood (if high-risk) - - --- - **Note:** This checklist is for DevOps team to verify all workflows and checks before merging.`; // Check if comment already exists const comments = await github.rest.issues.listComments({ @@ -363,22 +253,44 @@ jobs: ); if (existingComment) { - // Update existing comment - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body: checklist - }); - console.log('Updated existing DevOps Checklist comment'); - } else { - // Create new comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: checklist - }); - console.log('Created new DevOps Checklist comment'); + // Check if already submitted (check both visible marker and hidden marker) + if (existingComment.body.includes('✅ **CHECKLIST SUBMITTED**') || + existingComment.body.includes('CHECKLIST_SUBMITTED_LOCK')) { + console.log('Checklist already submitted and locked, cannot update'); + return; + } + // Don't update existing comment to preserve checkbox states + console.log('DevOps Checklist comment already exists, preserving user checkboxes'); + return; } + + // Only create new comment if it doesn't exist + const checklist = `## 🔧 DevOps Checklist + + 👋 **DevOps Team:** Please review and check the items below. + + --- + + ### ✅ Pre-Merge Verification + - [ ] All CI/CD workflows passing (check Actions tab) + - [ ] Code quality checks passed (Semgrep, Pre-commit) + - [ ] Security scans passed (no vulnerabilities) + - [ ] No secrets or credentials exposed (manual review) + + ### 📝 Documentation + - [ ] Deployment notes reviewed (see comment above) + - [ ] 🔗 [Rollback Guidelines](https://dhwaniris1-my.sharepoint.com/:b:/g/personal/technology_dhwaniris_com/IQBZ-x3H8jIjQoliD_JEKHfSAQq2pMXdy8wFAdISg7fInTE?e=gd7dsq) reviewed + + --- + + 💡 *Click the checkboxes above to mark items as complete.*`; + + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: checklist + }); + console.log('Created new DevOps Checklist comment'); diff --git a/.github/workflows/frappe-bench-tests.yml b/.github/workflows/frappe-bench-tests.yml new file mode 100644 index 0000000..f2c8929 --- /dev/null +++ b/.github/workflows/frappe-bench-tests.yml @@ -0,0 +1,261 @@ +name: Frappe Bench Unit Tests + +on: + pull_request: + branches: [ main, develop, development, master ] + push: + branches: [ main, develop, development, master ] + workflow_dispatch: + +permissions: + contents: read + +# This workflow is optional and won't block merges +concurrency: + group: frappe-bench-tests-${{ github.event_name }}-${{ github.ref }}-${{ github.event.number || github.sha }} + cancel-in-progress: false + +jobs: + frappe-bench-test: + name: 'Frappe Bench App Tests (Optional)' + runs-on: ubuntu-latest + # This job is optional and won't block merges + continue-on-error: true + if: hashFiles('**/__init__.py') != '' || hashFiles('**/hooks.py') != '' + + services: + mariadb: + image: mariadb:10.11 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test_frappe + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + mariadb-client \ + redis-server \ + curl \ + git \ + wget \ + xvfb \ + libfontconfig1 \ + libfreetype6 \ + libxrender1 \ + libjpeg-turbo8 \ + xfonts-75dpi \ + xfonts-base + + - name: Install Frappe Bench CLI + run: | + pip install frappe-bench + + - name: Initialize Frappe Bench + continue-on-error: true + run: | + echo "🚀 Initializing Frappe Bench environment..." + bench init --skip-redis-config-check --skip-assets --frappe-branch version-15 frappe-bench || { + echo "⚠️ Bench initialization failed, but continuing..." + exit 0 + } + cd frappe-bench + echo "✅ Bench initialized successfully" + + - name: Detect app name + id: app-name + working-directory: frappe-bench + run: | + # Try to detect app name from current directory structure + if [ -d "../apps" ]; then + APP_NAME=$(ls -d ../apps/*/ 2>/dev/null | head -1 | xargs basename) + elif [ -d "apps" ]; then + APP_NAME=$(ls -d apps/*/ 2>/dev/null | head -1 | xargs basename) + else + # Check if current repo is an app + if [ -f "../__init__.py" ] || [ -f "../hooks.py" ]; then + APP_NAME=$(basename $(cd .. && pwd)) + else + # Default: use repo name + APP_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) + fi + fi + + if [ -z "$APP_NAME" ] || [ "$APP_NAME" = "." ]; then + APP_NAME="gatepass_check" + fi + + echo "name=$APP_NAME" >> $GITHUB_OUTPUT + echo "Detected app name: $APP_NAME" + + - name: Get app into bench + working-directory: frappe-bench + continue-on-error: true + run: | + APP_NAME="${{ steps.app-name.outputs.name }}" + echo "📦 Installing app: $APP_NAME into bench..." + + # If app is in parent directory, create symlink or copy + if [ -d "../$APP_NAME" ] && [ -f "../$APP_NAME/__init__.py" ]; then + bench get-app $APP_NAME ../$APP_NAME || echo "⚠️ Failed to get app from ../$APP_NAME" + elif [ -d "../apps/$APP_NAME" ]; then + bench get-app $APP_NAME ../apps/$APP_NAME || echo "⚠️ Failed to get app from ../apps/$APP_NAME" + else + # Clone or use current repo as app + bench get-app $APP_NAME ${{ github.repository_url }} || \ + bench get-app $APP_NAME file://$(cd .. && pwd) || \ + echo "⚠️ App $APP_NAME not found, will try to continue anyway" + fi + + # Verify app is installed + if bench list-apps | grep -q "$APP_NAME"; then + echo "✅ App $APP_NAME successfully installed in bench" + else + echo "⚠️ App $APP_NAME not found in bench, but continuing..." + fi + + - name: Create test site + working-directory: frappe-bench + continue-on-error: true + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_ROOT_USER: root + DB_ROOT_PASSWORD: root + run: | + APP_NAME="${{ steps.app-name.outputs.name }}" + echo "🏗️ Creating test site with app: $APP_NAME..." + + # Wait for MariaDB to be ready + echo "Waiting for MariaDB to be ready..." + for i in {1..30}; do + if mysql -h 127.0.0.1 -P 3306 -u root -proot -e "SELECT 1" &>/dev/null; then + echo "✅ MariaDB is ready" + break + fi + echo "Waiting for MariaDB... ($i/30)" + sleep 2 + done + + # Create site + bench new-site test_site \ + --db-type mariadb \ + --admin-password admin \ + --no-mariadb-socket \ + --mariadb-host 127.0.0.1 \ + --mariadb-port 3306 \ + --mariadb-root-password root || { + echo "⚠️ Site creation failed, but continuing..." + exit 0 + } + + # Install app if site was created + if bench --site test_site list-apps &>/dev/null; then + echo "📦 Installing app $APP_NAME in test site..." + bench --site test_site install-app $APP_NAME || { + echo "⚠️ App installation failed, but continuing..." + } + echo "✅ Test site created and app installed" + else + echo "⚠️ Site creation may have failed, but continuing..." + fi + + - name: Run app-specific tests + working-directory: frappe-bench + continue-on-error: true + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + run: | + APP_NAME="${{ steps.app-name.outputs.name }}" + echo "🧪 Running unit tests for app: $APP_NAME" + + # Check if site exists + if ! bench --site test_site list-apps &>/dev/null; then + echo "⚠️ Test site not found, skipping tests" + exit 0 + fi + + # Check if app is installed + if ! bench --site test_site list-apps | grep -q "$APP_NAME"; then + echo "⚠️ App $APP_NAME not installed in test site, skipping tests" + exit 0 + fi + + # Try to run app-specific tests + echo "Running tests with bench run-tests..." + if bench --site test_site run-tests --app $APP_NAME; then + echo "✅ Tests passed for app $APP_NAME" + elif bench --site test_site run-tests --app $APP_NAME --coverage; then + echo "✅ Tests passed with coverage for app $APP_NAME" + else + echo "⚠️ No tests found for app $APP_NAME or test execution failed" + echo "This is optional and won't block the merge" + exit 0 + fi + + - name: Upload test results and logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: frappe-bench-test-results + path: | + frappe-bench/sites/test_site/logs/*.log + frappe-bench/sites/test_site/logs/*.txt + if-no-files-found: ignore + retention-days: 7 + + - name: Test Summary + if: always() + run: | + echo "## 📊 Frappe Bench Test Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This test job is **optional** and will not block merges." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -d "frappe-bench/sites/test_site" ]; then + echo "✅ Test site was created" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Test site was not created" >> $GITHUB_STEP_SUMMARY + fi + + APP_NAME="${{ steps.app-name.outputs.name }}" + if [ -d "frappe-bench/apps/$APP_NAME" ]; then + echo "✅ App $APP_NAME was installed in bench" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ App $APP_NAME was not found in bench" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "Check the workflow logs for detailed test results." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Test Environment" >> $GITHUB_STEP_SUMMARY + echo "- **Frappe Version:** version-15" >> $GITHUB_STEP_SUMMARY + echo "- **Python Version:** 3.11" >> $GITHUB_STEP_SUMMARY + echo "- **Node.js Version:** 18" >> $GITHUB_STEP_SUMMARY + echo "- **Database:** MariaDB 10.11" >> $GITHUB_STEP_SUMMARY + diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 491df7b..d7822ff 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -82,6 +82,26 @@ jobs: with: python-version: '3.13' + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run OWASP Dependency-Check + uses: dependency-check/Dependency-Check_Action@main + id: depcheck + with: + project: 'gatepass-check' + path: '.' + format: 'JSON' + args: > + --enableRetired + --enableExperimental + --failOnCVSS 7 + --out reports + token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + - name: Run pip-audit run: | pip install pip-audit @@ -102,6 +122,7 @@ jobs: with: name: security-reports-${{ github.run_id }} path: | + reports/dependency-check-report.json pip-audit-report.json safety-report.json bandit-report.json @@ -119,6 +140,31 @@ jobs: let comment = '## 🔒 Security Scan Results\n\n'; let hasIssues = false; + // Check OWASP Dependency-Check + try { + if (fs.existsSync('reports/dependency-check-report.json')) { + const report = JSON.parse(fs.readFileSync('reports/dependency-check-report.json', 'utf8')); + if (report.dependencies && report.dependencies.length > 0) { + const vulnerableDeps = report.dependencies.filter(dep => + dep.vulnerabilities && dep.vulnerabilities.length > 0 + ); + if (vulnerableDeps.length > 0) { + hasIssues = true; + comment += `### ⚠️ OWASP Dependency-Check Findings\n\n`; + comment += `Found ${vulnerableDeps.length} vulnerable dependency(ies) with ${vulnerableDeps.reduce((sum, d) => sum + d.vulnerabilities.length, 0)} total vulnerability(ies).\n\n`; + vulnerableDeps.slice(0, 10).forEach(dep => { + const cves = dep.vulnerabilities.map(v => v.name).join(', '); + const severity = dep.vulnerabilities.map(v => v.severity || 'UNKNOWN').join(', '); + comment += `- **${dep.fileName || dep.packagePath}**: ${cves} (Severity: ${severity})\n`; + }); + comment += `\n`; + } + } + } + } catch (e) { + console.log('Error reading OWASP Dependency-Check report:', e); + } + // Check pip-audit try { if (fs.existsSync('pip-audit-report.json')) { @@ -208,7 +254,7 @@ jobs: security-summary: name: Security Summary runs-on: ubuntu-latest - needs: [codeql-analysis, secret-scanning, dependency-scanning] + needs: [codeql-analysis, secret-scanning, dependency-scanning, container-scanning] if: always() steps: @@ -224,7 +270,9 @@ jobs: |-----------|--------| | CodeQL Analysis | ${needs.codeql-analysis.result === 'success' ? '✅ Passed' : needs.codeql-analysis.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | | Secret Scanning | ${needs.secret-scanning.result === 'success' ? '✅ Passed' : needs.secret-scanning.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | - | Dependency Scan | ${needs.dependency-scanning.result === 'success' ? '✅ Passed' : needs.dependency-scanning.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | + | OWASP Dependency-Check | ${needs.dependency-scanning.result === 'success' ? '✅ Passed' : needs.dependency-scanning.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | + | Dependency Scan (pip-audit/safety) | ${needs.dependency-scanning.result === 'success' ? '✅ Passed' : needs.dependency-scanning.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | + | Container Scan (Trivy) | ${needs.container-scanning && needs.container-scanning.result === 'success' ? '✅ Passed' : needs.container-scanning && needs.container-scanning.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | **View detailed results in the Security tab or workflow artifacts.**`;