diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1c8c47a..378772f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,21 @@ -# Description +# Pull Request - - +## Summary + + ## Type of Change - -- [ ] Bug fix -- [ ] New feature -- [ ] Breaking change -- [ ] Documentation update -- [ ] Code quality improvement (refactoring, tests, performance) +- [ ] feat: New feature or enhancement +- [ ] fix: Bug fix +- [ ] breaking: Breaking change +- [ ] docs: Documentation only +- [ ] chore: Maintenance / tooling +- [ ] refactor: Refactoring without behavior change + +## Checklist + +- [ ] Tests added or updated +- [ ] `Invoke-Build -Task Test` passes locally +- [ ] Help updated if a public function was added or changed +- [ ] No secrets or environment-specific values introduced diff --git a/.github/workflows/pr-summary.yml b/.github/workflows/pr-summary.yml new file mode 100644 index 0000000..d3295f5 --- /dev/null +++ b/.github/workflows/pr-summary.yml @@ -0,0 +1,142 @@ +name: PR Summary +run-name: "${{ github.event.repository.name }} | PR Summary | #${{ github.event.pull_request.number }}" +permissions: read-all + +on: + pull_request: + types: [opened] + +jobs: + generate: + name: Generate PR Summary + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Pre-fill PR body + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const branchName = pr.head.ref; + + // Detect semantic type from branch prefix + const typeMap = { + 'feat': 'feat', 'feature': 'feat', 'add': 'feat', + 'fix': 'fix', 'bugfix': 'fix', 'hotfix': 'fix', 'patch': 'fix', + 'security': 'fix', 'sec': 'fix', + 'breaking': 'breaking', 'major': 'breaking', + 'docs': 'docs', 'doc': 'docs', + 'chore': 'chore', + 'refactor': 'refactor', 'refac': 'refactor', 'cleanup': 'refactor' + }; + const prefix = branchName.split('/')[0]; + const detectedType = typeMap[prefix] || null; + + // Get changed files + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number + }); + + // Classify each file by area (mirrors visual signal labels) + function classifyFile(filename) { + if (/^src\/.+\.Tests\.ps1$/.test(filename)) return 'tests'; + if (/^src\/.+\.(ps1|psd1)$/.test(filename)) return 'cmdlets'; + if (/^tests\/Integration\//.test(filename)) return 'tests'; + if (/^tests\/PSScriptAnalyzer\//.test(filename)) return 'lint'; + if (/^tests\/InjectionHunter\//.test(filename)) return 'security'; + if (/^\.github\/workflows\//.test(filename)) return 'ci'; + if (/^\.github\/actions\//.test(filename)) return 'ci'; + if (/^\.github\/DOCS_TEMPLATE\//.test(filename)) return 'template'; + if (/^(AGENTS\.md|\.github\/copilot-instructions\.md)$/.test(filename)) return 'agents'; + if (/^(docs\/|README\.md|CONTRIBUTING\.md)/.test(filename)) return 'docs'; + if (/\.build\.ps1$|^GitVersion\.yml$/.test(filename)) return 'build'; + if (/^\.vscode\//.test(filename)) return 'vscode'; + if (/^\.devcontainer\//.test(filename)) return 'devcontainer'; + if (/^(requirements\.psd1|\.github\/dependabot\.yml)$/.test(filename)) return 'dependencies'; + if (/^\.github\//.test(filename)) return 'github'; + return 'other'; + } + + const areaLabel = { + cmdlets: '๐Ÿ“ฆ Source', + tests: '๐Ÿงช Tests', + lint: '๐Ÿ” Lint', + security: '๐Ÿ”’ Security', + ci: 'โš™๏ธ CI/CD', + build: '๐Ÿ”จ Build', + docs: '๐Ÿ“š Docs', + agents: '๐Ÿค– Agents', + template: '๐Ÿ“‹ Template', + vscode: '๐Ÿ’ป VS Code', + devcontainer: '๐Ÿณ Dev Container', + github: '๐Ÿ™ GitHub', + dependencies: '๐Ÿ“ฆ Dependencies', + other: '๐Ÿ“„ Other' + }; + + // Group files by area + const grouped = {}; + for (const file of files) { + const area = classifyFile(file.filename); + if (!grouped[area]) grouped[area] = []; + grouped[area].push(file.filename); + } + + // Build changed areas table + const rows = Object.entries(grouped) + .map(([area, areaFiles]) => { + const label = areaLabel[area] || area; + const fileList = areaFiles.map(f => `\`${f}\``).join('
'); + return `| ${label} | ${fileList} |`; + }) + .join('\n'); + const changesTable = `| Area | Files |\n|------|-------|\n${rows}`; + + // Build type of change checklist โ€” auto-check detected type + const types = [ + ['feat', 'feat: New feature or enhancement'], + ['fix', 'fix: Bug fix'], + ['breaking', 'breaking: Breaking change'], + ['docs', 'docs: Documentation only'], + ['chore', 'chore: Maintenance / tooling'], + ['refactor', 'refactor: Refactoring without behavior change'] + ]; + const typeChecklist = types + .map(([key, label]) => `- [${detectedType === key ? 'x' : ' '}] ${label}`) + .join('\n'); + + // Compose final body + const body = [ + '## Summary', + '', + '> โœ๏ธ Auto-generated โ€” replace this with a description of what and why.', + '', + `**Branch:** \`${branchName}\``, + '', + '## Changed Areas', + '', + changesTable, + '', + '## Type of Change', + '', + typeChecklist, + '', + '## Checklist', + '', + '- [ ] Tests added or updated', + '- [ ] `Invoke-Build -Task Test` passes locally', + '- [ ] Help updated if a public function was added or changed', + '- [ ] No secrets or environment-specific values introduced' + ].join('\n'); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + body + });