diff --git a/README.md b/README.md index 804f205e..9e879b7c 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,37 @@ docker: password: password ``` +if Jira is configured for the repository we create a new issue (story) for the PR and assign it to the owner. +On new commit create closed sub-task under the PR story with the commiter as assignee +On reviewed PR create closed sub-task under the PR story with the reviewer as assignee + +- `server`: FQDN Jira server url +- `project`: project key to open the issue +- `token`: Jira token +- `epic`: epic name, if provided a new issue will be created under the epic +- `user-mapping`: mapping from github username to jira username if different + +`jira` setting can be placed as global (for all repositories) or per repository. +`jira` in repository setting will override `jira` in global setting for the repository + +```yaml +jira: + server: jira server url + project: project key to open the issue + token: jira token + epic: epic name # Optional + user-mapping: + github username: jira username +``` + +To enable jira for a repository, set `jira-tracking: true` in repository settings + +```yaml +repositories: + my-repository: + jira-tracking: true +``` + ## Supported actions Following actions are done automatically: diff --git a/example.config.yaml b/example.config.yaml index b993d9b0..98026b9a 100644 --- a/example.config.yaml +++ b/example.config.yaml @@ -19,6 +19,13 @@ auto-verified-and-merged-users: - "renovate[bot]" - "pre-commit-ci[bot]" +jira: + server: + project: + tokan: + user-mapping: + : # if github user is not the same as jira + repositories: my-repository: name: my-org/my-repository @@ -66,3 +73,13 @@ repositories: can-be-merged-required-labels: # check for extra labels to set PR as can be merged - my-label1 - my-label2 + + jira-tracking: true + + jira: # override Jira global settings + server: + project: + token: + epic: # Optional + user-mapping: + : # if github user is not the same as jira diff --git a/poetry.lock b/poetry.lock index cfb9d721..55b785bc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -40,6 +40,46 @@ files = [ {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] +[[package]] +name = "bcrypt" +version = "4.1.2" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, + {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, + {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, + {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, + {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, + {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, + {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "blinker" version = "1.7.0" @@ -404,6 +444,17 @@ files = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "deprecated" version = "1.2.14" @@ -794,6 +845,33 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jira" +version = "3.8.0" +description = "Python library for interacting with JIRA via REST APIs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jira-3.8.0-py3-none-any.whl", hash = "sha256:12190dc84dad00b8a6c0341f7e8a254b0f38785afdec022bd5941e1184a5a3fb"}, + {file = "jira-3.8.0.tar.gz", hash = "sha256:63719c529a570aaa01c3373dbb5a104dab70381c5be447f6c27f997302fa335a"}, +] + +[package.dependencies] +defusedxml = "*" +packaging = "*" +Pillow = ">=2.1.0" +requests = ">=2.10.0" +requests-oauthlib = ">=1.1.0" +requests-toolbelt = "*" +typing-extensions = ">=3.7.4.2" + +[package.extras] +async = ["requests-futures (>=0.9.7)"] +cli = ["ipython (>=4.0.0)", "keyring"] +docs = ["furo", "sphinx (>=5.0.0)", "sphinx-copybutton"] +opt = ["PyJWT", "filemagic (>=1.6)", "requests-jwt", "requests-kerberos"] +test = ["MarkupSafe (>=0.23)", "PyYAML (>=5.1)", "docutils (>=0.12)", "flaky", "oauthlib", "parameterized (>=0.8.1)", "pytest (>=6.0.0)", "pytest-cache", "pytest-cov", "pytest-instafail", "pytest-sugar", "pytest-timeout (>=1.3.1)", "pytest-xdist (>=2.2)", "requests-mock", "requires.io", "tenacity", "wheel (>=0.24.0)", "yanc (>=0.3.3)"] + [[package]] name = "keyring" version = "24.3.0" @@ -818,6 +896,30 @@ completion = ["shtab (>=1.1.0)"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" version = "2.1.5" @@ -901,6 +1003,17 @@ files = [ [package.dependencies] traitlets = "*" +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "more-itertools" version = "10.2.0" @@ -977,6 +1090,36 @@ files = [ {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, ] +[[package]] +name = "netaddr" +version = "1.2.1" +description = "A network address manipulation library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "netaddr-1.2.1-py3-none-any.whl", hash = "sha256:bd9e9534b0d46af328cf64f0e5a23a5a43fca292df221c85580b27394793496e"}, + {file = "netaddr-1.2.1.tar.gz", hash = "sha256:6eb8fedf0412c6d294d06885c110de945cf4d22d2b510d0404f4e06950857987"}, +] + +[package.extras] +nicer-shell = ["ipython"] + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "packaging" version = "23.2" @@ -988,6 +1131,27 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "paramiko" +version = "3.4.0" +description = "SSH2 protocol library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "paramiko-3.4.0-py3-none-any.whl", hash = "sha256:43f0b51115a896f9c00f59618023484cb3a14b98bbceab43394a39c6739b7ee7"}, + {file = "paramiko-3.4.0.tar.gz", hash = "sha256:aac08f26a31dc4dffd92821527d1682d99d52f9ef6851968114a8728f3c274d3"}, +] + +[package.dependencies] +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" + +[package.extras] +all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=2.0)"] + [[package]] name = "parso" version = "0.8.3" @@ -1003,6 +1167,17 @@ files = [ qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["docopt", "pytest (<6.0.0)"] +[[package]] +name = "pbr" +version = "6.0.0" +description = "Python Build Reasonableness" +optional = false +python-versions = ">=2.6" +files = [ + {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, + {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -1028,6 +1203,92 @@ files = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] +[[package]] +name = "pillow" +version = "10.3.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, + {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, + {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, + {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, + {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, + {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, + {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, + {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, + {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, + {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, + {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, + {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, + {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, + {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, + {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, + {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + [[package]] name = "pkginfo" version = "1.9.6" @@ -1224,6 +1485,23 @@ files = [ plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyhelper-utils" +version = "0.0.13" +description = "Collective utility functions for python projects" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "pyhelper_utils-0.0.13.tar.gz", hash = "sha256:e975df235016bbb73e550e8c53cac85f787521f8d9100a18e1410ca264ba843c"}, +] + +[package.dependencies] +ipdb = ">=0.13.13,<0.14.0" +python-rrmngmnt = ">=0.1.32,<0.2.0" +python-simple-logger = ">=1.0.19,<2.0.0" +requests = ">=2.31.0,<3.0.0" +rich = ">=13.7.1,<14.0.0" + [[package]] name = "pyjwt" version = "2.8.0" @@ -1284,14 +1562,30 @@ files = [ [package.dependencies] tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +[[package]] +name = "python-rrmngmnt" +version = "0.1.32" +description = "Tool to manage remote systems and services" +optional = false +python-versions = "*" +files = [ + {file = "python-rrmngmnt-0.1.32.zip", hash = "sha256:0953f9e9e3911d4a310282513261ed7572fbc269f78c8e3d0680f84ba9955526"}, +] + +[package.dependencies] +netaddr = "*" +paramiko = "*" +pbr = "*" +six = "*" + [[package]] name = "python-simple-logger" -version = "1.0.11" +version = "1.0.22" description = "A simple logger for python" optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "python_simple_logger-1.0.11.tar.gz", hash = "sha256:6c800093d276ea63be175d99537ddaca79a295b0f865602c0582a1464dd45b72"}, + {file = "python_simple_logger-1.0.22.tar.gz", hash = "sha256:4f8bd4fcb1e94117dbdc96429c73d8f328e0fa6a267ef5559ee6697a5d8e77c9"}, ] [package.dependencies] @@ -1491,6 +1785,24 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "requests-toolbelt" version = "1.0.0" @@ -1505,6 +1817,25 @@ files = [ [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" version = "0.4.2" @@ -1916,4 +2247,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "fae0e8acd7290aca991432f334c8d1907693506aff4c24630d0d277d397779b2" +content-hash = "83f857fec2bf34ce8fdf452c661eebac0f6d3ea1c28af427c1c576cfee68569e" diff --git a/pyproject.toml b/pyproject.toml index e3c3a57b..b98570ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,8 @@ colorama = "^0.4.6" ruff = "^0.4.0" timeout-sampler = "^0.0.25" requests = "^2.31.0" +jira = "^3.8.0" +pyhelper-utils = "^0.0.13" [tool.poetry.group.dev.dependencies] ipdb = "^0.13.13" diff --git a/webhook_server_container/app.py b/webhook_server_container/app.py index 8a005023..cc5877cc 100644 --- a/webhook_server_container/app.py +++ b/webhook_server_container/app.py @@ -14,9 +14,9 @@ set_all_in_progress_check_runs_to_queued, set_repositories_settings, ) +from pyhelper_utils.general import ignore_exceptions from webhook_server_container.utils.helpers import ( get_api_with_highest_rate_limit, - ignore_exceptions, ) from webhook_server_container.utils.webhook import create_webhook diff --git a/webhook_server_container/libs/github_api.py b/webhook_server_container/libs/github_api.py index 5aa8d02a..a9b89852 100644 --- a/webhook_server_container/libs/github_api.py +++ b/webhook_server_container/libs/github_api.py @@ -15,6 +15,7 @@ from timeout_sampler import TimeoutSampler, TimeoutExpiredError from webhook_server_container.libs.config import Config +from webhook_server_container.libs.jira_api import JiraApi from webhook_server_container.utils.constants import ( ADD_STR, APPROVED_BY_LABEL_PREFIX, @@ -33,6 +34,7 @@ HAS_CONFLICTS_LABEL_STR, HOLD_LABEL_STR, IN_PROGRESS_STR, + JIRA_STR, LGTM_STR, NEEDS_REBASE_LABEL_STR, PYTHON_MODULE_INSTALL_STR, @@ -48,12 +50,13 @@ PRE_COMMIT_STR, OTHER_MAIN_BRANCH, ) +from pyhelper_utils.general import ignore_exceptions from webhook_server_container.utils.dockerhub_rate_limit import DockerHub from webhook_server_container.utils.helpers import ( get_api_with_highest_rate_limit, extract_key_from_dict, get_github_repo_api, - ignore_exceptions, + get_value_from_dicts, run_command, get_apis_and_tokes_from_config, ) @@ -75,6 +78,9 @@ def __init__(self, hook_data, repositories_app_api, missing_app_repositories): self.parent_committer = None self.log_uuid = shortuuid.uuid()[:5] self.container_repo_dir = "/tmp/repository" + self.jira_conn = None + self.jira_track_pr = False + self.issue_title = None # filled by self._repo_data_from_config() self.dockerhub_username = None @@ -90,11 +96,15 @@ def __init__(self, hook_data, repositories_app_api, missing_app_repositories): self.github_app_id = None self.container_release = None self.can_be_merged_required_labels = [] + self.jira = None + self.jira_tracking = False + self.jira_enabled_repository = False # End of filled by self._repo_data_from_config() self.config = Config() self._repo_data_from_config() self._set_log_prefix_color() + # self.log_repository_features() self.github_app_api = self.get_github_app_api() @@ -115,11 +125,24 @@ def __init__(self, hook_data, repositories_app_api, missing_app_repositories): self.dockerhub = DockerHub(username=self.dockerhub_username, password=self.dockerhub_password) self.pull_request = self._get_pull_request() + self.owners_content = self.get_owners_content() + if self.pull_request: self.last_commit = self._get_last_commit() self.parent_committer = self.pull_request.user.login - self.owners_content = self.get_owners_content() + if self.jira_enabled_repository: + reviewers_and_approvers = self.reviewers + self.approvers + if self.parent_committer in reviewers_and_approvers: + self.jira_assignee = self.jira_user_mapping.get(self.parent_committer, self.parent_committer) + self.jira_track_pr = True + self.issue_title = f"[AUTO:FROM:GITHUB] PR [{self.pull_request.number}]: {self.pull_request.title}" + self.app.logger.info(f"{self.log_prefix} Jira tracking is enabled for the current pull request.") + else: + self.app.logger.info( + f"{self.log_prefix} Jira tracking is disabled for the current pull request. " + f"Committer {self.parent_committer} is not in {reviewers_and_approvers}" + ) self.supported_user_labels_str = "".join([f" * {label}\n" for label in USER_LABELS_DICT.keys()]) self.welcome_msg = f""" @@ -266,17 +289,44 @@ def _repo_data_from_config(self): if not repo_data: raise RepositoryNotFoundError(f"Repository {self.repository_name} not found in config file") - self.github_app_id = config_data["github-app-id"] - self.webhook_url = config_data.get("webhook_ip") + self.github_app_id = get_value_from_dicts( + primary_dict=repo_data, secondary_dict=config_data, key="github-app-id" + ) self.repository_full_name = repo_data["name"] - self.pypi = repo_data.get("pypi") - self.verified_job = repo_data.get("verified_job", True) - self.tox_enabled = repo_data.get("tox") - self.tox_python_version = repo_data.get("tox_python_version", "python") - self.slack_webhook_url = repo_data.get("slack_webhook_url") + self.pypi = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="pypi") + self.verified_job = get_value_from_dicts( + primary_dict=repo_data, secondary_dict=config_data, key="verified-job", return_on_none=True + ) + self.tox_enabled = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="tox") + self.tox_python_version = get_value_from_dicts( + primary_dict=repo_data, secondary_dict=config_data, key="tox-python-version", return_on_none="python" + ) + self.slack_webhook_url = get_value_from_dicts( + primary_dict=repo_data, secondary_dict=config_data, key="slack_webhook_url" + ) self.build_and_push_container = repo_data.get("container") - self.dockerhub = repo_data.get("docker") - self.pre_commit = repo_data.get("pre-commit") + self.dockerhub = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="docker") + self.pre_commit = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="pre-commit") + self.jira = get_value_from_dicts(primary_dict=repo_data, secondary_dict=config_data, key="jira") + + if self.jira: + self.jira_server = self.jira.get("server") + self.jira_project = self.jira.get("project") + self.jira_token = self.jira.get("token") + self.jira_epic = self.jira.get("epic") + self.jira_user_mapping = self.jira.get("user-mapping", {}) + + # Check if repository is enabled for jira + self.jira_tracking = get_value_from_dicts( + primary_dict=repo_data, secondary_dict=config_data, key="jira-tracking" + ) + if self.jira_tracking: + self.jira_enabled_repository = all([self.jira_server, self.jira_project, self.jira_token]) + if not self.jira_enabled_repository: + # if not (self.jira_enabled_repository := all([self.jira_server, self.jira_project, self.jira_token])): + self.app.logger.error( + f"{self.log_prefix} Jira configuration is not valid. Server: {self.jira_server}, Project: {self.jira_project}, Token: {self.jira_token}" + ) if self.dockerhub: self.dockerhub_username = self.dockerhub["username"] @@ -292,11 +342,12 @@ def _repo_data_from_config(self): self.container_command_args = self.build_and_push_container.get("args") self.container_release = self.build_and_push_container.get("release") - self.auto_verified_and_merged_users = config_data.get( - "auto-verified-and-merged-users", - repo_data.get("auto-verified-and-merged-users", []), + self.auto_verified_and_merged_users = get_value_from_dicts( + primary_dict=repo_data, secondary_dict=config_data, key="auto-verified-and-merged-users", return_on_none=[] + ) + self.can_be_merged_required_labels = get_value_from_dicts( + primary_dict=repo_data, secondary_dict=config_data, key="can-be-merged-required-labels", return_on_none=[] ) - self.can_be_merged_required_labels = config_data.get("can-be-merged-required-labels", []) def _get_pull_request(self, number=None): if number: @@ -746,6 +797,22 @@ def process_pull_request_webhook_data(self): self.app.logger.info(f"{self.log_prefix} Creating welcome comment") self.pull_request.create_issue_comment(self.welcome_msg) self.create_issue_for_new_pull_request() + + if self.jira_track_pr: + self.get_jira_conn() + if not self.jira_conn: + self.app.logger.error(f"{self.log_prefix} Jira connection not found") + return + + self.app.logger.info(f"{self.log_prefix} Creating Jira story") + jira_story_key = self.jira_conn.create_story( + title=self.issue_title, + body=self.pull_request.html_url, + epic_key=self.jira_epic, + assignee=self.jira_assignee, + ) + self._add_label(label=f"{JIRA_STR}:{jira_story_key}") + self.process_opened_or_synchronize_pull_request(pull_request_branch=pull_request_branch) if hook_action == "synchronize": @@ -758,12 +825,29 @@ def process_pull_request_webhook_data(self): ): self._remove_label(label=_label_name) + if self.jira_track_pr: + if _story_key := self.get_story_key_with_jira_connection(): + self.app.logger.info(f"{self.log_prefix} Creating sub-task for Jira story {_story_key}") + self.jira_conn.create_closed_subtask( + title=f"{self.issue_title}: New commit from {self.parent_committer}", + parent_key=_story_key, + assignee=self.jira_assignee, + body=f"PR: {self.pull_request.title}, new commit pushed by {self.parent_committer}", + ) + self.process_opened_or_synchronize_pull_request(pull_request_branch=pull_request_branch) if hook_action == "closed": self.close_issue_for_merged_or_closed_pr(hook_action=hook_action) - is_merged = pull_request_data.get("merged") + + if self.jira_track_pr: + if _story_key := self.get_story_key_with_jira_connection(): + self.app.logger.info(f"{self.log_prefix} Closing Jira story") + self.jira_conn.close_issue( + key=_story_key, comment=f"PR: {self.pull_request.title} is closed. Megred: {is_merged}" + ) + if is_merged: self.app.logger.info(f"{self.log_prefix} PR is merged") @@ -822,12 +906,38 @@ def process_pull_request_review_webhook_data(self): approved changes_requested """ + reviewed_user = self.hook_data["review"]["user"]["login"] + + review_state = self.hook_data["review"]["state"] self.manage_reviewed_by_label( - review_state=self.hook_data["review"]["state"], + review_state=review_state, action=ADD_STR, - reviewed_user=self.hook_data["review"]["user"]["login"], + reviewed_user=reviewed_user, ) + if self.jira_track_pr: + _story_label = [_label for _label in self.pull_request.labels if _label.name.startswith(JIRA_STR)] + if _story_label: + if reviewed_user == self.parent_committer: + self.app.logger.info( + f"{self.log_prefix} Skipping Jira review sub-task creation for review by {reviewed_user} which is parent committer" + ) + return + + _story_key = _story_label[0].name.split(":")[-1] + self.get_jira_conn() + if not self.jira_conn: + self.app.logger.error(f"{self.log_prefix} Jira connection not found") + return + + self.app.logger.info(f"{self.log_prefix} Creating sub-task for Jira story {_story_key}") + self.jira_conn.create_closed_subtask( + title=f"{self.issue_title}: reviewed by: {reviewed_user} - {review_state}", + parent_key=_story_key, + assignee=self.jira_user_mapping.get(reviewed_user, self.parent_committer), + body=f"PR: {self.pull_request.title}, reviewed by: {reviewed_user}", + ) + def manage_reviewed_by_label(self, review_state, action, reviewed_user): self.app.logger.info( f"{self.log_prefix} " @@ -1539,3 +1649,41 @@ def get_checkrun_text(self, err, out): return f"```\n{err}\n\n{out}\n```"[:65534] else: return f"```\n{err}\n\n{out}\n```" + + @ignore_exceptions(logger=FLASK_APP.logger) + def get_jira_conn(self): + self.jira_conn = JiraApi( + server=self.jira_server, + project=self.jira_project, + token=self.jira_token, + ) + + def log_repository_features(self): + repository_features = f""" + auto-verified-and-merged-users: {self.auto_verified_and_merged_users} + can-be-merged-required-labels: {self.can_be_merged_required_labels} + pypi: {self.pypi} + verified-job: {self.verified_job} + tox-enabled: {self.tox_enabled} + tox-python-version: {self.tox_python_version} + docker: {self.dockerhub} + pre-commit: {self.pre_commit} + slack-webhook-url: {self.slack_webhook_url} + container: {self.build_and_push_container} + jira-tracking: {self.jira_tracking} + jira-server: {self.jira_server} + jira-project: {self.jira_project} + jira-token: {self.jira_token} + jira-enabled-repository: {self.jira_enabled_repository} + jira-user-mapping: {self.jira_user_mapping} +""" + self.app.logger.info(f"{self.log_prefix} Repository features: {repository_features}") + + def get_story_key_with_jira_connection(self): + _story_label = [_label for _label in self.pull_request.labels if _label.name.startswith(JIRA_STR)] + if _story_key := _story_label[0].name.split(":")[-1]: + self.get_jira_conn() + if not self.jira_conn: + self.app.logger.error(f"{self.log_prefix} Jira connection not found") + return None + return _story_key diff --git a/webhook_server_container/libs/jira_api.py b/webhook_server_container/libs/jira_api.py new file mode 100644 index 00000000..8b3da57e --- /dev/null +++ b/webhook_server_container/libs/jira_api.py @@ -0,0 +1,58 @@ +from typing import Any, Dict +from jira import JIRA +from pyhelper_utils.general import ignore_exceptions + +from webhook_server_container.utils.constants import FLASK_APP + + +class JiraApi: + def __init__(self, server: str, project: str, token: str): + self.server = server + self.project = project + self.token = token + + self.conn = JIRA( + server=self.server, + token_auth=self.token, + ) + self.conn.my_permissions() + self.fields: Dict[str, Any] = {"project": {"key": self.project}} + + @ignore_exceptions(logger=FLASK_APP.logger) + def create_story(self, title: str, body: str, epic_key: str, assignee: str) -> str: + self.fields.update({ + "summary": title, + "description": body, + "issuetype": {"name": "Story"}, + "assignee": {"name": assignee}, + }) + if epic_key: + if epic_custom_field := self.get_epic_custom_field(): + self.fields.update({epic_custom_field: epic_key}) + + _issue = self.conn.create_issue(fields=self.fields) + return _issue.key + + @ignore_exceptions(logger=FLASK_APP.logger) + def create_closed_subtask(self, title: str, body: str, parent_key: str, assignee: str) -> None: + self.fields.update({ + "summary": title, + "description": body, + "parent": {"key": parent_key}, + "issuetype": {"name": "Sub-task"}, + "assignee": {"name": assignee}, + }) + _issue = self.conn.create_issue(fields=self.fields) + self.close_issue(key=_issue.key) + + @ignore_exceptions(logger=FLASK_APP.logger) + def close_issue(self, key: str, comment: str = "") -> None: + self.conn.transition_issue( + issue=key, + transition="closed", + comment=comment, + ) + + def get_epic_custom_field(self) -> str: + _epic_field_id = [cf["id"] for cf in self.conn.fields() if "Epic Link" in cf["name"]] + return _epic_field_id[0] if _epic_field_id else "" diff --git a/webhook_server_container/utils/constants.py b/webhook_server_container/utils/constants.py index 3d27eac6..dde0e9d0 100644 --- a/webhook_server_container/utils/constants.py +++ b/webhook_server_container/utils/constants.py @@ -34,6 +34,7 @@ HAS_CONFLICTS_LABEL_STR = "has-conflicts" HOLD_LABEL_STR = "hold" SIZE_LABEL_PREFIX = "size/" +JIRA_STR = "JIRA" # Gitlab colors require a '#' prefix; e.g: # USER_LABELS_DICT = {HOLD_LABEL_STR: "B60205", VERIFIED_LABEL_STR: "0E8A16", WIP_STR: "B60205", LGTM_STR: "0E8A16"} @@ -58,6 +59,7 @@ CHANGED_REQUESTED_BY_LABEL_PREFIX: "F5621C", CHERRY_PICK_LABEL_PREFIX: "F09C74", BRANCH_LABEL_PREFIX: "1D76DB", + JIRA_STR: "1D76DB", } ALL_LABELS_DICT = {**STATIC_LABELS_DICT, **DYNAMIC_LABELS_DICT} diff --git a/webhook_server_container/utils/github_repository_settings.py b/webhook_server_container/utils/github_repository_settings.py index b03a934b..2981889d 100644 --- a/webhook_server_container/utils/github_repository_settings.py +++ b/webhook_server_container/utils/github_repository_settings.py @@ -16,9 +16,9 @@ STATIC_LABELS_DICT, TOX_STR, ) +from pyhelper_utils.general import ignore_exceptions from webhook_server_container.utils.helpers import ( get_github_repo_api, - ignore_exceptions, ) diff --git a/webhook_server_container/utils/helpers.py b/webhook_server_container/utils/helpers.py index 95a6acd2..19aa7c89 100644 --- a/webhook_server_container/utils/helpers.py +++ b/webhook_server_container/utils/helpers.py @@ -1,8 +1,8 @@ import datetime import shlex import subprocess -from functools import wraps - +from typing import Any, Dict, Optional +from pyhelper_utils.general import ignore_exceptions from colorama import Fore from github import Github @@ -23,22 +23,6 @@ def extract_key_from_dict(key, _dict): yield result -def ignore_exceptions(logger=None): - def wrapper(func): - @wraps(func) - def inner(*args, **kwargs): - try: - return func(*args, **kwargs) - except Exception as ex: - if logger: - logger.error(f"{func.__name__}({args} {kwargs}). Error: {ex}") - return None - - return inner - - return wrapper - - @ignore_exceptions(logger=FLASK_APP.logger) def get_github_repo_api(github_api, repository): return github_api.get_repo(repository) @@ -163,3 +147,14 @@ def log_rate_limit(rate_limit, api_user): f"Reset in {rate_limit.core.reset} [{datetime.timedelta(seconds=time_for_limit_reset)}] " f"(UTC time is {datetime.datetime.now(tz=datetime.timezone.utc)})" ) + + +def get_value_from_dicts( + primary_dict: Dict[Any, Any], secondary_dict: Dict[Any, Any], key: str, return_on_none: Optional[Any] = None +) -> Any: + """ + Get value from two dictionaries. + + If value is not found in primary_dict, try to get it from secondary_dict, otherwise return return_on_none. + """ + return primary_dict.get(key, secondary_dict.get(key, return_on_none)) diff --git a/webhook_server_container/utils/webhook.py b/webhook_server_container/utils/webhook.py index 132f1986..b89d8b38 100644 --- a/webhook_server_container/utils/webhook.py +++ b/webhook_server_container/utils/webhook.py @@ -2,7 +2,8 @@ from webhook_server_container.utils.constants import FLASK_APP -from webhook_server_container.utils.helpers import get_github_repo_api, ignore_exceptions +from webhook_server_container.utils.helpers import get_github_repo_api +from pyhelper_utils.general import ignore_exceptions @ignore_exceptions(logger=FLASK_APP.logger)