Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#31 Clean up in preparation for release #37

Merged
merged 20 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]
python-version: ["3.11", "3.12", "3.13"]
env:
MAIN_PYTHON_VERSION: "3.12" # same as Ubuntu 24 LTS

Expand Down Expand Up @@ -48,6 +48,7 @@ jobs:
run: |
poetry run pytest --cov=check_done --cov-branch --cov-report=xml
- name: Upload coverage reports to Codecov
if: ${{ matrix.python-version == env.MAIN_PYTHON_VERSION }}
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Check_done changes

## Version 1.0.0, 2024-11-27

- Initial release
6 changes: 2 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Repository permissions:

- Issues: read-only
- Pull requests: read-only
- Metadata: read-ony (mandatory and enabled automatically)
- Metadata: read-only (mandatory and enabled automatically)

Organization permissions:

Expand All @@ -56,14 +56,12 @@ Many coding guidelines are automatically enforced (and some even fixed automatic
poetry run pre-commit run --all-files
```

## Release cheatsheet
## Release cheat-sheet

This section only relevant for developers with access to the PyPI project.

To add a new release, first update the `pyproject.toml`:

.. code-block:: toml

```toml
[tool.poetry]
version = "1.x.x"
Expand Down
30 changes: 14 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[![PyPI](https://img.shields.io/pypi/v/check_done)](https://pypi.org/project/pygount/)
[![PyPI](https://img.shields.io/pypi/v/check_done)](https://pypi.org/project/check_done/)
[![Python Versions](https://img.shields.io/pypi/pyversions/check_done.svg)](https://www.python.org/downloads/)
[![Build Status](https://github.com/siisurit/check_done/actions/workflows/build.yml/badge.svg)](https://github.com/roskakori/pygount/actions/workflows/build.yml)
[![Build Status](https://github.com/siisurit/check_done/actions/workflows/build.yml/badge.svg)](https://github.com/siisurit/check_done/actions/workflows/build.yml)
[![codecov](https://codecov.io/gh/siisurit/check_done/graph/badge.svg?token=UIJZUCUJII)](https://codecov.io/gh/siisurit/check_done)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![License](https://img.shields.io/github/license/siisurit/check_done)](https://opensource.org/licenses/BSD-3-Clause)
Expand All @@ -9,26 +9,24 @@

Check_done is a command line tool to check that GitHub issues and pull requests in a project board with a status of "Done" are really done.

For each issue or pull request in the "Done" column of the project board, it checks that:
It checks that:

- It is closed.
- It has an assignee.
- It is assigned to a milestone.
- All tasks are completed (list with checkboxes in the description).
- All tasks are completed (checkboxes in the description).

For pull requests, it additionally checks if they reference an issue.
Additionally, for pull requests, it checks if they reference an issue.

This ensures a consistent quality on done issues and pull requests, and helps to notice if they were accidentally deemed to be done too early.

## Installation

In order to gain access to your project board, issues, and pull requests, check_done needs to be authorized. The exact way to do that depends on whether your project belongs to a single user or a GitHub organization.
In order to gain access to your project board, issues, and pull requests, check_done needs to be authorized. The exact way to do that depends on whether your project belongs to a GitHub user or organization.

For user projects, [create a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with the permission: `read:project`.

For organization projects, follow the instructions to [Installing a GitHub App from a third party](https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party) using the [Check_done app](https://github.com/apps/check-done-app).

Remember the **app ID** and **private key** of the installed app.
For organization projects, follow the instructions to [Installing a GitHub App from a third party](https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party) using the [Check_done app](https://github.com/apps/check-done-app). Remember the **app ID** and **private key** of the installed app in order to use for configuration.

## Configuration

Expand Down Expand Up @@ -57,8 +55,8 @@ For an organization project:

```yaml
project_url: "https://github.com/orgs/my_username/projects/1/views/1"
check_done_github_app_id: "1234567"
check_done_github_app_private_key: "-----BEGIN RSA PRIVATE KEY-----
github_app_id: "1234567"
github_app_private_key: "-----BEGIN RSA PRIVATE KEY-----
something_something
-----END RSA PRIVATE KEY-----
"
Expand All @@ -70,16 +68,16 @@ In order to avoid having to commit tokens and keys into your repository, you can
personal_access_token: ${MY_PERSONAL_ACCESS_TOKEN_ENVVAR}
```

### Changing the board status column to check
### Changing the project status to check

By default, check_done checks all issues and pull requests in the column rightmost/last column of the project board. If you left the default names when creating the GitHub project board, this would be the `"✅ Done"` column.
By default, check_done checks all issues and pull requests in the last selectable project status. If you left the default names when creating the GitHub project board, this would be the `"✅ Done"` project status.

If you want to check a different column, you can specify its name with this option. For example:
If you want to check a different project status, you can specify a partial or exact matching name with this option. For example:

```yaml
project_status_name_to_check = "Done"
```

The name takes the first column that partially matches the case sensitivity. For example, `"Done"` will also match `"✅ Done"`, but not `"done"`.
The name takes the first project status that partially matches the case sensitivity. For example, `"Done"` will also match `"✅ Done"`, but not `"done"`.

If no column matches, the resulting error messages will tell you the exact names of all available columns.
If no project status matches, the resulting error messages will show you the exact name of the available project status selections.
6 changes: 4 additions & 2 deletions check_done/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

logger = logging.getLogger(__name__)

_HELP_DESCRIPTION = "Checks that finished issues and pull requests in a GitHub project board column are really done."
_HELP_DESCRIPTION = (
'Check that GitHub issues and pull requests in a project board with a status of "Done" are really done.'
)


def check_done_command(arguments=None) -> int:
Expand Down Expand Up @@ -74,5 +76,5 @@ def main():
sys.exit(check_done_command())


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
main()
14 changes: 7 additions & 7 deletions check_done/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ class ConfigurationInfo:
personal_access_token: str | None = None

# Required for organization owned project
check_done_github_app_id: str | None = None
check_done_github_app_private_key: str | None = None
github_app_id: str | None = None
github_app_private_key: str | None = None

# Optional
project_status_name_to_check: str | None = None
Expand All @@ -51,8 +51,8 @@ class ConfigurationInfo:
"project_url",
"project_status_name_to_check",
"personal_access_token",
"check_done_github_app_id",
"check_done_github_app_private_key",
"github_app_id",
"github_app_private_key",
mode="before",
)
def value_from_env(cls, value: Any | None):
Expand All @@ -76,8 +76,8 @@ def validate_authentication_and_set_project_details(self):
self.personal_access_token is not None and not self.is_project_owner_of_type_organization
)
has_organizational_authentication = (
self.check_done_github_app_id is not None
and self.check_done_github_app_private_key is not None
self.github_app_id is not None
and self.github_app_private_key is not None
and self.is_project_owner_of_type_organization
)
if not has_user_authentication ^ has_organizational_authentication:
Expand Down Expand Up @@ -138,7 +138,7 @@ def default_config_path() -> Path:
previous_config_folder = None
result = None
while result is None and config_folder != previous_config_folder:
# TODO#32 Check yaml and yml.
# TODO#32 Check yaml and yml, if both exist yaml is picked over yml.
config_path_to_check = (config_folder / CONFIG_BASE_NAME).with_suffix(".yaml")
if config_path_to_check.is_file():
result = config_path_to_check
Expand Down
20 changes: 9 additions & 11 deletions check_done/done_project_items_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@ def done_project_items_info(configuration_info: ConfigurationInfo) -> list[Proje

is_project_owner_of_type_organization = configuration_info.is_project_owner_of_type_organization
if is_project_owner_of_type_organization:
check_done_github_app_id = configuration_info.check_done_github_app_id
check_done_github_app_private_key = configuration_info.check_done_github_app_private_key
access_token = resolve_organization_access_token(
project_owner_name, check_done_github_app_id, check_done_github_app_private_key
)
github_app_id = configuration_info.github_app_id
github_app_private_key = configuration_info.github_app_private_key
access_token = resolve_organization_access_token(project_owner_name, github_app_id, github_app_private_key)
else:
access_token = configuration_info.personal_access_token

Expand All @@ -55,7 +53,7 @@ def done_project_items_info(configuration_info: ConfigurationInfo) -> list[Proje
)

project_status_name_to_check = configuration_info.project_status_name_to_check
project_status_option_id = matching_project_state_option_id(
project_status_option_id = matching_project_status_option_id(
project_single_select_field_infos,
project_status_name_to_check,
project_number,
Expand All @@ -74,7 +72,7 @@ def matching_project_id(project_infos: list[ProjectV2Node], project_number: int,
) from None


def matching_project_state_option_id(
def matching_project_status_option_id(
project_single_select_field_infos: list[ProjectV2SingleSelectFieldNode],
project_status_name_to_check: str | None,
project_number: int,
Expand Down Expand Up @@ -117,14 +115,14 @@ def matching_project_state_option_id(

def filtered_project_item_infos_by_done_status(
project_item_infos: list[ProjectV2ItemNode],
project_state_option_id: str,
project_status_option_id: str,
) -> list[ProjectItemInfo]:
result = []
for project_item_info in project_item_infos:
has_project_state_option = (
has_project_status_option = (
project_item_info.field_value_by_name is not None
and project_item_info.field_value_by_name.option_id == project_state_option_id
and project_item_info.field_value_by_name.option_id == project_status_option_id
)
if has_project_state_option:
if has_project_status_option:
result.append(project_item_info.content)
return result
24 changes: 9 additions & 15 deletions check_done/organization_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,38 @@ class AuthenticationError(Exception):
"""Error raised due to failed JWT authentication process."""


def resolve_organization_access_token(
organization_name: str, check_done_github_app_id: str, check_done_github_app_private_key: str
) -> str:
def resolve_organization_access_token(organization_name: str, github_app_id: str, github_app_private_key: str) -> str:
"""
Generates the necessary access token for an organization from the installed GitHub app instance in said organization
"""
jwt_token = generate_jwt_token(check_done_github_app_id, check_done_github_app_private_key)
jwt_token = generate_jwt_token(github_app_id, github_app_private_key)
session = requests.Session()
session.headers = {"Accept": "application/vnd.github+json"}
session.auth = HttpBearerAuth(jwt_token)
try:
check_done_github_app_installation_id = resolve_check_done_github_app_installation_id(
session, organization_name
)
result = resolve_access_token_from_check_done_github_app_installation_id(
session, check_done_github_app_installation_id
)
github_app_installation_id = resolve_github_app_installation_id(session, organization_name)
result = resolve_access_token_from_github_app_installation_id(session, github_app_installation_id)
except Exception as error:
raise AuthenticationError(
f"Cannot resolve organization access token from JWT authentication process: {error}"
) from error
return result


def generate_jwt_token(check_done_github_app_id: str, check_done_github_app_private_key: str) -> str:
def generate_jwt_token(github_app_id: str, github_app_private_key: str) -> str:
"""Generates a JWT token for authentication with GitHub."""
try:
payload = {
"exp": _EXPIRES_AT,
"iat": _ISSUED_AT,
"iss": check_done_github_app_id,
"iss": github_app_id,
}
return jwt.encode(payload, check_done_github_app_private_key, algorithm="RS256")
return jwt.encode(payload, github_app_private_key, algorithm="RS256")
except Exception as error:
raise AuthenticationError(f"Cannot generate JWT token: {error}") from error


def resolve_check_done_github_app_installation_id(session: Session, organization_name: str) -> str:
def resolve_github_app_installation_id(session: Session, organization_name: str) -> str:
"""Fetches the installation ID for the organization."""
response = session.get(f"https://api.github.com/orgs/{organization_name}/installation")

Expand All @@ -65,7 +59,7 @@ def resolve_check_done_github_app_installation_id(session: Session, organization
)


def resolve_access_token_from_check_done_github_app_installation_id(session: Session, installation_id: str) -> str:
def resolve_access_token_from_github_app_installation_id(session: Session, installation_id: str) -> str:
"""Retrieves the access token using the installation ID."""
response = session.post(f"https://api.github.com/app/installations/{installation_id}/access_tokens")
if response.status_code == 201 and response.json().get("token") is not None:
Expand Down
Loading