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..1c11db5 --- /dev/null +++ b/.github/workflows/add-pr-to-devops.yml @@ -0,0 +1,258 @@ +name: Add PR to DevOps Board + +on: + pull_request: + types: [opened, reopened, synchronize] + 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 = projects.find(p => { + const name = p.name.toLowerCase(); + // Try exact match with various dash characters (hyphen, en-dash, em-dash) + return name === 'dhwani - devops release & qc board' || + name === 'dhwani – devops release & qc board' || // en-dash (U+2013) + name === 'dhwani — devops release & qc board' || // em-dash (U+2014) + name === 'dhwani - devops release and qc board' || + name === 'dhwani – devops release and qc board' || + (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')) || + (name.includes('devops') && name.includes('release')) || + (name.includes('devops') && name.includes('qc')); + }); + + // If not found, try just "devops" + if (!devopsProject) { + devopsProject = projects.find(p => + p.name.toLowerCase().includes('devops') + ); + } + + 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..b53d3f4 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,60 @@ 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 { + // Try with just team slug first + 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 from ${devopsTeam}`); + } catch (teamError) { + console.log(`⚠️ Could not request review from team ${devopsTeam}:`, teamError.message); + console.log(` Error details:`, JSON.stringify(teamError, null, 2)); + // Try alternative: get team ID and use GraphQL + try { + const { data: teams } = await github.rest.teams.list({ + org: 'dhwani-ris', + }); + const devopsTeamData = teams.find(t => t.slug === 'dhwani-devops'); + if (devopsTeamData) { + console.log(` Found team ID: ${devopsTeamData.id}, trying alternative method...`); + } + } catch (e) { + console.log(` Could not fetch team info:`, e.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/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/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.**`;