diff --git a/.github/scripts/validate-pr-title.js b/.github/scripts/validate-pr-title.js new file mode 100644 index 0000000..245ddb5 --- /dev/null +++ b/.github/scripts/validate-pr-title.js @@ -0,0 +1,66 @@ +const nlp = require('compromise') +const title = process.env.PR_TITLE || '' + +let isValidTitle = true + +function logSuccess(message) { + console.log(`✅ ${message}`) +} + +function logFailure(message) { + isValidTitle = false + console.error(`❌ ${message}`) +} + +function capitalized(string) { + if (!string) return ''; + return string[0].toUpperCase() + string.substring(1); +} + +// Rule 1: PR title must not be empty +if (title) { + logSuccess(`PR title is not empty`) +} else { + logFailure(`PR title must not be empty`) +} + +// Rule 2: PR title must be 72 characters or less +if (title.length <= 72) { + logSuccess(`PR title is ${title.length} characters`) +} else { + logFailure(`PR title must be 72 characters or less (currently ${title.length} characters)`) +} + +// Rule 3: PR title must begin with a capital letter +if (/^[A-Z]/.test(title)) { + logSuccess(`PR title begins with a capital letter`) +} else { + logFailure('PR title must begin with a capital letter') +} + +// Rule 4: PR title must end with a letter or number +if (/[A-Za-z0-9]$/.test(title)) { + logSuccess(`PR title ends with a letter or number`) +} else { + logFailure('PR title must end with a letter or number') +} + +// Rule 5: PR title must be written in the imperative +const firstWord = title.split(' ')[0] +const firstWordLowercased = firstWord.toLowerCase() +const firstWordCapitalized = capitalized(firstWord) +const firstWordAsImperativeVerb = nlp(firstWord).verbs().toInfinitive().out('text') +const firstWordAsImperativeVerbLowercased = firstWordAsImperativeVerb.toLowerCase() +const firstWordAsImperativeVerbCapitalized = capitalized(firstWordAsImperativeVerb) + +if (firstWordLowercased === firstWordAsImperativeVerbLowercased) { + logSuccess(`PR title is written in the imperative`) +} else if (firstWordAsImperativeVerb) { + logFailure(`PR title must be written in the imperative ("${firstWordAsImperativeVerbCapitalized}" instead of "${firstWordCapitalized}")`) +} else { + logFailure(`PR title must begin with a verb and be written in the imperative`) +} + +if (!isValidTitle) { + process.exit(1) +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76b0792..ee19975 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: branches: - main pull_request: + types: [opened, reopened, synchronize] branches: - '*' diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml new file mode 100644 index 0000000..161bf97 --- /dev/null +++ b/.github/workflows/pr-labels.yml @@ -0,0 +1,44 @@ +name: PR Labels + +on: + pull_request: + types: [opened, labeled, unlabeled, reopened, synchronize] + +permissions: + contents: read + pull-requests: read + +jobs: + check-for-required-labels: + name: Check For Required Labels + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Validate PR has required labels + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=${{ github.event.pull_request.number }} + REPO=${{ github.repository }} + + REQUIRED_LABELS=("bug" "ci/cd" "documentation" "enhancement" "formatting" "refactoring" "testing") + LABELS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name') + + echo "PR labels:" + echo "$LABELS" + + for required in "${REQUIRED_LABELS[@]}"; do + if echo "$LABELS" | grep -q "^$required$"; then + echo "✅ Found required label: $required" + exit 0 + fi + done + + echo "❌ PR is missing a required label." + echo "At least one of the following labels is required:" + printf '%s\n' "${REQUIRED_LABELS[@]}" + exit 1 + shell: bash diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..6e2648b --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,30 @@ +name: PR Title + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +permissions: + contents: read + pull-requests: read + +jobs: + validate-pr-title: + name: Validate PR Title + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: latest + + - name: Install dependencies + run: npm install compromise + + - name: Validate PR title + run: node .github/scripts/validate-pr-title.js + env: + PR_TITLE: ${{ github.event.pull_request.title }}