Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!--
<!--
Thanks for contributing!
PLEASE:
- Read our contributing guidelines: https://github.com/gurock/trcli/blob/main/CONTRIBUTING.md
Expand All @@ -11,10 +11,10 @@ PLEASE:
How are we solving the problem?

### Changes
What changes where made?
What changes were made?

### Potential impacts
What could potentially be affected by the implemented changes?
What could potentially be affected by the implemented changes?

### Steps to test
Happy path to test implemented scenario
Expand Down
176 changes: 176 additions & 0 deletions .github/workflows/pr-checklist.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
name: PR Checklist Enforcement

on:
pull_request:
types: [opened, edited, synchronize, reopened]
pull_request_review:
types: [submitted]

permissions:
pull-requests: write
contents: read
checks: write

jobs:
check-pr-requirements:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Check PR checklist completion
id: checklist
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prBody = context.payload.pull_request.body || '';

// Count checked and unchecked items
const checkedItems = (prBody.match(/- \[x\]/gi) || []).length;
const uncheckedItems = (prBody.match(/- \[ \]/gi) || []).length;
const totalItems = checkedItems + uncheckedItems;

// Check specific required items
const hasIssueReference = /Issue being resolved.*https?:\/\//.test(prBody);
const hasSolutionDescription = /Solution description[\s\S]{20,}/.test(prBody);
const hasTestSteps = /Steps to test[\s\S]{20,}/.test(prBody);

// Check if PR Tasks are complete
const prTasksComplete = checkedItems === totalItems && totalItems > 0;

core.setOutput('checked_items', checkedItems);
core.setOutput('unchecked_items', uncheckedItems);
core.setOutput('total_items', totalItems);
core.setOutput('pr_tasks_complete', prTasksComplete);
core.setOutput('has_issue_reference', hasIssueReference);
core.setOutput('has_solution', hasSolutionDescription);
core.setOutput('has_test_steps', hasTestSteps);

return {
checkedItems,
uncheckedItems,
totalItems,
prTasksComplete,
hasIssueReference,
hasSolutionDescription,
hasTestSteps
};

- name: Check test coverage
id: coverage_check
continue-on-error: true
run: |
# This will be checked by the main build workflow
# We just verify that tests exist for this PR
echo "coverage_check=pending" >> $GITHUB_OUTPUT

- name: Create or update check run
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const checkedItems = parseInt('${{ steps.checklist.outputs.checked_items }}');
const uncheckedItems = parseInt('${{ steps.checklist.outputs.unchecked_items }}');
const totalItems = parseInt('${{ steps.checklist.outputs.total_items }}');
const prTasksComplete = '${{ steps.checklist.outputs.pr_tasks_complete }}' === 'true';
const hasIssueReference = '${{ steps.checklist.outputs.has_issue_reference }}' === 'true';
const hasSolution = '${{ steps.checklist.outputs.has_solution }}' === 'true';
const hasTestSteps = '${{ steps.checklist.outputs.has_test_steps }}' === 'true';

// Determine check status
let conclusion = 'success';
let summary = '✅ All PR requirements met!';

const issues = [];

if (!prTasksComplete && totalItems > 0) {
issues.push(`- ${uncheckedItems} checklist items remaining`);
}

if (!hasIssueReference) {
issues.push('- Missing issue reference link');
}

if (!hasSolution) {
issues.push('- Solution description needs more details (min 20 chars)');
}

if (!hasTestSteps) {
issues.push('- Test steps need more details (min 20 chars)');
}

if (issues.length > 0) {
conclusion = 'neutral';
summary = '⚠️ PR checklist incomplete:\n\n' + issues.join('\n');
}

// Create check run
const checkRun = await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'PR Checklist',
head_sha: context.payload.pull_request.head.sha,
status: 'completed',
conclusion: conclusion,
output: {
title: 'PR Requirements Check',
summary: summary,
text: `### Checklist Progress
- Checked items: ${checkedItems}
- Unchecked items: ${uncheckedItems}
- Total items: ${totalItems}

### Required Sections
- Issue Reference: ${hasIssueReference ? '✅' : '❌'}
- Solution Description: ${hasSolution ? '✅' : '❌'}
- Test Steps: ${hasTestSteps ? '✅' : '❌'}

${conclusion === 'neutral' ? '\n⚠️ **Note:** This is a soft requirement. You can still merge the PR, but completing these items improves code review quality.' : ''}
`
}
});

- name: Add label based on checklist status
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prTasksComplete = '${{ steps.checklist.outputs.pr_tasks_complete }}' === 'true';
const hasIssueReference = '${{ steps.checklist.outputs.has_issue_reference }}' === 'true';
const hasSolution = '${{ steps.checklist.outputs.has_solution }}' === 'true';
const hasTestSteps = '${{ steps.checklist.outputs.has_test_steps }}' === 'true';

if (prTasksComplete && hasIssueReference && hasSolution && hasTestSteps) {
// Remove incomplete label if exists
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'incomplete-pr'
});
} catch (e) {
// Label might not exist
}

// Add ready label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['ready-for-review']
});
} else {
// Remove ready label if exists
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'ready-for-review'
});
} catch (e) {
// Label might not exist
}
}
132 changes: 132 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: PR Validation

on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions:
pull-requests: write
contents: read
issues: write

