Skip to content

Commit 6f7ed82

Browse files
feat: Allow creation of PRs upon wrapper updates (#111)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Pull request creation added for environment and wrapper updates with CLI control. * Per-file (per-Snakefile) PR option for finer-grained changes. * Optional automatic labeling of PRs for updated items. * Unified and more reliable PR automation powering the above behaviors. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent d98dffd commit 6f7ed82

File tree

4 files changed

+223
-144
lines changed

4 files changed

+223
-144
lines changed

snakedeploy/client.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,30 @@ def get_parser():
166166
help="Only warn if conda env evaluation fails and go on with the other envs.",
167167
)
168168

169+
def add_create_pr_args(subparser, entity: str):
170+
subparser.add_argument(
171+
"--create-prs",
172+
action="store_true",
173+
help=f"Create pull request for each updated {entity}. "
174+
"Requires GITHUB_TOKEN and GITHUB_REPOSITORY (the repo name) and one of GITHUB_REF_NAME or GITHUB_BASE_REF "
175+
"(preferring the latter, representing the target branch, e.g. main or master) environment "
176+
"variables to be set (the latter three are available when running as github action). "
177+
"In order to enable further actions (e.g. checks) to run on the generated PRs, the "
178+
"provided GITHUB_TOKEN may not be the default github actions token. See here for "
179+
"options: https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs.",
180+
)
181+
subparser.add_argument(
182+
"--entity-regex",
183+
help=f"Regular expression for deriving an entity name from the {entity} file name "
184+
"(will be used for adding a label and for title and description). Has to contain a group 'entity' "
185+
"(e.g. '(?P<entity>.+)/environment.yaml').",
186+
)
187+
subparser.add_argument(
188+
"--pr-add-label",
189+
action="store_true",
190+
help="Add a label to the PR. Has to be used in combination with --entity-regex.",
191+
)
192+
169193
update_conda_envs = subparsers.add_parser(
170194
"update-conda-envs",
171195
help="Update given conda environment definition files (in YAML format) "
@@ -187,28 +211,7 @@ def get_parser():
187211
action="store_true",
188212
help="also pin the updated environments (see pin-conda-envs subcommand).",
189213
)
190-
update_conda_envs.add_argument(
191-
"--create-prs",
192-
action="store_true",
193-
help="Create pull request for each updated environment. "
194-
"Requires GITHUB_TOKEN and GITHUB_REPOSITORY (the repo name) and one of GITHUB_REF_NAME or GITHUB_BASE_REF "
195-
"(preferring the latter, representing the target branch, e.g. main or master) environment "
196-
"variables to be set (the latter three are available when running as github action). "
197-
"In order to enable further actions (e.g. checks) to run on the generated PRs, the "
198-
"provided GITHUB_TOKEN may not be the default github actions token. See here for "
199-
"options: https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs.",
200-
)
201-
update_conda_envs.add_argument(
202-
"--entity-regex",
203-
help="Regular expression for deriving an entity name from the environment file name "
204-
"(will be used for adding a label and for title and description). Has to contain a group 'entity' "
205-
"(e.g. '(?P<entity>.+)/environment.yaml').",
206-
)
207-
update_conda_envs.add_argument(
208-
"--pr-add-label",
209-
action="store_true",
210-
help="Add a label to the PR. Has to be used in combination with --entity-regex.",
211-
)
214+
add_create_pr_args(update_conda_envs, "environment")
212215
update_conda_envs.add_argument(
213216
"--warn-on-error",
214217
action="store_true",
@@ -223,6 +226,12 @@ def get_parser():
223226
update_snakemake_wrappers.add_argument(
224227
"snakefiles", nargs="+", help="Paths to Snakefiles which should be updated."
225228
)
229+
add_create_pr_args(update_snakemake_wrappers, "snakefile")
230+
update_snakemake_wrappers.add_argument(
231+
"--per-snakefile-prs",
232+
action="store_true",
233+
help="Create one PR per Snakefile instead of a single PR for all.",
234+
)
226235

227236
scaffold_snakemake_plugin = subparsers.add_parser(
228237
"scaffold-snakemake-plugin",
@@ -312,7 +321,13 @@ def help(return_code=0):
312321
warn_on_error=args.warn_on_error,
313322
)
314323
elif args.subcommand == "update-snakemake-wrappers":
315-
update_snakemake_wrappers(args.snakefiles)
324+
update_snakemake_wrappers(
325+
args.snakefiles,
326+
create_prs=args.create_prs,
327+
per_snakefile_prs=args.per_snakefile_prs,
328+
entity_regex=args.entity_regex,
329+
pr_add_label=args.pr_add_label,
330+
)
316331
elif args.subcommand == "scaffold-snakemake-plugin":
317332
scaffold_plugin(args.plugin_type)
318333
except UserError as e:

snakedeploy/conda.py

Lines changed: 15 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
1-
from collections import namedtuple
21
import copy
32
import json
4-
import os
53
from pathlib import Path
64
import subprocess as sp
75
import tempfile
86
import re
97
from glob import glob
108
from itertools import chain
11-
import github
12-
from urllib3.util.retry import Retry
139
import random
10+
from typing import Optional
1411

1512
from packaging import version as packaging_version
1613
import yaml
17-
from github import Github, GithubException
18-
from reretry import retry
1914

2015
from snakedeploy.exceptions import UserError
2116
from snakedeploy.logger import logger
17+
from snakedeploy.prs import PR, get_repo
2218
from snakedeploy.utils import YamlDumper
2319
from snakedeploy.conda_version import VersionOrder
2420

@@ -66,9 +62,6 @@ def update_conda_envs(
6662
)
6763

6864

69-
File = namedtuple("File", "path, content, is_updated, msg")
70-
71-
7265
class CondaEnvProcessor:
7366
def __init__(self, conda_frontend="mamba"):
7467
self.conda_frontend = conda_frontend
@@ -84,22 +77,16 @@ def __init__(self, conda_frontend="mamba"):
8477
def process(
8578
self,
8679
conda_env_paths,
87-
create_prs=False,
88-
update_envs=True,
89-
pin_envs=True,
90-
pr_add_label=False,
91-
entity_regex=None,
92-
warn_on_error=False,
80+
create_prs: bool = False,
81+
update_envs: bool = True,
82+
pin_envs: bool = True,
83+
pr_add_label: bool = False,
84+
entity_regex: Optional[str] = None,
85+
warn_on_error: bool = False,
9386
):
9487
repo = None
9588
if create_prs:
96-
g = Github(
97-
os.environ["GITHUB_TOKEN"],
98-
retry=Retry(
99-
total=10, status_forcelist=(500, 502, 504), backoff_factor=0.3
100-
),
101-
)
102-
repo = g.get_repo(os.environ["GITHUB_REPOSITORY"]) if create_prs else None
89+
repo = get_repo()
10390
conda_envs = list(chain.from_iterable(map(glob, conda_env_paths)))
10491
random.shuffle(conda_envs)
10592

@@ -109,19 +96,6 @@ def process(
10996
)
11097
for conda_env_path in conda_envs:
11198
if create_prs:
112-
entity = conda_env_path
113-
if entity_regex is not None:
114-
m = re.match(entity_regex, conda_env_path)
115-
if m is None:
116-
raise UserError(
117-
f"Given --entity-regex did not match any {conda_env_path}."
118-
)
119-
try:
120-
entity = m.group("entity")
121-
except IndexError:
122-
raise UserError(
123-
"No group 'entity' found in given --entity-regex."
124-
)
12599
if pr_add_label and not entity_regex:
126100
raise UserError(
127101
"Cannot add label to PR without --entity-regex specified."
@@ -132,11 +106,12 @@ def process(
132106
)
133107
mode = "bump" if update_envs else "pin"
134108
pr = PR(
135-
f"perf: auto{mode} {entity}",
136-
f"Automatic {mode} of {entity}.",
137-
f"auto{mode}/{entity.replace('/', '-')}",
109+
f"perf: auto{mode} {conda_env_path}",
110+
f"Automatic {mode} of {conda_env_path}.",
111+
f"auto{mode}/{conda_env_path.replace('/', '-')}",
138112
repo,
139-
label=entity if pr_add_label else None,
113+
entity=conda_env_path,
114+
label_entity_regex=entity_regex if pr_add_label else None,
140115
)
141116
else:
142117
pr = None
@@ -161,6 +136,7 @@ def process(
161136
else:
162137
raise UserError(msg)
163138
if create_prs:
139+
assert pr is not None
164140
pr.create()
165141

166142
def update_env(
@@ -303,83 +279,3 @@ def exec_conda(self, subcmd):
303279
universal_newlines=True,
304280
check=True,
305281
)
306-
307-
308-
class PR:
309-
def __init__(self, title, body, branch, repo, label=None):
310-
self.title = title
311-
self.body = body
312-
self.files = []
313-
self.branch = branch
314-
self.repo = repo
315-
self.base_ref = (
316-
os.environ.get("GITHUB_BASE_REF") or os.environ["GITHUB_REF_NAME"]
317-
)
318-
self.label = label
319-
320-
def add_file(self, filepath, content, is_updated, msg):
321-
self.files.append(File(str(filepath), content, is_updated, msg))
322-
323-
@retry(tries=2, delay=60)
324-
def create(self):
325-
if not self.files:
326-
logger.info("No files to commit.")
327-
return
328-
329-
branch_exists = False
330-
try:
331-
b = self.repo.get_branch(self.branch)
332-
logger.info(f"Branch {b} already exists.")
333-
branch_exists = True
334-
except GithubException as e:
335-
if e.status != 404:
336-
raise e
337-
logger.info(f"Creating branch {self.branch}...")
338-
self.repo.create_git_ref(
339-
ref=f"refs/heads/{self.branch}",
340-
sha=self.repo.get_branch(self.base_ref).commit.sha,
341-
)
342-
for file in self.files:
343-
sha = None
344-
if branch_exists:
345-
logger.info(f"Obtaining sha of {file.path} on branch {self.branch}...")
346-
try:
347-
# try to get sha if file exists
348-
sha = self.repo.get_contents(file.path, self.branch).sha
349-
except github.GithubException.UnknownObjectException as e:
350-
if e.status != 404:
351-
raise e
352-
elif file.is_updated:
353-
logger.info(
354-
f"Obtaining sha of {file.path} on branch {self.base_ref}..."
355-
)
356-
sha = self.repo.get_contents(file.path, self.base_ref).sha
357-
358-
if sha is not None:
359-
self.repo.update_file(
360-
file.path,
361-
file.msg,
362-
file.content,
363-
sha,
364-
branch=self.branch,
365-
)
366-
else:
367-
self.repo.create_file(
368-
file.path, file.msg, file.content, branch=self.branch
369-
)
370-
371-
pr_exists = any(
372-
pr.head.label.split(":", 1)[1] == self.branch
373-
for pr in self.repo.get_pulls(state="open", base=self.base_ref)
374-
)
375-
if pr_exists:
376-
logger.info("PR already exists.")
377-
else:
378-
pr = self.repo.create_pull(
379-
title=self.title,
380-
body=self.body,
381-
head=self.branch,
382-
base=self.base_ref,
383-
)
384-
pr.add_to_labels(self.label)
385-
logger.info(f"Created PR: {pr.html_url}")

0 commit comments

Comments
 (0)