Skip to content

Commit

Permalink
feat: Add support for GitHub Projects v2 (the "new" GitHub Projects)
Browse files Browse the repository at this point in the history
  • Loading branch information
thehale authored Dec 28, 2023
2 parents 0ad3118 + 8f3c7d5 commit 134fdcb
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 28 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pip install -r requirements.txt
```

## Configuration
### 1. Create a [Personal Access Token](https://github.com/settings/tokens) with the `repo` scope.
### 1. Create a [Personal Access Token](https://github.com/settings/tokens) with the `repo` scope. If Project V2, add `read:project` scope.
Do not share this token with anyone! It gives the bearer full control over all private repositories you have access to!

This is required to pull the Project Board data from GitHub's GraphQL API.
Expand All @@ -84,8 +84,8 @@ This allows the `.gitignore` to exclude your `config.json` from being accidental
| `repo_owner` | The username of the owner of the repo. <br/><br/> Example: `thehale` |
| `repo_name` | The name of the repo. <br/><br/> Example: `github-projects-burndown-chart`|
| `project_number` | The ID of the project for which you want to generate a burndown chart. This is found in the URL when looking at the project board on GitHub. <br/><br/> Example: `1` (from [`https://github.com/thehale/github-projects-burndown-chart/projects/1`](https://github.com/thehale/github-projects-burndown-chart/projects/1)) |
| `column_count` | A number >= the number of columns on the project board. (DEFAULT: 5)<br/><br/> A closer fit improves performance and reduces the chance of rate limiting from GitHub's GraphQL API. |
| `max_cards_per_column_count` | A number >= the maximum number of cards in any column on the project board. (DEFAULT: 50)<br/><br/> A closer fit improves performance and reduces the chance of rate limiting from GitHub's GraphQL API. |
| `column_count` | A number >= the number of columns on the project board. (DEFAULT: 5)<br/><br/> A closer fit improves performance and reduces the chance of rate limiting from GitHub's GraphQL API. If Project V2, it is optional. |
| `max_cards_per_column_count` | A number >= the maximum number of cards in any column on the project board. (DEFAULT: 50)<br/><br/> A closer fit improves performance and reduces the chance of rate limiting from GitHub's GraphQL API. If Project V2, it is optional. |
| `labels_per_issue_count` | A number >= the number of labels on any issue on project board. (DEFAULT: 5)<br/><br/> A closer fit improves performance and reduces the chance of rate limiting from GitHub's GraphQL API. |

`project_name.settings`
Expand All @@ -96,6 +96,7 @@ This allows the `.gitignore` to exclude your `config.json` from being accidental
| `chart_end_date` | (OPTIONAL) The last day to show on the burndown chart formatted as `YYYY-MM-DD`. <br/><br/> Used to change the end date of the chart without affecting the slope of the ideal burndown line (e.g. to show tasks that were completed after the official end of a sprint). <br/><br/> Example: `2021-10-24` |
| `points_label` | (OPTIONAL) The prefix for issue labels containing the point value of the issue. Removing this prefix must leave just an integer. If set to `null`, the burndown chart will count open issues instead of points.<br/><br/> Example: `Points: ` (with the space) |
| `calculators` | (OPTIONAL) A list of the calculator(s) to use to calculate the point burndown lines to show on the burndown chart. (DEFAULT: [`closed`])<br/><br/>_OPTIONS:_ `closed`, `assigned`, `created`, `taiga`<br/><br/> Example: [`taiga`, `closed`, `assigned`] |
| `version` | (OPTIONAL) The version number of GitHub Projects to use the burndown chart. (DEFAULT: [`1`])<br/><br/> OPTIONS: `1`, `2`<br/><br/> Example: `2` |

