每周五下午四点半,脑子就开始放空了。
不是因为快下班了。是因为知道接下来要干一件无聊的事:写周报。
打开Jira,翻这一周的任务列表。这个任务什么时候完成的来着?点进去看时间线。那个任务转给了谁来着?搜一下。然后打开GitLab,看看这周提交了哪些代码、关了哪些issue。
光翻记录就得二十分钟。翻完了还得组织语言,写成领导爱看的格式:“本周完成了XX,推进了YY,下周计划ZZ”。写得太平淡显得没干活,写得太夸张又心虚。
上个周五更离谱。写完周报已经六点半了,结果领导六点就走了,根本没看。白加了一个半小时的班。
盯着那份周报文档,忽然意识到一件事:这活的大部分信息都在Jira和GitLab里存着,本质上就是个人肉搬运工。
既然数据都在系统里,那就让脚本去搬。
用了什么
Jira的REST API能拉到指派给我的任务列表,包括状态、完成时间、优先级。
GitLab的API能拿到我的commit记录、merge request、关闭的issue。
两个数据源拿到之后,用Jinja2模板渲染成周报格式。
三个库:requests发请求,jinja2渲染模板,python-dateutil处理日期。
思路
算出本周的起止日期(周一到今天)
从Jira拉本周状态有变化的任务
从GitLab拉本周的commit和MR记录
把原始数据灌进模板,生成结构化的周报
输出Markdown文件,直接复制粘贴到邮件或OA系统
完整代码
先装依赖:
pip install requests jinja2 python-dateutil
import requestsimport osfrom datetime import datetime, timedeltafrom collections import defaultdictfrom jinja2 import Template# ========== 配置区 ==========# Jira 配置JIRA_BASE_URL = "https://your-company.atlassian.net"JIRA_EMAIL = "your_email@company.com"JIRA_API_TOKEN = "your_jira_api_token_here"# API Token在 https://id.atlassian.com/manage-profile/security/api-tokens 生成# GitLab 配置GITLAB_BASE_URL = "https://gitlab.your-company.com"GITLAB_TOKEN = "your_gitlab_private_token_here"# Token在 GitLab -> Settings -> Access Tokens 生成GITLAB_USERNAME = "your.username"# 你的GitLab用户名# 周报模板文件TEMPLATE_FILE = "weekly_report_template.md"# ========== 日期工具 ==========def get_week_range():"""获取本周一到当前日期的日期范围"""today = datetime.now()# 找到本周一monday = today-timedelta(days=today.weekday())monday = monday.replace(hour=0, minute=0, second=0, microsecond=0)return monday, todaydef format_date(dt):"""格式化日期为 YYYY-MM-DD"""if isinstance(dt, str):from dateutil import parserdt = parser.parse(dt)return dt.strftime("%Y-%m-%d")def format_datetime(dt):"""格式化日期为 MM-DD HH:MM"""if isinstance(dt, str):from dateutil importparserdt = parser.parse(dt)return dt.strftime("%m-%d %H:%M")# ========== Jira 数据获取 ==========def fetch_jira_tasks(start_date, end_date):""" 从Jira获取本周状态有变化的任务。 使用JQL查询:指派给我 且 本周有更新的issue。 """start_str = start_date.strftime("%Y-%m-%d")end_str = end_date.strftime("%Y-%m-%d")# JQL: 分配给我,且本周有更新jql = (f'assignee = currentUser() 'f'AND updated >= "{start_str}" 'f'AND updated <= "{end_str}" 'f'ORDER BY updated DESC' )url = f"{JIRA_BASE_URL}/rest/api/3/search"headers = {"Accept": "application/json"}params = {"jql": jql,"maxResults": 100,"fields": "summary,status,issuetype,priority,updated,created,resolution", }try:resp = requests.get(url,headers=headers,params=params,auth=(JIRA_EMAIL, JIRA_API_TOKEN),timeout=30 )resp.raise_for_status()data = resp.json()except requests.exceptions.RequestExceptionase:print(f"Jira请求失败: {e}")return []tasks = []for issue in data.get("issues", []):fields = issue["fields"]task = {"key": issue["key"],"summary": fields.get("summary", ""),"status": fields.get("status", {}).get("name", "未知"),"type": fields.get("issuetype", {}).get("name", "任务"),"priority": fields.get("priority", {}).get("name", "中"),"updated": format_datetime(fields.get("updated", "")),"url": f"{JIRA_BASE_URL}/browse/{issue['key']}", }tasks.append(task)return tasksdef categorize_jira_tasks(tasks):"""把Jira任务按状态分类"""categories = defaultdict(list)for task in tasks:status = task["status"]# 状态映射(不同公司的状态名可能不同,按需调整)if status in ["Done", "已完成", "Closed", "已关闭", "Resolved"]:categories["已完成"].append(task)elif status in ["In Progress", "进行中", "Development", "开发中"]:categories["进行中"].append(task)elif status in ["To Do", "待办", "Open", "Backlog", "新建"]:categories["待处理"].append(task)elif status in ["In Review", "评审中", "Testing", "测试中"]:categories["评审/测试中"].append(task)else:categories["其他"].append(task)return dict(categories)# ========== GitLab 数据获取 ==========def fetch_gitlab_activity(start_date, end_date):""" 从GitLab获取本周的代码活动。 包括:commits、merge requests、关闭的issues。 """headers = {"PRIVATE-TOKEN": GITLAB_TOKEN}start_iso = start_date.isoformat() +"Z"end_iso = end_date.isoformat() +"Z"result = {"commits": [],"merge_requests": [],"closed_issues": [], }# --- 获取当前用户信息 ---try:user_resp = requests.get(f"{GITLAB_BASE_URL}/api/v4/user",headers=headers,timeout=15 )user_resp.raise_for_status()user_data = user_resp.json()user_id = user_data["id"]except Exceptionase:print(f"GitLab用户信息获取失败: {e}")return result# --- 获取Merge Requests ---try:mr_resp = requests.get(f"{GITLAB_BASE_URL}/api/v4/users/{user_id}/merge_requests",headers=headers,params={"state": "all","updated_after": start_iso,"updated_before": end_iso,"per_page": 50, },timeout=15 )mr_resp.raise_for_status()for mr in mr_resp.json():result["merge_requests"].append({"title": mr["title"],"state": mr["state"],"project": mr.get("references", {}).get("full", "").split("!")[0],"updated": format_datetime(mr["updated_at"]),"url": mr["web_url"], })except Exception as e:print(f"GitLab MR获取失败: {e}")# --- 获取参与的commits ---# GitLab没有直接的用户commit列表API,通过events接口获取try:events_resp = requests.get(f"{GITLAB_BASE_URL}/api/v4/users/{user_id}/events",headers=headers,params={"action": "pushed","after": start_date.strftime("%Y-%m-%d"),"before": end_date.strftime("%Y-%m-%d"),"per_page": 50, },timeout=15 )events_resp.raise_for_status()for event in events_resp.json():push_data = event.get("push_data", {})result["commits"].append({"project": event.get("project_id", ""),"action": push_data.get("action", "pushed"),"branch": push_data.get("ref", "").replace("refs/heads/", ""),"commit_count": push_data.get("commit_count", 0),"date": format_datetime(event["created_at"]), })except Exception as e:print(f"GitLab events获取失败: {e}")# --- 获取关闭的issues ---try:issues_resp = requests.get(f"{GITLAB_BASE_URL}/api/v4/issues",headers=headers,params={"assignee_username": GITLAB_USERNAME,"state": "closed","updated_after": start_iso,"updated_before": end_iso,"per_page": 50, },timeout=15 )issues_resp.raise_for_status()for issue in issues_resp.json():result["closed_issues"].append({"title": issue["title"],"project": issue.get("references", {}).get("full", "").split("#")[0],"closed_at": format_datetime(issue.get("closed_at", issue["updated_at"])),"url": issue["web_url"], })except Exception as e:print(f"GitLab issues获取失败: {e}")return result# ========== 周报模板 ==========DEFAULT_TEMPLATE = """# 周报 - {{ author }}**周期:** {{ start_date }} ~ {{ end_date }}---## 本周工作完成情况{% if completed_tasks %}{% for task in completed_tasks %}- [{{ task.type }}] {{ task.summary }}({{ task.key }},{{ task.updated }}){% endfor %}{% else %}- 无{% endif %}## 进行中{% if in_progress_tasks %}{% for task in in_progress_tasks %}- [{{ task.type }}] {{ task.summary }}({{ task.key }},{{ task.updated }}){% endfor %}{% else %}- 无{% endif %}## 代码活动{% if merge_requests %}### Merge Requests{% for mr in merge_requests %}- {{ mr.title }} [{{ mr.state }}]({{ mr.updated }}){% endfor %}{% endif %}{% if commits %}### 代码提交{% for commit in commits %}- {{ commit.action }} to {{ commit.branch }},{{ commit.commit_count }} 个提交({{ commit.date }}){% endfor %}{% endif %}{% if closed_issues %}### 关闭的Issues{% for issue in closed_issues %}- {{ issue.title }}({{ issue.closed_at }}){% endfor %}{% endif %}{% if not merge_requests and not commits and not closed_issues %}- 本周无代码提交记录{% endif %}## 下周计划> (此处请手动补充)---## 数据统计| 指标 | 数量 ||------|------|| Jira已完成任务 | {{ completed_count }} || Jira进行中任务 | {{ in_progress_count }} || Merge Requests | {{ mr_count }} || 代码推送次数 | {{ push_count }} || 关闭Issues | {{ issue_count }} |"""# ========== 渲染周报 ==========def render_report(jira_categories, gitlab_data, start_date, end_date):"""用模板渲染周报内容"""template = Template(DEFAULT_TEMPLATE)completed = jira_categories.get("已完成", [])in_progress = jira_categories.get("进行中", [])report = template.render(author="Nicholas",start_date=start_date.strftime("%Y-%m-%d"),end_date=end_date.strftime("%Y-%m-%d"),completed_tasks=completed,in_progress_tasks=in_progress,merge_requests=gitlab_data.get("merge_requests", []),commits=gitlab_data.get("commits", []),closed_issues=gitlab_data.get("closed_issues", []),completed_count=len(completed),in_progress_count=len(in_progress),mr_count=len(gitlab_data.get("merge_requests", [])),push_count=len(gitlab_data.get("commits", [])),issue_count=len(gitlab_data.get("closed_issues", [])), )return report# ========== 演示模式 ==========def demo_data():""" 用模拟数据展示效果。 不需要真实API。 """jira_categories = {"已完成": [ {"key": "PROJ-1024", "summary": "用户注册流程增加手机号验证", "type": "需求", "status": "已完成", "updated": "09-04 15:30"}, {"key": "PROJ-1031", "summary": "修复订单列表分页bug", "type": "缺陷", "status": "已完成", "updated": "09-05 10:20"}, {"key": "PROJ-1038", "summary": "优化首页加载速度", "type": "优化", "status": "已完成", "updated": "09-06 11:45"}, {"key": "PROJ-1042", "summary": "对接第三方支付SDK升级", "type": "需求", "status": "已完成", "updated": "09-06 17:00"}, ],"进行中": [ {"key": "PROJ-1050", "summary": "数据导出功能开发", "type": "需求", "status": "进行中", "updated": "09-06 18:00"}, {"key": "PROJ-1053", "summary": "会员等级体系设计", "type": "需求", "status": "进行中", "updated": "09-05 14:30"}, ],"评审/测试中": [ {"key": "PROJ-1045", "summary": "搜索结果排序算法调整", "type": "优化", "status": "评审中", "updated": "09-06 09:00"}, ], }gitlab_data = {"merge_requests": [ {"title": "feat: 增加手机号验证流程", "state": "merged", "project": "backend/user-service", "updated": "09-04 14:20"}, {"title": "fix: 订单分页offset计算错误", "state": "merged", "project": "backend/order-service", "updated": "09-05 10:00"}, {"title": "perf: 首页接口缓存优化", "state": "merged", "project": "backend/gateway", "updated": "09-06 11:30"}, {"title": "feat: 数据导出功能(进行中)", "state": "opened", "project": "backend/report-service", "updated": "09-06 18:00"}, ],"commits": [ {"action": "pushed", "branch": "feature/phone-verify", "commit_count": 5, "date": "09-04 14:10"}, {"action": "pushed", "branch": "fix/order-pagination", "commit_count": 2, "date": "09-05 09:50"}, {"action": "pushed", "branch": "perf/homepage-cache", "commit_count": 3, "date": "09-06 11:20"}, {"action": "pushed", "branch": "feature/data-export", "commit_count": 8, "date": "09-06 17:50"}, ],"closed_issues": [ {"title": "订单列表翻页后数据重复", "project": "backend/order-service", "closed_at": "09-05 10:20"}, {"title": "首页LCP指标超过3秒", "project": "frontend/web-app", "closed_at": "09-06 11:45"}, ], }returnjira_categories, gitlab_data# ========== 主流程 ==========def main():print("="*50)print("周报自动生成工具")print(f"运行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")print("="*50)start_date, end_date = get_week_range()print(f"\n周报周期: {format_date(start_date)} ~ {format_date(end_date)}")# 尝试从真实API获取数据use_real_api = (JIRA_API_TOKEN!= "your_jira_api_token_here"andGITLAB_TOKEN!= "your_gitlab_private_token_here" )if use_real_api:print("\n>>> 从Jira获取任务数据...")jira_tasks = fetch_jira_tasks(start_date, end_date)print(f"获取到 {len(jira_tasks)} 个Jira任务")jira_categories = categorize_jira_tasks(jira_tasks)for cat, tasks in jira_categories.items():print(f" {cat}: {len(tasks)} 个")print("\n>>> 从GitLab获取代码活动...")gitlab_data = fetch_gitlab_activity(start_date, end_date)print(f"Merge Requests: {len(gitlab_data['merge_requests'])}")print(f"代码推送: {len(gitlab_data['commits'])}")print(f"关闭Issues: {len(gitlab_data['closed_issues'])}")else:print("\n使用演示数据(配置API Token后使用真实数据)")jira_categories, gitlab_data = demo_data()# 渲染周报print("\n>>> 生成周报...")report = render_report(jira_categories, gitlab_data, start_date, end_date)# 保存文件output_dir = "./weekly_reports"os.makedirs(output_dir, exist_ok=True)week_num = start_date.isocalendar()[1]output_file = os.path.join(output_dir, f"周报_2024_W{week_num:02d}.md")with open(output_file, "w", encoding="utf-8") as f:f.write(report)print(f"\n周报已保存: {output_file}")# 打印预览print("\n"+"="*50)print("周报预览")print("="*50)print(report)print("\n下周计划部分请手动补充。")print("完成。")if__name__ == "__main__":main()运行效果
==================================================周报自动生成工具运行时间: 2024-09-06 18:32:15==================================================周报周期: 2024-09-02 ~ 2024-09-06使用演示数据(配置API Token后使用真实数据)>>> 生成周报...周报已保存: ./weekly_reports/周报_2024_W36.md
生成的周报内容:
# 周报 - Nicholas**周期:** 2024-09-02 ~ 2024-09-06---## 本周工作完成情况- [需求] 用户注册流程增加手机号验证(PROJ-1024,09-04 15:30)- [缺陷] 修复订单列表分页bug(PROJ-1031,09-05 10:20)- [优化] 优化首页加载速度(PROJ-1038,09-06 11:45)- [需求] 对接第三方支付SDK升级(PROJ-1042,09-06 17:00)## 进行中- [需求] 数据导出功能开发(PROJ-1050,09-06 18:00)- [需求] 会员等级体系设计(PROJ-1053,09-05 14:30)## 代码活动### Merge Requests- feat: 增加手机号验证流程 [merged](09-04 14:20)- fix: 订单分页offset计算错误 [merged](09-05 10:00)- perf: 首页接口缓存优化 [merged](09-06 11:30)- feat: 数据导出功能(进行中) [opened](09-06 18:00)### 代码提交- pushed to feature/phone-verify,5 个提交(09-04 14:10)- pushed to fix/order-pagination,2 个提交(09-05 09:50)- pushed to perf/homepage-cache,3 个提交(09-06 11:20)- pushed to feature/data-export,8 个提交(09-06 17:50)### 关闭的Issues- 订单列表翻页后数据重复(09-05 10:20)- 首页LCP指标超过3秒(09-06 11:45)## 下周计划> (此处请手动补充)---## 数据统计| 指标 | 数量 ||------|------|| Jira已完成任务 | 4 || Jira进行中任务 | 2 || Merge Requests | 4 || 代码推送次数 | 4 || 关闭Issues | 2 |
该干的活一条不少,全在上面了。
只需要花两分钟在“下周计划”那里补几行字,再通读一遍看看有没有要调整的措辞,就可以发了。
唯一需要手动写的地方
“下周计划”我故意留空了。因为这个东西没法从系统里自动拉取,它需要你动脑子想想下周要干什么。
不过也就三五行的事,两分钟写完。跟以前翻一小时记录比起来,这点工作量可以忽略。
Jira API的一些注意事项
Jira的API Token不是在Jira里生成的,要到Atlassian账号管理页面去创建。很多人在这一步就卡住了。地址在代码注释里写了。
JQL查询语句是Jira的灵魂。assignee = currentUser() AND updated >= "2024-09-02"这条语句的意思是“分配给我的、本周有更新的任务”。你可以改成更复杂的条件,比如只查特定项目:project = "PROJ" AND ...。
状态名称因公司而异。代码里的categorize_jira_tasks函数做了中英文状态的映射。如果你的Jira里有自定义状态,往那个函数里加就行。
GitLab API要注意的
GitLab获取commit记录没有直接的“按用户查”接口。我用的是events接口,筛选action=pushed的事件。这样拿到的是推送记录而不是逐条commit,但对于周报来说够用了。
如果你的GitLab是私有部署的,GITLAB_BASE_URL改成你公司的地址就行。
配合定时任务
把这个脚本加到了Windows计划任务里,每周五下午四点自动跑。跑完之后桌面上就多了一个周报文件。
喝着下午茶,花两分钟补充下周计划,检查一遍措辞,发到OA系统。
以前写周报至少一个半小时。现在从脚本跑完到发出周报,不超过五分钟。
“无他,惟手熟尔”!有需要的用起来!关注微信公众号「Nicholas与Pypi」获取更多Python实战!------加入知识库与更多人一起学习------https://ima.qq.com/wiki/?shareId=f2628818f0874da17b71ffa0e5e8408114e7dbad46f1745bbd1cc1365277631c