You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2021/02/16 22:39:25 UTC
[trafficcontrol] branch master updated: Workflow to check for and
PR minor Go revision updates (#5506)
This is an automated email from the ASF dual-hosted git repository.
ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new cbf74a9 Workflow to check for and PR minor Go revision updates (#5506)
cbf74a9 is described below
commit cbf74a9f3b23e3ea2e25c0cb67a18038c2426ebc
Author: Zach Hoffman <zr...@apache.org>
AuthorDate: Tue Feb 16 15:39:11 2021 -0700
Workflow to check for and PR minor Go revision updates (#5506)
* Workflow to open a PR if a minor Go version update is available
* Check if the branch or PR exists before trying to create it
* Tests
* Rename workflow
* Quote and explain cron expression
* Add workflow_dispatch trigger
* Do not error out if a "go version" label was not found
* Spaces -> tabs
* Rename GoPRMaker module to go_pr_maker
* Reorder imports
* Add docstrings
* Remove HTML comments
* Revise grammar
* Update golang.org/x/ dependencies after updating the Go version
* If running from the apache organization, only run for the master branch
* Mention in the PR template that the PR also updates golang.org/x/
dependencies
* Remove `f`s from f-strings that can be regular strings
* Remove unused import
---
.github/actions/pr-to-update-go/README.rst | 74 ++++
.../pr-to-update-go/pr_to_update_go/__main__.py | 32 ++
.../pr-to-update-go/pr_to_update_go/constants.py | 23 ++
.../pr-to-update-go/pr_to_update_go/go_pr_maker.py | 390 +++++++++++++++++++++
.../pr-to-update-go/pr_to_update_go/pr_template.md | 79 +++++
.github/actions/pr-to-update-go/requirements.txt | 14 +
.github/actions/pr-to-update-go/setup.cfg | 37 ++
.github/actions/pr-to-update-go/setup.py | 20 ++
.../pr-to-update-go/tests/test_go_pr_maker.py | 36 ++
.../actions/pr-to-update-go/update_golang_org_x.sh | 56 +++
.github/workflows/pr-to-update-go.yml | 48 +++
11 files changed, 809 insertions(+)
diff --git a/.github/actions/pr-to-update-go/README.rst b/.github/actions/pr-to-update-go/README.rst
new file mode 100644
index 0000000..c902404
--- /dev/null
+++ b/.github/actions/pr-to-update-go/README.rst
@@ -0,0 +1,74 @@
+..
+..
+.. Licensed under the Apache License, Version 2.0 (the "License");
+.. you may not use this file except in compliance with the License.
+.. You may obtain a copy of the License at
+..
+.. http://www.apache.org/licenses/LICENSE-2.0
+..
+.. Unless required by applicable law or agreed to in writing, software
+.. distributed under the License is distributed on an "AS IS" BASIS,
+.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.. See the License for the specific language governing permissions and
+.. limitations under the License.
+..
+
+***************
+pr-to-update-go
+***************
+
+Opens a PR if a new minor Go revision is available.
+
+For example, if the ``GO_VERSION`` contains ``1.14.7`` but Go versions 1.15.1 and 1.14.8 are available, it will
+
+1. Create a branch named ``go-1.14.8`` to update the repo's Go version to 1.14.8
+2. Updates all golang.org/x/ dependencies of the project, since these are meant to be updated with the Go compiler.
+3. Open a PR targeting the ``master`` branch from branch ``go-1.14.8``
+
+Other behavior in this scenario:
+
+- If a branch named ``go-1.14.8`` already exists, no additional branch is created.
+- If a PR titled *Update Go version to 1.14.8* already exists, no additional PR is opened.
+
+Environment Variables
+=====================
+
++----------------------------+----------------------------------------------------------------------------------+
+| Environment Variable Name | Value |
++============================+==================================================================================+
+| ``GIT_AUTHOR_NAME`` | Optional. The username to associate with the commit that updates the Go version. |
++----------------------------+----------------------------------------------------------------------------------+
+| ``GITHUB_TOKEN`` | Required. ``${{ github.token }}`` or ``${{ secrets.GITHUB_TOKEN }}`` |
++----------------------------+----------------------------------------------------------------------------------+
+| ``GO_VERSION_FILE`` | Required. The file in the repo containing the version of Go used by the repo. |
++----------------------------+----------------------------------------------------------------------------------+
+
+
+Outputs
+=======
+
+``exit-code``
+-------------
+
+Exit code is 0 unless an error was encountered.
+
+Example usage
+=============
+
+.. code-block:: yaml
+
+ - name: PR to Update Go
+ run: python3 -m pr_to_update_go
+ env:
+ GIT_AUTHOR_NAME: asfgit
+ GITHUB_TOKEN: ${{ github.token }}
+ GO_VERSION_FILE: GO_VERSION
+
+Tests
+=====
+
+To run the unit tests:
+
+.. code-block:: shell
+
+ python3 -m unittest discover ./tests
diff --git a/.github/actions/pr-to-update-go/pr_to_update_go/__main__.py b/.github/actions/pr-to-update-go/pr_to_update_go/__main__.py
new file mode 100644
index 0000000..99b1c09
--- /dev/null
+++ b/.github/actions/pr-to-update-go/pr_to_update_go/__main__.py
@@ -0,0 +1,32 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import os
+import sys
+
+from github.MainClass import Github
+
+from pr_to_update_go.go_pr_maker import GoPRMaker
+from pr_to_update_go.constants import ENV_GITHUB_TOKEN
+
+
+def main() -> None:
+ try:
+ github_token: str = os.environ[ENV_GITHUB_TOKEN]
+ except KeyError:
+ print(f'Environment variable {ENV_GITHUB_TOKEN} must be defined.')
+ sys.exit(1)
+ gh = Github(login_or_token=github_token)
+ GoPRMaker(gh).run()
+
+
+main()
diff --git a/.github/actions/pr-to-update-go/pr_to_update_go/constants.py b/.github/actions/pr-to-update-go/pr_to_update_go/constants.py
new file mode 100644
index 0000000..b3d8770
--- /dev/null
+++ b/.github/actions/pr-to-update-go/pr_to_update_go/constants.py
@@ -0,0 +1,23 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from typing import Final
+
+ENV_GIT_AUTHOR_NAME: Final = 'GIT_AUTHOR_NAME'
+ENV_GITHUB_REPOSITORY: Final = 'GITHUB_REPOSITORY'
+ENV_GITHUB_REPOSITORY_OWNER: Final = 'GITHUB_REPOSITORY_OWNER'
+ENV_GITHUB_TOKEN: Final = 'GITHUB_TOKEN'
+ENV_GO_VERSION_FILE: Final = 'GO_VERSION_FILE'
+GIT_AUTHOR_EMAIL_TEMPLATE: Final = '{git_author_name}@users.noreply.github.com'
+GO_REPO_NAME: Final = 'golang/go'
+GO_VERSION_URL: Final = 'https://golang.org/dl/?mode=json'
+RELEASE_PAGE_URL: Final = 'https://golang.org/doc/devel/release.html'
diff --git a/.github/actions/pr-to-update-go/pr_to_update_go/go_pr_maker.py b/.github/actions/pr-to-update-go/pr_to_update_go/go_pr_maker.py
new file mode 100644
index 0000000..ada3672
--- /dev/null
+++ b/.github/actions/pr-to-update-go/pr_to_update_go/go_pr_maker.py
@@ -0,0 +1,390 @@
+#!/usr/bin/env python3
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""
+Generate pull requests that update a repository's Go version.
+
+Classes:
+
+ GoPRMaker
+
+"""
+import json
+import os
+import re
+import subprocess
+import sys
+from typing import Union
+
+import requests
+from github.Branch import Branch
+from github.Commit import Commit
+from github.GitCommit import GitCommit
+from github.GitTree import GitTree
+from github.GithubObject import NotSet
+from github.InputGitTreeElement import InputGitTreeElement
+from github.Requester import Requester
+
+from requests import Response
+
+from github.GithubException import BadCredentialsException, GithubException, UnknownObjectException
+from github.InputGitAuthor import InputGitAuthor
+from github.Label import Label
+from github.MainClass import Github
+from github.Milestone import Milestone
+from github.PaginatedList import PaginatedList
+from github.PullRequest import PullRequest
+from github.Repository import Repository
+
+from pr_to_update_go.constants import ENV_GITHUB_TOKEN, GO_VERSION_URL, ENV_GITHUB_REPOSITORY, \
+ ENV_GITHUB_REPOSITORY_OWNER, GO_REPO_NAME, RELEASE_PAGE_URL, ENV_GO_VERSION_FILE, \
+ ENV_GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL_TEMPLATE
+
+
+class GoPRMaker:
+ """
+ A class to generate pull requests for the purpose of updating the Go version in a repository.
+ """
+ gh: Github
+ latest_go_version: str
+ repo: Repository
+ author: InputGitAuthor
+
+ def __init__(self, gh: Github) -> None:
+ """
+ :param gh: Github
+ :rtype: None
+ """
+ self.gh = gh
+ repo_name: str = self.get_repo_name()
+ self.repo = self.get_repo(repo_name)
+
+ try:
+ git_author_name = self.getenv(ENV_GIT_AUTHOR_NAME)
+ git_author_email = GIT_AUTHOR_EMAIL_TEMPLATE.format(git_author_name=git_author_name)
+ self.author = InputGitAuthor(git_author_name, git_author_email)
+ except KeyError:
+ self.author = NotSet
+ print('Will commit using the default author')
+
+ def branch_exists(self, branch: str) -> bool:
+ """
+ :param branch:
+ :type branch:
+ :return:
+ :rtype: bool
+ """
+ try:
+ repo_go_version = self.get_repo_go_version(branch)
+ if self.latest_go_version == repo_go_version:
+ print(f'Branch {branch} already exists')
+ return True
+ except GithubException as e:
+ message = e.data.get('message')
+ if not re.match(r'No commit found for the ref', message):
+ raise e
+ return False
+
+ def update_branch(self, branch_name: str, sha: str) -> None:
+ """
+ :param branch_name:
+ :type branch_name:
+ :param sha:
+ :type sha:
+ :return:
+ :rtype: None
+ """
+ requester: Requester = self.repo._requester
+ patch_parameters = {
+ 'sha': sha,
+ }
+ requester.requestJsonAndCheck(
+ 'PATCH', self.repo.url + f'/git/refs/heads/{branch_name}', input=patch_parameters
+ )
+ return
+
+ def run(self) -> None:
+ """
+ :return:
+ :rtype: None
+ """
+ repo_go_version = self.get_repo_go_version()
+ self.latest_go_version = self.get_latest_major_upgrade(repo_go_version)
+ commit_message: str = f'Update Go version to {self.latest_go_version}'
+
+ source_branch_name: str = f'go-{self.latest_go_version}'
+ target_branch: str = 'master'
+ if repo_go_version == self.latest_go_version:
+ print(f'Go version is up-to-date on {target_branch}, nothing to do.')
+ return
+
+ if not self.branch_exists(source_branch_name):
+ commit: Commit = self.set_go_version(self.latest_go_version, commit_message,
+ source_branch_name)
+ update_golang_org_x_commit: Union[GitCommit, None] = self.update_golang_org_x(commit)
+ if isinstance(update_golang_org_x_commit, GitCommit):
+ sha: str = update_golang_org_x_commit.sha
+ self.update_branch(source_branch_name, sha)
+
+ owner: str = self.get_repo_owner()
+ self.create_pr(self.latest_go_version, commit_message, owner, source_branch_name,
+ target_branch)
+
+ @staticmethod
+ def getenv(env_name: str) -> str:
+ """
+ :param env_name: str
+ :return:
+ :rtype: str
+ """
+ return os.environ[env_name]
+
+ def get_repo(self, repo_name: str) -> Repository:
+ """
+ :param repo_name: str
+ :return:
+ :rtype: Repository
+ """
+ try:
+ repo: Repository = self.gh.get_repo(repo_name)
+ except BadCredentialsException:
+ print(f'Credentials from {ENV_GITHUB_TOKEN} were bad.')
+ sys.exit(1)
+ return repo
+
+ @staticmethod
+ def get_major_version(from_go_version: str) -> str:
+ """
+ :param from_go_version: str
+ :return:
+ :rtype: str
+ """
+ return re.search(pattern=r'^\d+\.\d+', string=from_go_version).group(0)
+
+ def get_latest_major_upgrade(self, from_go_version: str) -> str:
+ """
+ :param from_go_version: str
+ :return:
+ :rtype: str
+ """
+ major_version = self.get_major_version(from_go_version)
+ go_version_response: Response = requests.get(GO_VERSION_URL)
+ go_version_response.raise_for_status()
+ go_version_content: list = json.loads(go_version_response.content)
+ index = 0
+ fetched_go_version: str = ''
+ while True:
+ if not go_version_content[index]['stable']:
+ continue
+ go_version_name: str = go_version_content[index]['version']
+ fetched_go_version = re.search(pattern=r'[\d.]+', string=go_version_name).group(0)
+ if major_version == self.get_major_version(fetched_go_version):
+ break
+ index += 1
+ if major_version != self.get_major_version(fetched_go_version):
+ raise Exception(f'No supported {major_version} Go versions exist.')
+ print(f'Latest version of Go {major_version} is {fetched_go_version}')
+ return fetched_go_version
+
+ def get_repo_name(self) -> str:
+ """
+ :return:
+ :rtype: str
+ """
+ repo_name: str = self.getenv(ENV_GITHUB_REPOSITORY)
+ return repo_name
+
+ def get_repo_owner(self) -> str:
+ """
+ :return:
+ :rtype: str
+ """
+ repo_name: str = self.getenv(ENV_GITHUB_REPOSITORY_OWNER)
+ return repo_name
+
+ def get_go_milestone(self, go_version: str) -> str:
+ """
+ :param go_version: str
+ :return:
+ """
+ go_repo: Repository = self.get_repo(GO_REPO_NAME)
+ milestones: PaginatedList[Milestone] = go_repo.get_milestones(state='all', sort='due_on',
+ direction='desc')
+ milestone_title = f'Go{go_version}'
+ for milestone in milestones: # type: Milestone
+ if milestone.title == milestone_title:
+ print(f'Found Go milestone {milestone.title}')
+ return milestone.raw_data.get('html_url')
+ raise Exception(f'Could not find a milestone named {milestone_title}.')
+
+ @staticmethod
+ def get_release_notes_page() -> str:
+ """
+ :return:
+ :rtype: str
+ """
+ release_history_response: Response = requests.get(RELEASE_PAGE_URL)
+ release_history_response.raise_for_status()
+ return release_history_response.content.decode()
+
+ @staticmethod
+ def get_release_notes(go_version: str, release_notes_content: str) -> str:
+ """
+ :param go_version: str
+ :param release_notes_content: str
+ :return:
+ :rtype: str
+ """
+ go_version_pattern = go_version.replace('.', '\\.')
+ release_notes_pattern: str = f'<p>\\s*\\n\\s*go{go_version_pattern}.*?</p>'
+ release_notes_matches = re.search(release_notes_pattern, release_notes_content,
+ re.MULTILINE | re.DOTALL)
+ if release_notes_matches is None:
+ raise Exception(f'Could not find release notes on {RELEASE_PAGE_URL}')
+ release_notes = re.sub(r'[\s\t]+', ' ', release_notes_matches.group(0))
+ return release_notes
+
+ def get_pr_body(self, go_version: str, milestone_url: str) -> str:
+ """
+ :param go_version: str
+ :param milestone_url: str
+ :return:
+ :rtype: str
+ """
+ with open(os.path.dirname(__file__) + '/pr_template.md') as file:
+ pr_template = file.read()
+ go_major_version = self.get_major_version(go_version)
+
+ release_notes = self.get_release_notes(go_version, self.get_release_notes_page())
+ pr_body: str = pr_template.format(GO_VERSION=go_version, GO_MAJOR_VERSION=go_major_version,
+ RELEASE_NOTES=release_notes, MILESTONE_URL=milestone_url)
+ print('Templated PR body')
+ return pr_body
+
+ def get_repo_go_version(self, branch: str = 'master') -> str:
+ """
+ :param branch: str
+ :return:
+ :rtype: str
+ """
+ return self.repo.get_contents(self.getenv(ENV_GO_VERSION_FILE),
+ f'refs/heads/{branch}').decoded_content.rstrip().decode()
+
+ def set_go_version(self, go_version: str, commit_message: str,
+ source_branch_name: str) -> Commit:
+ """
+ :param go_version: str
+ :param commit_message: str
+ :param source_branch_name: str
+ :return:
+ :rtype: str
+ """
+ master: Branch = self.repo.get_branch('master')
+ sha: str = master.commit.sha
+ ref: str = f'refs/heads/{source_branch_name}'
+ self.repo.create_git_ref(ref, sha)
+
+ print(f'Created branch {source_branch_name}')
+ go_version_file: str = self.getenv(ENV_GO_VERSION_FILE)
+ go_file_contents = self.repo.get_contents(go_version_file, ref)
+ kwargs = {'path': go_version_file,
+ 'message': commit_message,
+ 'content': (go_version + '\n'),
+ 'sha': go_file_contents.sha,
+ 'branch': source_branch_name,
+ }
+ try:
+ git_author_name = self.getenv(ENV_GIT_AUTHOR_NAME)
+ git_author_email = GIT_AUTHOR_EMAIL_TEMPLATE.format(git_author_name=git_author_name)
+ author: InputGitAuthor = InputGitAuthor(name=git_author_name, email=git_author_email)
+ kwargs['author'] = author
+ kwargs['committer'] = author
+ except KeyError:
+ print('Committing using the default author')
+
+ commit: Commit = self.repo.update_file(**kwargs).get('commit')
+ print(f'Updated {go_version_file} on {self.repo.name}')
+ return commit
+
+ def update_golang_org_x(self, previous_commit: Commit) -> Union[GitCommit, None]:
+ """
+ :param previous_commit:
+ :type previous_commit:
+ :return:
+ :rtype: Union[GitCommit, None]
+ """
+ subprocess.run(['git', 'fetch', 'origin'], check=True)
+ subprocess.run(['git', 'checkout', previous_commit.sha], check=True)
+ script_path: str = '.github/actions/pr-to-update-go/update_golang_org_x.sh'
+ subprocess.run([script_path], check=True)
+ files_to_check: list[str] = ['go.mod', 'go.sum', 'vendor/modules.txt']
+ tree_elements: list[InputGitTreeElement] = []
+ for file in files_to_check:
+ diff_process = subprocess.run(['git', 'diff', '--exit-code', '--', file])
+ if diff_process.returncode == 0:
+ continue
+ with open(file) as stream:
+ content: str = stream.read()
+ tree_element: InputGitTreeElement = InputGitTreeElement(path=file, mode='100644',
+ type='blob', content=content)
+ tree_elements.append(tree_element)
+ if len(tree_elements) == 0:
+ print('No golang.org/x/ dependencies need to be updated.')
+ return
+ tree_hash = subprocess.check_output(
+ ['git', 'log', '-1', '--pretty=%T', previous_commit.sha]).decode().strip()
+ base_tree: GitTree = self.repo.get_git_tree(sha=tree_hash)
+ tree: GitTree = self.repo.create_git_tree(tree_elements, base_tree)
+ commit_message: str = f'Update golang.org/x/ dependencies for go{self.latest_go_version}'
+ previous_git_commit: GitCommit = self.repo.get_git_commit(previous_commit.sha)
+ git_commit: GitCommit = self.repo.create_git_commit(message=commit_message, tree=tree,
+ parents=[previous_git_commit],
+ author=self.author, committer=self.author)
+ print('Updated golang.org/x/ dependencies')
+ return git_commit
+
+ def create_pr(self, latest_go_version: str, commit_message: str, owner: str,
+ source_branch_name: str, target_branch: str) -> None:
+ """
+ :param latest_go_version: str
+ :param commit_message: str
+ :param owner: str
+ :param source_branch_name: str
+ :param target_branch: str
+ :return:
+ :rtype: None
+ """
+ prs: PaginatedList = self.gh.search_issues(
+ f'repo:{self.repo.full_name} is:pr is:open head:{source_branch_name}')
+ for list_item in prs:
+ pr: PullRequest = self.repo.get_pull(list_item.number)
+ if pr.head.ref != source_branch_name:
+ continue
+ print(f'Pull request for branch {source_branch_name} already exists:\n{pr.html_url}')
+ return
+
+ milestone_url: str = self.get_go_milestone(latest_go_version)
+ pr_body: str = self.get_pr_body(latest_go_version, milestone_url)
+ pr: PullRequest = self.repo.create_pull(
+ title=commit_message,
+ body=pr_body,
+ head=f'{owner}:{source_branch_name}',
+ base=target_branch,
+ maintainer_can_modify=True,
+ )
+ try:
+ go_version_label: Label = self.repo.get_label('go version')
+ pr.add_to_labels(go_version_label)
+ except UnknownObjectException:
+ print('Unable to find a label named "go version".')
+ print(f'Created pull request {pr.html_url}')
diff --git a/.github/actions/pr-to-update-go/pr_to_update_go/pr_template.md b/.github/actions/pr-to-update-go/pr_to_update_go/pr_template.md
new file mode 100644
index 0000000..a3ecdd0
--- /dev/null
+++ b/.github/actions/pr-to-update-go/pr_to_update_go/pr_template.md
@@ -0,0 +1,79 @@
+## What does this PR (Pull Request) do?
+- [x] This PR is not related to any Issue
+
+This PR makes the Go components of Traffic Control build using Go version {GO_VERSION} and updates the `golang.org/x/` dependencies..
+
+See the Go {GO_VERSION} [release notes](https://golang.org/doc/devel/release.html#go{GO_MAJOR_VERSION}):
+
+<!--
+The release notes are licensed with the Go license.
+
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+> {RELEASE_NOTES}
+
+## Which Traffic Control components are affected by this PR?
+- CDN in a Box - Enroller
+- Grove
+- Traffic Control Client (Go)
+- Traffic Monitor
+- Traffic Ops
+- Traffic Ops ORT
+- Traffic Stats
+- CI tests for Go components
+
+## What is the best way to verify this PR?
+Run unit tests and API tests. Since this is only a patch-level version update, [the only changes]({MILESTONE_URL}?closed=1) were bugfixes. Breaking changes would be unexpected.
+
+## The following criteria are ALL met by this PR
+- [x] Existing tests are sufficient, no additional tests necessary
+- [x] The documentation only mentions the major Go version, no documentation updates necessary.
+- [x] The changelog already mentions updating to Go {GO_MAJOR_VERSION}, no additional changelog message necessary.
+- [x] This PR includes any and all required license headers
+- [x] This PR does not include a database migration
+- [x] This PR **DOES NOT FIX A SERIOUS SECURITY VULNERABILITY** (see [the Apache Software Foundation's security guidelines](https://www.apache.org/security/) for details)
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
diff --git a/.github/actions/pr-to-update-go/requirements.txt b/.github/actions/pr-to-update-go/requirements.txt
new file mode 100644
index 0000000..05cf2ea
--- /dev/null
+++ b/.github/actions/pr-to-update-go/requirements.txt
@@ -0,0 +1,14 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+PyGithub
+requests
\ No newline at end of file
diff --git a/.github/actions/pr-to-update-go/setup.cfg b/.github/actions/pr-to-update-go/setup.cfg
new file mode 100644
index 0000000..167f562
--- /dev/null
+++ b/.github/actions/pr-to-update-go/setup.cfg
@@ -0,0 +1,37 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+[metadata]
+name = pr-to-update-go
+version = 0.0.0
+description = Opens a PR if a new minor Go revision is available.
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+author = Apache Traffic Control
+author_email = dev@trafficcontrol.apache.org
+classifiers = OSI Approved :: Apache Software License
+
+[options]
+python_requires = >=3.9
+packages = pr_to_update_go
+install_requires =
+ PyGithub
+ requests
+
+[options.entry_points]
+console_scripts = pr-to-update-go = pr_to_update_go:main
+
+[options.extras_require]
+test = unittest
+
+[options.package_data]
+pr_to_update_go = pr_template.md
diff --git a/.github/actions/pr-to-update-go/setup.py b/.github/actions/pr-to-update-go/setup.py
new file mode 100755
index 0000000..0b9856c
--- /dev/null
+++ b/.github/actions/pr-to-update-go/setup.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+The setuptools-based install script for the pr-to-update-go GitHub Action
+"""
+from setuptools import setup
+
+setup()
diff --git a/.github/actions/pr-to-update-go/tests/test_go_pr_maker.py b/.github/actions/pr-to-update-go/tests/test_go_pr_maker.py
new file mode 100644
index 0000000..1e0c378
--- /dev/null
+++ b/.github/actions/pr-to-update-go/tests/test_go_pr_maker.py
@@ -0,0 +1,36 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from unittest import TestCase
+
+from pr_to_update_go.go_pr_maker import GoPRMaker
+
+
+class TestGoPRMaker(TestCase):
+ def test_get_major_version(self) -> None:
+ version: str = '1.2.3'
+ expected_major_version: str = '1.2'
+ actual_major_version: str = GoPRMaker.get_major_version(version)
+ self.assertEqual(expected_major_version, actual_major_version)
+ return
+
+ def test_get_release_notes(self) -> None:
+ go_version: str = '4.15.6'
+ expected_release_notes: str = f'<p> go4.15.6 The expected release notes </p>'
+ release_notes_with_whitespace: str = f"""<p>
+ go{go_version} The expected release notes
+ </p>"""
+ content: str = f"""go4.15.5 text before
+ {release_notes_with_whitespace}
+ text <p>after</p> 4.15.7"""
+ actual_release_notes: str = GoPRMaker.get_release_notes(go_version, content)
+ self.assertEqual(expected_release_notes, actual_release_notes)
diff --git a/.github/actions/pr-to-update-go/update_golang_org_x.sh b/.github/actions/pr-to-update-go/update_golang_org_x.sh
new file mode 100755
index 0000000..4410b82
--- /dev/null
+++ b/.github/actions/pr-to-update-go/update_golang_org_x.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -o errexit -o nounset
+trap 'echo "Error on line ${LINENO} of ${0}"; exit 1' ERR
+
+# download_go downloads and installs the GO version specified in GO_VERSION
+download_go() {
+ . build/functions.sh
+ if verify_and_set_go_version; then
+ return
+ fi
+ go_version="$(cat "${GITHUB_WORKSPACE}/GO_VERSION")"
+ wget -O go.tar.gz "https://dl.google.com/go/go${go_version}.linux-amd64.tar.gz" --no-verbose
+ echo "Extracting Go ${go_version}..."
+ <<-'SUDO_COMMANDS' sudo sh
+ set -o errexit
+ go_dir="$(command -v go | xargs realpath | xargs dirname | xargs dirname)"
+ mv "$go_dir" "${go_dir}.unused"
+ tar -C /usr/local -xzf go.tar.gz
+ SUDO_COMMANDS
+ rm go.tar.gz
+ go version
+}
+
+GOROOT=/usr/local/go
+export PATH="${PATH}:${GOROOT}/bin"
+export GOPATH="${HOME}/go"
+
+download_go
+
+# update all golang.org/x dependencies in go.mod/go.sum
+go get -u \
+ golang.org/x/crypto \
+ golang.org/x/net \
+ golang.org/x/sys \
+ golang.org/x/text \
+ golang.org/x/xerrors
+
+# update vendor/modules.txt
+go mod vendor -v
diff --git a/.github/workflows/pr-to-update-go.yml b/.github/workflows/pr-to-update-go.yml
new file mode 100644
index 0000000..fb8575a
--- /dev/null
+++ b/.github/workflows/pr-to-update-go.yml
@@ -0,0 +1,48 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Is it time to update Go?
+
+on:
+ # run manually
+ workflow_dispatch:
+ schedule:
+ # 14:00 UTC every day
+ - cron: '0 14 * * *'
+
+jobs:
+ pr-to-update-go:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@master
+ if: ${{ (github.repository_owner == 'apache' && github.ref == 'refs/heads/master' ) || github.event_name != 'schedule' }}
+ id: checkout
+ - name: Install Python 3.9
+ uses: actions/setup-python@v2
+ if: ${{ steps.checkout.outcome == 'success' }}
+ with: { python-version: 3.9 }
+ - name: Install dependencies
+ if: ${{ steps.checkout.outcome == 'success' }}
+ run: pip install .github/actions/pr-to-update-go
+ - name: PR to Update Go
+ if: ${{ steps.checkout.outcome == 'success' }}
+ run: python3 -m pr_to_update_go
+ env:
+ GIT_AUTHOR_NAME: asfgit
+ GITHUB_TOKEN: ${{ github.token }}
+ GO_VERSION_FILE: GO_VERSION