From e53cc439338cc750ee953158e60c385e0d5813bf Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Mon, 14 Mar 2022 11:55:58 -0300 Subject: [PATCH] add ZebHub epics support --- .github/workflows/gh-pages.yml | 1 + README.md | 5 ++++ zcash-issue-dag.py | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 88be3c59..638ea53c 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -73,6 +73,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZENHUB_TOKEN: ${{ secrets.ZENHUB_TOKEN }} DAG_VIEW: zf + SHOW_EPICS: true - name: Render Halo2-focused DAG run: python3 ./zcash-issue-dag.py diff --git a/README.md b/README.md index f697df9b..93fd9259 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ For now, this repo hosts the script used to generate ECC's development dependenc ## Setup This project uses `poetry` for dependency management: https://python-poetry.org/ +It also depends on the Graphviz library; for Debian-based distros, install the `graphviz-dev` +package. + +After installing `poetry`, run `poetry install`. The scripts provided by this project require two environment variables: @@ -36,6 +40,7 @@ also supplied as environment variables: - `DAG_VIEW=[core|wallet|zf]`: The DAG to render (default: `core`). - `SHOW_MILESTONES=[true|false]`: Whether or not to render GitHub milestones as boxes (default: `false`). +- `SHOW_EPICS=[true|false]`: Whether or not to render ZenHub epics as boxes (default: `false`). - `INCLUDE_FINISHED=[true|false]`: Whether or not to include closed issues with no open blockers (default: `false`). Here's an example script for easily running the DAG generator: diff --git a/zcash-issue-dag.py b/zcash-issue-dag.py index 27d13354..aed67b60 100755 --- a/zcash-issue-dag.py +++ b/zcash-issue-dag.py @@ -75,6 +75,9 @@ PRUNE_FINISHED = strtobool(os.environ.get('PRUNE_FINISHED', 'true')) # Whether to group issues and PRs by milestone. SHOW_MILESTONES = strtobool(os.environ.get('SHOW_MILESTONES', 'false')) +# Whether to group issues and PRs by ZenHub epics. +SHOW_EPICS = strtobool(os.environ.get('SHOW_EPICS', 'false')) + class GitHubIssue: def __init__(self, repo_id, issue_number, data): @@ -177,6 +180,30 @@ class ZHDepsResourceHandler(drest.resource.ResourceHandler): for edge in response.data['dependencies'] ]) +class ZHEpicsResourceHandler(drest.resource.ResourceHandler): + def get(self, repo_id): + path = '/repositories/%d/epics' % repo_id + + try: + response = self.api.make_request('GET', path, {}) + except drest.exc.dRestRequestError as e: + msg = "%s (repo_id: %s)" % (e.msg, repo_id) + raise drest.exc.dRestRequestError(msg, e.response) + + return [i['issue_number'] for i in response.data['epic_issues']] + +class ZHEpicsIssuesResourceHandler(drest.resource.ResourceHandler): + def get(self, repo_id, epic_id): + path = '/repositories/%d/epics/%d' % (repo_id, epic_id) + + try: + response = self.api.make_request('GET', path, {}) + except drest.exc.dRestRequestError as e: + msg = "%s (repo_id: %s)" % (e.msg, repo_id) + raise drest.exc.dRestRequestError(msg, e.response) + + return [(i['repo_id'], i['issue_number']) for i in response.data['issues']] + class ZenHubAPI(drest.api.API): class Meta: baseurl = 'https://api.zenhub.com/p1' @@ -188,6 +215,8 @@ class ZenHubAPI(drest.api.API): def __init__(self, *args, **kw): super(ZenHubAPI, self).__init__(*args, **kw) self.add_resource('dependencies', ZHDepsResourceHandler) + self.add_resource('epics', ZHEpicsResourceHandler) + self.add_resource('epics_issues', ZHEpicsIssuesResourceHandler) def auth(self, *args, **kw): pass @@ -203,6 +232,24 @@ def main(): # Build the full dependency graph from ZenHub's per-repo APIs. dg = nx.compose_all([zapi.dependencies.get(x) for x in REPOS]) + if SHOW_EPICS: + epics_issues = [] + for repo_id in REPOS: + for epic_id in zapi.epics.get(repo_id): + epics_issues.append((repo_id, epic_id)) + + epics_mapping = download_issues(gapi, epics_issues) + epics_mapping = {k: v for (k, v) in epics_mapping.items() if v.state != 'closed'} + issues_by_epic = {} + for (i, ((repo_id, epic_id), epic)) in enumerate(epics_mapping.items()): + issues = set(zapi.epics_issues.get(repo_id, epic_id)) + issues_by_epic[epic] = issues + for i in issues: + # zapi.dependencies only returns nodes that have some connection, + # but we'd like to show all issues from epics even if they are + # disconnected. + dg.add_node(i) + # Fetch the issues within the graph. mapping = download_issues(gapi, dg.nodes) @@ -273,6 +320,12 @@ def main(): for (i, (milestone, nodes)) in enumerate(milestones.items()): ag.add_subgraph(nodes, 'cluster_%d' % i, label=milestone, color='blue') + if SHOW_EPICS: + for (i, (epic, issues)) in enumerate(issues_by_epic.items()): + issues = [n for n in dg if (n.repo_id, n.issue_number) in issues] + if issues: + ag.add_subgraph(issues, 'cluster_%d' % i, label=epic.title, color='blue') + # Draw the result! ag.graph_attr['rankdir'] = 'LR' ag.graph_attr['stylesheet'] = 'zcash-dag.css'