Skip to content

Commit

Permalink
Merge pull request #37 from Tomoko-hjf/HackathonBot
Browse files Browse the repository at this point in the history
【New feature】新增黑客松小助手
  • Loading branch information
Ligoml authored Sep 13, 2023
2 parents 7565a5c + e6aa11b commit 84a2063
Show file tree
Hide file tree
Showing 10 changed files with 787 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__
**/logs/*.md
**/logs/*.txt
9 changes: 9 additions & 0 deletions HackathonBot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM python:3.8

RUN pip install requests schedule

WORKDIR /home/code/HackathonBot

COPY . /home/code/HackathonBot

CMD ["python", "bot.py"]
131 changes: 131 additions & 0 deletions HackathonBot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# 黑客松小助手RFC

## 📄 背景

💡💡💡 `Hackathon5`开赛在即,本次大赛会有上百个赛题等着大家攻克,但统计这些赛题的报名信息和当前进度是一项繁重的工作。

如果有一个小助手💡可以帮助我们自动统计和展示赛题进度,那自然是极好的,所以`黑客松小助手`它来了!🎉🎉🎉

## 🔥 功能

`黑客松小助手`目前的主要功能如下,欢迎补充:

* 根据issue回复自动填写报名信息,完成任务认领。
* 根据提交的PR状态,自动更新issue中表单信息,完成状态变更。
* 汇总所有的赛题,形成总体任务看板。

## 🔥 运行方式
1、修改配置文件,其中每个配置的含义如下:
* access_token:账号token
* proxies:代理地址
* issue_url:黑客松 issue页面 url 地址, 注意结尾不要有斜杠
* repo_urls:监控的仓库列表
* task_num:总的任务数量
* start_time:黑客松开始时间,只会统计黑客松开始时间之后的PR(注意时间中的字母T和Z不能缺少)
* un_handle_tasks: 忽略不处理的题号,这部分留给人工处理
* removed_tasks:已删除的赛题
* type_names: 赛道名
* task_types:每个赛题所属的赛道,每个赛道是一个数组
* hackathon: 为True时代表黑客松任务。 为False时代表框架计划,此时 repo_urls 要设为 []

2、执行如下命令,代码会每两小时更新一次issue,每次更新后的issue内容会保存在`logs`文件夹下;
```shell
cd HackathonBot

python bot.py
```

### 注意事项
* issue 中任务表格的题号前后必须只能有一个空格,如 `| 1 |`
* issue 中必须有`看板信息`这几个字,并且表格后面接上`#####`这个特殊标志。

示例
```markdown
| 序号 | 难度 | 任务 ISSUE | 队伍名称/状态/PR | 完成队伍 |
| :--: | :--: |:--: | :--: |:--: |
| 1 | ⭐ | [新增星星任务](https://github.com/Tomoko-hjf/paddleviz/issues/1) | | |
| 2 | ⭐⭐ | [保护星星](https://github.com/Tomoko-hjf/paddleviz/issues/1) | | |
| 3 | ⭐⭐⭐ | [收集星星](https://github.com/Tomoko-hjf/paddleviz/issues/1) | | |
| 4 | ⭐⭐⭐⭐ | [飞桨之星](https://github.com/Tomoko-hjf/paddleviz/issues/1) | | |

## 看板信息

#####
```

## 🔥 实现方案

### 🚀 状态

在比赛期间,我们为参加比赛的大佬们设置了`五种状态`(如下),整体状态的变更顺序为:`报名状态` -> `提交RFC` -> `完成设计文档` -> `提交PR` -> `完成任务`

| 状态名称 | 状态标志 | 状态说明 |
| :--------------: | :----------------------------------------------------------: | :-------------------------: |
| 报名状态 | <img src="https://img.shields.io/badge/状态-报名-2ECC71" /> | 表明通过issue评论区进行报名 |
| 提交RFC状态 | <img src="https://img.shields.io/badge/状态-提交RFC-F1C40F" /> | 表明已经提交设计方案RFC |
| 完成设计文档状态 | <img src="https://img.shields.io/badge/状态-完成设计文档-3498DB" /> | 表明已经完成设计文档 |
| 提交PR状态 | <img src="https://img.shields.io/badge/状态-提交PR-F39C12" /> | 表明已经提交PR |
| 完成任务状态 | <img src="https://img.shields.io/badge/状态-完成任务-9B59B6" /> | 表明已经完成任务 |

### 🚀 榜单设计

为了更好地展示赛题进度,我们设计了榜单。整体的榜单设计如下,比赛进行期间,小助手会自动更新`Github ID/状态/PR`栏和`完成队伍`栏信息。

![image-20230729121046527](./images/1.png)

`Github ID/状态/PR`栏内容的格式为`Github ID + 状态 + Pr`,其中`PR`可以有多个,`赛题参加人数`也可以有多个,以回车`<br>`分隔。

### 🚀 报名信息监控

考虑到报名赛题不会产生任何`PR`,所以通过`评论`来实现此功能,其他状态均可以通过监控`PR`实现。

#### 实现逻辑

通过监控`issue`下的评论完成报名信息监控,实现逻辑如下:

* 获取`issue`下评论。
* 从评论中抽取报名信息,比如`Github ID``报名赛题编号`等信息。
* 更新`issue`中个人报名信息,将个人状态更新为`报名状态`

#### 报名格式

为了自动填写报名信息,需要在`issue`下回复报名信息,如果报名格式不正确,则会在comment区提示报名不正确,格式如下:

```
【报名】: 2、3
```

> 其中`【报名】: ` 后直接是`报名的赛题序号,多个赛题之间需要用`中文顿号、分隔,多个连续赛题可以用横线表示`2-5`
### 🚀 其他状态变更监控

`报名状态`外,剩下`四种状态`的变更可以通过监控`PR`的状态来完成, 具体的实现逻辑如下:

* 获取指定仓库下黑客松`开始之后`标题中包含`Hackathon No.`字样的所有`PR`,具体是`bot.py`文件的`repo_urls`变量下的所有仓库。
* 如果`PR``PaddlePaddle/community`仓库下的,说明该PR与设计文档有关。继续判断是否`merge`,如果未`merge`,说明状态为`提交RFC`;如果已经`merge`,说明状态为`完成设计文档`
* 如果`PR`不是`PaddlePaddle/community`仓库下的,说明该`PR`与提交代码有关。继续判断是否`merge`,如果未`merge`,说明状态为`提交PR`;如果已经`merge`,说明状态为`完成任务`
* 每次更新后的issue内容将会保存在`logs`文件夹下,文件名是更新日期。

> 对于人工处理的赛题,可以将赛题题号加入`utils.py`文件的`un_handle_tasks`变量中,小助手不会处理这些任务。
#### PR格式

为了完成状态变更,只需要在`PR`的标题中以`【Hackathon No.xxx】`开头即可,程序会自动提取赛题编号并更新榜单。

如果PR格式不正确,也会在comment区域进行提示。

### 🚀 看板功能
看板功能会按照赛道类型统计每个赛道赛题的认领数、完成率等信息。
![img](./images/board.png)

感谢 [@AndSonder](https://github.com/AndSonder) 提供看板样式的代码🍻。

## 🔥 代码结构

整体的代码文件分为三个:

* `utils.py`:负责拉取评论和PR、根据评论更新状态、根据PR更新状态。
* `bot.py`:小助手整体运行逻辑。
* `config.py`:常用的配置项


112 changes: 112 additions & 0 deletions HackathonBot/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import time
import json
import schedule

import utils
from config import config, logger


def update_issue_automatically():

try:
issue_url = config['issue_url']

# 1. 获取issue表格
response = utils.request_get_issue(issue_url)

# 从issue中提取题目列表
task_list = utils.process_issue(response['body'])

# 2. 根据评论更新表格
comment_url = issue_url + '/comments'
comments = utils.request_get_multi(comment_url)
for comment in comments:
utils.update_status_by_comment(task_list, comment)


# 3. 根据PR更新表格
# - 根据提出的PR更新状态为已提交
# - 根据close的PR更新状态为已完成
repo_urls = config['repo_urls']

for repo_url in repo_urls:
params = {
"state": "open"
}
pulls = utils.request_get_multi(repo_url, params)
for pull in pulls:
utils.update_status_by_pull(task_list, pull)

params = {
"state": "closed"
}
pulls = utils.request_get_multi(repo_url, params)
for pull in pulls:
if pull['merged_at']:
utils.update_status_by_pull(task_list, pull)


# 4. 更新榜单内容
updated_issue = response['body']
for task in task_list:
if task == None:
continue
num = int(task['num'].strip(' '))
start = updated_issue.find('| {} |'.format(num))
end = start + 1
while end < len(updated_issue) and updated_issue[end] != '\r' and updated_issue[end] != '\n':
end += 1

# TODO:这里后期需要定制化表头
row = '| {} | {} | {} | {} | {} |'.format(num, task['difficulty'], task['issue'], task['status'], task['team'])
updated_issue = f'{updated_issue[:start]}{row}{updated_issue[end:]}'


if config["hackathon"]:
# 5. 更新看板信息,框架计划不需要更新看板信息
board_info = utils.update_board(task_list)
start = updated_issue.find('看板信息')
while updated_issue[start] != '\n':
start += 1
end = updated_issue.find('#####', start)
updated_issue = updated_issue[: start + 1] + board_info + updated_issue[end:]

# 6. 处理换行符,写入日志存档
updated_issue = updated_issue.replace('\r', '')
file_name = time.strftime('%Y-%m-%dT%H-%M-%S', time.localtime())
with open('./logs/{}.md'.format(file_name), mode='w', encoding='utf-8') as f:
f.write(updated_issue)

# 7. 更新 issue
data = {}
data['body'] = updated_issue
data['title'] = response['title']
data['assignee'] = response['assignee']
data['state'] = response['state']
data['state_reason'] = response['state_reason']
data['milestone'] = response['milestone']
data['labels'] = response['labels']

res = utils.request_update_issue(issue_url, json.dumps(data))
# logger.info('更新issue内容返回结果: ' + str(res))

except Exception as e:
logger.exception(e)




if __name__ == '__main__':

# 运行一次查看效果
update_issue_automatically()

# 每两小时运行一次
schedule.every(2).hours.do(update_issue_automatically)

while True:
try:
schedule.run_pending()
time.sleep(100)
except Exception as e:
logger.error(e)
67 changes: 67 additions & 0 deletions HackathonBot/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import logging

# 监控的仓库列表
repo_urls = ['https://api.github.com/repos/PaddlePaddle/Paddle/pulls']

config = {
# 更新issue的token
'issue_token': '',

# 更新评论的token
'comment_token': '',

# 代理地址
'proxies': {
'http': 'http://127.0.0.1:7890',
'https': 'http://127.0.0.1:7890'
},

# 黑客松开始时间,只会统计黑客松开始时间之后的PR(注意时间中的字母T和Z不能缺少)
'start_time' : '2023-09-13T15:28:48Z',

# 黑客松 issue页面 url 地址, 注意结尾不要有斜杠
'issue_url': 'https://api.github.com/repos/PaddlePaddle/Paddle/issues/57262',

# 监控的仓库列表
'repo_urls': repo_urls,

# 总的任务数量
'task_num' : 11,

# 忽略不处理的题号,这部分留给人工处理
'un_handle_tasks' : [],

# 已删除的赛题
'removed_tasks' : [],

# 赛道名
'type_names' : ["热身赛"],

# 每个赛题所属的赛道,每个赛道是一个数组
'task_types' : [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]],

# 为True时代表黑客松任务。 为False时代表框架计划,此时 repo_urls 要设为 []
'hackathon': True

}

def get_logger():
logger = logging.getLogger('logger')
logger.setLevel(level=logging.DEBUG)

formatter = logging.Formatter('%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')

file_handler = logging.FileHandler('./logs/output.txt', encoding='utf-8')
file_handler.setLevel(level=logging.INFO)
file_handler.setFormatter(formatter)

stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
stream_handler.setFormatter(formatter)

logger.addHandler(file_handler)
logger.addHandler(stream_handler)

return logger

logger = get_logger()
Binary file added HackathonBot/images/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added HackathonBot/images/board.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions HackathonBot/logs/ori.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
| 序号 | 难度 | 任务 ISSUE | 队伍名称/状态/PR | 完成队伍 |
| :--: | :--: |:--: | :--: |:--: |
| 1 || [新增星星任务](https://github.com/Tomoko-hjf/paddleviz/issues/1) | | |
| 2 | ⭐⭐ | [保护星星](https://github.com/Tomoko-hjf/paddleviz/issues/1) | | |
| 3 | ⭐⭐⭐ | [收集星星](https://github.com/Tomoko-hjf/paddleviz/issues/1) | | |
| 4 | ⭐⭐⭐⭐ | [飞桨之星](https://github.com/Tomoko-hjf/paddleviz/issues/1) | | |

## 看板信息

#####
14 changes: 14 additions & 0 deletions HackathonBot/test_case.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
设计文档详见README,这里仅列举验收需要的测试样例

| 测试case | 测试名称 | 测试步骤 | 预期效果 | 测试结果 | 备注 |
| -------- | ------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -------- | ---- |
| 1 | 首次评论区信息抽取 | 1. 构造2个任务列表,每个任务列表包含5-10个任务</br>2. 构造至少2个github账号,按照规定格式报名若干任务,任务均已发布</br>规定格式:【报名】: 2、3`【报名】: ` 后直接是报名的任务序号,多个任务之间需要用中文顿号、分隔,多个连续任务可以用横线表示`2-5`。</br>3. 首次运行脚本 | 报名信息正确更新到表单中 | | |
| 2 | 二次评论区信息抽取 | 1. 基于case1,在评论区按照规定格式二次构造若干报名信息,任务均已发布</br>2. 在case1的基础上运行脚本 | 新增报名信息正确更新到表单中 | | |
| 3 | 新增/删除/修改任务列表信息 | 1. 在任务列表中随机删除1个任务(md中划掉)</br>2. 在任务列表中随机增加1个任务(新增一行,任务编号不重复)</br>3. 在任务列表中随机修改1个任务的难度和issue描述文案</br>4. 在task_list中补充增加的任务编号</br>5. 任意github账号报名删除任务的编号</br>6. 任意github账号报名新增任务的编号</br>7. 在case2的基础上运行脚本 | 1. 新增/删除/修改任务列表信息保留</br>2. 报名信息正确更新到表单中 | | |
| 4 | 评论区负样本信息抽取与报错提醒 | 1. 构造规定格式以外的评论信息,包括:</br>a. 不按照规定格式的报名信息,如</br>①【取消报名】:2、3</br>②【链接】:1-5</br>③报名:https://github.com/Tomoko-hjf/paddleviz/issues/1</br>④【报名】:1111111</br>b. 非报名信息,如问答或闲聊信息</br>2. 在case3的基础上运行脚本 | 1. 准确识别负样本信息,记录log</br>2. @github id,提醒对方未成功报名 | | |
| 5 | PR状态抽取(含负样本) | 1. 已报名的github账号在community下提交一个pr,pr标题按照规定格式</br>规定格式:在`PR`的标题中以`【Hackathon No.xxx】`开头</br>2. 未报名的github账号在community下提交一个pr,pr标题按照规定格式</br>3. 已报名的github账号在任意监控范围内的repo下提交一个pr,pr标题按照规定格式,但编号不在task_list中</br>4. 未报名的github账号在任意监控范围内的repo下提交一个pr,pr标题按照规定格式</br>5. 在case4的基础上运行脚本 | 1. 准确更新 1 2 4的PR信息</br>2. 3记录log,@github id,提醒对方编号有误 | | |
| 6 | 更改PR标题后的状态抽取 | 1. 更改case5中不符合规范的PR标题,使其正确</br>2. 在case5的基础上运行脚本 | 修改后PR被正确更新到表单中 | | |
| 7 | PR状态变更后抽取 | 1. community下合入一个pr</br>2. 监控范围内的repo合入一个pr</br>3. 在case6的基础上运行脚本 | rfc和pr状态被正确更新为完成设计文档和完成任务</br>备注:rfc和pr合入由黑客松组委会管理,可以**人工避免**要求rfc的任务绕过rfc提交直接合入pr的情况,因此不测试这个case | | |
| 8 | 多个PR指向一个任务,且具有不同状态(open/close/merge) | 1. 构造5个pr,标题中的任务序号指向同一个任务</br>2. 合入其中3个pr,其中2个pr由同一个github id提交</br>3. 关闭其中1个pr</br>4. 在case7的基础上运行脚本 | 5个pr的状态可以被准确更新到表单中</br>备注:用于应对一个任务对应多个pr的情况 | | |
| 9 | 看板功能 | | 验证上述测试样例中,看板统计是否正常 | | |
| 10 | | | | | |
Loading

0 comments on commit 84a2063

Please sign in to comment.