Skip to content

Commit a8707f8

Browse files
committedMar 28, 2021
Initial commit
0 parents  commit a8707f8

8 files changed

+197
-0
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea/

‎Dockerfile

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FROM python:3.9-alpine
2+
RUN apk add --no-cache git
3+
COPY . .
4+
ENTRYPOINT [ "/entrypoint.sh" ]

‎README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Pipenv diff
2+
3+
Pull request comments for dependency changes in Pipfile.lock.
4+
5+
![Screenshot](img/screenshot.png)

‎action.yml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: 'Pipenv diff'
2+
description: 'Pull request comments for dependency changes in Pipfile.lock.'
3+
author: 'atwalsh'
4+
inputs:
5+
base-sha:
6+
description: "Git SHA of the bash commit"
7+
required: true
8+
default: ${{ github.event.pull_request.base.sha }}
9+
head-sha:
10+
description: "Git SHA of the head commit"
11+
required: true
12+
default: ${{ github.event.pull_request.head.sha }}
13+
repo-token:
14+
description: 'Token for the repository. Can be passed in using `{{ secrets.GITHUB_TOKEN }}`.'
15+
required: true
16+
default: ${{ github.token }}
17+
runs:
18+
using: 'docker'
19+
image: 'Dockerfile'
20+
args:
21+
- ${{ inputs.base-sha }}
22+
- ${{ inputs.head-sha }}
23+
- ${{ inputs.repo-token }}
24+
branding:
25+
icon: "package"
26+
color: "white"

‎entrypoint.sh

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
printf "Using $(python -V)\n"
3+
pip install -r requirements.txt
4+
python main.py

‎img/screenshot.png

185 KB
Loading

