diff --git a/.config/constraints.txt b/.config/constraints.txt index a2183a5..ee57bd4 100644 --- a/.config/constraints.txt +++ b/.config/constraints.txt @@ -2,10 +2,9 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --all-extras --no-annotate --output-file=.config/constraints.txt --strip-extras pyproject.toml +# pip-compile --all-extras --no-annotate --output-file=.config/constraints.txt --strip-extras --unsafe-package=backports-tarfile --unsafe-package=cryptography --unsafe-package=exceptiongroup --unsafe-package=jeepney --unsafe-package=secretstorage --unsafe-package=twine pyproject.toml # babel==2.15.0 -backports-tarfile==1.2.0 beautifulsoup4==4.12.3 build==1.2.1 cachetools==5.3.3 @@ -18,13 +17,11 @@ charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 coverage==7.5.3 -cryptography==42.0.8 cssselect2==0.7.0 defusedxml==0.7.1 diskcache==5.6.3 distlib==0.3.8 docutils==0.21.2 -exceptiongroup==1.2.1 filelock==3.15.1 ghp-import==2.1.0 gitdb==4.0.11 @@ -35,7 +32,6 @@ iniconfig==2.0.0 jaraco-classes==3.4.0 jaraco-context==5.3.0 jaraco-functools==4.0.1 -jeepney==0.8.0 jinja2==3.1.4 keyring==25.2.1 markdown==3.6 @@ -57,6 +53,7 @@ packaging==24.1 paginate==0.5.6 pathspec==0.12.1 pillow==10.3.0 +pip==24.0 pipdeptree==2.22.0 pkginfo==1.11.1 platformdirs==4.2.2 @@ -79,7 +76,7 @@ requests==2.32.3 requests-toolbelt==1.0.0 rfc3986==2.0.0 rich==13.7.1 -secretstorage==3.3.3 +setuptools==70.0.0 shellingham==1.5.4 six==1.16.0 smmap==5.0.1 @@ -88,8 +85,8 @@ subprocess-tee==0.4.1 tinycss2==1.3.0 tomli==2.0.1 ; python_version < "3.11" tox==4.15.1 -twine==5.1.0 typer==0.12.3 +typer-config==1.4.0 typing-extensions==4.12.2 urllib3==2.2.2 virtualenv==20.26.2 @@ -98,5 +95,9 @@ webencodings==0.5.1 zipp==3.19.2 # The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools +# backports-tarfile +# cryptography +# exceptiongroup +# jeepney +# secretstorage +# twine diff --git a/.config/dictionary.txt b/.config/dictionary.txt index fb7e330..d7e9a66 100644 --- a/.config/dictionary.txt +++ b/.config/dictionary.txt @@ -7,9 +7,11 @@ PYTHONPYCACHEPREFIX REQPASS Sbarnea Sorin +autohide autorefs basepython cairosvg +cidrblock clib codenotify codespell @@ -21,6 +23,7 @@ distros envdir envlist envname +exceptiongroup facelessuser fontawesome gerrit @@ -29,6 +32,7 @@ gitreview hashseed htmlproofer inlinehilite +jeepney linejoin linenums magiclink @@ -53,15 +57,18 @@ pypa pypackage resolvelib ruamel +secretstorage setenv shellingham showconfig showlocals ssbarnea superfences +tablerender taskfile taskfiles testenv +timeago toxenv toxinidir typer diff --git a/.config/requirements.txt b/.config/requirements.txt index 4e276fc..8e53dfc 100644 --- a/.config/requirements.txt +++ b/.config/requirements.txt @@ -4,10 +4,12 @@ gitpython>=3.1.26 packaging>=22 # pytest, tox, build pip>=21.0.1 # py_package pluggy>=1.5.0 # typer and pytest indirect +pyyaml>=5.1 rich>=10.11.0 # typer indirect setuptools # py_package due to running setup.py shellingham>=1.5.4 subprocess-tee>=0.4.1 tomli>=2.0.1 ; python_version < "3.11" # tox twine>=3.4.1 # py_package +typer-config>=1.4.0 typer>=0.12.2 # completion tests will fail with older diff --git a/.github/SECURITY.md b/.github/SECURITY.md index d33273a..5487fe6 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -6,6 +6,6 @@ The only supported version is the latest release. ## Reporting a Vulnerability -Use [github reporting](https://github.com/pycontribs/mk/security) if you -found any security vulnerability that is better to not have a public bug raised -right away. +Use [github reporting](https://github.com/pycontribs/mk/security) if you found +any security vulnerability that is better to not have a public bug raised right +away. diff --git a/.github/lower-constraints.txt b/.github/lower-constraints.txt index 233ed9d..7adceb4 100644 --- a/.github/lower-constraints.txt +++ b/.github/lower-constraints.txt @@ -7,10 +7,12 @@ gitpython==3.1.26 packaging==22 pip==23.2 # py_package (before installation might fail, including with tox) pluggy==1.5.0 +pyyaml==5.1 rich==10.11.0 # typer indirect setuptools # py_package due to running setup.py shellingham==1.5.4 subprocess-tee==0.4.1 tomli >= 2.0.1 ; python_version < "3.11" # tox v4 twine==3.4.1 # py_package +typer-config==1.4.0 typer==0.12.2 diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index e5fd3f4..3961360 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -53,7 +53,7 @@ jobs: env: # Number of expected test passes, safety measure for accidental skip of # tests. Update value if you add/remove tests. - PYTEST_REQPASS: 7 + PYTEST_REQPASS: 8 steps: - uses: actions/checkout@v4 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f82cd59..04e5c5b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,8 +11,8 @@ repos: args: - --fix - --exit-non-zero-on-fix - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.8 + - repo: https://github.com/pycontribs/mirrors-prettier + rev: v3.3.2 hooks: - id: prettier always_run: true @@ -76,19 +76,21 @@ repos: - packaging - rich - subprocess-tee + - typer-config - typer>=0.12.2 - repo: https://github.com/pycqa/pylint rev: v3.2.3 hooks: - id: pylint additional_dependencies: - - gitpython - click-help-colors - diskcache + - gitpython - importlib-metadata - pytest - rich - subprocess-tee + - typer-config - typer>=0.12.2 - repo: https://github.com/jazzband/pip-tools rev: 7.4.1 @@ -97,7 +99,8 @@ repos: name: lock alias: lock always_run: true - entry: pip-compile -q --no-annotate --output-file=.config/constraints.txt pyproject.toml --all-extras --strip-extras + # keyring excluded in order to keep lockfile portable between linux and macos + entry: pip-compile pyproject.toml files: ^.config\/.*requirements.*$ language: python language_version: "3.10" # minimal we support officially @@ -109,7 +112,7 @@ repos: name: deps alias: deps always_run: true - entry: pip-compile -q --no-annotate --output-file=.config/constraints.txt pyproject.toml --all-extras --strip-extras --upgrade + entry: pip-compile pyproject.toml --upgrade files: ^.config\/.*requirements.*$ language: python language_version: "3.10" # minimal we support officially diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..432d551 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,21 @@ +--- +proseWrap: always +jsonRecursiveSort: true # prettier-plugin-sort-json +tabWidth: 2 +useTabs: false +overrides: + - files: + - "*.md" + options: + # compatibility with markdownlint + proseWrap: always + printWidth: 80 + - files: + - "*.yaml" + - "*.yml" + options: + # compatibility with yamllint + proseWrap: preserve +# https://github.com/prettier/prettier/issues/15141 +# plugins: +# - prettier-plugin-sort-json diff --git a/docs/README.md b/docs/README.md index a56571d..5e21d2a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,9 +1,9 @@ # Documentation of mk tool `mk` is a CLI tool that aims to ease contribution and maintenance for projects -by hiding repository implementation details from the casual contributor. With it, -you can contribute without having to know all the build and testing tools that -the project team already uses, which often have strange requirements. +by hiding repository implementation details from the casual contributor. With +it, you can contribute without having to know all the build and testing tools +that the project team already uses, which often have strange requirements. ![mk-command-line-screenshot](images/mk-social-preview.png) @@ -22,8 +22,8 @@ repository and expose their commands. `mk` is inspired by tools such [make][make], [waf][waf], [taskfile][taskfile], [tox][tox], [nox][nox], [npm][npm], [yarn][yarn] and [pre-commit][pre-commit], but **it does not aim to replace them**. Instead, it aims to provide a unified -interface for calling them that is friendly even for those that never used -these tools. +interface for calling them that is friendly even for those that never used these +tools. ## Installation @@ -102,8 +102,8 @@ status or that the testing did not leave untracked files. unknown. (#20) - Configuration file where additional actions can be added. (#21) - Dependencies between commands. While some tools support dependencies, many do - not. You should be able to declare that a specific command will run only - after another one already passed. (#22) + not. You should be able to declare that a specific command will run only after + another one already passed. (#22) - Ability to generate CI/CD pipelines so the user would spend less time writing non-portable configurations. (#23) diff --git a/docs/multirepo.md b/docs/multirepo.md new file mode 100644 index 0000000..a6ec078 --- /dev/null +++ b/docs/multirepo.md @@ -0,0 +1,77 @@ +# Multi-repo actions + +Since June 2024, `mk` project adopted the multi-repository commands from [pre] +project. The list of repositories is loaded from `~/.config/mk/mk.yml` that has +a format like below: + +```yaml title="~/.config/mk/mk.yml" +repos: + gh_org/gh_repo1: {} + gh_org/gh_repo2: {} +``` + +You can use a symlink to store this file in a different location. + +This feature is enabled only when all the below requirements are met: + +- [gh] command is installed +- The config file is present on on disk + + + +!!! note + + This feature is experimental and how it works might change between + each new release, including command names and configuration file format. + You are welcome to give your feedback about how we can make it more useful + and flexible for most people. + +## PRs + +This command lists non-draft open pull requests on all repositories, listed by +their age, with PR numbers being clickable hyperlinks. + +```bash +$ mk prs +ansible/tox-ansible 28 minutes ago Exclude *-py3.10-dev. #336 +ansible/vscode-ansible 2 hours ago Update the segment s #1375 +ansible-community/molecule-plugins 16 hours ago [pre-commit.ci] pre-c #238 +ansible/ansible-content-actions 2 days ago Install tox-ansible #14 +ansible/vscode-ansible 3 days ago Bump @types/vscode f #1351 +``` + +## Drafts + +This command lists draft releases on all your repositories, so you would know +which projects need to be released next. + +```bash +$ mk drafts +🟢 ansible/ansible-compat has no draft release. +🟢 ansible/ansible-creator has an empty draft release. +🟠 ansible/ansible-dev-environment draft release v24.4.4 created 35 days ago: + + ## Bugfixes + - Add some tests (#160) @cidrblock + +🟠 ansible/ansible-dev-tools draft release v24.6.0 created 27 days ago: + + ## Enhancements + - Bump tox-ansible from 24.6.0 to 24.6.14 in /.config (#267) + - Encapsulate community-ansible-dev-tools container building (#255) @ssbarnea + - Add lock extra to allow reproducible installation (#254) @ssbarnea +``` + +## Alerts + +This command displays open security alerts on your repositories. + +```bash +$ mk alerts +https://github.com/ansible/ansible-dev-tools/security/dependabot/18 +https://github.com/ansible/ansible-lint/security/dependabot/36 +https://github.com/ansible/vscode-ansible/security/dependabot/28 +``` + +[pre]: https://github.com/pycontribs/gh-pre +[gh]: https://cli.github.com/ diff --git a/mkdocs.yml b/mkdocs.yml index e727c72..ab16955 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,7 @@ site_name: mk tool documentation site_url: https://mk.readthedocs.io/ repo_url: https://github.com/pycontribs/mk edit_uri: blob/main/docs/ -copyright: Copyright © 2021-2023 Sorin Sbarnea +copyright: Copyright © Sorin Sbarnea docs_dir: docs strict: true @@ -17,13 +17,27 @@ theme: favicon: images/favicon.ico features: - announce.dismiss - - content.code.copy - content.action.edit + - content.action.view + - content.code.annotate + - content.code.copy + - content.tabs.link + - content.tooltips + - header.autohide - navigation.expand - - navigation.sections - - navigation.instant + - navigation.footer - navigation.indexes + - navigation.instant + - navigation.path + - navigation.prune + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top - navigation.tracking + - search.highlight + - search.share + - search.suggest - toc.integrate palette: # https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#primary-color @@ -60,10 +74,12 @@ plugins: - autorefs - markdown-exec - search - - social - - tags + - material/social + - material/tags # https://github.com/manuzhang/mkdocs-htmlproofer-plugin - - htmlproofer + - htmlproofer: + raise_error_excludes: + 404: ["https://github.com/*/security/*"] markdown_extensions: - admonition diff --git a/pyproject.toml b/pyproject.toml index 72bc1f4..be0c018 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ keywords = ["mk"] [project.scripts] mk = "mk.__main__:cli" +pre = "mk.pre:app" [project.entry-points."mk_tools"] ansible = "mk.tools.ansible:AnsibleTool" @@ -52,6 +53,7 @@ shell = "mk.tools.shell:ShellTool" taskfile = "mk.tools.taskfile:TaskfileTool" tox = "mk.tools.tox:ToxTool" nox = "mk.tools.nox:NoxTool" +pre = "mk.tools.pre:PreTool" [project.urls] homepage = "https://github.com/pycontribs/mk" @@ -96,6 +98,23 @@ color_output = true error_summary = true no_incremental = true +[tool.pip-tools] +no_annotate = true +quiet = true + +[tool.pip-tools.compile] +all_extras = true +output_file = ".config/constraints.txt" +strip_extras = true +unsafe_package = [ + "backports-tarfile", + "cryptography", + "exceptiongroup", + "jeepney", + "secretstorage", + "twine", +] + [tool.pylint."MESSAGES CONTROL"] # increase from default is 50 which is too aggressive max-statements = 60 @@ -140,10 +159,10 @@ lint.ignore = [ ] lint.select = ["ALL"] -[tool.ruff.flake8-pytest-style] +[tool.ruff.lint.flake8-pytest-style] parametrize-values-type = "tuple" -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["mk"] [tool.ruff.lint.per-file-ignores] diff --git a/src/mk/pre.py b/src/mk/pre.py new file mode 100644 index 0000000..7fda9c0 --- /dev/null +++ b/src/mk/pre.py @@ -0,0 +1,128 @@ +"""Expose features related to git repositories.""" + +from __future__ import annotations + +import datetime +import json +import os +from subprocess import run +from typing import Annotated + +import typer +from rich import box +from rich.console import Console +from rich.panel import Panel +from typer_config.decorators import use_yaml_config + +CFG_FILE = "~/.config/mk/mk.yml" + + +class TyperApp(typer.Typer): + """Our App.""" + + repos: list[str] + + +app = TyperApp() +console = Console() + + +@app.callback(invoke_without_command=True) +@use_yaml_config( + default_value=os.path.expanduser(CFG_FILE), + param_help=f"Configuration file ({CFG_FILE}).", +) +# https://github.com/tiangolo/typer/issues/533 +def default(repos: Annotated[list[str], typer.Option()] = []) -> None: + """Implicit entry point.""" + if repos is None: + repos = [] + # breakpoint() + app.repos = list(filter(lambda s: not s.startswith("_"), repos)) + + +@app.command() +def drafts() -> None: + """Pre helps you chain releases on github.""" + for repo in app.repos: + repo_link = f"[markdown.link][link=https://github.com/{repo}]{repo}[/][/]" + result = run( + f'gh api repos/{repo}/releases --jq "[.[] | select(.draft)"]', + text=True, + shell=True, # noqa: S602 + capture_output=True, + check=True, + ) + drafts_json = json.loads(result.stdout) + if not drafts_json or ( + isinstance(drafts_json, dict) and drafts_json["message"] == "Not Found" + ): + console.print(f"🟢 {repo_link} [dim]has no draft release.[/]") + continue + for draft in drafts_json: + created = datetime.datetime.fromisoformat(draft["created_at"]).replace( + tzinfo=datetime.timezone.utc, + ) + age = (datetime.datetime.now(tz=datetime.timezone.utc) - created).days + if not draft["body"].strip(): + console.print(f"🟢 {repo_link} [dim]has an empty draft release.[/]") + continue + + md = Panel(draft["body"].replace("\n\n", "\n").strip("\n"), box=box.MINIMAL) + msg = ( + f"🟠 {repo_link} draft release " + f"[link={draft['html_url']}][markdown.link]{draft['tag_name']}[/][/]" + f" created [repr.number]{age}[/] days ago:\n" + ) + console.print(msg, highlight=False, end="") + console.print(md, style="dim") + + +@app.command() +def prs() -> None: + """List pending pull-request.""" + # for user in TEAM: + # --review-requested=@{user} + # --owner=ansible --owner=ansible-community + cmd = ( + "GH_PAGER= gh search prs --draft=false --state=open --limit=100 --sort=updated" + ) + cmd += "".join(f" --repo={repo}" for repo in app.repos) + cmd += ( + " --template '{{range .}}{{tablerow .repository.nameWithOwner (timeago .updatedAt) " + '.title (hyperlink .url (printf "#%v" .number) ) }}{{end}}{{tablerender}}\' ' + "--json title,url,repository,updatedAt,number" + ) + console.print(f"[dim]{cmd}[/]", highlight=False) + os.system(cmd) # noqa: S605 + + +@app.command() +def alerts() -> None: + """List open alerts.""" + for repo in app.repos: + cmd = "GH_PAGER= gh " + cmd += f"api /repos/{repo}/dependabot/alerts" + cmd += " --jq='.[] | select(.state!=\"fixed\") | .html_url'" + result = run( + cmd, + text=True, + shell=True, # noqa: S602 + capture_output=True, + check=False, + ) + if result.returncode: + console.print( + f"[dim]{cmd}[/dim] failed with {result.returncode}\n" + f"{result.stdout}\n\n{result.stderr}", + ) + else: + if result.stdout: + console.print(result.stdout) + if result.stderr: + console.print(result.stderr) + + +if __name__ == "__main__": + # execute only if run as a script + app() diff --git a/src/mk/tools/pre.py b/src/mk/tools/pre.py new file mode 100644 index 0000000..5bddc4a --- /dev/null +++ b/src/mk/tools/pre.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import logging +import os +import shutil +from pathlib import Path + +from mk.exec import run_or_fail +from mk.pre import CFG_FILE +from mk.tools import Action, Tool + + +class PreTool(Tool): + name = "pre" + + def run(self, action: Action | None = None): + if action: + run_or_fail(["pre", action.name], tee=True) + + def is_present(self, path: Path) -> bool: + if not os.path.exists(os.path.expanduser(CFG_FILE)): + msg = f"Multi-repo feature was disabled because {CFG_FILE} was not found." + logging.debug(msg) + return False + if not shutil.which("gh"): + logging.warning("Unable to find gh tool. See https://cli.github.com/") + return False + return True + + def actions(self) -> list[Action]: + # for command in app.registered_commands: + # print(command.name, command.short_help, command.short_help, command.epilog, command.short_help) + # breakpoint() + return [ + Action(name="prs", description="[dim]Show open PRs[/dim]", tool=self), + Action( + name="drafts", + description="[dim]Show draft releases[/dim]", + tool=self, + ), + Action( + name="alerts", + description="[dim]Show dependabot security alerts[/dim]", + tool=self, + ), + ] diff --git a/test/pre.yml b/test/pre.yml new file mode 100644 index 0000000..124e4d7 --- /dev/null +++ b/test/pre.yml @@ -0,0 +1,12 @@ +--- +repos: + - ansible/ansible-compat + - ansible/ansible-lint + - ansible/ansible-navigator + - ansible/ansible-creator + - ansible/molecule + - ansible/tox-ansible + - ansible/pytest-ansible + - ansible/ansible-development-environment + - ansible/ansible-dev-tools + - ansible/creator-ee diff --git a/test/test_pre.py b/test/test_pre.py new file mode 100644 index 0000000..fd6e7d6 --- /dev/null +++ b/test/test_pre.py @@ -0,0 +1,14 @@ +"""Tests""" + +from typer.testing import CliRunner + +from mk.pre import app + +runner = CliRunner() + + +def test_main() -> None: + """CLI Tests""" + result = runner.invoke(app, ["--help", "--config=test/pre.yml"]) + assert result.exit_code == 0, result.stdout + assert "Pre helps you chain releases on github." in result.stdout