Skip to content

Commit c4898b7

Browse files
Planeshifterkgryte
andauthored
build: add workflow to encourage more challenging contributions
PR-URL: #5500 Co-authored-by: Athan Reines <kgryte@gmail.com> Reviewed-by: Athan Reines <kgryte@gmail.com>
1 parent 635e38f commit c4898b7

File tree

2 files changed

+229
-0
lines changed

2 files changed

+229
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env bash
2+
#
3+
# @license Apache-2.0
4+
#
5+
# Copyright (c) 2025 The Stdlib Authors.
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
19+
# Script to prevent contributors from opening too many "Good First PR"s.
20+
#
21+
# Usage: rate_limit_contributions <pr_number>
22+
#
23+
# Arguments:
24+
#
25+
# pr_number Pull request number.
26+
#
27+
# Environment variables:
28+
#
29+
# GITHUB_TOKEN GitHub token for authentication.
30+
31+
# Ensure that the exit status of pipelines is non-zero in the event that at least one of the commands in a pipeline fails:
32+
set -o pipefail
33+
34+
35+
# VARIABLES #
36+
37+
# Resolve the pull request number:
38+
pr_number="$1"
39+
40+
# Threshold of open "Good First PR"s above which to post comment:
41+
max_good_first_prs=5
42+
43+
# GitHub API base URL:
44+
github_api_url="https://api.github.com"
45+
46+
# Repository owner and name:
47+
repo_owner="stdlib-js"
48+
repo_name="stdlib"
49+
50+
51+
# FUNCTIONS #
52+
53+
# Error handler.
54+
#
55+
# $1 - error status
56+
on_error() {
57+
echo 'ERROR: An error was encountered during execution.' >&2
58+
exit "$1"
59+
}
60+
61+
# Prints a success message.
62+
print_success() {
63+
echo 'Success!' >&2
64+
}
65+
66+
# Performs a GitHub API request.
67+
#
68+
# $1 - HTTP method (GET or POST)
69+
# $2 - API endpoint
70+
# $3 - data for POST requests
71+
github_api() {
72+
local method="$1"
73+
local endpoint="$2"
74+
local data="$3"
75+
76+
# Initialize an array to hold curl headers:
77+
local headers=()
78+
79+
# If GITHUB_TOKEN is set, add the Authorization header:
80+
if [ -n "${GITHUB_TOKEN}" ]; then
81+
headers+=("-H" "Authorization: token ${GITHUB_TOKEN}")
82+
fi
83+
84+
# Determine the HTTP method and construct the curl command accordingly...
85+
case "${method}" in
86+
GET)
87+
curl -s "${headers[@]}" "${github_api_url}${endpoint}"
88+
;;
89+
POST)
90+
# For POST requests, always set the Content-Type header:
91+
headers+=("-H" "Content-Type: application/json")
92+
93+
# If data is provided, include it in the request:
94+
if [ -n "${data}" ]; then
95+
curl -s -X POST "${headers[@]}" -d "${data}" "${github_api_url}${endpoint}"
96+
else
97+
# Handle cases where POST data is required but not provided:
98+
echo "ERROR: POST request requires data."
99+
on_error 1
100+
fi
101+
;;
102+
*)
103+
echo "ERROR: Invalid HTTP method: ${method}."
104+
on_error 1
105+
;;
106+
esac
107+
}
108+
109+
# Main execution sequence.
110+
main() {
111+
local num_good_first_prs
112+
local good_first_pr
113+
local pr_details
114+
local pr_author
115+
local user_prs
116+
117+
if [ -z "${pr_number}" ]; then
118+
echo "ERROR: Pull request number is required." >&2
119+
on_error 1
120+
fi
121+
122+
# Fetch pull request details:
123+
pr_details=$(github_api "GET" "/repos/${repo_owner}/${repo_name}/pulls/${pr_number}")
124+
good_first_pr=$(echo "${pr_details}" | jq -r '.labels | any(.name == "Good First PR")')
125+
126+
pr_author=$(echo "${pr_details}" | jq -r '.user.login')
127+
128+
# Fetch other PRs of the same user:
129+
user_prs=$(github_api "GET" "/search/issues?q=state%3Aopen+author%3A${pr_author}+type%3Apr")
130+
131+
# Count number of PRs labeled "Good First PR":
132+
num_good_first_prs=$(echo "${user_prs}" | jq -r '.items | map( select( .labels | any( .name == "Good First PR" ))) | length')
133+
134+
if [ "${num_good_first_prs}" -gt "${max_good_first_prs}" ]; then
135+
# Post a comment on the PR:
136+
comment="Hello! 👋
137+
138+
We've noticed that you've been opening a number of PRs addressing good first issues. Thank you for your interest and enthusiasm!
139+
140+
Now that you've made a few contributions, we suggest no longer working on good first issues. Instead, we encourage you to prioritize cleaning up any PRs which have yet to be merged and then proceed to work on more involved tasks.
141+
142+
Not only does this ensure that other new contributors can work on things and get ramped up on all things stdlib, it also ensures that you can spend your time on more challenging problems. 🚀
143+
144+
For ideas for future PRs, feel free to search the codebase for TODOs and FIXMEs and be sure to check out other open issues on the issue tracker. Cheers!"
145+
146+
if ! github_api "POST" "/repos/${repo_owner}/${repo_name}/issues/${pr_number}/comments" "{\"body\":$(echo "${comment}" | jq -R -s -c .)}"; then
147+
echo "Failed to post comment on PR."
148+
on_error 1
149+
fi
150+
151+
exit 1
152+
else
153+
echo "Number of PRs below threshold."
154+
print_success
155+
exit 0
156+
fi
157+
}
158+
159+
main
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#/
2+
# @license Apache-2.0
3+
#
4+
# Copyright (c) 2025 The Stdlib Authors.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#/
18+
19+
# Workflow name:
20+
name: too_many_good_first_prs
21+
22+
# Workflow triggers:
23+
on:
24+
pull_request_target:
25+
types:
26+
- opened
27+
28+
# Workflow jobs:
29+
jobs:
30+
31+
# Define a job which encourages contributors opening too many "Good First PRs" to tackle more challenging problems:
32+
check_num_good_first_prs:
33+
34+
# Define job name:
35+
name: 'Check if contributor has opened too many "Good First PRs"'
36+
37+
# Only run this job if the pull request did not have label `automated-pr`:
38+
if: contains(github.event.pull_request.labels.*.name, 'automated-pr') == false
39+
40+
# Define job permissions:
41+
permissions:
42+
contents: read
43+
pull-requests: write
44+
45+
# Define the type of virtual host machine:
46+
runs-on: ubuntu-latest
47+
48+
# Define the sequence of job steps:
49+
steps:
50+
# Checkout the repository:
51+
- name: 'Checkout repository'
52+
# Pin action to full length commit SHA
53+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
54+
with:
55+
# Specify whether to remove untracked files before checking out the repository:
56+
clean: true
57+
58+
# Limit clone depth to the most recent commit:
59+
fetch-depth: 1
60+
61+
# Specify whether to download Git-LFS files:
62+
lfs: false
63+
timeout-minutes: 10
64+
65+
# Prevent contributors from opening too many "Good First PR"s:
66+
- name: 'Prevent contributors from opening too many "Good First PR"s'
67+
env:
68+
PR_NUMBER: ${{ github.event.pull_request.number }}
69+
run: |
70+
. "$GITHUB_WORKSPACE/.github/workflows/scripts/rate_limit_contributions" $PR_NUMBER

0 commit comments

Comments
 (0)