‎main.py

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import json
2+
import os
3+
import subprocess
4+
from typing import Dict
5+
6+
from github import Github
7+
8+
9+
def parse_pipfile_lock() -> Dict:
10+
"""
11+
Parse Pipfile.lock, returning a dict of dependencies with keys as dependency names and values as the version.
12+
13+
:return: Dict of Pipfile.lock dependencies.
14+
"""
15+
deps = {}
16+
with open('Pipfile.lock') as f:
17+
data = json.loads(f.read())
18+
for d_type in ('default', 'develop'):
19+
for name, meta in data[d_type].items():
20+
if 'version' not in meta and 'ref' in meta:
21+
deps[name] = str(meta['ref'][:7]) # For VCS dependencies, show the short commit SHA
22+
else:
23+
deps[name] = str(meta['version']).removeprefix('==')
24+
return deps
25+
26+
27+
def get_added_deps(base_deps, head_deps) -> Dict:
28+
"""
29+
Get newly added dependencies.
30+
31+
:param base_deps: Dict of parsed dependencies from `parse_pipfile_lock` from the base branch.
32+
:param head_deps: Dict of parsed dependencies from `parse_pipfile_lock` from the head branch.
33+
:return: Dict of new dependencies added in the the head branch.
34+
"""
35+
return {k: v for k, v in head_deps.items() if k not in base_deps}
36+
37+
38+
def get_changed_deps(base_deps, head_deps) -> Dict:
39+
"""
40+
Get changed dependencies.
41+
42+
:param base_deps: Dict of parsed dependencies from `parse_pipfile_lock` from the base branch.
43+
:param head_deps: Dict of parsed dependencies from `parse_pipfile_lock` from the head branch.
44+
:return: Dict of changed dependencies added in the the head branch.
45+
"""
46+
if not base_deps:
47+
return {}
48+
data = {}
49+
for name, version in base_deps.items():
50+
if name in head_deps and version != head_deps[name]:
51+
data[name] = {
52+
'base': version,
53+
'head': head_deps[name]
54+
}
55+
return data
56+
57+
58+
def get_removed_deps(base_deps, head_deps) -> Dict:
59+
"""
60+
Get removed dependencies.
61+
62+
:param base_deps: Dict of parsed dependencies from `parse_pipfile_lock` from the base branch.
63+
:param head_deps: Dict of parsed dependencies from `parse_pipfile_lock` from the head branch.
64+
:return: Dict of dependencies removed in the head branch.
65+
"""
66+
return {k: v for k, v in base_deps.items() if k not in head_deps}
67+
68+
69+
def generate_message(changed: dict, added: dict, removed: dict) -> str:
70+
"""
71+
Generate message text for GitHub PR comment.
72+
73+
:param changed: Dict of changed dependencies from `get_changed_deps`.
74+
:param added: Dict of changed dependencies from `get_added_deps`.
75+
:param removed: Dict of changed dependencies from `get_removed_deps`.
76+
:return: GitHub PR comment text.
77+
"""
78+
msg = '<!-- pipenv-diff -->\n\nDependency changes from `Pipfile.lock`:\n\n'
79+
if changed:
80+
txt = '\n'.join(sorted({f'{k} {v["base"]} => {v["head"]}' for k, v in changed.items()}))
81+
msg += f'**Changed**\n```\n{txt.strip()}\n```\n'
82+
if added:
83+
txt = '\n'.join(sorted({f'{k}=={v}' for k, v in added.items()}))
84+
msg += f'**Added**\n```\n{txt.strip()}\n```\n'
85+
if removed:
86+
txt = '\n'.join(sorted({f'{k}=={v}' for k, v in removed.items()}))
87+
msg += f'**Removed**\n```\n{txt.strip()}\n```\n'
88+
89+
return msg
90+
91+
92+
def create_comment(message: str) -> None:
93+
"""
94+
Create or update comment on PR.
95+
96+
:param message: Message text from `generate_message`.
97+
"""
98+
# Load full PR event data file
99+
with open(os.environ['GITHUB_EVENT_PATH']) as f:
100+
event_data = f.read()
101+
event_data = json.loads(event_data)
102+
103+
# Get the GitHub repo and PR
104+
g = Github(os.environ['INPUT_REPO-TOKEN'])
105+
repo = g.get_repo(os.environ['GITHUB_REPOSITORY'])
106+
pull = repo.get_pull(int(event_data['number']))
107+
108+
# Check for existing comment
109+
existing_issue_id = None
110+
for c in pull.get_issue_comments():
111+
if c.body.startswith('<!-- pipenv-diff -->'):
112+
existing_issue_id = c.id
113+
break
114+
115+
# Create or update the PR comment
116+
if existing_issue_id:
117+
comment = pull.get_issue_comment(existing_issue_id)
118+
comment.edit(message)
119+
else:
120+
pull.create_issue_comment(message)
121+
122+
123+
def run():
124+
# Fetch all commits
125+
print('Running `git fetch origin --no-tags --prune --unshallow`')
126+
subprocess.run(['git', 'fetch', 'origin', os.environ['GITHUB_BASE_REF'], '--no-tags', '--prune', '--unshallow'],
127+
stdout=subprocess.PIPE)
128+
129+
# Checkout PR base revision and get dependencies
130+
base_sha = os.environ['INPUT_BASE-SHA']
131+
print(f'Checking out base revision {base_sha}')
132+
subprocess.run(['git', 'checkout', base_sha], stdout=subprocess.PIPE)
133+
base_deps = parse_pipfile_lock()
134+
135+
# Checkout PR head revision and get dependencies
136+
head_sha = os.environ['INPUT_HEAD-SHA']
137+
print(f'Checking out head revision {head_sha}')
138+
subprocess.run(['git', 'checkout', head_sha], stdout=subprocess.PIPE)
139+
head_deps = parse_pipfile_lock()
140+
141+
if head_deps == base_deps:
142+
print('No dependency changes.')
143+
return
144+
145+
# Check for changed, added, and removed dependencies
146+
changed_deps = get_changed_deps(base_deps, head_deps)
147+
added_deps = get_added_deps(base_deps, head_deps)
148+
removed_deps = get_removed_deps(base_deps, head_deps)
149+
150+
# Generate and create PR comment
151+
msg = generate_message(changed_deps, added_deps, removed_deps)
152+
create_comment(msg)
153+
154+
155+
if __name__ == '__main__':
156+
run()

‎requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PyGithub==1.54.1

0 commit comments

Comments
 (0)
Please sign in to comment.