Skip to content

Commit

Permalink
Add a Zashi pipeline render
Browse files Browse the repository at this point in the history
This shows the progress of implementing tracked bugs and features,
and deploying them to the mobile SDKs and Zashi apps.

Closes #64.
  • Loading branch information
str4d committed Oct 31, 2024
1 parent 3f769cb commit a666c1a
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ jobs:
DAG_VIEW: transparent-deprecation
TERMINATE_AT: "zcash/zcash#4203"

- name: Render Zashi pipeline
run: python3 ./zashi-pipeline.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ZENHUB_TOKEN: ${{ secrets.ZENHUB_TOKEN }}

- name: Copy the index page
run: cp ./index.html ./public

Expand Down
94 changes: 94 additions & 0 deletions helpers/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def __init__(self, repo_id, issue_number, data, REPOS):
if data is not None:
labels = [label['name'] for label in data['labels']['nodes']]
self.title = data['title']
self.labels = labels
self.is_release = 'C-release' in labels
self.is_target = 'C-target' in labels
self.is_pr = 'merged' in data
Expand All @@ -126,6 +127,7 @@ def __init__(self, repo_id, issue_number, data, REPOS):
else:
# If we can't fetch issue data, assume we don't care.
self.title = ''
self.labels = []
self.url = None
self.is_release = False
self.is_target = False
Expand Down Expand Up @@ -212,3 +214,95 @@ def chunks(lst, n):
ret[(repo, issue)] = GitHubIssue(repo, issue, issue_data, REPOS)

return ret


def fetch_issues_with_labels(op, labels, repos):
for (repo_id, (repo, issue_cursor, pr_cursor)) in repos:
conn = op.repository(
owner=repo[0],
name=repo[1],
__alias__='repo%d' % repo_id,
)

if issue_cursor != -1:
issues = conn.issues(
labels=labels,
first=50,
after=issue_cursor,
)
issues.nodes.number()
issues.nodes.labels(first=50).nodes().name()
issues.nodes.state()
issues.nodes.milestone().title()
issues.nodes.title()
issues.nodes.url()
issues.page_info.has_next_page()
issues.page_info.end_cursor()

if pr_cursor != -1:
prs = conn.pull_requests(
labels=labels,
first=50,
after=pr_cursor,
)
prs.nodes.number()
prs.nodes.labels(first=50).nodes().name()
prs.nodes.state()
prs.nodes.milestone().title()
prs.nodes.title()
prs.nodes.url()
prs.nodes.merged()
prs.page_info.has_next_page()
prs.page_info.end_cursor()


def download_issues_with_labels(endpoint, labels, REPOS):
ret = {}
repos = {repo_id: (repo, None, None) for (repo_id, repo) in REPOS.items()}

while True:
op = Operation(schema.Query)
fetch_issues_with_labels(op, labels, repos.items())

d = endpoint(op)
data = op + d

done = []
for (repo_id, (repo, _, _)) in repos.items():
repo_data = data['repo%d' % repo_id]

if hasattr(repo_data, 'issues'):
for issue in repo_data.issues.nodes:
ret[(repo_id, issue.number)] = GitHubIssue(repo_id, issue.number, issue, REPOS)
if repo_data.issues.page_info.has_next_page:
issue_cursor = repo_data.issues.page_info.end_cursor
else:
issue_cursor = -1
else:
issue_cursor = -1

if hasattr(repo_data, 'pull_requests'):
for pr in repo_data.pull_requests.nodes:
ret[(repo_id, pr.number)] = GitHubIssue(repo_id, pr.number, pr, REPOS)
if repo_data.pull_requests.page_info.has_next_page:
pr_cursor = repo_data.pull_requests.page_info.end_cursor
else:
pr_cursor = -1
else:
pr_cursor = -1

if issue_cursor == -1 and pr_cursor == -1:
done.append(repo_id)
else:
repos[repo_id] = (repo, issue_cursor, pr_cursor)

for repo_id in done:
del repos[repo_id]

if len(repos) > 0:
print('.', end='', flush=True)
else:
print()
break

return ret
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ <h1>Zcash Developers Hub</h1>
<p><a href="zcash-zcashd-deprecation-dag"><code>zcashd</code> deprecation DAG</a></p>
<p><a href="zcash-sprout-deprecation-dag">Sprout pool deprecation DAG</a></p>
<p><a href="zcash-transparent-deprecation-dag">Transparent pool deprecation DAG</a></p>
<p><a href="zashi-pipeline">Zashi pipeline</a></p>
</body>
</html>
217 changes: 217 additions & 0 deletions zashi-pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
#!/usr/bin/env python3

import networkx as nx
from str2bool import str2bool as strtobool

import os
import re
from textwrap import wrap
from urllib.parse import urlparse

from helpers import github, zenhub

GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
ZENHUB_TOKEN = os.environ.get('ZENHUB_TOKEN')

# IDs of repos we look for releases in.
RUST = 85334928
ANDROID_SDK = 151763639
SWIFT_SDK = 185480114
ZASHI_ANDROID = 390808594
ZASHI_IOS = 387551125

