Skip to content
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

Sync improvement addressing #787 #821

Merged
merged 38 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6674751
initial commit new sync branch
KevinMenden Jan 7, 2021
88315c5
added function barebones
KevinMenden Jan 7, 2021
80947b2
create sync PRs from extra branch instead of TEMPLATE
KevinMenden Jan 7, 2021
3e1cb91
looking for open PRs
KevinMenden Jan 7, 2021
23f8169
sucessfully closing open PRs
KevinMenden Jan 8, 2021
ac41553
delete PR update function
KevinMenden Jan 8, 2021
ee32afb
typo
KevinMenden Jan 8, 2021
563b836
raise error if branch exists
KevinMenden Jan 8, 2021
cd94c1c
Create numbered new branch if already existing
KevinMenden Jan 12, 2021
b6510af
fixed automated branch naming
KevinMenden Jan 12, 2021
e19c324
add comment to closed PRs
KevinMenden Jan 12, 2021
ad323dc
fixed typos
KevinMenden Jan 12, 2021
04ed298
update sync tests
KevinMenden Jan 12, 2021
532b255
added github auth token to create-lint-wf action
KevinMenden Jan 18, 2021
ec3885b
testing different token
KevinMenden Feb 1, 2021
e399f8b
revert token change
KevinMenden Feb 1, 2021
61644ab
test again with nf-core bot token
KevinMenden Feb 1, 2021
ef970fe
removed assert statement
KevinMenden Feb 8, 2021
ad3b277
removed second assert statement
KevinMenden Feb 8, 2021
5645ef2
testing without GITHUB_TOKEN
KevinMenden Feb 8, 2021
d60d7e2
bufix
KevinMenden Feb 8, 2021
53ecbcc
removed remaining assert statements
KevinMenden Feb 8, 2021
e2cb5b2
switched exception to log.error
KevinMenden Feb 8, 2021
d3a748f
updated changelog
KevinMenden Feb 11, 2021
a0d98b1
Merge branch 'dev' into sync-improvement
KevinMenden Feb 11, 2021
547d562
Apply suggestions from code review
KevinMenden Feb 12, 2021
6c912ae
check for token in sync
KevinMenden Feb 12, 2021
4681a6d
moved token checking code
KevinMenden Feb 12, 2021
8a288c3
commenting closed PR instead of changing it
KevinMenden Feb 12, 2021
bc302ee
don't close new PR
KevinMenden Feb 15, 2021
1dbed0f
Merge branch 'master' into sync-improvement
ewels Feb 16, 2021
488f63b
Sync: Clean up simple stuff from review on PR
ewels Feb 16, 2021
8e1bdaf
Minor tidying up for credentials checks, merge two small functions
ewels Feb 16, 2021
7d04fbd
Reorder functions to match order of execution
ewels Feb 16, 2021
277b867
Refactor close / comment code to avoid reusing variable names
ewels Feb 16, 2021
94106e5
Deleted outdated sync pytests
ewels Feb 16, 2021
4b66b65
Minor tweaks to PR body text
ewels Feb 16, 2021
dc2148f
Fix / refactor code that closes open PRs
ewels Feb 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/create-lint-wf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
sudo ln -s /tmp/nextflow/nextflow /usr/local/bin/nextflow

