Skip to content

Commit b8e66c7

Browse files
authored
Merge pull request #244 from creativecommons/update-projects
Add issues and pull requests from Shafiya-Heena project to TimidRobot project
2 parents 196f645 + 71a5a53 commit b8e66c7

File tree

3 files changed

+379
-5
lines changed

3 files changed

+379
-5
lines changed

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,8 @@ This manages new issues and pull requests to ensure they are properly tracked
9494
in a GitHub project:
9595
- [possumbilities project][proj_possumbilities]: _Web Development and Web
9696
Support_
97-
- [Shafiya-Heena project][proj-shafiya-heena]: _IT Support, Platforms, and
98-
Systems_
99-
- [TimidRobot project][proj_timidrobot]: _Application Programming and
100-
Management_
97+
- [TimidRobot project][proj_timidrobot]: _Application Programming, IT Support,
98+
Management, Platforms, and Systems_
10199

102100
[manage_issues]: .github/workflows/manage_issues.yml
103101
[manage_new_issues]: manage_new_issues_and_pull_requests.py

_ARCHIVE/move_issues.py

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Ensure all open issues and pull requests are tracked in an appropriate project
4+
and and status column.
5+
"""
6+
7+
# Standard library
8+
import argparse
9+
import sys
10+
import textwrap
11+
import traceback
12+
13+
# Third-party
14+
import yaml
15+
from pygments import highlight
16+
from pygments.formatters import TerminalFormatter
17+
from pygments.lexers import PythonTracebackLexer
18+
19+
# First-party/Local
20+
import ccos.log
21+
from ccos import gh_utils
22+
23+
LOG = ccos.log.setup_logger()
24+
PROJECTS_YAML = "ccos/manage/projects.yml"
25+
26+
27+
def setup():
28+
"""Instantiate and configure argparse and logging.
29+
30+
Return argsparse namespace.
31+
"""
32+
ap = argparse.ArgumentParser(description=__doc__)
33+
ap.add_argument(
34+
"-c",
35+
"--count",
36+
default=None,
37+
type=int,
38+
help="only update specified number of issues and pull requests (COUNT"
39+
" of 3 may result in 6 updates)",
40+
)
41+
ap.add_argument(
42+
"-n",
43+
"--dryrun",
44+
action="store_true",
45+
help="dry run: do not make any changes",
46+
)
47+
args = ap.parse_args()
48+
return args
49+
50+
51+
def read_project_data():
52+
LOG.info("Reading project data YAML file")
53+
with open(PROJECTS_YAML, "r") as file_obj:
54+
project_data = yaml.safe_load(file_obj)
55+
return project_data
56+
57+
58+
def update_project_data(github_gql_client, project_data):
59+
LOG.info("Updating project data from GitHub GraphQL API")
60+
query = gh_utils.gql_query(
61+
"""
62+
query {
63+
organization(login:"creativecommons") {
64+
projectsV2(first: 100) {
65+
edges {
66+
node {
67+
id
68+
number
69+
title
70+
field(name: "Status") {
71+
__typename
72+
... on ProjectV2SingleSelectField {
73+
id
74+
name
75+
opt_triage: options(names: "Triage") {
76+
id
77+
}
78+
opt_backlog: options(
79+
names: "Backlog"
80+
) {
81+
id
82+
}
83+
opt_in_review: options(
84+
names: "In review"
85+
) {
86+
id
87+
}
88+
}
89+
}
90+
}
91+
}
92+
}
93+
}
94+
}
95+
"""
96+
)
97+
result = github_gql_client.execute(query)
98+
for edge in result["organization"]["projectsV2"]["edges"]:
99+
node = edge["node"]
100+
title = node["title"]
101+
field = node["field"]
102+
if title in project_data.keys():
103+
project = project_data[title]
104+
project["id"] = node["id"]
105+
project["number"] = node["number"]
106+
project["status_field_id"] = field["id"]
107+
project["status_triage_id"] = field["opt_triage"][0]["id"]
108+
project["status_backlog_id"] = field["opt_backlog"][0]["id"]
109+
project["status_in_review_id"] = field["opt_in_review"][0]["id"]
110+
return project_data
111+
112+
113+
def get_shafiya_items(github_gql_client):
114+
LOG.info("Searching for issues and/or pull requests in Shafiya project")
115+
# https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests
116+
search_query = (
117+
"org:creativecommons"
118+
" state:open"
119+
" project:creativecommons/22" # Shafiya-Heena project
120+
)
121+
cursor = ""
122+
edges = []
123+
next_page = True
124+
while next_page is True:
125+
query = gh_utils.gql_query(
126+
"""
127+
query($cursor: String, $search_query: String!) {
128+
search(
129+
after: $cursor
130+
first: 100
131+
query: $search_query
132+
type: ISSUE
133+
) {
134+
edges {
135+
node {
136+
__typename
137+
... on Issue {
138+
createdAt
139+
id
140+
labels(first: 100) {
141+
edges {
142+
node {
143+
name
144+
}
145+
}
146+
}
147+
number
148+
repository {
149+
name
150+
}
151+
}
152+
... on PullRequest {
153+
createdAt
154+
id
155+
number
156+
repository {
157+
name
158+
}
159+
}
160+
}
161+
}
162+
pageInfo{
163+
endCursor
164+
hasNextPage
165+
}
166+
}
167+
}
168+
"""
169+
)
170+
params = {"cursor": cursor, "search_query": search_query}
171+
result = github_gql_client.execute(query, variable_values=params)
172+
edges += result["search"]["edges"]
173+
cursor = result["search"]["pageInfo"]["endCursor"]
174+
next_page = result["search"]["pageInfo"]["hasNextPage"]
175+
176+
items = {"issues": [], "prs": []}
177+
for edge in edges:
178+
node = edge["node"]
179+
created = node["createdAt"]
180+
item_id = node["id"]
181+
number = node["number"]
182+
repo = node["repository"]["name"]
183+
type_ = node["__typename"]
184+
if type_ == "Issue":
185+
labels = []
186+
for label_edge in node["labels"]["edges"]:
187+
labels.append(label_edge["node"]["name"])
188+
items["issues"].append(
189+
[repo, number, created, item_id]
190+
)
191+
elif type_ == "PullRequest":
192+
items["prs"].append([repo, number, created, item_id])
193+
items["issues"].sort()
194+
items["prs"].sort()
195+
LOG.info(
196+
f"Found {len(items['issues']) + len(items['prs'])} open and untracked"
197+
f" items: {len(items['issues'])} issues, {len(items['prs'])} pull"
198+
" requests"
199+
)
200+
return items
201+
202+
203+
def track_items(args, github_gql_client, project_data, items):
204+
if args.dryrun:
205+
noop = "dryrun (no-op): "
206+
else:
207+
noop = ""
208+
209+
query_add_item_to_project = gh_utils.gql(
210+
"""
211+
mutation($project_id: ID!, $item_id: ID!) {
212+
addProjectV2ItemById(
213+
input: {
214+
projectId: $project_id
215+
contentId: $item_id
216+
}
217+
) {
218+
item {
219+
id
220+
}
221+
}
222+
}
223+
"""
224+
)
225+
query_set_status_option = gh_utils.gql(
226+
"""
227+
mutation(
228+
$field_id: ID!
229+
$item_id: ID!
230+
$project_id: ID!
231+
$option_id: String
232+
) {
233+
updateProjectV2ItemFieldValue(
234+
input: {
235+
fieldId: $field_id
236+
itemId: $item_id
237+
projectId: $project_id
238+
value: {
239+
singleSelectOptionId: $option_id
240+
}
241+
}
242+
) {
243+
projectV2Item {
244+
id
245+
}
246+
}
247+
}
248+
"""
249+
)
250+
251+
# Add issues to projects
252+
if args.count is None:
253+
count = len(items["issues"])
254+
else:
255+
count = min(args.count, len(items["issues"]))
256+
LOG.info(f"{noop}Adding {count} item(s) to projects")
257+
for item in items["issues"][0 : args.count]: # noqa: E203
258+
repo, number, _, item_id = item
259+
# identify appropriate project
260+
project_id = None
261+
field_id = None
262+
for project in project_data.keys():
263+
if repo in project_data[project]["repos"]:
264+
project_id = project_data[project]["id"]
265+
field_id = project_data[project]["status_field_id"]
266+
break
267+
if not project_id:
268+
LOG.error(f"missing project assignment for repository: {repo}")
269+
sys.exit(1)
270+
# add issue to project
271+
if not args.dryrun:
272+
params = {"project_id": project_id, "item_id": item_id}
273+
result = github_gql_client.execute(
274+
query_add_item_to_project, variable_values=params
275+
)
276+
item_id = result["addProjectV2ItemById"]["item"]["id"]
277+
LOG.change_indent(+1)
278+
LOG.info(f"{repo}#{number} added to {project} project")
279+
# move issue to Status: Backlog
280+
if not args.dryrun:
281+
option_id = project_data[project]["status_backlog_id"]
282+
status = "Backlog"
283+
params = {
284+
"field_id": field_id,
285+
"item_id": item_id,
286+
"project_id": project_id,
287+
"option_id": option_id,
288+
}
289+
result = github_gql_client.execute(
290+
query_set_status_option, variable_values=params
291+
)
292+
ditto = len(f"{repo}#{number}") * "^"
293+
# 90 is bright black (gray)
294+
LOG.info(f"\u001b[90m{ditto}\u001b[0m moved to Status: {status}")
295+
LOG.change_indent(-1)
296+
297+
# Add pull requests to projects
298+
if args.count is None:
299+
count = len(items["prs"])
300+
else:
301+
count = min(args.count, len(items["prs"]))
302+
LOG.info(
303+
f"{noop}Adding {count} open and untracked pull requests to projects"
304+
)
305+
for item in items["prs"][0 : args.count]: # noqa: E203
306+
repo, number, _, item_id = item
307+
# identify appropriate project
308+
project_id = None
309+
field_id = None
310+
for project in project_data.keys():
311+
if repo in project_data[project]["repos"]:
312+
project_id = project_data[project]["id"]
313+
field_id = project_data[project]["status_field_id"]
314+
break
315+
if not project_id:
316+
LOG.error(f"missing project assignment for repository: {repo}")
317+
sys.exit(1)
318+
# add pull request to project
319+
if not args.dryrun:
320+
params = {"project_id": project_id, "item_id": item_id}
321+
result = github_gql_client.execute(
322+
query_add_item_to_project, variable_values=params
323+
)
324+
item_id = result["addProjectV2ItemById"]["item"]["id"]
325+
LOG.change_indent(+1)
326+
LOG.info(f"{repo}#{number} added to {project} project")
327+
# move pull request to Status: In review
328+
if not args.dryrun:
329+
option_id = project_data[project]["status_in_review_id"]
330+
params = {
331+
"field_id": field_id,
332+
"item_id": item_id,
333+
"project_id": project_id,
334+
"option_id": option_id,
335+
}
336+
result = github_gql_client.execute(
337+
query_set_status_option, variable_values=params
338+
)
339+
ditto = len(f"{repo}#{number}") * "^"
340+
# 90 is bright black (gray)
341+
LOG.info(f"\u001b[90m{ditto}\u001b[0m moved to Status: In review")
342+
LOG.change_indent(-1)
343+
344+
345+
def main():
346+
args = setup()
347+
348+
github_gql_client = gh_utils.setup_github_gql_client()
349+
350+
project_data = read_project_data()
351+
project_data = update_project_data(github_gql_client, project_data)
352+
353+
items = get_shafiya_items(github_gql_client)
354+
355+
track_items(args, github_gql_client, project_data, items)
356+
357+
358+
if __name__ == "__main__":
359+
try:
360+
main()
361+
except KeyboardInterrupt:
362+
LOG.info("Halted via KeyboardInterrupt.")
363+
sys.exit(130)
364+
except SystemExit as e:
365+
sys.exit(e.code)
366+
# Last
367+
except Exception:
368+
traceback_formatted = textwrap.indent(
369+
highlight(
370+
traceback.format_exc(),
371+
PythonTracebackLexer(),
372+
TerminalFormatter(),
373+
),
374+
" ",
375+
)
376+
LOG.critical(f"Unhandled exception:\n{traceback_formatted}")
377+
sys.exit(1)

manage_new_issues_and_pull_requests.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ def get_untracked_items(github_gql_client):
117117
"org:creativecommons"
118118
" state:open"
119119
" -project:creativecommons/15" # TimidRobot project
120-
" -project:creativecommons/22" # Shafiya-Heena project
121120
" -project:creativecommons/23" # possumbilities project
122121
)
123122
cursor = ""

0 commit comments

Comments
 (0)