Extend awards action (#1937)
* remove debug job, restrict create-pull-request to only awards.txt, add documentation * make create-pull-request use a custom branch, and filter that out, so PRs generated by action don't invoke action again
This commit is contained in:
parent
8a764f0f75
commit
27104302d5
32
.github/workflows/extend-awards.yml
vendored
Normal file
32
.github/workflows/extend-awards.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: extend-awards
|
||||
run-name: Extending awards
|
||||
on:
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
if_merged:
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'closed' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.head.ref != 'extend-awards/patch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- run: pip install requests
|
||||
- run: python extend-awards.py
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
add-paths: awards.csv
|
||||
branch: extend-awards/patch
|
||||
commit-message: Extending awards.csv
|
||||
title: Extending awards.csv
|
||||
body: A PR was merged that solves an issue and awards.csv should be extended.
|
54
docs/dev/extend-awards.md
Normal file
54
docs/dev/extend-awards.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Automatically extend awards.csv
|
||||
|
||||
## Overview
|
||||
|
||||
Whenever a pull request (PR) is merged in the [stacker.news](https://github.com/stackernews/stacker.news) repository, a [GitHub Action](https://docs.github.com/en/actions) is triggered:
|
||||
|
||||
If the merged PR solves an issue with [award tags](https://github.com/stackernews/stacker.news?tab=readme-ov-file#contributing),
|
||||
the amounts due to the PR and issue authors are calculated and corresponding lines are added to the [awards.csv](https://github.com/stackernews/stacker.news/blob/master/awards.csv) file,
|
||||
and a PR is opened for this change.
|
||||
|
||||
## Action
|
||||
|
||||
The action is defined in [.github/workflows/extend-awards.yml](.github/workflows/extend-awards.yml).
|
||||
|
||||
Filters on the event type and parameters ensure the action is [triggered only on merged PRs](https://stackoverflow.com/questions/60710209/trigger-github-actions-only-when-pr-is-merged).
|
||||
|
||||
The primary job consists of several steps:
|
||||
- [checkout](https://github.com/actions/checkout) checks out the repository
|
||||
- [setup-python](https://github.com/actions/setup-python) installs [Python](https://en.wikipedia.org/wiki/Python_(programming_language))
|
||||
- [pip](https://en.wikipedia.org/wiki/Pip_%28package_manager%29) installs the [requests](https://docs.python-requests.org/en/latest/index.html) module
|
||||
- a script (see below) is executed, which appends lines to [awards.csv](awards.csv) if needed
|
||||
- [create-pull-request](https://github.com/peter-evans/create-pull-request) looks for modified files and creates (or updates) a PR
|
||||
|
||||
## Script
|
||||
|
||||
The script is [extend-awards.py](extend-awards.py).
|
||||
|
||||
The script extracts from the [environment](https://en.wikipedia.org/wiki/Environment_variable) an authentication token needed for the [GitHub REST API](https://docs.github.com/en/rest/about-the-rest-api/about-the-rest-api) and the [context](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs) containing the event details including the merged PR (formatted in [JSON](https://en.wikipedia.org/wiki/JSON)).
|
||||
|
||||
In the merged PR's title and body it searches for the first [GitHub issue URL](https://github.com/stackernews/stacker.news/issues/) or any number with a hash symbol (#) prefix, and takes this as the issue being solved by the PR.
|
||||
|
||||
Using the GitHub REST API it fetches the issue and analyzes its tags for difficulty and priority.
|
||||
|
||||
It fetches the issue's timeline and counts the number of reviews completed with status 'changes requested' to calculate the amount reduction.
|
||||
|
||||
It calculates the amounts due to the PR author and the issue author.
|
||||
|
||||
It reads the existing awards.csv file to suppress appending redundant lines (same user, PR, and issue) and fill known receive methods (same user).
|
||||
|
||||
Finally, it appends zero, one, or two lines to the awards.csv file.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
In the GitHub web interface under 'Actions' each invokation of the action can be viewed, including environment and [output and errors](https://en.wikipedia.org/wiki/Standard_streams) of the script. First, the specific invokation is selected, then the job 'if_merged', then the step 'Run python extend-awards.py'. The environment is found by expanding the inner 'Run python extended-awards.py' on the first line.
|
||||
|
||||
The normal output includes details about the issue number found, the amount calculation, or the reason for not appending lines.
|
||||
|
||||
The error output may include a [Python traceback](https://realpython.com/python-traceback/) which helps to explain the error.
|
||||
|
||||
The environment contains in GITHUB_CONTEXT the event details, which may be required to understand the error.
|
||||
|
||||
## Security considerations
|
||||
|
||||
The create-pull-request step requires [workflow permissions](https://github.com/peter-evans/create-pull-request#workflow-permissions).
|
104
extend-awards.py
Normal file
104
extend-awards.py
Normal file
@ -0,0 +1,104 @@
|
||||
import json, os, re, requests
|
||||
|
||||
difficulties = {'good-first-issue':20000,'easy':100000,'medium':250000,'medium-hard':500000,'hard':1000000}
|
||||
priorities = {'low':0.5,'medium':1.5,'high':2,'urgent':3}
|
||||
ignored = ['huumn', 'ekzyis']
|
||||
fn = 'awards.csv'
|
||||
|
||||
sess = requests.Session()
|
||||
headers = {'Authorization':'Bearer %s' % os.getenv('GITHUB_TOKEN') }
|
||||
awards = []
|
||||
|
||||
def getIssue(n):
|
||||
url = 'https://api.github.com/repos/stackernews/stacker.news/issues/' + n
|
||||
r = sess.get(url, headers=headers)
|
||||
j = json.loads(r.text)
|
||||
return j
|
||||
|
||||
def findIssueInPR(j):
|
||||
p = re.compile('(#|https://github.com/stackernews/stacker.news/issues/)([0-9]+)')
|
||||
for m in p.finditer(j['title']):
|
||||
return m.group(2)
|
||||
if not 'body' in j or j['body'] is None:
|
||||
return
|
||||
for s in j['body'].split('\n'):
|
||||
for m in p.finditer(s):
|
||||
return m.group(2)
|
||||
|
||||
def addAward(user, kind, pr, issue, difficulty, priority, count, amount):
|
||||
if amount >= 1000000 and amount % 1000000 == 0:
|
||||
amount = str(int(amount / 1000000)) + 'm'
|
||||
elif amount >= 1000 and amount % 1000 == 0:
|
||||
amount = str(int(amount / 1000)) + 'k'
|
||||
for a in awards:
|
||||
if a[0] == user and a[1] == kind and a[2] == pr:
|
||||
print('found existing entry %s' % a)
|
||||
if a[8] != amount:
|
||||
print('warning: amount %s != %s' % (a[8], amount))
|
||||
return
|
||||
if count < 1:
|
||||
count = ''
|
||||
addr = '???'
|
||||
for a in awards:
|
||||
if a[0] == user and a[9] != '???':
|
||||
addr = a[9]
|
||||
print('adding %s,%s,%s,%s,%s,%s,%s,,%s,%s,???' % (user, kind, pr, issue, difficulty, priority, count, amount, addr))
|
||||
with open(fn, 'a') as f:
|
||||
print('%s,%s,%s,%s,%s,%s,%s,,%s,%s,???' % (user, kind, pr, issue, difficulty, priority, count, amount, addr), file=f)
|
||||
|
||||
def countReviews(pr):
|
||||
url = 'https://api.github.com/repos/stackernews/stacker.news/issues/%s/timeline' % pr
|
||||
r = sess.get(url, headers=headers)
|
||||
j = json.loads(r.text)
|
||||
count = 0
|
||||
for e in j:
|
||||
if e['event'] == 'reviewed' and e['state'] == 'changes_requested':
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def checkPR(i):
|
||||
pr = str(i['number'])
|
||||
print('pr %s' % pr)
|
||||
n = findIssueInPR(i)
|
||||
if not n:
|
||||
print('pr %s does not solve an issue' % pr)
|
||||
return
|
||||
print('solves issue %s' % n)
|
||||
j = getIssue(n)
|
||||
difficulty = ''
|
||||
amount = 0
|
||||
priority = ''
|
||||
multiplier = 1
|
||||
for l in j['labels']:
|
||||
for d in difficulties:
|
||||
if l['name'] == 'difficulty:' + d:
|
||||
difficulty = d
|
||||
amount = difficulties[d]
|
||||
for p in priorities:
|
||||
if l['name'] == 'priority:' + p:
|
||||
priority = p
|
||||
multiplier = priorities[p]
|
||||
if amount * multiplier <= 0:
|
||||
print('issue gives no award')
|
||||
return
|
||||
count = countReviews(pr)
|
||||
if count >= 10:
|
||||
print('too many reviews, no award')
|
||||
return
|
||||
if count > 0:
|
||||
print('%d reviews, %d%% reduction' % (count, count * 10))
|
||||
award = amount * multiplier * (10 - count) / 10
|
||||
print('award is %d' % award)
|
||||
if i['user']['login'] not in ignored:
|
||||
addAward(i['user']['login'], 'pr', '#' + pr, '#' + n, difficulty, priority, count, award)
|
||||
if j['user']['login'] not in ignored:
|
||||
count = 0
|
||||
addAward(j['user']['login'], 'issue', '#' + pr, '#' + n, difficulty, priority, count, int(award / 10))
|
||||
|
||||
with open(fn, 'r') as f:
|
||||
for s in f:
|
||||
s = s.split('\n')[0]
|
||||
awards.append(s.split(','))
|
||||
|
||||
j = json.loads(os.getenv('GITHUB_CONTEXT'))
|
||||
checkPR(j['event']['pull_request'])
|
Loading…
x
Reference in New Issue
Block a user