Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
pr_commands.py
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file pr_commands.py
1 #!/usr/bin/env python3
2 from dataclasses import dataclass
3 from typing import List, Dict, Any
4 from pathlib import Path
5 import shlex
6 import asyncio
7 import functools
8 import os
9 import click
10 
11 import typer
12 import gidgethub
13 from gidgethub.aiohttp import GitHubAPI
14 import aiohttp
15 
16 
17 def wrap_async(fn):
18  @functools.wraps(fn)
19  def wrapper(*args, **kwargs):
20  return asyncio.run(fn(*args, **kwargs))
21 
22  return wrapper
23 
24 
25 class CommandError(Exception):
26  pass
27 
28 
29 @dataclass
30 class Context:
31  pr: Dict[str, Any]
32  sender: str
33  github_token: str
34 
35 
36 @click.group()
37 def app():
38  pass
39 
40 
41 @app.group()
43  pass
44 
45 
46 @run_experiment.command()
47 @click.option("--revert-sha", "-r", multiple=True)
48 @click.pass_obj
49 @wrap_async
50 async def atlas(ctx: Context, revert_sha: List[str]):
51  gitlab_trigger_token = os.environ["GITLAB_TRIGGER_TOKEN"]
52  gitlab_trigger_url = os.environ["GITLAB_TRIGGER_URL"]
53  async with aiohttp.ClientSession() as session:
54  gh = GitHubAPI(session, "acts-commands", oauth_token=ctx.github_token)
55 
56  pr = ctx.pr
57 
58  head_clone_url = pr["head"]["repo"]["clone_url"]
59  head_branch = pr["head"]["ref"]
60  head_sha = pr["head"]["sha"]
61 
62  variable_summary = f"""
63 | Variable | Value |
64 |------|------|
65 | `ACTS_GIT_REPO` | {head_clone_url} |
66 | `ACTS_REF` | `{head_branch}` |
67 | `SOURCE_SHA` | {head_sha} |
68 | `REVERT_SHAS` | {",".join(revert_sha)} |
69  """
70 
71  body = f"""
72 @{ctx.sender}
73 ðŸŸ¡ I'm going to trigger an ATLAS experiment pipeline for you:
74 
75 {variable_summary}
76  """
77  comment = await gh.post(pr["comments_url"], data={"body": body})
78 
79  variables = {
80  "ACTS_GIT_REPO": head_clone_url,
81  "ACTS_REF": head_branch,
82  "SOURCE_SHA": head_sha,
83  "PR_URL": pr["url"],
84  "REVERT_SHAS": ",".join(revert_sha),
85  "REPORT_COMMENT_URL": comment["url"],
86  }
87  data = {
88  "token": gitlab_trigger_token,
89  "ref": "main",
90  **{f"variables[{k}]": v for k, v in variables.items()},
91  }
92  print(gitlab_trigger_url)
93  print(data)
94  async with session.post(
95  url=gitlab_trigger_url,
96  data=data,
97  ) as resp:
98  if resp.status != 201:
99  body = f"""
100 @{ctx.sender}
101 ðŸ”´ I'm sorry, I couldn't run your command because of an error:
102 ```
103 {await resp.text()}
104 ```
105 {variable_summary}
106  """
107  await gh.post(comment["url"], data={"body": body})
108 
109  return
110 
111  data = await resp.json()
112  pipeline_url = data["web_url"]
113 
114  body = f"""
115 @{ctx.sender}
116 ðŸŸ¡ I triggered an ATLAS experiment [pipeline]({pipeline_url}) for you
117 
118 {variable_summary}
119  """
120  await gh.post(comment["url"], data={"body": body})
121 
122 
123 async def get_author_in_team(gh: GitHubAPI, author: str, allow_team: str) -> bool:
124  allow_org, allow_team = allow_team.split("/", 1)
125 
126  try:
127  membership = await gh.getitem(
128  f"/orgs/{allow_org}/teams/{allow_team}/memberships/{author}"
129  )
130  return True
131  except gidgethub.BadRequest as e:
132  if e.status_code != 404:
133  raise e
134 
135  return False
136 
137 
138 async def preflight(
139  token: str, pr_url: str, sender: str, repository: str, allow_team: str
140 ):
141  async with aiohttp.ClientSession() as session:
142  gh = GitHubAPI(session, "acts-commands", oauth_token=token)
143 
144  if not await get_author_in_team(gh, sender, allow_team):
145  raise RuntimeError(f"{sender} is not in {allow_team}")
146 
147  return await gh.getitem(pr_url)
148 
149 
150 async def report_error(token: str, pr: Dict[str, Any], sender: str, error: Exception):
151  async with aiohttp.ClientSession() as session:
152  gh = GitHubAPI(session, "acts-commands", oauth_token=token)
153 
154  body = f"""
155 @{sender}
156 ðŸ”´ I'm sorry, I couldn't run your command because of an error:
157 ```
158 {error}
159 ```
160 """
161  await gh.post(pr["comments_url"], data={"body": body})
162 
163 
164 def main(
165  pr: str = typer.Option(),
166  body: str = typer.Option(),
167  sender: str = typer.Option(),
168  repository: str = typer.Option(),
169  allow_team: str = typer.Option("acts-project/ci-perms", envvar="ALLOW_TEAM"),
170 ):
171  if Path(body).exists():
172  body = Path(body).read_text().strip()
173 
174  if len(body.split("\n")) > 1:
175  raise typer.BadParameter("Body must be a single line")
176 
177  if not body.startswith("/"):
178  raise typer.BadParameter("Body must start with a slash")
179  body = body[1:]
180 
181  args = shlex.split(body)
182 
183  token = os.environ["GITHUB_TOKEN"]
184  pr = asyncio.run(preflight(token, pr, sender, repository, allow_team))
185 
186  try:
187  app(
188  args,
189  obj=Context(pr=pr, github_token=token, sender=sender),
190  standalone_mode=False,
191  )
192  except (CommandError, click.exceptions.ClickException) as e:
193  asyncio.run(report_error(token, pr, sender, e))
194 
195 
196 typer.run(main)