Skip to content

Commit

Permalink
Replace instances of hard-coded repo name with environment variable i…
Browse files Browse the repository at this point in the history
…n GitHub actions (#7818)

Replace instances of hard-coded repo name with environment variable
  • Loading branch information
ocket8888 committed Sep 27, 2023
1 parent 5c1d852 commit 0bc433d
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
# limitations under the License.
#
import os
from typing import Final, Optional
from typing import Final

ENV_GIT_AUTHOR_NAME: Final[str] = "GIT_AUTHOR_NAME"
ENV_GITHUB_REPOSITORY: Final[str] = "GITHUB_REPOSITORY"
ENV_GITHUB_REPOSITORY_OWNER: Final[str] = "GITHUB_REPOSITORY_OWNER"
ENV_GITHUB_SERVER_URL: Final[str] = "GITHUB_SERVER_URL"
ENV_GITHUB_TOKEN: Final[str] = "GITHUB_TOKEN"
ENV_PR_GITHUB_TOKEN: Final[str] = "PR_GITHUB_TOKEN"
ENV_GITHUB_REF_NAME: Final[str] = "GITHUB_REF_NAME"
Expand All @@ -42,7 +43,7 @@ def getenv(env_name: str) -> str:
"""
Gets environment variable :param env_name:
"""
env_var: Optional[str, None] = os.environ.get(env_name)
env_var = os.environ.get(env_name)
if env_var is None:
raise NameError(f"Environment variable {env_name} is not defined")
return env_var
Expand All @@ -51,6 +52,7 @@ def getenv(env_name: str) -> str:
GIT_AUTHOR_NAME: Final[str] = getenv(ENV_GIT_AUTHOR_NAME)
GITHUB_REPOSITORY: Final[str] = getenv(ENV_GITHUB_REPOSITORY)
GITHUB_REPOSITORY_OWNER: Final[str] = getenv(ENV_GITHUB_REPOSITORY_OWNER)
GITHUB_SERVER_URL: Final[str] = getenv(ENV_GITHUB_SERVER_URL)
GITHUB_TOKEN: Final[str] = getenv(ENV_GITHUB_TOKEN)
PR_GITHUB_TOKEN: Final[str] = getenv(ENV_PR_GITHUB_TOKEN)
GITHUB_REF_NAME: Final[str] = getenv(ENV_GITHUB_REF_NAME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
# {DESCRIPTION}
# Collaborators are contributors, other than committers, who have had {ISSUE_THRESHOLD} or more Issue-closing Pull Requests merged
# in the past {SINCE_DAYS_AGO} days. If you want to be an Apache Traffic Control collaborator:
# 1. Read our contribution guidelines at https://github.com/apache/trafficcontrol/blob/master/CONTRIBUTING.md
# 2. Find an Issue to work on: https://github.com/apache/trafficcontrol/issues?q=is:issue+is:open+label:"good+first+issue"+no:assignee
# 1. Read our contribution guidelines at {REPO_URL}/blob/master/CONTRIBUTING.md
# 2. Find an Issue to work on: {REPO_URL}/issues?q=is:issue+is:open+label:"good+first+issue"+no:assignee
# 3. Get coding! For questions on how to contribute, you can reach the ATC community on
# - The #traffic-control channel of the ASF Slack (invite link: https://s.apache.org/tc-slack-request)
# - The ATC Development mailing list: https://trafficcontrol.apache.org/mailing_lists
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ This PR uses [`.asf.yaml`](https://s.apache.org/asfyamltriage) to assign the Git
<hr/>
{EXPIRE} If you want to be an Apache Traffic Control collaborator next month:

1. Read our [contribution guidelines](https://github.com/apache/trafficcontrol/blob/master/CONTRIBUTING.md)
2. Find an Issue to work on (recommended issues have the [good first issue](https://github.com/apache/trafficcontrol/issues?q=is:issue+is:open+label:"good+first+issue"+no:assignee) label) and ask to be assigned
1. Read our [contribution guidelines]({REPO_URL}/blob/master/CONTRIBUTING.md)
2. Find an Issue to work on (recommended issues have the [good first issue]({REPO_URL}/issues?q=is:issue+is:open+label:"good+first+issue"+no:assignee) label) and ask to be assigned
3. Get coding! For questions on how to contribute, you can reach the ATC community on
- The `#traffic-control` channel of the ASF Slack ([invite link](https://s.apache.org/tc-slack-request))
- The ATC Dev [mailing list](https://trafficcontrol.apache.org/mailing_lists) ([archives](https://lists.apache.org/list?dev@trafficcontrol.apache.org:lte=5y:))
<hr/>

## Which Traffic Control components are affected by this PR?
- Other: [`.asf.yaml`](https://github.com/apache/trafficcontrol/blob/master/.asf.yaml)
- Other: [`.asf.yaml`]({REPO_URL}/blob/master/.asf.yaml)

## What is the best way to verify this PR?
Verify that the fixed Issues listed above are linked to [PRs from the past {SINCE_DAYS_AGO} days](https://github.com/apache/trafficcontrol/pulls?q=is:pr+linked:issue+merged:{SINCE_DAY}..{TODAY})
Verify that the fixed Issues listed above are linked to [PRs from the past {SINCE_DAYS_AGO} days]({REPO_URL}/pulls?q=is:pr+linked:issue+merged:{SINCE_DAY}..{TODAY})

## PR submission checklist
- [ ] This PR has tests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,50 @@
import sys
from datetime import date, timedelta
from http.client import NOT_FOUND
from typing import Optional, Final
from typing import Any, Hashable, NotRequired, Optional, Final, TypedDict
from xml.dom import minidom
from xml.dom.minidom import Node, Element

from xml.dom.minidom import Node
import yaml

from github.Commit import Commit
from github.ContentFile import ContentFile
from github.GithubException import GithubException, UnknownObjectException
from github.InputGitAuthor import InputGitAuthor
from github.Issue import Issue
from github.MainClass import Github
from github.NamedUser import NamedUser
from github.Repository import Repository

from assign_triage_role.constants import (GH_TIMELINE_EVENT_TYPE_CROSS_REFERENCE, ASF_YAML_FILE,
APACHE_LICENSE_YAML, GIT_AUTHOR_EMAIL_TEMPLATE, SINGLE_PR_TEMPLATE_FILE,
SINGLE_CONTRIBUTOR_TEMPLATE_FILE, PR_TEMPLATE_FILE, EMPTY_CONTRIB_LIST_LIST,
EMPTY_LIST_OF_CONTRIBUTORS, CONGRATS, EXPIRE, GITHUB_REPOSITORY, GIT_AUTHOR_NAME,
GITHUB_REPOSITORY_OWNER, MINIMUM_COMMITS, SINCE_DAYS_AGO, GITHUB_REF_NAME, PR_GITHUB_TOKEN)
from assign_triage_role.constants import (
GH_TIMELINE_EVENT_TYPE_CROSS_REFERENCE,
ASF_YAML_FILE,
APACHE_LICENSE_YAML,
GIT_AUTHOR_EMAIL_TEMPLATE,
GITHUB_SERVER_URL,
SINGLE_PR_TEMPLATE_FILE,
SINGLE_CONTRIBUTOR_TEMPLATE_FILE,
PR_TEMPLATE_FILE,
EMPTY_CONTRIB_LIST_LIST,
EMPTY_LIST_OF_CONTRIBUTORS,
CONGRATS,
EXPIRE,
GITHUB_REPOSITORY,
GIT_AUTHOR_NAME,
GITHUB_REPOSITORY_OWNER,
MINIMUM_COMMITS,
SINCE_DAYS_AGO,
GITHUB_REF_NAME,
PR_GITHUB_TOKEN
)

class UpdateFileArgs(TypedDict):
path: str
message: str
content: str
sha: str
branch: str

author: NotRequired[InputGitAuthor]
committer: NotRequired[InputGitAuthor]


class TriageRoleAssigner(Github):
Expand All @@ -50,7 +75,7 @@ class TriageRoleAssigner(Github):
target_branch_name: str
owner: str

def __init__(self, *args, **kwargs) -> None:
def __init__( self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
repo_name = GITHUB_REPOSITORY
self.repo = self.get_repo(repo_name)
Expand All @@ -74,8 +99,7 @@ def get_committers(self) -> set[str]:
"""
return {user.login for user in self.repo.get_collaborators() if user.permissions.push}

def prs_by_contributor(self, committers: set[str]) -> dict[
NamedUser, list[(Issue, Issue)]]:
def prs_by_contributor(self, committers: set[str]) -> dict[NamedUser, list[tuple[Issue, Issue]]]:
"""
Returns a dict of Pull Requests, associated by committer, within the last
:var self.since_day: days of :var self.today:.
Expand All @@ -86,7 +110,7 @@ def prs_by_contributor(self, committers: set[str]) -> dict[
query = (f"repo:{repo_name} is:issue linked:pr is:closed closed:"
f"{self.since_day()}..{self.today}")
linked_issues = self.search_issues(query=query)
prs_by_contributor: dict[NamedUser, list[(Issue, Issue)]] = {}
prs_by_contributor: dict[NamedUser, list[tuple[Issue, Issue]]] = {}
for linked_issue in linked_issues:
timeline = linked_issue.get_timeline()
pull_request: Optional[Issue] = None
Expand All @@ -95,14 +119,14 @@ def prs_by_contributor(self, committers: set[str]) -> dict[
continue
if event.event != GH_TIMELINE_EVENT_TYPE_CROSS_REFERENCE:
continue
pr_text: dict[str] = event.raw_data["source"]["issue"]
pr_text: dict[str, Any] = event.raw_data["source"]["issue"]
if "pull_request" not in pr_text:
continue
pull_request = Issue(self.repo.__getattribute__("_requester"), event.raw_headers,
pr_text, completed=True)
# Do not break, in case the Issue has ever been linked to more than 1 PR in the past
if pull_request is None:
raise Exception(
raise LookupError(
f"Unable to find a linked Pull Request for Issue {self.repo.full_name}#{linked_issue.number}")
# Skip unmerged PRs
if "merged_at" not in pull_request.pull_request.raw_data:
Expand All @@ -116,7 +140,7 @@ def prs_by_contributor(self, committers: set[str]) -> dict[
return prs_by_contributor

def ones_who_meet_threshold(self, prs_by_contributor: dict[NamedUser,
list[(Issue, Issue)]]) -> dict[str, list[(Issue, Issue)]]:
list[tuple[Issue, Issue]]]) -> dict[str, list[tuple[Issue, Issue]]]:
"""
Returns a dict of contributors who had at least self.minimum_commits Issue-closing Pull
Requests merged in the past self.since_days_ago days
Expand All @@ -135,24 +159,37 @@ def ones_who_meet_threshold(self, prs_by_contributor: dict[NamedUser,
if len(pull_requests) >= self.minimum_commits
}

def set_collaborators_in_asf_yaml(self, prs_by_contributor: dict[str, list[(Issue, Issue)]],
description: str) -> None:
def set_collaborators_in_asf_yaml(
self,
prs_by_contributor: dict[str, list[tuple[Issue, Issue]]],
description: str,
repo_url: str
) -> None:
"""
Writes the list of collaborators to .asf.yaml
"""
collaborators: list[str] = list(prs_by_contributor)
with open(ASF_YAML_FILE, encoding="utf-8") as stream:
github_key: Final[str] = "github"
collaborators_key: Final[str] = "collaborators"
asf_yaml: dict[str, dict] = yaml.safe_load(stream)
asf_yaml: dict[str, dict[Hashable, Any]] = yaml.safe_load(stream)
if github_key not in asf_yaml:
asf_yaml[github_key]: dict[str, dict]() = {}
asf_yaml[github_key] = {}
asf_yaml[github_key][collaborators_key] = collaborators

with open(os.path.join(os.path.dirname(__file__), APACHE_LICENSE_YAML),
encoding="utf-8") as stream:
apache_license = stream.read().format(DESCRIPTION=description,
ISSUE_THRESHOLD=self.minimum_commits, SINCE_DAYS_AGO=self.since_days_ago)
with open(
os.path.join(
os.path.dirname(__file__),
APACHE_LICENSE_YAML
),
encoding="utf-8"
) as stream:
apache_license = stream.read().format(
DESCRIPTION=description,
ISSUE_THRESHOLD=self.minimum_commits,
SINCE_DAYS_AGO=self.since_days_ago,
REPO_URL=repo_url
)

with open(ASF_YAML_FILE, "w", encoding="utf-8") as stream:
stream.write(apache_license)
Expand All @@ -171,8 +208,10 @@ def push_changes(self, source_branch_name: str, commit_message: str) -> Commit:
with open(ASF_YAML_FILE, encoding="utf-8") as stream:
asf_yaml = stream.read()

asf_yaml_contentfile: ContentFile = self.repo.get_contents(ASF_YAML_FILE, source_branch_ref)
kwargs = {"path": ASF_YAML_FILE,
asf_yaml_contentfile = self.repo.get_contents(ASF_YAML_FILE, source_branch_ref)
if isinstance(asf_yaml_contentfile, list):
asf_yaml_contentfile = asf_yaml_contentfile[0]
kwargs: UpdateFileArgs = {"path": ASF_YAML_FILE,
"message": commit_message,
"content": asf_yaml,
"sha": asf_yaml_contentfile.sha,
Expand All @@ -186,16 +225,20 @@ def push_changes(self, source_branch_name: str, commit_message: str) -> Commit:
except KeyError:
print("Committing using the default author")

commit: Commit = self.repo.update_file(**kwargs).get("commit")
commit = self.repo.update_file(**kwargs).get("commit")
if not isinstance(commit, Commit):
raise TypeError(f"expected a commit, but got: {type(commit)}")
print(f"Updated {ASF_YAML_FILE} on {self.repo.name} branch {source_branch_name}")
return commit

def get_repo_file_contents(self, branch: str) -> str:
"""
Uses the GitHub API to get the contents of .asf.yaml
"""
return self.repo.get_contents(ASF_YAML_FILE,
f"refs/heads/{branch}").decoded_content.rstrip().decode()
asf_file = self.repo.get_contents(ASF_YAML_FILE, f"refs/heads/{branch}")
if isinstance(asf_file, list):
asf_file = asf_file[0]
return asf_file.decoded_content.rstrip().decode()

def branch_exists(self, branch: str) -> bool:
"""
Expand All @@ -210,7 +253,7 @@ def branch_exists(self, branch: str) -> bool:
return False

@staticmethod
def list_of_contributors(prs_by_contributor: dict[str, list[(Issue, Issue)]],
def list_of_contributors(prs_by_contributor: dict[str, list[tuple[Issue, Issue]]],
today: date) -> tuple[str, str, str]:
"""
Returns a list of contributors in a tuple, along with :var congrats: and :var expire:,
Expand All @@ -237,11 +280,13 @@ def remove_comments(pr_body: str) -> str:
"""
Removes comments from the Pull Request body
"""
body: Element = minidom.parseString(f"<body>{pr_body}</body>").firstChild
body = minidom.parseString(f"<body>{pr_body}</body>").firstChild
if body is None:
raise ValueError("failed to parse PR body")
return "".join(node.toxml()
for node in body.childNodes if node.nodeType != Node.COMMENT_NODE)

def get_pr_body(self, prs_by_contributor: dict[str, list[(Issue, Issue)]]) -> str:
def get_pr_body(self, prs_by_contributor: dict[str, list[tuple[Issue, Issue]]]) -> str:
"""
Renders the Pull Request template
"""
Expand All @@ -255,7 +300,7 @@ def get_pr_body(self, prs_by_contributor: dict[str, list[(Issue, Issue)]]) -> st
encoding="utf-8") as stream:
pr_template = stream.read()

def contrib_list(contributor: str, pr_tuples: list[(Issue, Issue)]) -> str:
def contrib_list(contributor: str, pr_tuples: list[tuple[Issue, Issue]]) -> str:
pr_list = "\n".join(
pr_line_template.format(ISSUE_NUMBER=linked_issue.number, PR_NUMBER=pr.number
) for pr, linked_issue in pr_tuples)
Expand All @@ -270,11 +315,19 @@ def contrib_list(contributor: str, pr_tuples: list[(Issue, Issue)]) -> str:
list_of_contributors, congrats, expire = self.list_of_contributors(prs_by_contributor,
self.today)

pr_body = pr_template.format(CONTRIB_LIST_LIST=contrib_list_list,
MONTH=self.today.strftime("%B"), CONGRATS=congrats,
LIST_OF_CONTRIBUTORS=list_of_contributors, EXPIRE=expire,
ISSUE_THRESHOLD=self.minimum_commits, SINCE_DAYS_AGO=self.since_days_ago,
SINCE_DAY=self.since_day(), TODAY=self.today)
repo_url = "/".join((GITHUB_SERVER_URL, GITHUB_REPOSITORY))
pr_body = pr_template.format(
CONTRIB_LIST_LIST=contrib_list_list,
MONTH=self.today.strftime("%B"),
CONGRATS=congrats,
LIST_OF_CONTRIBUTORS=list_of_contributors,
EXPIRE=expire,
ISSUE_THRESHOLD=self.minimum_commits,
SINCE_DAYS_AGO=self.since_days_ago,
SINCE_DAY=self.since_day(),
TODAY=self.today,
REPO_URL=repo_url
)
# If on a fork, do not ping users or reference Issues or Pull Requests
if self.repo.parent is not None:
pr_body = re.sub(r"@(?!trafficcontrol)([A-Za-z0-9]+)", r"@\1", pr_body)
Expand All @@ -283,7 +336,7 @@ def contrib_list(contributor: str, pr_tuples: list[(Issue, Issue)]) -> str:
print("Templated PR body")
return pr_body

def create_pr(self, prs_by_contributor: dict[str, list[(Issue, Issue)]], commit_message: str,
def create_pr(self, prs_by_contributor: dict[str, list[tuple[Issue, Issue]]], commit_message: str,
source_branch_name: str) -> None:
"""
Submits a Pull Request
Expand Down Expand Up @@ -321,7 +374,8 @@ def run(self) -> None:
committers = self.get_committers()
prs_by_contributor = self.ones_who_meet_threshold(self.prs_by_contributor(committers))
description = f"ATC Collaborators for {self.today.strftime('%B %Y')}"
self.set_collaborators_in_asf_yaml(prs_by_contributor, description)
repo_url = "/".join((GITHUB_SERVER_URL, GITHUB_REPOSITORY))
self.set_collaborators_in_asf_yaml(prs_by_contributor, description, repo_url)

source_branch_name: Final[str] = f"collaborators-{self.today.strftime('%Y-%m')}"
commit_message = description
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/assign-triage-role/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ author_email = dev@trafficcontrol.apache.org
classifiers = OSI Approved :: Apache Software License

[options]
python_requires = >=3.10
python_requires = >=3.11
packages = assign_triage_role
install_requires =
PyGithub
Expand Down
2 changes: 1 addition & 1 deletion .github/containers/trafficserver-alpine/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ services:
- linux/amd64
- linux/arm64
# for example, ghcr.io/apache/trafficcontrol/ci/trafficserver-alpine:9.1.2
image: ${CONTAINER:-ghcr.io/apache/trafficcontrol/ci/trafficserver-alpine}:${ATS_VERSION}
image: ${CONTAINER:-ghcr.io/${GITHUB_REPOSITORY}/ci/trafficserver-alpine}:${ATS_VERSION}
4 changes: 2 additions & 2 deletions .github/workflows/assign-triage-role.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ jobs:
uses: actions/checkout@master
if: ${{ (github.repository_owner == 'apache' && github.ref == 'refs/heads/master' ) || github.event_name != 'schedule' }}
id: checkout
- name: Install Python 3.10
- name: Install Python 3.11
uses: actions/setup-python@v2
if: ${{ steps.checkout.outcome == 'success' }}
with: { python-version: '3.10' } # Must be quoted to include the trailing 0
with: { python-version: '3.11' } # Must be quoted to include the trailing 0s of versions that have them
- name: Install assign_triage_role Python module and dependencies
if: ${{ steps.checkout.outcome == 'success' }}
run: pip install .github/actions/assign-triage-role
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/container-trafficserver-alpine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.

name: Container ghcr.io/apache/trafficcontrol/ci/trafficserver-alpine
name: Container ghcr.io/${{ github.repository }}/ci/trafficserver-alpine

env:
CONTAINER: ghcr.io/${{ github.repository }}/ci/trafficserver-alpine
Expand Down

0 comments on commit 0bc433d

Please sign in to comment.