- name: Run nf-core/tools
env:
GITHUB_AUTH_TOKEN: ${{ secrets.nf_core_bot_auth_token }}
ewels marked this conversation as resolved.
Show resolved Hide resolved
run: |
nf-core --log-file log.txt create -n testpipeline -d "This pipeline is for testing" -a "Testing McTestface"
nf-core --log-file log.txt lint nf-core-testpipeline
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* Singularity images in module files are now discovered and fetched
* Direct downloads of Singularity images in python allowed (much faster than running `singularity pull`)
* Downloads now work with `$NXF_SINGULARITY_CACHEDIR` so that pipelines sharing containers have efficient downloads
* changed behaviour of `nf-core sync` command as discussed in [[#787]](https://github.com/nf-core/tools/issues/787)
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved

### Linting

Expand Down
224 changes: 140 additions & 84 deletions nf_core/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def __init__(
self.pipeline_dir = os.path.abspath(pipeline_dir)
self.from_branch = from_branch
self.original_branch = None
self.merge_branch = "nf-core-template-merge-{}".format(nf_core.__version__)
self.made_changes = False
self.make_pr = make_pr
self.gh_pr_returned_data = {}
Expand All @@ -86,6 +87,7 @@ def sync(self):

self.inspect_sync_dir()
self.get_wf_config()
self.close_open_template_merge_pull_requests()
self.checkout_template_branch()
self.delete_template_branch_files()
self.make_template_pipeline()
Expand All @@ -95,6 +97,8 @@ def sync(self):
if self.made_changes and self.make_pr:
try:
self.push_template_branch()
self.create_merge_base_branch()
self.push_merge_branch()
self.make_pull_request()
except PullRequestException as e:
self.reset_target_dir()
Expand Down Expand Up @@ -239,60 +243,31 @@ def push_template_branch(self):
except git.exc.GitCommandError as e:
raise PullRequestException("Could not push TEMPLATE branch:\n {}".format(e))

def make_pull_request(self):
"""Create a pull request to a base branch (default: dev),
from a head branch (default: TEMPLATE)

Returns: An instance of class requests.Response
def close_open_template_merge_pull_requests(self):
"""Get all template merging branches (starting with 'nf-core-template-merge-')
and check for any open PRs from these branches to the self.from_branch
If open PRs are found, add a comment and close them
"""
# Check that we know the github username and repo name
try:
assert self.gh_username is not None
assert self.gh_repo is not None
except AssertionError:
raise PullRequestException("Could not find GitHub username and repo name")

# If we've been asked to make a PR, check that we have the credentials
try:
assert os.environ.get("GITHUB_AUTH_TOKEN", "") != ""
except AssertionError:
raise PullRequestException(
"Environment variable GITHUB_AUTH_TOKEN not set - cannot make PR\n"
"Make a PR at the following URL:\n https://github.com/{}/compare/{}...TEMPLATE".format(
self.gh_repo, self.original_branch
)
)

log.info("Submitting a pull request via the GitHub API")

pr_title = "Important! Template update for nf-core/tools v{}".format(nf_core.__version__)
pr_body_text = (
"A new release of the main template in nf-core/tools has just been released. "
"This automated pull-request attempts to apply the relevant updates to this pipeline.\n\n"
"Please make sure to merge this pull-request as soon as possible. "
"Once complete, make a new minor release of your pipeline. "
"For instructions on how to merge this PR, please see "
"[https://nf-co.re/developers/sync](https://nf-co.re/developers/sync#merging-automated-prs).\n\n"
"For more information about this release of [nf-core/tools](https://github.com/nf-core/tools), "
"please see the [nf-core/tools v{tag} release page](https://github.com/nf-core/tools/releases/tag/{tag})."
).format(tag=nf_core.__version__)

# Try to update an existing pull-request
if self.update_existing_pull_request(pr_title, pr_body_text) is False:
# None found - make a new pull-request
self.submit_pull_request(pr_title, pr_body_text)

def update_existing_pull_request(self, pr_title, pr_body_text):
"""
List existing pull-requests between TEMPLATE and self.from_branch
log.info("Checking for open PRs from template merge branches")
# Get list of all branches
branch_list = [b.name for b in self.repo.branches]
# Subset to template merging branches
branch_list = [b for b in branch_list if b.startswith("nf-core-template-merge-")]
# Check for open PRs and close if found
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved
for branch in branch_list:
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved
self.close_open_pr(branch)
except Exception as e:
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved
raise log.error("Could not close open pull requests! {}".format(e))
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved

If one is found, attempt to update it with a new title and body text
If none are found, return False
def close_open_pr(self, branch):
"""Given a branch, check for open PRs from that branch to self.from_branch
and close if PRs have been found
"""
assert os.environ.get("GITHUB_AUTH_TOKEN", "") != ""
log.info("Checking branch: {}".format(branch))
# Look for existing pull-requests
list_prs_url = "https://api.github.com/repos/{}/pulls?head=nf-core:TEMPLATE&base={}".format(
self.gh_repo, self.from_branch
list_prs_url = "https://api.github.com/repos/{}/pulls?head={}&base={}".format(
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved
self.gh_repo, branch, self.from_branch
)
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved
r = requests.get(
url=list_prs_url,
Expand All @@ -305,18 +280,25 @@ def update_existing_pull_request(self, pr_title, pr_body_text):
r_json = r.content
r_pp = r.content

# PR worked
if r.status_code == 200:
log.debug("GitHub API listing existing PRs:\n{}".format(r_pp))

# No open PRs
if len(r_json) == 0:
log.info("No open PRs found between TEMPLATE and {}".format(self.from_branch))
log.info("No open PRs found between {} and {}".format(branch, self.from_branch))
return False

# Update existing PR
# Close existing PR
pr_title = "Important! Template update for nf-core/tools v{} Closed because outdated!".format(
nf_core.__version__
)
pr_body_text = (
"A new release of the main template in nf-core/tools has just been released. "
"This automated pull-request attempts to apply the relevant updates to this pipeline.\n\n"
"This pull-request is outdated and has been closed. A new pull-request has been created instead."
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these overwrite the main title and body of the open PR? I wonder if it's better to just add a comment to the PR instead of editing this..

(Bonus points if we can do it after creating the new PR and post a link to the PR that closed it..)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 okay I'll see if how to do that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linting code used to post GitHub comments:

tools/nf_core/lint.py

Lines 1473 to 1525 in 9b3eec9

def github_comment(self):
"""
If we are running in a GitHub PR, try to post results as a comment
"""
if os.environ.get("GITHUB_TOKEN", "") == "":
log.debug("Environment variable GITHUB_TOKEN not found")
return
if os.environ.get("GITHUB_COMMENTS_URL", "") == "":
log.debug("Environment variable GITHUB_COMMENTS_URL not found")
return
try:
headers = {"Authorization": "token {}".format(os.environ["GITHUB_TOKEN"])}
# Get existing comments - GET
get_r = requests.get(url=os.environ["GITHUB_COMMENTS_URL"], headers=headers)
if get_r.status_code == 200:
# Look for an existing comment to update
update_url = False
for comment in get_r.json():
if comment["user"]["login"] == "github-actions[bot]" and comment["body"].startswith(
"\n#### `nf-core lint` overall result"
):
# Update existing comment - PATCH
log.info("Updating GitHub comment")
update_r = requests.patch(
url=comment["url"],
data=json.dumps({"body": self.get_results_md().replace("Posted", "**Updated**")}),
headers=headers,
)
return
# Create new comment - POST
if len(self.warned) > 0 or len(self.failed) > 0:
r = requests.post(
url=os.environ["GITHUB_COMMENTS_URL"],
data=json.dumps({"body": self.get_results_md()}),
headers=headers,
)
try:
r_json = json.loads(r.content)
response_pp = json.dumps(r_json, indent=4)
except:
r_json = r.content
response_pp = r.content
if r.status_code == 201:
log.info("Posted GitHub comment: {}".format(r_json["html_url"]))
log.debug(response_pp)
else:
log.warning("Could not post GitHub comment: '{}'\n{}".format(r.status_code, response_pp))
except Exception as e:
log.warning("Could not post GitHub comment: {}\n{}".format(os.environ["GITHUB_COMMENTS_URL"], e))

I removed it because the linting runs on the PR base, so didn't have sufficient authentication when the PR came from someone's fork. But here we have full auth permissions and it's coming from a branch on the same repo so it should work fine I think..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to resurrect that code and put it to some use again! 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh that's nice!! thanks 👌

pr_update_api_url = r_json[0]["url"]
pr_content = {"title": pr_title, "body": pr_body_text}
pr_content = {"state": "closed", "title": pr_title, "body": pr_body_text}

r = requests.patch(
ewels marked this conversation as resolved.
Show resolved Hide resolved
url=pr_update_api_url,
Expand All @@ -333,53 +315,127 @@ def update_existing_pull_request(self, pr_title, pr_body_text):
# PR update worked
if r.status_code == 200:
log.debug("GitHub API PR-update worked:\n{}".format(r_pp))
log.info("Updated GitHub PR: {}".format(r_json["html_url"]))
log.info("Closed GitHub PR: {}".format(r_json["html_url"]))
return True
# Something went wrong
else:
log.warning("Could not update PR ('{}'):\n{}\n{}".format(r.status_code, pr_update_api_url, r_pp))
log.warning("Could not close PR ('{}'):\n{}\n{}".format(r.status_code, pr_update_api_url, r_pp))
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved
return False

# Something went wrong
else:
log.warning("Could not list open PRs ('{}')\n{}\n{}".format(r.status_code, list_prs_url, r_pp))
return False

def create_merge_base_branch(self):
"""Create a new branch from the updated TEMPLATE branch
This branch will then be used to create the PR
"""
# Check if branch exists already
branch_list = [b.name for b in self.repo.branches]
if self.merge_branch in branch_list:
original_merge_branch = self.merge_branch
# Try to create new branch with number at the end
# If <branch_name>-2 already exists, increase the number until branch is new
branch_no = 2
self.merge_branch = original_merge_branch + "-" + str(branch_no)
ewels marked this conversation as resolved.
Show resolved Hide resolved
while self.merge_branch in branch_list:
branch_no += 1
self.merge_branch = original_merge_branch + "-" + str(branch_no)
ewels marked this conversation as resolved.
Show resolved Hide resolved
log.info(
"Branch already existed: '{}', creating branch '{}' instead.".format(
original_merge_branch, self.merge_branch
)
)

# Create new branch and checkout
log.info("Checking out merge base branch {}".format(self.merge_branch))
try:
self.repo.create_head(self.merge_branch)
except git.exc.GitCommandError:
raise SyncException("Could not create new branch '{}'".format(self.merge_branch))
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved

def push_merge_branch(self):
"""Push the newly created merge branch to the remote repository"""
log.info("Pushing {} branch to remote".format(self.merge_branch))
try:
origin = self.repo.remote()
origin.push(self.merge_branch)
except git.exc.GitCommandError as e:
raise PullRequestException("Could not push {} branch:\n {}".format(self.merge_branch, e))
KevinMenden marked this conversation as resolved.
Show resolved Hide resolved

def make_pull_request(self):
"""Create a pull request to a base branch (default: dev),
from a head branch (default: TEMPLATE)

Returns: An instance of class requests.Response
"""
# Check that we know the github username and repo name
if self.gh_username is None and self.gh_repo is None:
raise PullRequestException("Could not find GitHub username and repo name")
ewels marked this conversation as resolved.
Show resolved Hide resolved

# If we've been asked to make a PR, check that we have the credentials
if os.environ.get("GITHUB_AUTH_TOKEN", "") == "":
raise PullRequestException(
"Environment variable GITHUB_AUTH_TOKEN not set - cannot make PR\n"
"Make a PR at the following URL:\n https://github.com/{}/compare/{}...TEMPLATE".format(
self.gh_repo, self.original_branch
)
)

ewels marked this conversation as resolved.
Show resolved Hide resolved
log.info("Submitting a pull request via the GitHub API")

pr_title = "Important! Template update for nf-core/tools v{}".format(nf_core.__version__)
pr_body_text = (
"A new release of the main template in nf-core/tools has just been released. "
"This automated pull-request attempts to apply the relevant updates to this pipeline.\n\n"
"Please make sure to merge this pull-request as soon as possible. "
"Once complete, make a new minor release of your pipeline. "
"For instructions on how to merge this PR, please see "
"[https://nf-co.re/developers/sync](https://nf-co.re/developers/sync#merging-automated-prs).\n\n"
"For more information about this release of [nf-core/tools](https://github.com/nf-core/tools), "
"please see the [nf-core/tools v{tag} release page](https://github.com/nf-core/tools/releases/tag/{tag})."
).format(tag=nf_core.__version__)

# Make new pull-request
self.submit_pull_request(pr_title, pr_body_text)
ewels marked this conversation as resolved.
Show resolved Hide resolved

def submit_pull_request(self, pr_title, pr_body_text):
"""
Create a new pull-request on GitHub
"""
assert os.environ.get("GITHUB_AUTH_TOKEN", "") != ""
ewels marked this conversation as resolved.
Show resolved Hide resolved
pr_content = {
"title": pr_title,
"body": pr_body_text,
"maintainer_can_modify": True,
"head": "TEMPLATE",
"base": self.from_branch,
}

r = requests.post(
url="https://api.github.com/repos/{}/pulls".format(self.gh_repo),
data=json.dumps(pr_content),
auth=requests.auth.HTTPBasicAuth(self.gh_username, os.environ.get("GITHUB_AUTH_TOKEN")),
)
try:
self.gh_pr_returned_data = json.loads(r.content)
returned_data_prettyprint = json.dumps(self.gh_pr_returned_data, indent=4)
except:
self.gh_pr_returned_data = r.content
returned_data_prettyprint = r.content
if not os.environ.get("GITHUB_AUTH_TOKEN", "") == "":
ewels marked this conversation as resolved.
Show resolved Hide resolved
pr_content = {
"title": pr_title,
"body": pr_body_text,
"maintainer_can_modify": True,
"head": self.merge_branch,
"base": self.from_branch,
}

r = requests.post(
url="https://api.github.com/repos/{}/pulls".format(self.gh_repo),
data=json.dumps(pr_content),
auth=requests.auth.HTTPBasicAuth(self.gh_username, os.environ.get("GITHUB_AUTH_TOKEN")),
)
try:
self.gh_pr_returned_data = json.loads(r.content)
returned_data_prettyprint = json.dumps(self.gh_pr_returned_data, indent=4)
except:
self.gh_pr_returned_data = r.content
returned_data_prettyprint = r.content

# PR worked
if r.status_code == 201:
log.debug("GitHub API PR worked:\n{}".format(returned_data_prettyprint))
log.info("GitHub PR created: {}".format(self.gh_pr_returned_data["html_url"]))
# PR worked
if r.status_code == 201:
log.debug("GitHub API PR worked:\n{}".format(returned_data_prettyprint))
log.info("GitHub PR created: {}".format(self.gh_pr_returned_data["html_url"]))

# Something went wrong
# Something went wrong
else:
raise PullRequestException(
"GitHub API returned code {}: \n{}".format(r.status_code, returned_data_prettyprint)
)
else:
raise PullRequestException(
"GitHub API returned code {}: \n{}".format(r.status_code, returned_data_prettyprint)
)
raise PullRequestException("Environment variable GITHUB_AUTH_TOKEN not set - cannot make PR")

def reset_target_dir(self):
"""
Expand Down
6 changes: 3 additions & 3 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def __init__(self, data, status_code):
self.status_code = status_code
self.content = json.dumps(data)

url_template = "https://api.github.com/repos/{}/response/pulls?head=nf-core:TEMPLATE&base=None"
url_template = "https://api.github.com/repos/{}/response/pulls?head=TEMPLATE&base=None"
if kwargs["url"] == url_template.format("no_existing_pr"):
response_data = []
return MockResponse(response_data, 200)
Expand Down Expand Up @@ -258,9 +258,9 @@ def test_make_pull_request_bad_response(self, mock_get, mock_post):
@mock.patch("requests.get", side_effect=mocked_requests_get)
@mock.patch("requests.patch", side_effect=mocked_requests_patch)
def test_update_existing_pull_request(self, mock_get, mock_patch):
""" Try discovering a PR and updating it """
""" Try closing a PR """
psync = nf_core.sync.PipelineSync(self.pipeline_dir)
psync.gh_username = "existing_pr"
psync.gh_repo = "existing_pr/response"
os.environ["GITHUB_AUTH_TOKEN"] = "test"
assert psync.update_existing_pull_request("title", "body") is True
assert psync.close_open_pr("TEMPLATE") is True