|
| 1 | +name: Check PR Lines Changed |
| 2 | +description: 'Checks the number of lines changed in a PR and manages size labels accordingly.' |
| 3 | + |
| 4 | +inputs: |
| 5 | + max-lines: |
| 6 | + description: 'Maximum allowed total lines changed' |
| 7 | + required: false |
| 8 | + default: '1000' |
| 9 | + base-ref: |
| 10 | + description: 'Default base branch to compare against (if not running on a PR)' |
| 11 | + required: false |
| 12 | + default: 'main' |
| 13 | + ignore-patterns: |
| 14 | + description: 'Regex pattern for files to ignore when calculating changes' |
| 15 | + required: false |
| 16 | + default: '(\.lock$)' |
| 17 | + xs-max-size: |
| 18 | + description: 'Maximum lines for XS size' |
| 19 | + required: false |
| 20 | + default: '10' |
| 21 | + s-max-size: |
| 22 | + description: 'Maximum lines for S size' |
| 23 | + required: false |
| 24 | + default: '100' |
| 25 | + m-max-size: |
| 26 | + description: 'Maximum lines for M size' |
| 27 | + required: false |
| 28 | + default: '500' |
| 29 | + l-max-size: |
| 30 | + description: 'Maximum lines for L size' |
| 31 | + required: false |
| 32 | + default: '1000' |
| 33 | + |
| 34 | +runs: |
| 35 | + using: composite |
| 36 | + steps: |
| 37 | + - name: Checkout code |
| 38 | + uses: actions/checkout@v4 |
| 39 | + |
| 40 | + - name: Calculate changed lines |
| 41 | + id: line-count |
| 42 | + env: |
| 43 | + BASE_BRANCH: ${{ github.event.pull_request.base.ref || inputs.base-ref }} |
| 44 | + IGNORE_PATTERNS: ${{ inputs.ignore-patterns }} |
| 45 | + shell: bash |
| 46 | + run: | |
| 47 | + set -e |
| 48 | +
|
| 49 | + echo "Using base branch: $BASE_BRANCH" |
| 50 | +
|
| 51 | + # Instead of a full fetch, perform incremental fetches at increasing depth |
| 52 | + # until the merge-base between origin/<BASE_BRANCH> and HEAD is present. |
| 53 | + fetch_with_depth() { |
| 54 | + local depth=$1 |
| 55 | + echo "Attempting to fetch with depth $depth..." |
| 56 | + git fetch --depth="$depth" origin "$BASE_BRANCH" |
| 57 | + } |
| 58 | +
|
| 59 | + depths=(1 10 100) |
| 60 | + merge_base_found=false |
| 61 | +
|
| 62 | + for d in "${depths[@]}"; do |
| 63 | + fetch_with_depth "$d" |
| 64 | + if git merge-base "origin/$BASE_BRANCH" HEAD > /dev/null 2>&1; then |
| 65 | + echo "Merge base found with depth $d." |
| 66 | + merge_base_found=true |
| 67 | + break |
| 68 | + else |
| 69 | + echo "Merge base not found with depth $d, increasing depth..." |
| 70 | + fi |
| 71 | + done |
| 72 | +
|
| 73 | + # If we haven't found the merge base with shallow fetches, unshallow the repo. |
| 74 | + if [ "$merge_base_found" = false ]; then |
| 75 | + echo "Could not find merge base with shallow fetches, fetching full history..." |
| 76 | + git fetch --unshallow origin "$BASE_BRANCH" || git fetch origin "$BASE_BRANCH" |
| 77 | + fi |
| 78 | +
|
| 79 | + # Calculate additions and deletions across all changes between the base and HEAD, |
| 80 | + # filtering out files matching the ignore pattern. |
| 81 | + additions=$(git diff "origin/$BASE_BRANCH"...HEAD --numstat | grep -Ev "$IGNORE_PATTERNS" | awk '{add += $1} END {print add+0}') |
| 82 | + deletions=$(git diff "origin/$BASE_BRANCH"...HEAD --numstat | grep -Ev "$IGNORE_PATTERNS" | awk '{del += $2} END {print del+0}') |
| 83 | + total=$((additions + deletions)) |
| 84 | +
|
| 85 | + echo "Additions: $additions, Deletions: $deletions, Total: $total" |
| 86 | + { |
| 87 | + echo "lines-changed=$total" |
| 88 | + echo "additions=$additions" |
| 89 | + echo "deletions=$deletions" |
| 90 | + } >> "$GITHUB_OUTPUT" |
| 91 | +
|
| 92 | + - name: Check line count limit |
| 93 | + uses: actions/github-script@v7 |
| 94 | + env: |
| 95 | + LINES_CHANGED: ${{ steps.line-count.outputs.lines-changed }} |
| 96 | + ADDITIONS: ${{ steps.line-count.outputs.additions }} |
| 97 | + DELETIONS: ${{ steps.line-count.outputs.deletions }} |
| 98 | + MAX_LINES: ${{ inputs.max-lines }} |
| 99 | + XS_MAX_SIZE: ${{ inputs.xs-max-size }} |
| 100 | + S_MAX_SIZE: ${{ inputs.s-max-size }} |
| 101 | + M_MAX_SIZE: ${{ inputs.m-max-size }} |
| 102 | + L_MAX_SIZE: ${{ inputs.l-max-size }} |
| 103 | + with: |
| 104 | + script: | |
| 105 | + const { |
| 106 | + LINES_CHANGED, |
| 107 | + ADDITIONS, |
| 108 | + DELETIONS, |
| 109 | + MAX_LINES, |
| 110 | + XS_MAX_SIZE, |
| 111 | + S_MAX_SIZE, |
| 112 | + M_MAX_SIZE, |
| 113 | + L_MAX_SIZE, |
| 114 | + } = process.env; |
| 115 | +
|
| 116 | + const total = parseInt(LINES_CHANGED, 10) || 0; |
| 117 | + const additions = parseInt(ADDITIONS, 10) || 0; |
| 118 | + const deletions = parseInt(DELETIONS, 10) || 0; |
| 119 | +
|
| 120 | + // Thresholds from inputs with fallback to defaults |
| 121 | + const maxLines = parseInt(MAX_LINES, 10) || 1000; |
| 122 | + const xsMaxSize = parseInt(XS_MAX_SIZE, 10) || 10; |
| 123 | + const sMaxSize = parseInt(S_MAX_SIZE, 10) || 100; |
| 124 | + const mMaxSize = parseInt(M_MAX_SIZE, 10) || 500; |
| 125 | + const lMaxSize = parseInt(L_MAX_SIZE, 10) || 1000; |
| 126 | +
|
| 127 | + // Print summary |
| 128 | + console.log('Summary:'); |
| 129 | + console.log(` - Additions: ${additions}`); |
| 130 | + console.log(` - Deletions: ${deletions}`); |
| 131 | + console.log(` - Total: ${total}`); |
| 132 | + console.log(` - Limit: ${maxLines}`); |
| 133 | +
|
| 134 | + // Determine size label based on configured criteria |
| 135 | + let sizeLabel = ''; |
| 136 | + if (total <= xsMaxSize) { |
| 137 | + sizeLabel = 'size-XS'; |
| 138 | + } else if (total <= sMaxSize) { |
| 139 | + sizeLabel = 'size-S'; |
| 140 | + } else if (total <= mMaxSize) { |
| 141 | + sizeLabel = 'size-M'; |
| 142 | + } else if (total <= lMaxSize) { |
| 143 | + sizeLabel = 'size-L'; |
| 144 | + } else { |
| 145 | + sizeLabel = 'size-XL'; |
| 146 | + } |
| 147 | +
|
| 148 | + console.log(` - Size category: ${sizeLabel}`); |
| 149 | +
|
| 150 | + // Manage PR labels |
| 151 | + const owner = context.repo.owner; |
| 152 | + const repo = context.repo.repo; |
| 153 | + const issue_number = context.payload.pull_request.number; |
| 154 | +
|
| 155 | + try { |
| 156 | + const existingSizeLabels = ['size-XS', 'size-S', 'size-M', 'size-L', 'size-XL']; |
| 157 | +
|
| 158 | + // Get current labels |
| 159 | + const currentLabels = await github.rest.issues.listLabelsOnIssue({ |
| 160 | + owner, |
| 161 | + repo, |
| 162 | + issue_number |
| 163 | + }); |
| 164 | +
|
| 165 | + const currentLabelNames = currentLabels.data.map(l => l.name); |
| 166 | +
|
| 167 | + // Build new label set: keep non-size labels and add the new size label |
| 168 | + const newLabels = currentLabelNames |
| 169 | + .filter(name => !existingSizeLabels.includes(name)) // Remove all size labels |
| 170 | + .concat(sizeLabel); // Add the correct size label |
| 171 | +
|
| 172 | + // Check if labels need updating |
| 173 | + const currentSizeLabel = currentLabelNames.find(name => existingSizeLabels.includes(name)); |
| 174 | + if (currentSizeLabel === sizeLabel && currentLabelNames.length === newLabels.length) { |
| 175 | + console.log(`✅ Correct label '${sizeLabel}' already present, no changes needed`); |
| 176 | + } else { |
| 177 | + // Update all labels in a single API call |
| 178 | + await github.rest.issues.setLabels({ |
| 179 | + owner, |
| 180 | + repo, |
| 181 | + issue_number, |
| 182 | + labels: newLabels |
| 183 | + }); |
| 184 | +
|
| 185 | + if (currentSizeLabel && currentSizeLabel !== sizeLabel) { |
| 186 | + console.log(` - Replaced '${currentSizeLabel}' with '${sizeLabel}'`); |
| 187 | + } else if (!currentSizeLabel) { |
| 188 | + console.log(`✅ Added '${sizeLabel}' label to PR #${issue_number}`); |
| 189 | + } else { |
| 190 | + console.log(`✅ Updated labels for PR #${issue_number}`); |
| 191 | + } |
| 192 | + } |
| 193 | + } catch (error) { |
| 194 | + console.log(`⚠️ Could not manage labels: ${error.message}`); |
| 195 | + } |
| 196 | +
|
| 197 | + // Check if exceeds limit |
| 198 | + if (total > maxLines) { |
| 199 | + console.log(`❌ Error: Total changed lines (${total}) exceed the limit of ${maxLines}.`); |
| 200 | + process.exit(1); |
| 201 | + } else { |
| 202 | + console.log(`✅ Success: Total changed lines (${total}) are within the limit of ${maxLines}.`); |
| 203 | + } |
0 commit comments