#### Organization Projects
All settings are the same as for the [Repository Projects](#repository-projects), except `repo_owner` and `repo_name` are replaced with `organization_name` as shown below.
Expand Down
34 changes: 28 additions & 6 deletions src/github_projects_burndown_chart/gh/api_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import tempfile

from config import config, secrets
from .project import Project
from .queries import OrganizationProject, RepositoryProject
from .project import Project, ProjectV1, ProjectV2
from .queries import OrganizationProject, OrganizationProjectV2, RepositoryProject, RepositoryProjectV2

# Set up logging
__logger = logging.getLogger(__name__)
Expand All @@ -18,18 +18,40 @@
__logger.addHandler(__ch)


def get_repository_project() -> dict:
__project_v2_queries = {
'repository': RepositoryProjectV2,
'organization': OrganizationProjectV2,
}


def get_repository_project() -> Project:
query_variables = config['query_variables']
query_response = gh_api_query(RepositoryProject, query_variables)
project_data = query_response['data']['repository']['project']
return Project(project_data)
return ProjectV1(project_data)


def get_organization_project() -> dict:
def get_organization_project() -> Project:
query_variables = config['query_variables']
query_response = gh_api_query(OrganizationProject, query_variables)
project_data = query_response['data']['organization']['project']
return Project(project_data)
return ProjectV1(project_data)


def get_project_v2(project_type) -> Project:
query = __project_v2_queries[project_type]
query_variables = config['query_variables'].copy()
query_response = gh_api_query(query, query_variables)
project_data = query_response['data'][project_type]['projectV2']
page_info = project_data['items']['pageInfo']
while page_info['hasNextPage']:
query_variables['cursor'] = page_info['endCursor']
query_response = gh_api_query(query, query_variables)
items = query_response['data'][project_type]['projectV2']['items']
project_data['items']['nodes'].extend(items['nodes'])
page_info = items['pageInfo']

return ProjectV2(project_data)


def gh_api_query(query: str, variables: dict) -> dict:
Expand Down
44 changes: 33 additions & 11 deletions src/github_projects_burndown_chart/gh/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@


class Project:
def __init__(self, project_data):
self.name = project_data['name']
self.columns = self.__parse_columns(project_data)

def __parse_columns(self, project_data):
columns_data = project_data['columns']['nodes']
columns = [Column(column_data) for column_data in columns_data]
return columns
columns = None

@property
def total_points(self):
Expand All @@ -23,15 +16,44 @@ def cards(self):
return [card for column in self.columns for card in column.cards]


class Column:
def __init__(self, column_data):
self.cards = self.__parse_cards(column_data)
class ProjectV1(Project):
def __init__(self, project_data):
self.name = project_data['name']
self.columns = self.__parse_columns(project_data)

def __parse_columns(self, project_data):
columns_data = project_data['columns']['nodes']
columns = [Column(self.__parse_cards(column_data)) for column_data in columns_data]
return columns

def __parse_cards(self, column_data):
cards_data = column_data['cards']['nodes']
cards = [Card(card_data) for card_data in cards_data]
return cards


class ProjectV2(Project):
def __init__(self, project_data):
self.name = project_data['title']
self.columns = self.__parse_columns(project_data)

def __parse_columns(self, project_data):
column_dict = {None: []}
for option in project_data['field']['options']:
column_dict[option['name']] = []

for item_data in project_data['items']['nodes']:
status = (item_data.get('fieldValueByName') or {}).get('name')
column_dict[status].append(Card(item_data))

columns = [Column(column_data) for column_data in column_dict.values()]
return columns


class Column:
def __init__(self, cards):
self.cards = cards

def get_total_points(self):
return sum([card.points for card in self.cards])

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
query OrganizationProject($organization_name: String!, $project_number: Int!, $labels_per_issue_count: Int!, $cursor: String) {
organization(login: $organization_name) {
projectV2(number: $project_number) {
title
field(name: "Status") {
... on ProjectV2SingleSelectField {
options {
name
}
}
}
items(first: 100, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
content {
...on Issue {
title
timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) {
nodes {
__typename
... on AssignedEvent {
createdAt
}
}
}
createdAt
closedAt
labels(first: $labels_per_issue_count) {
nodes {
name
}
}
}
...on PullRequest {
title
timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) {
nodes {
__typename
... on AssignedEvent {
createdAt
}
}
}
createdAt
closedAt
labels(first: $labels_per_issue_count) {
nodes {
name
}
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
query RepositoryProject($repo_owner: String!, $repo_name: String!, $project_number: Int!, $labels_per_issue_count: Int!, $cursor: String) {
repository(owner: $repo_owner, name: $repo_name) {
projectV2(number: $project_number) {
title
field(name: "Status") {
... on ProjectV2SingleSelectField {
options {
name
}
}
}
items(first: 100, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
content {
...on Issue {
title
timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) {
nodes {
__typename
... on AssignedEvent {
createdAt
}
}
}
createdAt
closedAt
labels(first: $labels_per_issue_count) {
nodes {
name
}
}
}
...on PullRequest {
title
timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) {
nodes {
__typename
... on AssignedEvent {
createdAt
}
}
}
createdAt
closedAt
labels(first: $labels_per_issue_count) {
nodes {
name
}
}
}
}
}
}
}
}
}
6 changes: 6 additions & 0 deletions src/github_projects_burndown_chart/gh/queries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@

with open(os.path.join(__location__, 'OrganizationProject.graphql')) as query:
OrganizationProject = query.read()

with open(os.path.join(__location__, 'RepositoryProjectV2.graphql')) as query:
RepositoryProjectV2 = query.read()

with open(os.path.join(__location__, 'OrganizationProjectV2.graphql')) as query:
OrganizationProjectV2 = query.read()
18 changes: 10 additions & 8 deletions src/github_projects_burndown_chart/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from chart.burndown import *
from config import config
from discord import webhook
from gh.api_wrapper import get_organization_project, get_repository_project
from gh.api_wrapper import get_organization_project, get_repository_project, get_project_v2
from gh.project import Project
from util import calculators, colors
from util.stats import *
Expand All @@ -22,12 +22,14 @@ def parse_cli_args():
return parser.parse_args()


def download_project_data(args):
if args.project_type == 'repository':
project: Project = get_repository_project()
elif args.project_type == 'organization':
project: Project = get_organization_project()
return project
def download_project_data(project_type: str, project_version: int) -> Project:
if project_version == 2:
return get_project_v2(project_type)

if project_type == 'repository':
return get_repository_project()
elif project_type == 'organization':
return get_organization_project()


def prepare_chart_data(stats: ProjectStats):
Expand Down Expand Up @@ -55,7 +57,7 @@ def prepare_chart_data(stats: ProjectStats):
if __name__ == '__main__':
args = parse_cli_args()
config.set_project(args.project_type, args.project_name)
project = download_project_data(args)
project = download_project_data(args.project_type, config['settings'].get('version', 1))
stats = ProjectStats(project, config.utc_sprint_start(),
config.utc_chart_end() or config.utc_sprint_end())
# Generate the burndown chart
Expand Down

0 comments on commit 134fdcb

Please sign in to comment.