An easy-to-use burndown chart generator for GitHub Project Boards.
0

Configure Feed

Select the types of activity you want to include in your feed.

Compatible with ProjectV2

shigetoshi.komatsu (Dec 19, 2023, 3:11 PM +0900) f2340798 0d6f34dc

+203 -18
+3 -2
README.md
··· 84 84 | `repo_owner` | The username of the owner of the repo. <br/><br/> Example: `thehale` | 85 85 | `repo_name` | The name of the repo. <br/><br/> Example: `github-projects-burndown-chart`| 86 86 | `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)) | 87 - | `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. | 88 - | `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. | 87 + | `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. | 88 + | `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. | 89 89 | `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. | 90 90 91 91 `project_name.settings` ··· 96 96 | `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` | 97 97 | `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) | 98 98 | `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`] | 99 + | `v2` | (OPTIONAL) ProjectV2 or not. If set to `true`, the burndown chart is as Project V2. | 99 100 100 101 #### Organization Projects 101 102 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.
+8 -6
src/github_projects_burndown_chart/main.py
··· 3 3 from chart.burndown import * 4 4 from config import config 5 5 from discord import webhook 6 - from gh.api_wrapper import get_organization_project, get_repository_project 6 + from gh.api_wrapper import get_organization_project, get_repository_project, get_project_v2 7 7 from gh.project import Project 8 8 from util import calculators, colors 9 9 from util.stats import * ··· 22 22 return parser.parse_args() 23 23 24 24 25 - def download_project_data(project_type: str) -> Project: 25 + def download_project_data(project_type: str, v2: bool) -> Project: 26 + if v2: 27 + return get_project_v2(project_type) 28 + 26 29 if project_type == 'repository': 27 - project: Project = get_repository_project() 30 + return get_repository_project() 28 31 elif project_type == 'organization': 29 - project: Project = get_organization_project() 30 - return project 32 + return get_organization_project() 31 33 32 34 33 35 def prepare_chart_data(stats: ProjectStats): ··· 55 57 if __name__ == '__main__': 56 58 args = parse_cli_args() 57 59 config.set_project(args.project_type, args.project_name) 58 - project = download_project_data(args.project_type) 60 + project = download_project_data(args.project_type, config['settings']['v2']) 59 61 stats = ProjectStats(project, config.utc_sprint_start(), 60 62 config.utc_chart_end() or config.utc_sprint_end()) 61 63 # Generate the burndown chart
+26 -4
src/github_projects_burndown_chart/gh/api_wrapper.py
··· 7 7 import tempfile 8 8 9 9 from config import config, secrets 10 - from .project import Project 11 - from .queries import OrganizationProject, RepositoryProject 10 + from .project import Project, ProjectV1, ProjectV2 11 + from .queries import OrganizationProject, OrganizationProjectV2, RepositoryProject, RepositoryProjectV2 12 12 13 13 # Set up logging 14 14 __logger = logging.getLogger(__name__) ··· 18 18 __logger.addHandler(__ch) 19 19 20 20 21 + __project_v2_queries = { 22 + 'repository': RepositoryProjectV2, 23 + 'organization': OrganizationProjectV2, 24 + } 25 + 26 + 21 27 def get_repository_project() -> Project: 22 28 query_variables = config['query_variables'] 23 29 query_response = gh_api_query(RepositoryProject, query_variables) 24 30 project_data = query_response['data']['repository']['project'] 25 - return Project(project_data) 31 + return ProjectV1(project_data) 26 32 27 33 28 34 def get_organization_project() -> Project: 29 35 query_variables = config['query_variables'] 30 36 query_response = gh_api_query(OrganizationProject, query_variables) 31 37 project_data = query_response['data']['organization']['project'] 32 - return Project(project_data) 38 + return ProjectV1(project_data) 39 + 40 + 41 + def get_project_v2(project_type) -> Project: 42 + query = __project_v2_queries[project_type] 43 + query_variables = config['query_variables'].copy() 44 + query_response = gh_api_query(query, query_variables) 45 + project_data = query_response['data'][project_type]['projectV2'] 46 + page_info = project_data['items']['pageInfo'] 47 + while page_info['hasNextPage']: 48 + query_variables['cursor'] = page_info['endCursor'] 49 + query_response = gh_api_query(query, query_variables) 50 + items = query_response['data'][project_type]['projectV2']['items'] 51 + project_data['items']['nodes'].extend(items['nodes']) 52 + page_info = items['pageInfo'] 53 + 54 + return ProjectV2(project_data) 33 55 34 56 35 57 def gh_api_query(query: str, variables: dict) -> dict:
+28 -6
src/github_projects_burndown_chart/gh/project.py
··· 5 5 6 6 7 7 class Project: 8 + columns = None 9 + 10 + @property 11 + def total_points(self): 12 + return sum([column.get_total_points() for column in self.columns]) 13 + 14 + @property 15 + def cards(self): 16 + return [card for column in self.columns for card in column.cards] 17 + 18 + 19 + class ProjectV1(Project): 8 20 def __init__(self, project_data): 9 21 self.name = project_data['name'] 10 22 self.columns = self.__parse_columns(project_data) ··· 19 31 cards = [Card(card_data) for card_data in cards_data] 20 32 return cards 21 33 22 - @property 23 - def total_points(self): 24 - return sum([column.get_total_points() for column in self.columns]) 25 34 26 - @property 27 - def cards(self): 28 - return [card for column in self.columns for card in column.cards] 35 + class ProjectV2(Project): 36 + def __init__(self, project_data): 37 + self.name = project_data['title'] 38 + self.columns = self.__parse_columns(project_data) 39 + 40 + def __parse_columns(self, project_data): 41 + column_dict = {} 42 + for option in project_data['field']['options']: 43 + column_dict[option['name']] = [] 44 + 45 + for item_data in project_data['items']['nodes']: 46 + status = item_data['fieldValueByName']['name'] 47 + column_dict[status].append(Card(item_data)) 48 + 49 + columns = [Column(column_data) for column_data in column_dict.values()] 50 + return columns 29 51 30 52 31 53 class Column:
+66
src/github_projects_burndown_chart/gh/queries/OrganizationProjectV2.graphql
··· 1 + query OrganizationProject($organization_name: String!, $project_number: Int!, $labels_per_issue_count: Int!, $cursor: String) { 2 + organization(login: $organization_name) { 3 + projectV2(number: $project_number) { 4 + title 5 + field(name: "Status") { 6 + ... on ProjectV2SingleSelectField { 7 + options { 8 + name 9 + } 10 + } 11 + } 12 + items(first: 100, after: $cursor) { 13 + pageInfo { 14 + hasNextPage 15 + endCursor 16 + } 17 + nodes { 18 + id 19 + fieldValueByName(name: "Status") { 20 + ... on ProjectV2ItemFieldSingleSelectValue { 21 + name 22 + } 23 + } 24 + content { 25 + ...on Issue { 26 + title 27 + timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) { 28 + nodes { 29 + __typename 30 + ... on AssignedEvent { 31 + createdAt 32 + } 33 + } 34 + } 35 + createdAt 36 + closedAt 37 + labels(first: $labels_per_issue_count) { 38 + nodes { 39 + name 40 + } 41 + } 42 + } 43 + ...on PullRequest { 44 + title 45 + timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) { 46 + nodes { 47 + __typename 48 + ... on AssignedEvent { 49 + createdAt 50 + } 51 + } 52 + } 53 + createdAt 54 + closedAt 55 + labels(first: $labels_per_issue_count) { 56 + nodes { 57 + name 58 + } 59 + } 60 + } 61 + } 62 + } 63 + } 64 + } 65 + } 66 + }
+66
src/github_projects_burndown_chart/gh/queries/RepositoryProjectV2.graphql
··· 1 + query RepositoryProject($repo_owner: String!, $repo_name: String!, $project_number: Int!, $labels_per_issue_count: Int!, $cursor: String) { 2 + repository(owner: $repo_owner, name: $repo_name) { 3 + projectV2(number: $project_number) { 4 + title 5 + field(name: "Status") { 6 + ... on ProjectV2SingleSelectField { 7 + options { 8 + name 9 + } 10 + } 11 + } 12 + items(first: 100, after: $cursor) { 13 + pageInfo { 14 + hasNextPage 15 + endCursor 16 + } 17 + nodes { 18 + id 19 + fieldValueByName(name: "Status") { 20 + ... on ProjectV2ItemFieldSingleSelectValue { 21 + name 22 + } 23 + } 24 + content { 25 + ...on Issue { 26 + title 27 + timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) { 28 + nodes { 29 + __typename 30 + ... on AssignedEvent { 31 + createdAt 32 + } 33 + } 34 + } 35 + createdAt 36 + closedAt 37 + labels(first: $labels_per_issue_count) { 38 + nodes { 39 + name 40 + } 41 + } 42 + } 43 + ...on PullRequest { 44 + title 45 + timelineItems(first: 20, itemTypes: [ASSIGNED_EVENT]) { 46 + nodes { 47 + __typename 48 + ... on AssignedEvent { 49 + createdAt 50 + } 51 + } 52 + } 53 + createdAt 54 + closedAt 55 + labels(first: $labels_per_issue_count) { 56 + nodes { 57 + name 58 + } 59 + } 60 + } 61 + } 62 + } 63 + } 64 + } 65 + } 66 + }
+6
src/github_projects_burndown_chart/gh/queries/__init__.py
··· 11 11 12 12 with open(os.path.join(__location__, 'OrganizationProject.graphql')) as query: 13 13 OrganizationProject = query.read() 14 + 15 + with open(os.path.join(__location__, 'RepositoryProjectV2.graphql')) as query: 16 + RepositoryProjectV2 = query.read() 17 + 18 + with open(os.path.join(__location__, 'OrganizationProjectV2.graphql')) as query: 19 + OrganizationProjectV2 = query.read()