jobs:
validate-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Check for Issue Reference
id: check_issue
run: |
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"

# Check if PR title or body contains issue reference
# Accepts: TRCLI-### (JIRA), GIT-### (GitHub), #123 (GitHub), issues/123
if echo "$PR_TITLE $PR_BODY" | grep -qE "TRCLI-[0-9]+|GIT-[0-9]+|#[0-9]+|issues/[0-9]+"; then
echo "issue_found=true" >> $GITHUB_OUTPUT

# Extract the issue key/number
if echo "$PR_TITLE $PR_BODY" | grep -qE "TRCLI-[0-9]+"; then
ISSUE_KEY=$(echo "$PR_TITLE $PR_BODY" | grep -oE "TRCLI-[0-9]+" | head -1)
echo "issue_key=$ISSUE_KEY" >> $GITHUB_OUTPUT
echo "issue_type=JIRA" >> $GITHUB_OUTPUT
elif echo "$PR_TITLE $PR_BODY" | grep -qE "GIT-[0-9]+"; then
ISSUE_KEY=$(echo "$PR_TITLE $PR_BODY" | grep -oE "GIT-[0-9]+" | head -1)
echo "issue_key=$ISSUE_KEY" >> $GITHUB_OUTPUT
echo "issue_type=GitHub" >> $GITHUB_OUTPUT
elif echo "$PR_TITLE $PR_BODY" | grep -qE "#[0-9]+"; then
ISSUE_KEY=$(echo "$PR_TITLE $PR_BODY" | grep -oE "#[0-9]+" | head -1)
echo "issue_key=$ISSUE_KEY" >> $GITHUB_OUTPUT
echo "issue_type=GitHub" >> $GITHUB_OUTPUT
elif echo "$PR_BODY" | grep -qE "issues/[0-9]+"; then
ISSUE_KEY=$(echo "$PR_BODY" | grep -oE "issues/[0-9]+" | head -1)
echo "issue_key=$ISSUE_KEY" >> $GITHUB_OUTPUT
echo "issue_type=GitHub" >> $GITHUB_OUTPUT
fi
else
echo "issue_found=false" >> $GITHUB_OUTPUT
fi

- name: Comment on PR if issue reference missing
if: steps.check_issue.outputs.issue_found == 'false'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## ⚠️ Missing Issue Reference

This PR does not reference an issue. Please include a reference in either:

**JIRA tickets:**
- PR title: "feat(api): TRCLI-123 Add new endpoint"
- PR description: "Resolves TRCLI-123"

**GitHub issues:**
- PR title: "feat(api): GIT-123 Add new endpoint"
- PR description: "Resolves GIT-123" or "Fixes #123"
- Or link to the GitHub issue

This helps with tracking and project management. Thank you!`
})

- name: Check PR Description Completeness
id: check_description
run: |
PR_BODY="${{ github.event.pull_request.body }}"

# Check for required sections
if echo "$PR_BODY" | grep -q "Issue being resolved"; then
echo "has_issue=true" >> $GITHUB_OUTPUT
else
echo "has_issue=false" >> $GITHUB_OUTPUT
fi

if echo "$PR_BODY" | grep -q "Solution description"; then
echo "has_solution=true" >> $GITHUB_OUTPUT
else
echo "has_solution=false" >> $GITHUB_OUTPUT
fi

if echo "$PR_BODY" | grep -q "Steps to test"; then
echo "has_test_steps=true" >> $GITHUB_OUTPUT
else
echo "has_test_steps=false" >> $GITHUB_OUTPUT
fi

- name: Generate PR Validation Summary
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issueFound = '${{ steps.check_issue.outputs.issue_found }}' === 'true';
const issueKey = '${{ steps.check_issue.outputs.issue_key }}';
const issueType = '${{ steps.check_issue.outputs.issue_type }}';
const hasIssue = '${{ steps.check_description.outputs.has_issue }}' === 'true';
const hasSolution = '${{ steps.check_description.outputs.has_solution }}' === 'true';
const hasTestSteps = '${{ steps.check_description.outputs.has_test_steps }}' === 'true';

const issueStatus = issueFound
? `✅ Found: ${issueKey} (${issueType})`
: '⚠️ Missing';

const summary = `## 📋 PR Validation Summary

| Check | Status |
|-------|--------|
| Issue Reference | ${issueStatus} |
| Issue Section | ${hasIssue ? '✅ Present' : '⚠️ Missing'} |
| Solution Description | ${hasSolution ? '✅ Present' : '⚠️ Missing'} |
| Test Steps | ${hasTestSteps ? '✅ Present' : '⚠️ Missing'} |

${issueFound && hasSolution && hasTestSteps ? '✅ All checks passed!' : '⚠️ Some optional sections are missing. Consider adding them for better review context.'}
`;

await core.summary
.addRaw(summary)
.write();
15 changes: 11 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Pre-commit hooks for code quality, security, and policy enforcement
# Install: pip install pre-commit
# Setup: pre-commit install && pre-commit install --hook-type commit-msg
# Run manually: pre-commit run --all-files

repos:
- repo: https://github.com/psf/black
rev: stable
# Code formatting
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
language_version: python3.8
- id: black
language_version: python3.10
args: [--line-length=120]
Loading