REPOS = {
**github.CORE_REPOS,
**github.WALLET_REPOS,
}


class TrackedIssue:
def __init__(self, issue):
self.issue = issue


def main():
gapi = github.api(GITHUB_TOKEN)
zapi = zenhub.api(ZENHUB_TOKEN)

print('Fetching tracked issues')
tracked_issues = github.download_issues_with_labels(gapi, ['C-tracked-bug', 'C-tracked-feature'], REPOS)

# The repos we care about are now:
# - Any repo containing a tracked issue.
# - The wallet repos where releases occur.
tracked_repos = set([repo_id for (repo_id, _) in tracked_issues])
repos = {
**github.WALLET_REPOS,
}
for repo_id in tracked_repos:
repos[repo_id] = REPOS[repo_id]
workspaces = {
workspace_id: [repo_id for repo_id in repos if repo_id in repos]
for (workspace_id, repos) in zenhub.WORKSPACE_SETS.items()
}

# Build the full dependency graph from ZenHub's per-workspace API.
print('Fetching graph')
dg = nx.compose_all([
zenhub.get_dependency_graph(zapi, workspace_id, repos)
for (workspace_id, repos) in workspaces.items()
if len(repos) > 0
])

print('Rendering deployment pipeline')

# Ensure that the tracked issues all exist in the graph. This is a no-op for
# issues that are already present.
start_at = set([issue for issue in tracked_issues])
for i in start_at:
dg.add_node(i)

# Replace the graph with the subgraph that only includes the tracked
# issues and their descendants.
descendants = [nx.descendants(dg, n) for n in start_at]
dg = nx.subgraph(dg, start_at.union(*descendants))

# Fetch the issues within the graph.
mapping = github.download_issues(gapi, dg.nodes, repos)

# Relabel the graph
dg = nx.relabel_nodes(dg, mapping)

# Filter out unknown issues
unknown = [n for n in dg if n.repo_id not in repos]
if len(unknown) > 0:
dg.remove_nodes_from(unknown)

# Apply property annotations
for (source, sink) in dg.edges:
attrs = dg.edges[source, sink]
attrs['is_open'] = 0 if source.state == 'closed' else 1

# Render the HTML version!
html_header = '''<!DOCTYPE html>
<html>
<head>
<title>Zashi Pipeline</title>
<style>
body {
color: #1f2328;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
}
a {
color: #0969da;
}
table {
border-collapse: collapse;
width: 100%;
width: max-content;
max-width: 100%;
overflow: auto;
}
table tr {
border-top: 1px solid #d1d9e0b3;
}
table th, table td {
border: 1px solid #d1d9e0b3;
padding: 6px 13px;
text-align: center;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
color: #f0f6fc;
}
a {
color: #4493f8;
}
table tr {
border-top: 1px solid #3d444db3;
}
table th, table td {
border: 1px solid #3d444db3;
}
}
</style>
</head>
<body>
<h1>Zashi Pipeline</h1>
<p>🐞 = bug, πŸ’‘ = feature, βœ… = implemented / released, πŸ›‘ = unfinished, πŸ“₯ = unassigned / DAG needs updating</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Issue</th>
<th>Rust crate</th>
<th>Android SDK</th>
<th>Swift SDK</th>
<th>Zashi Android</th>
<th>Zashi iOS</th>
</tr>
</thead>
<tbody>
'''
html_footer = '''
</tbody>
</table>
</body>
</html>
'''
with open('public/zashi-pipeline.html', 'w') as f:
f.write(html_header)

for issue in tracked_issues.values():
f.write('<tr>')

if 'C-tracked-bug' in issue.labels:
f.write('<td>🐞</td>')
else:
f.write('<td>πŸ’‘</td>')

f.write('<td>{} <a href="{}">{}</a></td>'.format(
'βœ…' if issue.state == 'closed' else 'πŸ›‘',
issue.url,
issue.title,
))

children = nx.descendants(dg, issue)
def find_child_release(repo_id):
for child in children:
if child.repo_id == repo_id and 'C-release' in child.labels:
# Extract version number from title
if repo_id == RUST:
version = re.search(r'zcash_[^ ]+ \d+(\.\d+)+', child.title).group()
else:
version = re.search(r'\d+(\.\d+)+', child.title).group()

f.write('<td>{} <a href="{}">{}</a></td>'.format(
'βœ…' if child.state == 'closed' else 'πŸ›‘',
child.url,
version,
))
return

# Release not found in this repo
f.write('<td>πŸ“₯</td>')

find_child_release(RUST)
find_child_release(ANDROID_SDK)
find_child_release(SWIFT_SDK)
find_child_release(ZASHI_ANDROID)
find_child_release(ZASHI_IOS)

f.write('</tr>')

f.write(html_footer)


if __name__ == '__main__':
if GITHUB_TOKEN and ZENHUB_TOKEN:
main()
else:
print('Please set the GITHUB_TOKEN and ZENHUB_TOKEN environment variables.')

0 comments on commit a666c1a

Please sign in to comment.