Skip to content

Commit

Permalink
Merge pull request #33 from siisurit/31-prepare-for-release
Browse files Browse the repository at this point in the history
#31 Prepare for release
  • Loading branch information
roskakori authored Nov 21, 2024
2 parents 8c0dac8 + 4e492a2 commit 344bbfb
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 137 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,4 @@ pyrightconfig.json
/.idea/ruff.xml

# End of https://www.toptal.com/developers/gitignore/api/pycharm,python
/schema.graphql
92 changes: 92 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Contributing

## Project setup

In case you want to play with the source code or contribute changes proceed as follows:

1. Check out the project from GitHub:
```bash
git clone https://github.com/roskakori/check_done.git
cd check_done
```
2. Install [poetry](https://python-poetry.org/).
3. Run the setup script to prepare the poetry environment and pre-commit hooks:
```bash
sh scripts/set_up_project.sh
```

## Testing

To run the test suite:

```bash
poetry run pytest
```

To build and browse the coverage report in HTML format:

```bash
sh scripts/test_coverage.sh
open htmlcov/index.html # macOS only
```

## Testing the GitHub app

In order to test your fork as GitHub app, you need to create your own as described in [Creating GitHub apps](https://docs.github.com/en/apps/creating-github-apps).

When asked for permissions that app requires, specify:

Repository permissions:

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

Organization permissions:

- Projects: read-only

## Coding guidelines

The code throughout uses a natural naming schema avoiding abbreviations, even for local variables and parameters.

Many coding guidelines are automatically enforced (and some even fixed automatically) by the pre-commit hook. If you want to check and clean up the code without performing a commit, run:

```bash
poetry run pre-commit run --all-files
```

## Release cheatsheet

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"
```

Next build the project and run the tests to ensure everything works:

```bash
poetry build
poetry run pytest
```

Then create a tag in the repository:

```bash
git tag --annotate --message "Tag version 1.x.x" v1.x.x
git push --tags
```

Publish the new version on PyPI:

```bash
poetry publish
```

Finally, add a release based on the tag from above to the [release page](https://github.com/siisurit/check_done/releases).
88 changes: 47 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,78 +1,84 @@
# check_done

check_done is a command line tool to check that finished issues and pull requests in a GitHub project board column are really done.

## Configuration
[![PyPI](https://img.shields.io/pypi/v/check_done)](https://pypi.org/project/pygount/)
[![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)
[![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)

To use check_done for your project, you need to configure the fields in the `yaml` file found in the `configuration` folder.
# check_done

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

#### project_url (required)
For each issue or pull request in the "Done" column of the project board, it checks that:

The project URL as is when viewing your GitHub V2 project. E.g.: `"https://github.com/orgs/my_project_owner_name/projects/1/views/1"`
- It is closed.
- It has an assignee.
- It is assigned to a milestone.
- All tasks are completed (list with check boxes in the description).

#### project_status_name_to_check (optional)
For pull requests, it additionally checks if they reference an issue.

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 will be the `"✅ Done"` column.
This ensures a consistent quality on done issues and pull request, and helps to notice if they were accidentally deemed to be done too early.

If you want to check a different column, you can specify its name with this option. For example:
## Installation

```yaml
project_status_name_to_check = "Done"
```
In order to gain access to your project board, issues, and pull request, check_done needs to autorize. The exact way to do that depends on whether your project belogs to a single user or an GitHub organization.

The name takes the first column that partially matches case sensitively. For example, `"Done"` will also match `"✅ Done"`, but not `"done"`.
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`.

If no column matches, the resulting error messages will tell you the exact names of all available columns.
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).

### Authorization settings for a project belonging to a GitHub user
Remember the **app ID** and **private key** of the installed app.

#### personal_access_token
## Configuration

A personal access token with the permission: `read:project`.
Check_done can be configured by having a `.check_done.yaml` in your current directory, or any of directory above.

Example:
Alternatively, you can specify a specific location, for example:

```yaml
project_url: "https://github.com/orgs/my_username/projects/1/views/1"
personal_access_token: "ghp_xxxxxxxxxxxxxxxxxxxxxx"
# Since no `project_status_name_to_check` was specified, the checks will apply to the last project status/column.
```bash
check_done --config some/path/.check_done.yaml
```

### Authorization settings for a project belonging to a GitHub organization
### Project and authentication

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 [Siisurit's check_done app](https://github.com/apps/siisurit-s-check-done).
A minimum configuration requires the URL of the project board and the authentication information from the installation.

You can also derive your own GitHub app from it with the following permissions:
The project URL can be seen in the web browser's URL bar when visiting the project board, for example: `https://github.com/users/my-username/projects/1/views/1` (for a user) or `https://github.com/my-organization/projects/1/` (for an organization).

- `pull requests`
- `projects`
- `read:issues`

Remember the App ID and the app private key. Then add the following settings to the configuration file:
An example for a user project could look like this:

```yaml
check_done_github_app_id = ... # Typically a decimal number with at least 6 digits
check_done_github_app_private_key = ""-----BEGIN RSA PRIVATE KEY..."
project_url: "https://github.com/users/my-username/projects/1/views/1"
personal_access_token: "ghp_xxxxxxxxxxxxxxxxxxxxxx"
```
Example:
For an organization project:
```yaml
project_url: "https://github.com/orgs/my_username/projects/1/views/1"
check_done_github_app_id: "0123456"
check_done_github_app_id: "1234567"
check_done_github_app_private_key: "-----BEGIN RSA PRIVATE KEY-----
something_something
-----END RSA PRIVATE KEY-----
"
project_status_name_to_check: "Done" # This will match the name of a project status/column containing "Done" like "✅ Done". The checks will then be applied to this project status/column.
```
### Using environment variables and examples
You can use environment variables for the values in the configuration yaml by starting them with a `$` symbol and wrapping it with curly braces. For example:
In order to avoid having to commit token and keys into your repository, you can use environment variables for the values in the configuration YAML by starting them with a `$` symbol and wrapping it with curly braces. For example:

```yaml
personal_access_token: ${MY_PERSONAL_ACCESS_TOKEN_ENVVAR}
```

### Changing the board status column 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 will be the `"✅ Done"` column.

If you want to check a different column, you can specify its name with this option. For example:

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

The name takes the first column that partially matches case sensitively. 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.
91 changes: 41 additions & 50 deletions check_done/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import check_done
from check_done.config import (
CONFIG_BASE_NAME,
default_config_path,
map_from_yaml_file_path,
resolve_configuration_yaml_file_path_from_root_path,
resolve_root_repository_path,
validate_configuration_info_from_yaml_map,
)
from check_done.done_project_items_info import done_project_items_info
Expand All @@ -20,56 +20,10 @@
_HELP_DESCRIPTION = "Checks that finished issues and pull requests in a GitHub project board column are really done."


class Command:
"""Command interface for check_done"""

def __init__(self):
self.args = None

@staticmethod
def argument_parser():
parser = argparse.ArgumentParser(prog="check_done", description=_HELP_DESCRIPTION)
parser.add_argument("--version", "-v", action="version", version="%(prog)s " + check_done.__version__)
# TODO#13: Is this implementation of the --root-dir argument proper? If not how to improve it?
parser.add_argument(
"--root-dir",
"-r",
type=Path,
default=resolve_root_repository_path(),
help="Specify a different root directory to check for the YAML config file.",
)
return parser

def apply_arguments(self, arguments=None):
parser = self.argument_parser()
self.args = parser.parse_args(arguments)

def execute(self):
configuration_yaml_path = resolve_configuration_yaml_file_path_from_root_path(self.args.root_dir)
yaml_map = map_from_yaml_file_path(configuration_yaml_path)
configuration_info = validate_configuration_info_from_yaml_map(yaml_map)
done_project_items = done_project_items_info(configuration_info)
done_project_items_count = len(done_project_items)
if done_project_items_count == 0:
logger.info("Nothing to check. Project has no items in the selected project status.")
else:
warnings = warnings_for_done_project_items(done_project_items)
if len(warnings) == 0:
logger.info(
f"All project items are correct, "
f"{done_project_items_count!s} checked in the selected project status. "
)
else:
for warning in warnings:
logger.warning(warning)


def check_done_command(arguments=None):
def check_done_command(arguments=None) -> int:
result = 1
command = Command()
try:
command.apply_arguments(arguments)
command.execute()
execute(arguments)
result = 0
except KeyboardInterrupt:
logger.error("Interrupted as requested by user.") # noqa: TRY400
Expand All @@ -78,6 +32,43 @@ def check_done_command(arguments=None):
return result


def execute(arguments=None):
parser = _argument_parser()
args = parser.parse_args(arguments)
configuration_yaml_path = args.config or default_config_path()
yaml_map = map_from_yaml_file_path(configuration_yaml_path)
configuration_info = validate_configuration_info_from_yaml_map(yaml_map)
done_project_items = done_project_items_info(configuration_info)
done_project_items_count = len(done_project_items)
if done_project_items_count == 0:
logger.info("Nothing to check. Project has no items in the selected project status.")
else:
warnings = warnings_for_done_project_items(done_project_items)
if len(warnings) == 0:
logger.info(
f"All project items are correct, "
f"{done_project_items_count!s} checked in the selected project status. "
)
else:
for warning in warnings:
logger.warning(warning)


def _argument_parser():
parser = argparse.ArgumentParser(prog="check_done", description=_HELP_DESCRIPTION)
parser.add_argument(
"--config",
"-c",
type=Path,
help=(
f"Path to configuration file with project URL, authentication details, and possibly other options; "
f"default: {CONFIG_BASE_NAME}.yaml in the current working directory or any of the above."
),
)
parser.add_argument("--version", "-v", action="version", version="%(prog)s " + check_done.__version__)
return parser


def main():
logging.basicConfig(level=logging.INFO)
sys.exit(check_done_command())
Expand Down
31 changes: 19 additions & 12 deletions check_done/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@

import yaml
from dotenv import load_dotenv
from git import Repo
from pydantic import Field, field_validator, model_validator
from pydantic.dataclasses import dataclass

load_dotenv()

CONFIG_FILE_NAME = ".check_done.yaml"
CONFIG_BASE_NAME = ".check_done"
_GITHUB_ORGANIZATION_NAME_AND_PROJECT_NUMBER_URL_REGEX = re.compile(
r"https://github\.com/orgs/(?P<organization_name>[a-zA-Z0-9\-]+)/projects/(?P<project_number>[0-9]+).*"
)
Expand Down Expand Up @@ -133,14 +132,22 @@ def map_from_yaml_file_path(configuration_path: Path) -> dict:
return result


def resolve_root_repository_path(path: Path | str = ".", search_parent_directories=True) -> Path:
root_repository = Repo(path, search_parent_directories=search_parent_directories)
result = Path(root_repository.git.rev_parse("--show-toplevel"))
return result


def resolve_configuration_yaml_file_path_from_root_path(root_path: Path) -> Path:
result = Path(root_path / CONFIG_FILE_NAME)
if not Path.is_file(result):
raise FileNotFoundError(f"Configuration file missing in the specified root path: {root_path}") from None
def default_config_path() -> Path:
initial_config_folder = Path().absolute()
config_folder = initial_config_folder
previous_config_folder = None
result = None
while result is None and config_folder != previous_config_folder:
# TODO#32 Check yaml and 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
else:
previous_config_folder = config_folder
config_folder = config_folder.parent
if result is None:
raise FileNotFoundError(
f"Cannot find configuration file by looking for {CONFIG_BASE_NAME}.yaml "
f"and traversing upwards starting in {initial_config_folder}"
)
return result
3 changes: 0 additions & 3 deletions check_done/graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ def __call__(self, request):
return request


# TODO#13: Is the lru_cache useful? There shouldn't be multiple identical queries due to GraphQL pagination.


@lru_cache
def minimized_graphql(graphql_query: str) -> str:
single_spaced_query = re.sub(r"\s+", " ", graphql_query)
Expand Down
Loading

0 comments on commit 344bbfb

Please sign in to comment.