-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Setup github action to aggregate info for account-recovery requests #4389
Open
djwooten
wants to merge
9
commits into
pypi:main
Choose a base branch
from
djwooten:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+676
−0
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
062c71e
setup test action
a5ec1c4
moved workflow
9cad05a
get the correct issue tracking repo information through the action
c4008b9
typo
f3649b8
actually post comment
06ad72b
improve summary table and clean utils
59c70d2
remove local debug code
93852da
re-enable labeling
aa93a63
cleanup
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
name: Issue Label Trigger | ||
|
||
on: | ||
issues: | ||
types: [labeled] | ||
|
||
jobs: | ||
parse-issue: | ||
runs-on: ubuntu-latest | ||
if: contains(github.event.issue.labels.*.name, 'account-recovery') | ||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v2 | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: '3.x' | ||
|
||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install -r .github/workflows/autoreplies/requirements.txt | ||
|
||
- name: Run Python script | ||
run: python .github/workflows/autoreplies/check_account_recovery.py | ||
env: | ||
ISSUE_NUMBER: ${{ github.event.issue.number }} | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
GITHUB_ISSUE_OWNER: ${{ github.repository_owner }} | ||
GITHUB_ISSUE_REPO: ${{ github.event.repository.name }} |
Empty file.
187 changes: 187 additions & 0 deletions
187
.github/workflows/autoreplies/check_account_recovery.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
"""Parse a GitHub issue to automatically aggregate package ownership information to facilitate account recovery. | ||
|
||
Steps | ||
1) finds all PyPI packages maintained by the user | ||
2) checks each PyPI package to see if its source code repository listed at PyPI belongs to the github user | ||
3) adds a comment to the issue summarizing the package ownership information | ||
|
||
If the github user owns the source code repositories for all of the PyPI packages, or is an administrator for the github | ||
organization that owns them, then the issue is automatically labeled with "fasttrack". | ||
|
||
Environment Variables | ||
--------------------- | ||
GITHUB_ISSUE_OWNER | ||
The owner (e.g., "pypi") of the issue repository | ||
|
||
GITHUB_ISSUE_REPO | ||
The repository (e.g., "support") where the issue is located | ||
|
||
ISSUE_NUMBER | ||
The number of the issue to process | ||
|
||
GITHUB_TOKEN | ||
(Optional) A GitHub token with permissions to comment on the issue and read the repository. | ||
""" | ||
|
||
import os | ||
import sys | ||
|
||
import pypi_utils | ||
import gh_utils | ||
|
||
|
||
# Issue body headers | ||
PYPI_USER_HEADER = "PyPI Username" | ||
|
||
# Ownership status levels | ||
BELONGS = 0 | ||
ORG_ADMIN = 1 | ||
ORG_MEMBER = 2 | ||
UNKNOWN_OWERNSHIP = 3 | ||
NO_REPO = 4 | ||
|
||
# This notice indicates that the final determination of account recovery rests with the PyPI team | ||
BOT_NOTICE = ( | ||
"### NOTE\n\n" | ||
"_This action was performed automatically by a bot and **does not guarantee account recovery**. Account recovery" | ||
" requires manual approval processing by the PyPI team._" | ||
) | ||
|
||
|
||
def sanitize_pypi_user(username: str) -> str: | ||
"""Remove any backticks from the username. | ||
|
||
Some users write their usernames like: | ||
`username` | ||
for pretty markdown purposes, but we don't want the backticks. | ||
""" | ||
return username.strip().replace("`", "") | ||
|
||
|
||
def format_markdown_table(rows: list) -> str: | ||
"""Format a list of rows into a markdown table. | ||
|
||
Parameters | ||
---------- | ||
rows: list | ||
A list of rows to format into a table. Each row should be [package_link, repo_url, ownership_level] where | ||
ownership_level is an int indicating which column to mark with an "X". | ||
""" | ||
header = ["Package", "Repository", "Owner", "Admin", "Member", "Unknown", "No Repo"] | ||
row_strings = [] | ||
row_strings.append(" | ".join(header)) | ||
row_strings.append(" | ".join(["---"] * 2 + [":-:"] * (len(header) - 2))) | ||
for row in rows: | ||
row_fields = [""] * len(header) | ||
row_fields[0] = row[0] | ||
row_fields[1] = row[1] | ||
row_fields[2 + row[2]] = "X" | ||
row_strings.append(" | ".join(row_fields)) | ||
return "\n".join(row_strings) | ||
|
||
|
||
def format_markdown_package_link(package_name: str) -> str: | ||
return f"[{package_name}](https://pypi.org/project/{package_name})" | ||
|
||
|
||
def format_markdown_pypi_user_link(pypi_user: str) -> str: | ||
return f"[{pypi_user}](https://pypi.org/user/{pypi_user}/)" | ||
|
||
|
||
def format_markdown_gh_user_link(gh_user: str) -> str: | ||
return f"[{gh_user}](https://github.com/{gh_user}/)" | ||
|
||
|
||
if __name__ == "__main__": | ||
issue_number = os.environ.get("ISSUE_NUMBER", "4386") | ||
github_token = os.environ.get("GITHUB_TOKEN", None) | ||
github_issue_owner = os.environ.get("GITHUB_ISSUE_OWNER", "pypi") | ||
github_issue_repo = os.environ.get("GITHUB_ISSUE_REPO", "support") | ||
|
||
issue_data = gh_utils.fetch_issue_details( | ||
github_issue_owner, github_issue_repo, issue_number, github_token=github_token | ||
) | ||
|
||
gh_user = issue_data["user"] | ||
gh_user_link = format_markdown_gh_user_link(gh_user) | ||
|
||
if PYPI_USER_HEADER not in issue_data["body"]: | ||
raise ValueError(f"Issue body does not contain expected header: {PYPI_USER_HEADER}") | ||
|
||
pypi_user = sanitize_pypi_user(issue_data["body"]["PyPI Username"]) | ||
pypi_user_link = format_markdown_pypi_user_link(pypi_user) | ||
|
||
try: | ||
packages = pypi_utils.get_packages_by_user(pypi_user) | ||
except ValueError as e: | ||
raise e | ||
|
||
# If the pypi user is not a maintainer for any packages | ||
if not packages: | ||
gh_utils.add_issue_comment( | ||
f"User {pypi_user_link} has no packages", | ||
github_issue_owner, | ||
github_issue_repo, | ||
issue_number, | ||
github_token=github_token, | ||
) | ||
sys.exit() | ||
|
||
# Loop over all packages to see if they belong to the user | ||
package_ownership = [] # List of [package_name, repo_url, ownership_status] | ||
for package_name in packages: | ||
pypi_package_link = format_markdown_package_link(package_name) | ||
package = pypi_utils.get_pypi_project_info(package_name) | ||
|
||
# Package has source code repo listed at PyPI | ||
if "repository_url" not in package or not package["repository_url"]: | ||
package_ownership.append([pypi_package_link, "", NO_REPO]) | ||
continue | ||
|
||
package_repo_url = package["repository_url"] | ||
|
||
# Package source repo directly belongs to the gh_user | ||
if gh_utils.does_user_own_repo(package_repo_url, gh_user): | ||
package_ownership.append([pypi_package_link, package_repo_url, BELONGS]) | ||
continue | ||
|
||
# If package source repo belongs to an organization - check if the gh_user is a member or admin | ||
org_status = gh_utils.get_user_role_in_org(package_repo_url, gh_user) | ||
if org_status == "admin": | ||
package_ownership.append([pypi_package_link, package_repo_url, ORG_ADMIN]) | ||
elif org_status == "member": | ||
package_ownership.append([pypi_package_link, package_repo_url, ORG_MEMBER]) | ||
|
||
# Otherwise the source repo may not belong to the gh_user | ||
else: | ||
package_ownership.append([pypi_package_link, package_repo_url, UNKNOWN_OWERNSHIP]) | ||
|
||
# Add a comment to the issue with the package ownership information | ||
table = format_markdown_table(package_ownership) | ||
|
||
# Count how many packages are not owned or administered by the user | ||
num_unverified = len([row for row in package_ownership if row[2] > ORG_ADMIN]) | ||
|
||
if num_unverified == 0: | ||
label = "fasttrack" | ||
else: | ||
label = "" | ||
Comment on lines
+162
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know what your policy would call for here. This code considers repos to be owned by the user if they are directly owned, or if they belong to an organization that the github user is an admin for. |
||
|
||
comment = "\n\n".join(["### Package Ownership", table, BOT_NOTICE]) | ||
|
||
try: | ||
gh_utils.add_issue_comment( | ||
comment, github_issue_owner, github_issue_repo, issue_number, github_token=github_token | ||
) | ||
except Exception as e: | ||
print(f"Failed to add comment to issue {issue_number}: {e}") | ||
print("Comment:") | ||
print(comment) | ||
|
||
if label: | ||
try: | ||
gh_utils.add_label_to_issue( | ||
label, github_issue_owner, github_issue_repo, issue_number, github_token=github_token | ||
) | ||
except Exception as e: | ||
print(f"Failed to add label to issue {issue_number}: {e}") |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a notice like this is important so that people understand that the github action isn't actually able to recover anybody's account for them.