Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions .github/scripts/validate-pr-title.js
Original file line number Diff line number Diff line change
@@ -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);
}
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: CI

on:
workflow_dispatch:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize]
branches:
- '*'

jobs:
build-and-test:
name: Build, Test, & Report Coverage
runs-on: macos-latest

steps:
- name: Checkout source code
uses: actions/checkout@v4

- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest

- name: Install Brewfile dependencies
run: brew bundle install

- name: Run SwiftFormat in lint mode
run: swiftformat --lint .

- name: Install package dependencies
run: swift package resolve

- name: Build the package
run: swift build

- name: Run tests with code coverage
run: swift test --enable-code-coverage

- name: Gather code coverage
run: xcrun llvm-cov export -format="lcov" .build/debug/swift-mockingPackageTests.xctest/Contents/MacOS/swift-mockingPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage_report.lcov

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage_report.lcov
token: ${{ secrets.CODECOV_TOKEN }}
slug: fetch-rewards/swift-mocking
verbose: true
fail_ci_if_error: true
44 changes: 44 additions & 0 deletions .github/workflows/pr-labels.yml
Original file line number Diff line number Diff line change
@@ -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" "dependencies" "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
30 changes: 30 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 2 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ignore:
- "Tests"