├── .circleci └── config.yml ├── .github ├── CODEOWNERS └── workflows │ ├── build_and_test.yaml │ └── release.yaml ├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── actions ├── add_field_value.py ├── add_field_value.yaml ├── add_gadget.py ├── add_gadget.yaml ├── assign_issue.yaml ├── attach_file_to_issue.py ├── attach_file_to_issue.yaml ├── attach_files_to_issue.py ├── attach_files_to_issue.yaml ├── bulk_link_issue.py ├── bulk_link_issue.yaml ├── comment_issue.py ├── comment_issue.yaml ├── copy_dashboard.py ├── copy_dashboard.yaml ├── create_dashboard.py ├── create_dashboard.yaml ├── create_issue.py ├── create_issue.yaml ├── delete_dashboard.py ├── delete_dashboard_item_property.py ├── delete_dashboard_item_property.yaml ├── delete_dashbord.yaml ├── get_available_gadgets.py ├── get_available_gadgets.yaml ├── get_dashboard_gadgets.py ├── get_dashboard_gadgets.yaml ├── get_dashboard_item_property.py ├── get_dashboard_item_property.yaml ├── get_dashboard_item_property_keys.py ├── get_dashboard_item_property_keys.yaml ├── get_issue.py ├── get_issue.yaml ├── get_issue_attachments.py ├── get_issue_attachments.yaml ├── get_issue_comments.py ├── get_issue_comments.yaml ├── get_issue_links.py ├── get_issue_links.yaml ├── lib │ ├── __init__.py │ ├── base.py │ ├── formatters.py │ └── utils.py ├── link_issue.py ├── link_issue.yaml ├── remove_gadget.py ├── remove_gadget.yaml ├── run.py ├── search_issues.py ├── search_issues.yaml ├── set_dashboard_item_property.py ├── set_dashboard_item_property.yaml ├── transition_issue.py ├── transition_issue.yaml ├── transition_issue_by_name.yaml ├── update_dashboard.py ├── update_dashboard.yaml ├── update_dashboard_automatic_refresh.py ├── update_dashboard_automatic_refresh.yaml ├── update_dashboard_item_property.py ├── update_dashboard_item_property.yaml ├── update_field_value.py ├── update_field_value.yaml ├── update_gadget.py └── update_gadget.yaml ├── config.schema.yaml ├── icon.png ├── jira.yaml.example ├── pack.yaml ├── requirements.txt ├── sensors ├── jira_sensor.py ├── jira_sensor.yaml ├── jira_sensor_for_apiv2.py └── jira_sensor_for_apiv2.yaml └── tests ├── __init__.py ├── fixtures ├── blank.yaml ├── dashboard.json ├── dashboard_item_property.json ├── dashboard_item_property_key.json ├── full_auth_passwd.yaml └── gadget.json ├── lib ├── __init__.py └── actions.py ├── test_action_add_gadget.py ├── test_action_copy_dashboard.py ├── test_action_create_dashboard.py ├── test_action_delete_dashboard.py ├── test_action_delete_dashboard_item_property.py ├── test_action_get_available_gadgets.py ├── test_action_get_dashboard_gadgets.py ├── test_action_get_dashboard_item_property.py ├── test_action_get_dashboard_item_property_keys.py ├── test_action_remove_gadget.py ├── test_action_run.py ├── test_action_set_dashboard_item_property.py ├── test_action_update_dashboard.py ├── test_action_update_dashboard_automatic_refresh.py ├── test_action_update_dashboard_property.py ├── test_action_update_gadget.py ├── test_sensor_jira_sensor.py └── test_sensor_jira_sensor_for_apiv2.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | circleci_is_disabled_job: 5 | docker: 6 | - image: cimg/base:stable 7 | steps: 8 | - run: 9 | shell: /bin/bash 10 | command: echo CircleCI disabled on StackStorm-Exchange 11 | 12 | workflows: 13 | version: 2 14 | circleci_is_disabled: 15 | jobs: 16 | - circleci_is_disabled_job 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. Unless a later match takes precedence, users and 6 | # teams which are put in owners will be requested for review 7 | # when someone opens a pull request. 8 | 9 | # This is base configuration. These owners could review the 10 | # whole file in this repository. 11 | * @StackStorm-Exchange/tsc @floatingstatic 12 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | schedule: 6 | # NOTE: We run this weekly at 1 am UTC on every Saturday 7 | - cron: '0 1 * * 6' 8 | 9 | jobs: 10 | # This is mirrored in the release workflow. 11 | build_and_test: 12 | name: 'Build and Test' 13 | uses: StackStorm-Exchange/ci/.github/workflows/pack-build_and_test.yaml@master 14 | with: 15 | enable-common-libs: true 16 | #apt-cache-version: v0 17 | #py-cache-version: v0 18 | python-version: '["3.8", "3.9"]' 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | # the default branch 7 | - master 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | # This mirrors build_and_test workflow 14 | build_and_test: 15 | name: 'Build and Test' 16 | uses: StackStorm-Exchange/ci/.github/workflows/pack-build_and_test.yaml@master 17 | with: 18 | enable-common-libs: true 19 | #apt-cache-version: v0 20 | #py-cache-version: v0 21 | python-version: '["3.8", "3.9"]' 22 | 23 | tag_release: 24 | needs: build_and_test 25 | name: Tag Release 26 | uses: StackStorm-Exchange/ci/.github/workflows/pack-tag_release.yaml@master 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 3.2.0 4 | - Add new feature to ``jira.get_issue`` to allow for stripping of Jinja templating artifacts from resulting output. (Removes instances of {{ }} from results.) 5 | 6 | Example: You pull a jira with ``code`` block in a comment or the description. To the API that shows up as {{ code }} which is jinja Templating and will cause 7 | issues when trying to use that output anywhere else in a workflow as it cannot find the `code` variable in the context. 8 | 9 | 10 | ## 3.0.1 11 | 12 | - Fixed bug with `update_dashboard` action sending the wrong payload. 13 | 14 | ## 3.0.0 15 | 16 | - Drop support for `python 3.6` 17 | - Add new ``jira.add_gadget`` action 18 | - Add new ``jira.copy_dashboard`` action 19 | - Add new ``jira.create_dashboard`` action 20 | - Add new ``jira.delete_dashboard_item_property`` action 21 | - Add new ``jira.delete_dashboard`` action 22 | - Add new ``jira.get_available_gadgets`` action 23 | - Add new ``jira.get_dashboard_gadgets`` action 24 | - Add new ``jira.get_dashboard_item_property_keys`` action 25 | - Add new ``jira.get_dashboard_item_property`` action 26 | - Add new ``jira.remove_gadget`` actionn 27 | - Add new ``jira.set_dashboard_item_property`` action 28 | - Add new ``jira.update_dashboard_automatic_refresh`` action 29 | - Add new ``jira.update_dashboard_item_property`` action 30 | - Add new ``jira.update_dashboard`` action 31 | - Add new ``jira.update_gadget`` action 32 | 33 | ## 2.6.0 34 | 35 | - Add new ``jira.get_issue_links`` action 36 | 37 | - Evaluate if an issue has a priority set before attempting to get the priority 38 | 39 | ## 2.5.1 40 | 41 | - Improve handling of `priority` field in update_field_value action to address [#65] 42 | 43 | ## 2.5.0 44 | 45 | - Added multithreading in linking multiple issue functionality to speed up the response. 46 | 47 | ## 2.4.2 48 | 49 | - Update `formatters.py` to include `priority` field 50 | 51 | ## 2.4.1 52 | 53 | - Update `search_issue` to include `include_components` and `include_subtasks` as flags 54 | 55 | ## 2.4.0 56 | 57 | - `add_field_value` and `update_field_value` actions now return a dictionary representation of the issue being modified. Previously these actions would return 58 | only the `labels` field if it exists as an attribute. This addresses [#53](https://github.com/StackStorm-Exchange/stackstorm-jira/issues/53) but is also beneficial for displaying other field values (inclusive of `labels`) that may have been updated. 59 | 60 | - Fix for [#54](https://github.com/StackStorm-Exchange/stackstorm-jira/issues/54) which prevents callers of the `update_field_value` action from updating `labels` which must be passed as a list via the api. As labels cannot contain spaces we split 61 | the `value` field of this action on whitespace in the case where `field` == `"labels"`. Example invocation: 62 | 63 | ``` 64 | st2 action execute jira.update_field_value issue_key=NETOPS-1 field=labels value='Label1 Label2' 65 | ``` 66 | 67 | ## 2.3.1 68 | 69 | - Update `README.md` to include `api_token` as an auth method 70 | 71 | ## 2.3.0 72 | 73 | - Add new `api_token` auth method. This authentication method is different than a `pat` authentication request. (PR #54) 74 | - Added `pat` and `cookie` auth methods to the sensors. 75 | 76 | ## 2.2.0 77 | 78 | - Adjust jql in sensor to better support large JIRA projects 79 | - Detect new issues by id vs comparing to an in-memory list 80 | 81 | ## 2.1.0 82 | 83 | - Add new ``jira.bulk_link_issues`` action (PR #50) 84 | 85 | ## 2.0.0 86 | 87 | - [#48](https://github.com/StackStorm-Exchange/stackstorm-jira/issues/48) Update `jira==3.2.0` 88 | 89 | ## 1.1.0 90 | 91 | - Add PAT-based authentication (PR #47) 92 | 93 | ## 1.0.0 94 | 95 | * Drop Python 2.7 support 96 | 97 | ## 0.16.0 98 | 99 | - Add new ``jira.get_issue_components`` action 100 | - Add new ``jira.get_issue_subtasks`` action 101 | 102 | ## 0.15.0 103 | 104 | - Add new action `link_issue`. This allows linking issues together 105 | 106 | ## 0.14.0 107 | 108 | - Support cookie-based authentication (PR #42) 109 | 110 | ## 0.13.1 111 | 112 | - Remove cryptography, pyjwt, pyyaml requirements since we don't use them (PR #41) 113 | 114 | ## 0.13.0 115 | 116 | - Add ``validate`` option to pack config to enable validating credentials 117 | before running any actions (PR #33) 118 | Special thanks to @guymatz for this contribution 119 | 120 | ## 0.12.1 121 | 122 | - Minor linting change 123 | 124 | ## 0.12.0 125 | 126 | - Add new ``jira.add_field_value`` action 127 | 128 | ## 0.11.0 129 | 130 | - Add new ``jira.transition_issue_by_name`` action 131 | 132 | ## 0.10.1 133 | 134 | - Updated PyYAML to 4.2b4 for CVE-2017-18342 135 | 136 | ## 0.10.0 137 | 138 | - Add new ``jira.assign_issues`` action 139 | 140 | ## 0.9.0 141 | 142 | - Add new ``jira.issues_tracker_for_apiv2`` sensor 143 | 144 | ## 0.8.1 145 | 146 | - Version bump to fix tagging issue, no code changes 147 | 148 | ## 0.8.0 149 | 150 | - Adding support for BASIC authentication 151 | 152 | ## 0.7.1 153 | 154 | - Return custom fields in formatter 155 | 156 | ## 0.7.0 157 | 158 | - Add new ``jira.search_issues`` action 159 | 160 | ## 0.6.0 161 | 162 | - Add new ``jira.get_issue_comments`` action 163 | - Add new ``jira.get_issue_attachments`` action 164 | - Add new ``include_comments`` and ``include_attachments`` parameter to 165 | ``jira.get_issue`` action which allows users to retrieve comments and 166 | attachments in a single call when retrieving issue details. For backward 167 | compatibility reasons, both arguments default to ``False``. 168 | 169 | ## 0.5.1 170 | 171 | - Added 'verify' option to disable SSL certificate verification 172 | 173 | ## 0.5.0 174 | 175 | - Updated action `runner_type` from `run-python` to `python-script` 176 | 177 | ## 0.4.0 178 | 179 | - Rename `config.yaml` to `config.schema.yaml` and update to use schema. 180 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jira integration pack 2 | 3 | This pack consists of a sample Jira sensor and a Jira action. 4 | 5 | ## Installation 6 | 7 | You will need to have `gcc` installed on your system. 8 | For Ubuntu systems, run `sudo apt-get install gcc`. 9 | For Redhat/CentOS systems, run `sudo yum install gcc libffi-devel python-devel openssl-devel`. 10 | To build the python cryptography dependency (part of the following `st2 pack install` command) 2GB of RAM is recommended. 11 | In some cases adding a swap file may eliminate strange gcc compiler errors. 12 | 13 | Then install this pack with: `st2 pack install jira` 14 | 15 | ## Configuration 16 | 17 | Copy the example configuration in [jira.yaml.example](./jira.yaml.example) 18 | to `/opt/stackstorm/configs/jira.yaml` and edit as required. 19 | 20 | * ``url`` - URL of the Jira instance (e.g. ``https://myproject.atlassian.net``) 21 | * ``poll_interval`` - Polling interval - default 30s 22 | * ``project`` - Key of the project which will be used as a default with some of the actions which 23 | don't require or allow you to specify a project (e.g. ``STORM``). 24 | * ``verify`` - Verify SSL certificates. Default True. Set to False to disable verification 25 | * ``auth_method`` - Specify either `basic`, `oauth`, `api_token` or `PAT` authentication 26 | 27 | Include the following settings when using the `oauth` auth_method: 28 | 29 | * ``rsa_cert_file`` - Path to the file with a private key 30 | * ``oauth_token`` - OAuth token 31 | * ``oauth_secret`` - OAuth secret 32 | * ``consumer_key`` - Consumer key 33 | 34 | Include the following settings when using the `basic` auth_method: 35 | 36 | * ``username`` - Username 37 | * ``password`` - Password 38 | 39 | Include the following settings when using the `PAT` auth_method: 40 | 41 | * ``token`` - PAT token 42 | 43 | Include the following settings when using the `api_token` auth_method: 44 | 45 | * ``token`` - api token 46 | * ``username`` - Username 47 | 48 | If using the `oauth` auth_method, take a look at the OAuth section below for further setup instructions. 49 | 50 | You can also use dynamic values from the datastore. See the 51 | [docs](https://docs.stackstorm.com/reference/pack_configs.html) for more info. 52 | 53 | **Note** : When modifying the configuration in `/opt/stackstorm/configs/` please 54 | remember to tell StackStorm to load these new values by running 55 | `st2ctl reload --register-configs` 56 | 57 | ### OAuth 58 | 59 | ### Disclaimer 60 | 61 | This documentation is written as of 06/17/2014. 62 | Jira 6.3 implements OAuth1. 63 | Most of this document would need to be revised when Jira switches to OAuth2. 64 | 65 | ### Steps 66 | 67 | 1. Generate RSA public/private key pair 68 | 69 | ```shell 70 | $ openssl genrsa -out stackstorm-jira.pem 4096 71 | ``` 72 | 73 | 2. Create the public key associated with that private key 74 | 75 | ```shell 76 | $ openssl rsa -in stackstorm-jira.pem -pubout 77 | ``` 78 | 79 | 3. Generate a consumer key. You can use python uuid.uuid4() to do this, for example: 80 | 81 | ```shell 82 | $ python 83 | Python 2.7.10 (default, Jul 30 2016, 19:40:32) 84 | [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin 85 | Type "help", "copyright", "credits" or "license" for more information. 86 | >>> import uuid 87 | >>> print uuid.uuid4() 88 | 210660f1-ca8a-40d5-a6ee-295ccbf3074d 89 | >>> 90 | ``` 91 | 92 | 4. Configure Jira for external access: 93 | 94 | * Go to AppLinks section of your Jira at `https://{jira_server}/plugins/servlet/applinks/listApplicationLinks` 95 | * Create a Generic Application with some fake URL 96 | * Click Edit, choose IncomingAuthentication, plug in the consumer key and RSA public key you generated. 97 | 98 | 5. Get access token using this [script](https://github.com/lakshmi-kannan/jira-oauth-access-token-generator/blob/master/generate_access_token.py). 99 | You may need to install additional libraries to run that script, and you will need to edit the script to use your file locations. 100 | Check the [README](https://github.com/lakshmi-kannan/jira-oauth-access-token-generator/blob/master/README.md) file for more information. 101 | The access token is printed at the end of running that script. 102 | Save these keys somewhere safe. 103 | 104 | 6. Plug in the access token and access secret into the sensor or action. 105 | You are good to make Jira calls. 106 | Note: OAuth token expires. 107 | You'll have to repeat the process based on the expiry date. 108 | 109 | ## Sensors 110 | 111 | * ``JiraSensor`` - Sensor which monitors JIRA for new tickets. 112 | * ``JiraSensorForAPIv2`` - Sensor which monitors JIRA for new tickets. 113 | 114 | The sensor monitors for new tickets and sends a trigger into the system whenever there is a new ticket. 115 | 116 | ## Actions 117 | 118 | * ``add_field_value`` - Add a field to a particular JIRA issue. 119 | * ``assign_issue`` - Assigning an issue to a user. 120 | * ``attach_file_to_issue`` - Attach a file to JIRA issue / ticket. 121 | * ``attach_files_to_issue`` - Attach multiple files to JIRA issue / ticket. 122 | * ``bulk_link_issue`` - Link many JIRA issues to another JIRA issue. 123 | * ``comment_issue`` - Comment on a JIRA issue / ticket. 124 | * ``create_issue`` - Create a new JIRA issue / ticket. 125 | * ``get_issue`` - Retrieve information about a particular JIRA issue. 126 | * ``get_issue_attachments`` - Retrieve attachments for a particular JIRA issue. 127 | * ``get_issue_comments`` - Retrieve comments for a particular JIRA issue. 128 | * ``get_issue_links`` - Retrieve linked issues for a particular JIRA issue. 129 | * ``link_issue`` - Link one JIRA issue to another JIRA issue. 130 | * ``search_issues`` - Search JIRA issues with a JQL query. 131 | * ``transition_issue`` - Do a transition on a JIRA issue / ticket. 132 | * ``transition_issue_by_name`` - Do a transition on a JIRA issue / ticket. 133 | * ``update_field_value`` - Update a field in a particular JIRA issue. 134 | * ``add_gadget`` - Add a gadget to an existing JIRA dashboard. 135 | * ``copy_dashboard`` - Copy an existing JIRA dashboard 136 | * ``create_dashboard`` - Create a JIRA dashboard 137 | * ``delete_dashboard_item_property`` - Delete a JIRA dashboard item property. 138 | * ``delete_dashboard`` - Delete a JIRA dashboard. 139 | * ``get_available_gadgets`` - Get all available JIRA dashboard gadgets. 140 | * ``get_dashboard_gadgets`` - Get all JIRA dashboard gadgets for an existing dashboard. 141 | * ``get_dashboard_item_property_keys`` - Get the dashboard item (gadget) property keys for an existing dashboard. 142 | * ``get_dashboard_item_property`` - Get a dashboard item (gadget) property from a gadget on JIRA dashboard. 143 | * ``remove_gadget`` - Remove a gadget from a JIRA dashboard. 144 | * ``set_dashboard_item_property`` - Set the value of a dashboard item (gadget) property on a JIRA dashboard. 145 | * ``update_dashboard_automatic_refresh`` - Set the refresh interval for JIRA dashboard in minutes. 146 | * ``update_dashboard_item_property`` - Update the value of a dashboard item (gadget) property on a JIRA dashboard. 147 | * ``update_dashboard`` - Update an existing JIRA dashboard. 148 | * ``update_gadget`` - Update an existing gadget on a JIRA dashboard. 149 | 150 | ## Maintainers 151 | Active pack maintainers with review & write repository access and expertise with JIRA: 152 | * Jeremiah Millay ([@floatingstatic](https://github.com/floatingstatic)), Fastly 153 | -------------------------------------------------------------------------------- /actions/add_field_value.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.formatters import to_issue_dict 3 | 4 | __all__ = [ 5 | 'AddFieldValue' 6 | ] 7 | 8 | 9 | class AddFieldValue(BaseJiraAction): 10 | def run(self, issue_key, field, value): 11 | issue = self._client.issue(issue_key) 12 | issue.add_field_value(field, value) 13 | return to_issue_dict(issue) 14 | -------------------------------------------------------------------------------- /actions/add_field_value.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: add_field_value 3 | runner_type: python-script 4 | description: Add a field to a particular JIRA issue. 5 | enabled: true 6 | entry_point: add_field_value.py 7 | parameters: 8 | issue_key: 9 | type: string 10 | description: Issue key (e.g. PROJECT-1000). 11 | required: true 12 | field: 13 | type: string 14 | description: the field name. 15 | required: true 16 | value: 17 | type: string 18 | description: the field text itself. 19 | required: true 20 | -------------------------------------------------------------------------------- /actions/add_gadget.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.utils import remove_empty_attributes 3 | 4 | __all__ = ["AddGadgetAction"] 5 | 6 | 7 | class AddGadgetAction(BaseJiraAction): 8 | def run( 9 | self, 10 | dashboard_id, 11 | color=None, 12 | ignore_uri_and_module_key_validation=None, 13 | module_key=None, 14 | position=None, 15 | title=None, 16 | uri=None, 17 | ): 18 | data = remove_empty_attributes( 19 | { 20 | "dashboard_id": dashboard_id, 21 | "color": color, 22 | "ignore_uri_and_module_key_validation": ignore_uri_and_module_key_validation, 23 | "module_key": module_key, 24 | "position": position, 25 | "title": title, 26 | "uri": uri, 27 | } 28 | ) 29 | 30 | gadget = self._client.add_gadget_to_dashboard(**data) 31 | return gadget.raw 32 | -------------------------------------------------------------------------------- /actions/add_gadget.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: add_gadget 3 | runner_type: python-script 4 | description: Add a gadget to a dashboard. 5 | enabled: true 6 | entry_point: add_gadget.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard to add the gadget to. 11 | required: true 12 | color: 13 | type: string 14 | description: The color of the gadget. 15 | default: blue 16 | enum: 17 | - blue 18 | - red 19 | - yellow 20 | - green 21 | - cyan 22 | - purple 23 | - gray 24 | - white 25 | required: false 26 | ignore_uri_and_module_key_validation: 27 | type: boolean 28 | description: | 29 | Whether to ignore the validation of the module key and URI. 30 | For example, when a gadget is created that is part of an 31 | application that is not installed. 32 | default: false 33 | required: false 34 | module_key: 35 | type: string 36 | description: | 37 | The module to use in the gadget. Mutually exclusive with `uri`. 38 | required: false 39 | position: 40 | type: object 41 | description: | 42 | A dictionary (object) containing positioning information for the 43 | gadget like - {"column": 0, "row": 0} 44 | required: false 45 | title: 46 | type: string 47 | description: The title of the gadget. 48 | required: false 49 | uri: 50 | type: string 51 | description: | 52 | The uri to the module to use in the gadget. Mutually exclusive 53 | with `uri`. 54 | -------------------------------------------------------------------------------- /actions/assign_issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: assign_issue 3 | runner_type: python-script 4 | description: Assigning an issue to a user. 5 | enabled: true 6 | entry_point: run.py 7 | parameters: 8 | action: 9 | default: assign_issue 10 | immutable: true 11 | type: string 12 | issue: 13 | type: string 14 | description: Issue key (e.g. PROJECT-1000). 15 | required: true 16 | assignee: 17 | type: string 18 | description: Name of user (e.g. charlie). 19 | required: true 20 | -------------------------------------------------------------------------------- /actions/attach_file_to_issue.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = [ 4 | 'AttachFileToJiraIssueAction' 5 | ] 6 | 7 | 8 | class AttachFileToJiraIssueAction(BaseJiraAction): 9 | 10 | def run(self, issue_key, file_path, file_name=None): 11 | if not file_name: 12 | file_name = None 13 | 14 | with open(file_path, 'rb') as fp: 15 | attachment = self._client.add_attachment( 16 | issue=issue_key, 17 | attachment=fp, 18 | filename=file_name) 19 | 20 | result = { 21 | "issue": issue_key, 22 | "filename": attachment.filename, 23 | "size": attachment.size, 24 | "created_at": attachment.created 25 | } 26 | 27 | return result 28 | -------------------------------------------------------------------------------- /actions/attach_file_to_issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: attach_file_to_issue 3 | runner_type: python-script 4 | description: Attach a file to JIRA issue / ticket. 5 | enabled: true 6 | entry_point: attach_file_to_issue.py 7 | parameters: 8 | file_path: 9 | type: string 10 | description: Path of the file to attach to issue. 11 | required: true 12 | file_name: 13 | type: string 14 | description: Optional file name for the attachment. If not provided, name of the file on disk is assumed. 15 | required: false 16 | issue_key: 17 | type: string 18 | description: Issue key (e.g. PROJECT-1000). 19 | required: true 20 | -------------------------------------------------------------------------------- /actions/attach_files_to_issue.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = [ 4 | 'AttachFilesToJiraIssueAction' 5 | ] 6 | 7 | 8 | class AttachFilesToJiraIssueAction(BaseJiraAction): 9 | 10 | def run(self, issue_key, file_paths): 11 | result = [] 12 | 13 | for file_path in file_paths: 14 | with open(file_path, 'rb') as fp: 15 | attachment = self._client.add_attachment( 16 | issue=issue_key, 17 | attachment=fp, 18 | filename=None) 19 | 20 | item = { 21 | "issue": issue_key, 22 | "filename": attachment.filename, 23 | "size": attachment.size, 24 | "created_at": attachment.created 25 | } 26 | result.append(item) 27 | 28 | return result 29 | -------------------------------------------------------------------------------- /actions/attach_files_to_issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: attach_files_to_issue 3 | runner_type: python-script 4 | description: Attach multiple files to JIRA issue / ticket. 5 | enabled: true 6 | entry_point: attach_files_to_issue.py 7 | parameters: 8 | file_paths: 9 | type: array 10 | items: 11 | type: string 12 | description: List to file paths to attach. 13 | required: true 14 | issue_key: 15 | type: string 16 | description: Issue key (e.g. PROJECT-1000). 17 | required: true 18 | -------------------------------------------------------------------------------- /actions/bulk_link_issue.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from threading import Semaphore 3 | from lib.base import BaseJiraAction 4 | 5 | 6 | class BulkLinkJiraIssueAction(BaseJiraAction): 7 | def link_issues( 8 | self, 9 | semaphore, 10 | issue_key=None, 11 | target_issue=None, 12 | direction=None, 13 | link_type=None, 14 | ): 15 | with semaphore: 16 | outward_issue_key = "" 17 | inward_issue_key = "" 18 | if direction == "outward": 19 | outward_issue_key = issue_key 20 | inward_issue_key = target_issue 21 | response = self._client.create_issue_link( 22 | link_type, inward_issue_key, outward_issue_key 23 | ) 24 | 25 | if direction == "inward": 26 | inward_issue_key = issue_key 27 | outward_issue_key = target_issue 28 | response = self._client.create_issue_link( 29 | link_type, inward_issue_key, outward_issue_key 30 | ) 31 | response_output = { 32 | "inward_issue": inward_issue_key, 33 | "outward_issue": outward_issue_key, 34 | "response": response, 35 | } 36 | print(response_output) 37 | 38 | def run(self, issue_key_list, target_issue, direction, link_type): 39 | threads = list() 40 | semaphore = Semaphore(10) 41 | for issue_key in issue_key_list: 42 | x = threading.Thread( 43 | target=self.link_issues, 44 | args=(semaphore, issue_key, target_issue, direction, link_type), 45 | ) 46 | threads.append(x) 47 | x.start() 48 | 49 | for thread in threads: 50 | thread.join() 51 | -------------------------------------------------------------------------------- /actions/bulk_link_issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: bulk_link_issue 3 | runner_type: python-script 4 | description: "Receives a list of Jiras ticket issue keys and a target Jira ticket, lazy link the list of Jira tickets to the parent" 5 | enabled: true 6 | entry_point: bulk_link_issue.py 7 | parameters: 8 | issue_key_list: 9 | type: array 10 | description: List of tickets to link to/from the target issue. 11 | required: true 12 | target_issue: 13 | type: string 14 | description: Target issue to link the list of tickets to/from. 15 | required: true 16 | direction: 17 | type: string 18 | description: Direction for link relation. Outward links 1:many (one ticket relates to many children). Inward links many:1 (many tickets relate to one child). 19 | enum: 20 | - outward 21 | - inward 22 | required: true 23 | default: outward 24 | link_type: 25 | type: string 26 | description: The type of link to create. 27 | required: true 28 | default: relates to -------------------------------------------------------------------------------- /actions/comment_issue.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.formatters import to_comment_dict 3 | 4 | __all__ = [ 5 | 'CommentJiraIssueAction' 6 | ] 7 | 8 | 9 | class CommentJiraIssueAction(BaseJiraAction): 10 | 11 | def run(self, issue_key, comment_text): 12 | comment = self._client.add_comment(issue_key, comment_text) 13 | result = to_comment_dict(comment) 14 | return result 15 | -------------------------------------------------------------------------------- /actions/comment_issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: comment_issue 3 | runner_type: python-script 4 | description: Comment on a JIRA issue / ticket. 5 | enabled: true 6 | entry_point: comment_issue.py 7 | parameters: 8 | comment_text: 9 | type: string 10 | description: the comment itself. 11 | required: true 12 | issue_key: 13 | type: string 14 | description: Issue key (e.g. PROJECT-1000). 15 | required: true 16 | -------------------------------------------------------------------------------- /actions/copy_dashboard.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.utils import remove_empty_attributes 3 | 4 | __all__ = ["CopyJiraDashboardAction"] 5 | 6 | 7 | class CopyJiraDashboardAction(BaseJiraAction): 8 | def run( 9 | self, 10 | id, 11 | name, 12 | description=None, 13 | edit_permissions=None, 14 | share_permissions=None, 15 | ): 16 | data = remove_empty_attributes( 17 | { 18 | "id": id, 19 | "name": name, 20 | "description": description, 21 | "edit_permissions": edit_permissions, 22 | "share_permissions": share_permissions, 23 | } 24 | ) 25 | 26 | dashboard = self._client.copy_dashboard(**data) 27 | return dashboard.raw 28 | -------------------------------------------------------------------------------- /actions/copy_dashboard.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: copy_dashboard 3 | runner_type: python-script 4 | description: Copy an existing JIRA dashboard. 5 | enabled: true 6 | entry_point: copy_dashboard.py 7 | parameters: 8 | id: 9 | type: string 10 | description: The ID of the Dashboard to copy. 11 | required: true 12 | name: 13 | type: string 14 | description: The name of the new dashboard. 15 | required: true 16 | description: 17 | type: string 18 | description: A description of the dashboard. 19 | required: false 20 | edit_permissions: 21 | type: array 22 | items: 23 | type: object 24 | required: false 25 | description: A list of permission objects to grant editing permissions. 26 | share_permissions: 27 | type: array 28 | items: 29 | type: object 30 | required: false 31 | description: A list of permissions objects to grant sharing permissions. 32 | -------------------------------------------------------------------------------- /actions/create_dashboard.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.utils import remove_empty_attributes 3 | 4 | __all__ = ["CreateJiraDashboardAction"] 5 | 6 | 7 | class CreateJiraDashboardAction(BaseJiraAction): 8 | def run( 9 | self, name, description=None, edit_permissions=None, share_permissions=None 10 | ): 11 | data = remove_empty_attributes( 12 | { 13 | "name": name, 14 | "description": description, 15 | "edit_permissions": edit_permissions, 16 | "share_permissions": share_permissions, 17 | } 18 | ) 19 | 20 | dashboard = self._client.create_dashboard(**data) 21 | return dashboard.raw 22 | -------------------------------------------------------------------------------- /actions/create_dashboard.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: create_dashboard 3 | runner_type: python-script 4 | description: Create a new JIRA dashboard. 5 | enabled: true 6 | entry_point: create_dashboard.py 7 | parameters: 8 | name: 9 | type: string 10 | description: The name of the new dashboard. 11 | required: true 12 | description: 13 | type: string 14 | description: A description of the dashboard. 15 | required: false 16 | edit_permissions: 17 | type: array 18 | items: 19 | type: object 20 | required: false 21 | description: A list of permission objects to grant editing permissions. 22 | share_permissions: 23 | type: array 24 | items: 25 | type: object 26 | required: false 27 | description: A list of permissions objects to grant sharing permissions. 28 | -------------------------------------------------------------------------------- /actions/create_issue.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.formatters import to_issue_dict 3 | 4 | __all__ = [ 5 | 'CreateJiraIssueAction' 6 | ] 7 | 8 | 9 | class CreateJiraIssueAction(BaseJiraAction): 10 | 11 | def run(self, summary, type, description=None, 12 | project=None, extra_fields=None): 13 | project = project or self.config['project'] 14 | data = { 15 | 'project': {'key': project}, 16 | 'summary': summary, 17 | 'issuetype': {'name': type} 18 | } 19 | 20 | if description: 21 | data['description'] = description 22 | 23 | if extra_fields: 24 | data.update(extra_fields) 25 | 26 | issue = self._client.create_issue(fields=data) 27 | result = to_issue_dict(issue) 28 | return result 29 | -------------------------------------------------------------------------------- /actions/create_issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: create_issue 3 | runner_type: python-script 4 | description: Create a new JIRA issue / ticket. 5 | enabled: true 6 | entry_point: create_issue.py 7 | parameters: 8 | summary: 9 | type: string 10 | description: Issue summary. 11 | required: true 12 | type: 13 | type: string 14 | description: "Issue type. Typical issues type include Task, Story, Epic, Bug. You can also specify a custom issue type." 15 | default: Task 16 | required: true 17 | description: 18 | type: string 19 | description: Issue description. 20 | required: false 21 | project: 22 | type: string 23 | description: destination Project in Jira. 24 | required: false 25 | extra_fields: 26 | type: object 27 | description: "extra fields like priority, labels, custom fields, etc" 28 | required: false 29 | -------------------------------------------------------------------------------- /actions/delete_dashboard.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["DeleteJiraDashboardAction"] 4 | 5 | 6 | class DeleteJiraDashboardAction(BaseJiraAction): 7 | def run(self, dashboard_id): 8 | dashboard = self._client.dashboard(dashboard_id) 9 | response = dashboard.delete() 10 | result = {"status_code": response.status_code, "response_text": response.text} 11 | return bool(response.status_code == 204), result 12 | -------------------------------------------------------------------------------- /actions/delete_dashboard_item_property.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["DeleteDashboardItemPropertyAction"] 4 | 5 | 6 | class DeleteDashboardItemPropertyAction(BaseJiraAction): 7 | def run(self, dashboard_id, item_id, property_key): 8 | dashboard_item_property = self._client.dashboard_item_property( 9 | dashboard_id, 10 | item_id, 11 | property_key, 12 | ) 13 | response = dashboard_item_property.delete(dashboard_id, item_id) 14 | result = {"status_code": response.status_code, "response_text": response.text} 15 | return bool(response.status_code == 204), result 16 | -------------------------------------------------------------------------------- /actions/delete_dashboard_item_property.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: delete_dashboard_item_property 3 | runner_type: python-script 4 | description: Delete the property of an item (gadget) on a dashboard. 5 | enabled: true 6 | entry_point: delete_dashboard_item_property.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard. 11 | required: true 12 | item_id: 13 | type: string 14 | description: The ID of the dashboard item (gadget). 15 | required: true 16 | property_key: 17 | type: string 18 | description: The item property key. 19 | required: true 20 | -------------------------------------------------------------------------------- /actions/delete_dashbord.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: delete_dashboard 3 | runner_type: python-script 4 | description: delete an existing JIRA dashboard. 5 | enabled: true 6 | entry_point: delete_dashboard.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard to delete. 11 | required: true 12 | -------------------------------------------------------------------------------- /actions/get_available_gadgets.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["GetAvailableGadgetsAction"] 4 | 5 | 6 | class GetAvailableGadgetsAction(BaseJiraAction): 7 | def run(self): 8 | gadgets = self._client.all_dashboard_gadgets() 9 | return [gadget.raw for gadget in gadgets] 10 | -------------------------------------------------------------------------------- /actions/get_available_gadgets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: get_available_gadgets 3 | runner_type: python-script 4 | description: Get a list of available gadgets. 5 | enabled: true 6 | entry_point: get_available_gadgets.py 7 | -------------------------------------------------------------------------------- /actions/get_dashboard_gadgets.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["GetDashboardGadgetsAction"] 4 | 5 | 6 | class GetDashboardGadgetsAction(BaseJiraAction): 7 | def run(self, dashboard_id): 8 | gadgets = self._client.dashboard_gadgets(dashboard_id) 9 | return [gadget.raw for gadget in gadgets] 10 | -------------------------------------------------------------------------------- /actions/get_dashboard_gadgets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: get_dashboard_gadgets 3 | runner_type: python-script 4 | description: Get all the gadgets on a dashboard. 5 | enabled: true 6 | entry_point: get_dashboard_gadgets.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard get gadgets from. 11 | required: true 12 | -------------------------------------------------------------------------------- /actions/get_dashboard_item_property.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["GetDashboardItemPropertyAction"] 4 | 5 | 6 | class GetDashboardItemPropertyAction(BaseJiraAction): 7 | def run(self, dashboard_id, item_id, property_key): 8 | dashboard_item_property = self._client.dashboard_item_property( 9 | dashboard_id, item_id, property_key 10 | ) 11 | return dashboard_item_property.raw 12 | -------------------------------------------------------------------------------- /actions/get_dashboard_item_property.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: get_dashboard_item_property 3 | runner_type: python-script 4 | description: Get the property of an item (gadget) on a dashboard. 5 | enabled: true 6 | entry_point: get_dashboard_item_property.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard. 11 | required: true 12 | item_id: 13 | type: string 14 | description: The ID of the dashboard item (gadget). 15 | required: true 16 | property_key: 17 | type: string 18 | description: The item property key. 19 | required: true 20 | -------------------------------------------------------------------------------- /actions/get_dashboard_item_property_keys.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["GetDashboardItemPropertyKeysAction"] 4 | 5 | 6 | class GetDashboardItemPropertyKeysAction(BaseJiraAction): 7 | def run(self, dashboard_id, item_id): 8 | dashboard_item_property_keys = self._client.dashboard_item_property_keys( 9 | dashboard_id, item_id 10 | ) 11 | return [ 12 | item_property_key.raw for item_property_key in dashboard_item_property_keys 13 | ] 14 | -------------------------------------------------------------------------------- /actions/get_dashboard_item_property_keys.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: get_dashboard_item_property_keys 3 | runner_type: python-script 4 | description: Get the property keys of an item (gadget) on a dashboard. 5 | enabled: true 6 | entry_point: get_dashboard_item_property_keys.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard. 11 | required: true 12 | item_id: 13 | type: string 14 | description: The ID of the dashboard item (gadget). 15 | required: true 16 | -------------------------------------------------------------------------------- /actions/get_issue.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.formatters import to_issue_dict 3 | 4 | __all__ = [ 5 | 'GetJiraIssueAction' 6 | ] 7 | 8 | 9 | class GetJiraIssueAction(BaseJiraAction): 10 | def run(self, issue_key, include_comments=False, include_attachments=False, 11 | include_customfields=False, include_components=False, include_subtasks=False, 12 | include_links=False, sanitize_formatting=False): 13 | issue = self._client.issue(issue_key) 14 | result = to_issue_dict(issue=issue, include_comments=include_comments, 15 | include_attachments=include_attachments, 16 | include_customfields=include_customfields, 17 | include_components=include_components, 18 | include_subtasks=include_subtasks, 19 | include_links=include_links) 20 | 21 | def strip_braces(data): 22 | if isinstance(data, dict): 23 | return {k: strip_braces(v) for k, v in data.items()} 24 | elif isinstance(data, list): 25 | return [strip_braces(element) for element in data] 26 | elif isinstance(data, str): 27 | return data.replace("{{", "").replace("}}", "") 28 | else: 29 | return data 30 | 31 | return strip_braces(result) if sanitize_formatting else result 32 | -------------------------------------------------------------------------------- /actions/get_issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: get_issue 3 | runner_type: python-script 4 | description: Retrieve information about a particular JIRA issue. 5 | enabled: true 6 | entry_point: get_issue.py 7 | parameters: 8 | issue_key: 9 | type: string 10 | description: Issue key (e.g. PROJECT-1000). 11 | required: true 12 | include_comments: 13 | type: boolean 14 | description: True to include issue comments. 15 | required: true 16 | default: false 17 | include_attachments: 18 | type: boolean 19 | description: True to include issue attachments. 20 | required: true 21 | default: false 22 | include_customfields: 23 | type: boolean 24 | description: True to include custom fields. 25 | required: true 26 | default: false 27 | include_components: 28 | type: boolean 29 | description: True to include custom fields. 30 | required: true 31 | default: false 32 | include_subtasks: 33 | type: boolean 34 | description: True to include custom fields. 35 | required: true 36 | default: false 37 | include_links: 38 | type: boolean 39 | description: True to include linked issues. 40 | required: true 41 | default: false 42 | sanitize_formatting: 43 | type: boolean 44 | description: When set to true removes jinja template artifacts. 45 | required: true 46 | default: false 47 | -------------------------------------------------------------------------------- /actions/get_issue_attachments.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.formatters import to_attachment_dict 3 | 4 | __all__ = [ 5 | 'GetJiraIssueAttachmentsAction' 6 | ] 7 | 8 | 9 | class GetJiraIssueAttachmentsAction(BaseJiraAction): 10 | def run(self, issue_key): 11 | issue = self._client.issue(issue_key) 12 | 13 | result = [] 14 | 15 | for attachment in issue.fields.attachment: 16 | item = to_attachment_dict(attachment) 17 | result.append(item) 18 | 19 | return result 20 | -------------------------------------------------------------------------------- /actions/get_issue_attachments.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: get_issue_attachments 3 | runner_type: python-script 4 | description: Retrieve attachments for a particular JIRA issue. 5 | enabled: true 6 | entry_point: get_issue_attachments.py 7 | parameters: 8 | issue_key: 9 | type: string 10 | description: Issue key (e.g. PROJECT-1000). 11 | required: true 12 | -------------------------------------------------------------------------------- /actions/get_issue_comments.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.formatters import to_comment_dict 3 | 4 | __all__ = [ 5 | 'GetJiraIssueCommentsAction' 6 | ] 7 | 8 | 9 | class GetJiraIssueCommentsAction(BaseJiraAction): 10 | def run(self, issue_key): 11 | issue = self._client.issue(issue_key) 12 | 13 | result = [] 14 | 15 | for comment in issue.fields.comment.comments: 16 | item = to_comment_dict(comment) 17 | result.append(item) 18 | 19 | return result 20 | -------------------------------------------------------------------------------- /actions/get_issue_comments.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: get_issue_comments 3 | runner_type: python-script 4 | description: Retrieve comments for a particular JIRA issue. 5 | enabled: true 6 | entry_point: get_issue_comments.py 7 | parameters: 8 | issue_key: 9 | type: string 10 | description: Issue key (e.g. PROJECT-1000). 11 | required: true 12 | -------------------------------------------------------------------------------- /actions/get_issue_links.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.formatters import to_links_dict 3 | 4 | __all__ = [ 5 | 'GetJiraIssueLinksAction' 6 | ] 7 | 8 | 9 | class GetJiraIssueLinksAction(BaseJiraAction): 10 | def run(self, issue_key): 11 | issue = self._client.issue(issue_key) 12 | 13 | result = [to_links_dict(i) for i in issue.fields.issuelinks] 14 | return result 15 | -------------------------------------------------------------------------------- /actions/get_issue_links.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: get_issue_links 3 | runner_type: python-script 4 | description: Retrieve links for a particular JIRA issue. 5 | enabled: true 6 | entry_point: get_issue_links.py 7 | parameters: 8 | issue_key: 9 | type: string 10 | description: Issue key (e.g. PROJECT-1000). 11 | required: true 12 | -------------------------------------------------------------------------------- /actions/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackStorm-Exchange/stackstorm-jira/486b27b151b423a72c04117cab3ebaf5247bccb4/actions/lib/__init__.py -------------------------------------------------------------------------------- /actions/lib/base.py: -------------------------------------------------------------------------------- 1 | from jira import JIRA 2 | import base64 3 | 4 | # from st2common.runners.base_action import Action 5 | __all__ = [ 6 | 'BaseJiraAction' 7 | ] 8 | 9 | 10 | class Action(object): 11 | def __init__(self, config): 12 | self.config = config 13 | 14 | 15 | class BaseJiraAction(Action): 16 | def __init__(self, config): 17 | super(BaseJiraAction, self).__init__(config=config) 18 | self._client = self._get_client() 19 | 20 | def _get_client(self): 21 | config = self.config 22 | 23 | options = {'server': config['url'], 'verify': config['verify']} 24 | 25 | auth_method = config['auth_method'] 26 | 27 | if auth_method == 'oauth': 28 | rsa_cert_file = config['rsa_cert_file'] 29 | rsa_key_content = self._get_file_content(file_path=rsa_cert_file) 30 | 31 | oauth_creds = { 32 | 'access_token': config['oauth_token'], 33 | 'access_token_secret': config['oauth_secret'], 34 | 'consumer_key': config['consumer_key'], 35 | 'key_cert': rsa_key_content 36 | } 37 | 38 | client = JIRA(options=options, oauth=oauth_creds) 39 | 40 | elif auth_method == 'basic': 41 | basic_creds = (config['username'], config['password']) 42 | client = JIRA(options=options, basic_auth=basic_creds, 43 | validate=config.get('validate', False)) 44 | 45 | elif auth_method == 'pat': 46 | headers = JIRA.DEFAULT_OPTIONS["headers"].copy() 47 | headers["Authorization"] = f"Bearer {config['token']}" 48 | client = JIRA(server=config['url'], options={"headers": headers}) 49 | 50 | elif auth_method == 'cookie': 51 | basic_creds = (config['username'], config['password']) 52 | client = JIRA(options=options, auth=basic_creds) 53 | 54 | elif auth_method == 'api_token': 55 | headers = JIRA.DEFAULT_OPTIONS["headers"].copy() 56 | b64_header = base64.b64encode(f"{config['username']}:{config['token']}".encode()) 57 | headers["Authorization"] = f"Basic {b64_header.decode()}" 58 | client = JIRA(server=config['url'], options={"headers": headers}) 59 | 60 | else: 61 | msg = ('You must set auth_method to either "oauth", ', 62 | '"basic", "pat", "api_token", or "cookie" in your Jira pack config file.') 63 | raise Exception(msg) 64 | 65 | return client 66 | 67 | def _get_file_content(self, file_path): 68 | with open(file_path, 'r') as fp: 69 | content = fp.read() 70 | 71 | return content 72 | -------------------------------------------------------------------------------- /actions/lib/formatters.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'to_issue_dict', 3 | 'to_comment_dict' 4 | ] 5 | 6 | 7 | def to_issue_dict(issue, include_comments=False, include_attachments=False, 8 | include_customfields=False, include_components=False, include_subtasks=False, 9 | include_links=False): 10 | """ 11 | :rtype: ``dict`` 12 | """ 13 | split = issue.permalink().split(' - ', 1) 14 | url = split[0] 15 | 16 | if issue.fields.resolution: 17 | resolution = issue.fields.resolution.name 18 | else: 19 | resolution = None 20 | 21 | if issue.fields.reporter: 22 | reporter = issue.fields.reporter.displayName 23 | else: 24 | reporter = None 25 | 26 | if issue.fields.assignee: 27 | assignee = issue.fields.assignee.displayName 28 | else: 29 | assignee = None 30 | 31 | result = { 32 | 'id': issue.id, 33 | 'key': issue.key, 34 | 'url': url, 35 | 'summary': issue.fields.summary, 36 | 'description': issue.fields.description, 37 | 'status': issue.fields.status.name, 38 | 'priority': issue.fields.priority.name if hasattr(issue.fields, 'priority') else None, 39 | 'resolution': resolution, 40 | 'labels': issue.fields.labels if hasattr(issue.fields, 'labels') else [], 41 | 'reporter': reporter, 42 | 'assignee': assignee, 43 | 'created_at': issue.fields.created, 44 | 'updated_at': issue.fields.updated, 45 | 'resolved_at': issue.fields.resolutiondate 46 | } 47 | 48 | if include_customfields: 49 | for key in issue.raw['fields']: 50 | if not key.startswith('customfield_'): 51 | continue 52 | 53 | result[key] = issue.raw['fields'][key] 54 | 55 | if include_comments: 56 | result['comments'] = [] 57 | 58 | for comment in issue.fields.comment.comments: 59 | item = to_comment_dict(comment) 60 | result['comments'].append(item) 61 | 62 | if include_attachments: 63 | result['attachments'] = [] 64 | 65 | for attachment in issue.fields.attachment: 66 | item = to_attachment_dict(attachment) 67 | result['attachments'].append(item) 68 | 69 | if include_components: 70 | result['components'] = [to_component_dict(c) for c in issue.fields.components] 71 | 72 | if include_subtasks: 73 | result['subtasks'] = [to_subtask_dict(s) for s in issue.fields.subtasks] 74 | 75 | if include_links: 76 | result['links'] = [to_links_dict(i) for i in issue.fields.issuelinks] 77 | 78 | return result 79 | 80 | 81 | def to_comment_dict(comment): 82 | """ 83 | :rtype: ``dict`` 84 | """ 85 | result = { 86 | 'id': comment.id, 87 | 'body': comment.body 88 | } 89 | return result 90 | 91 | 92 | def to_component_dict(component): 93 | """ 94 | :rtype: ``dict`` 95 | """ 96 | result = { 97 | 'id': component.id, 98 | 'name': component.name 99 | } 100 | return result 101 | 102 | 103 | def to_subtask_dict(subtask): 104 | """ 105 | :rtype: ``dict`` 106 | """ 107 | result = { 108 | 'id': subtask.id, 109 | 'key': subtask.key, 110 | 'summary': subtask.fields.summary 111 | } 112 | return result 113 | 114 | 115 | def to_attachment_dict(attachment): 116 | """ 117 | :rtype: ``dict`` 118 | """ 119 | result = { 120 | 'filename': attachment.filename, 121 | 'size': attachment.size, 122 | 'created_at': attachment.created, 123 | 'content': attachment.content, 124 | } 125 | return result 126 | 127 | 128 | def to_links_dict(issue): 129 | """ 130 | :rtype: ``dict`` 131 | """ 132 | result = { 133 | 'id': issue.raw.get('id'), 134 | 'key': issue.raw.get('outwardIssue', issue.raw.get('inwardIssue')).get('key'), 135 | 'summary': issue.raw.get('outwardIssue', issue.raw.get('inwardIssue')) 136 | .get('fields').get('summary'), 137 | 'status': issue.raw.get('outwardIssue', issue.raw.get('inwardIssue')).get('fields') 138 | .get('status').get('name'), 139 | 'type': issue.raw.get('type').get('outward') if issue.raw.get('outwardIssue') 140 | else issue.raw.get('type').get('inward'), 141 | } 142 | return result 143 | 144 | 145 | def fmt_field_value(field, value): 146 | """ 147 | Returns specific field values in formats required by JIRA 148 | """ 149 | if field == "priority": 150 | value = {"name": value} 151 | if field == "labels": 152 | value = value.split() 153 | return value 154 | -------------------------------------------------------------------------------- /actions/lib/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | __all__ = ["remove_empty_attributes"] 6 | 7 | 8 | def remove_empty_attributes(data: dict[str, Any]) -> dict[str, Any]: 9 | """A convenience function to remove key/value pairs with `None` for a value. 10 | Args: 11 | data: A dictionary. 12 | Returns: 13 | Dict[str, Any]: A dictionary with no `None` key/value pairs. 14 | """ 15 | return {key: val for key, val in data.items() if val is not None} 16 | -------------------------------------------------------------------------------- /actions/link_issue.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = [ 4 | 'LinkJiraIssueAction' 5 | ] 6 | 7 | 8 | class LinkJiraIssueAction(BaseJiraAction): 9 | def run(self, inward_issue_key=None, outward_issue_key=None, link_type=None): 10 | issue = self._client.create_issue_link(link_type, inward_issue_key, outward_issue_key) 11 | return issue 12 | -------------------------------------------------------------------------------- /actions/link_issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: link_issue 3 | runner_type: python-script 4 | description: Link one JIRA issue to another JIRA issue. 5 | enabled: true 6 | entry_point: link_issue.py 7 | parameters: 8 | inward_issue_key: 9 | type: string 10 | description: The issue key to link FROM (e.g. PROJECT-1000). 11 | required: true 12 | outward_issue_key: 13 | type: string 14 | description: The issue key to link TO (e.g. PROJECT-999). 15 | required: true 16 | link_type: 17 | type: string 18 | description: The type of link to create. 19 | required: true 20 | default: relates to 21 | -------------------------------------------------------------------------------- /actions/remove_gadget.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["RemoveGadgetAction"] 4 | 5 | 6 | class RemoveGadgetAction(BaseJiraAction): 7 | def run(self, dashboard_id, gadget_id): 8 | dashboard = self._client.dashboard(dashboard_id) 9 | gadget = next( 10 | gadget for gadget in dashboard.gadgets if str(gadget.id) == gadget_id 11 | ) 12 | response = gadget.delete(dashboard_id) 13 | result = {"status_code": response.status_code, "response_text": response.text} 14 | return bool(response.status_code == 204), result 15 | -------------------------------------------------------------------------------- /actions/remove_gadget.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: remove_gadget 3 | runner_type: python-script 4 | description: Remove a gadget from a dashboard. 5 | enabled: true 6 | entry_point: remove_gadget.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard to remove the gadget from. 11 | required: true 12 | gadget_id: 13 | type: string 14 | description: The ID of the Gadget to remove. 15 | required: true 16 | -------------------------------------------------------------------------------- /actions/run.py: -------------------------------------------------------------------------------- 1 | from jira.exceptions import JIRAError 2 | from lib.base import BaseJiraAction 3 | 4 | __all__ = [ 5 | 'ActionManager' 6 | ] 7 | 8 | 9 | class ActionManager(BaseJiraAction): 10 | 11 | def run(self, action, **kwargs): 12 | try: 13 | if action == 'transition_issue_by_name': 14 | action = 'transition_issue' 15 | kwargs['transition'] = self.transition_name_to_id(**kwargs) 16 | del kwargs['transition_name'] 17 | return (True, getattr(self._client, action)(**kwargs)) 18 | except JIRAError as error: 19 | return (False, str(error)) 20 | except AttributeError: 21 | return (False, 'Action "%s" is not implemented' % action) 22 | 23 | def transition_name_to_id(self, issue, transition_name): 24 | transitions = self._client.transitions(issue) 25 | res = list(filter(lambda x: x.get("name") == transition_name, 26 | transitions)) 27 | if bool(res): 28 | return res[0].get("id") 29 | return None 30 | -------------------------------------------------------------------------------- /actions/search_issues.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.formatters import to_issue_dict 3 | 4 | __all__ = [ 5 | 'SearchJiraIssuesAction' 6 | ] 7 | 8 | 9 | class SearchJiraIssuesAction(BaseJiraAction): 10 | def run(self, query, start_at=0, max_results=50, 11 | include_comments=False, include_attachments=False, 12 | include_customfields=False, include_components=False, 13 | include_subtasks=False): 14 | issues = self._client.search_issues(query, startAt=start_at, 15 | maxResults=max_results) 16 | results = [] 17 | 18 | for issue in issues: 19 | results.append(to_issue_dict(issue=issue, 20 | include_comments=include_comments, 21 | include_attachments=include_attachments, 22 | include_customfields=include_customfields, 23 | include_components=include_components, 24 | include_subtasks=include_subtasks)) 25 | return results 26 | -------------------------------------------------------------------------------- /actions/search_issues.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: search_issues 3 | runner_type: python-script 4 | description: Search JIRA issues with a JQL query 5 | enabled: true 6 | entry_point: search_issues.py 7 | parameters: 8 | query: 9 | type: string 10 | description: JQL query string 11 | required: true 12 | start_at: 13 | type: integer 14 | description: Offset for pagination 15 | required: false 16 | default: 0 17 | max_results: 18 | type: integer 19 | description: Maximum number of returned results 20 | default: 50 21 | required: false 22 | include_comments: 23 | type: boolean 24 | description: True to include issue comments. 25 | required: true 26 | default: false 27 | include_attachments: 28 | type: boolean 29 | description: True to include issue attachments. 30 | required: true 31 | default: false 32 | include_customfields: 33 | type: boolean 34 | description: True to include custom fields. 35 | required: true 36 | default: false 37 | include_components: 38 | type: boolean 39 | description: True to include issue components. 40 | required: true 41 | default: false 42 | include_subtasks: 43 | type: boolean 44 | description: True to include issue subtasks. 45 | required: true 46 | default: false -------------------------------------------------------------------------------- /actions/set_dashboard_item_property.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["SetDashboardItemPropertyAction"] 4 | 5 | 6 | class SetDashboardItemPropertyAction(BaseJiraAction): 7 | def run(self, dashboard_id, item_id, property_key, value): 8 | dashboard_item_property = self._client.set_dashboard_item_property( 9 | dashboard_id, item_id, property_key, value 10 | ) 11 | return dashboard_item_property.raw 12 | -------------------------------------------------------------------------------- /actions/set_dashboard_item_property.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: set_dashboard_item_property 3 | runner_type: python-script 4 | description: Set the property of an item (gadget) on a dashboard. 5 | enabled: true 6 | entry_point: set_dashboard_item_property.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard. 11 | required: true 12 | item_id: 13 | type: string 14 | description: The ID of the dashboard item (gadget). 15 | required: true 16 | property_key: 17 | type: string 18 | description: The item property key. 19 | required: true 20 | value: 21 | type: object 22 | description: The value of the property. 23 | required: true 24 | -------------------------------------------------------------------------------- /actions/transition_issue.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["TransitionJiraIssueAction"] 4 | 5 | 6 | class TransitionJiraIssueAction(BaseJiraAction): 7 | def run(self, issue_key, transition, fields): 8 | result = self._client.transition_issue(issue_key, transition, fields=fields) 9 | return result 10 | -------------------------------------------------------------------------------- /actions/transition_issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: transition_issue 3 | runner_type: python-script 4 | description: Do a transition on a JIRA issue / ticket. 5 | enabled: true 6 | entry_point: transition_issue.py 7 | parameters: 8 | issue_key: 9 | type: string 10 | description: Issue key (e.g. PROJECT-1000). 11 | required: true 12 | transition: 13 | type: string 14 | description: ID of transition (e.g. 11, 21, etc). 15 | required: true 16 | fields: 17 | type: object 18 | description: >- 19 | Fields to update on the issue. For example to set resolution to "Fixed" 20 | {"resolution": {"name": "Fixed"}}. 21 | required: false 22 | -------------------------------------------------------------------------------- /actions/transition_issue_by_name.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: transition_issue_by_name 3 | runner_type: python-script 4 | description: Do a transition on a JIRA issue / ticket. 5 | enabled: true 6 | entry_point: run.py 7 | parameters: 8 | action: 9 | default: transition_issue_by_name 10 | immutable: true 11 | type: string 12 | issue: 13 | type: string 14 | description: Issue key (e.g. PROJECT-1000). 15 | required: true 16 | transition_name: 17 | type: string 18 | description: Name of transition (e.g. Close, Start Progress, etc). 19 | required: true 20 | -------------------------------------------------------------------------------- /actions/update_dashboard.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.utils import remove_empty_attributes 3 | 4 | __all__ = ["UpdateJiraDashboardAction"] 5 | 6 | 7 | class UpdateJiraDashboardAction(BaseJiraAction): 8 | def run( 9 | self, 10 | dashboard_id, 11 | name, 12 | description=None, 13 | edit_permissions=None, 14 | share_permissions=None, 15 | ): 16 | data = remove_empty_attributes( 17 | { 18 | "name": name, 19 | "description": description, 20 | "editPermissions": edit_permissions, 21 | "sharePermissions": share_permissions, 22 | } 23 | ) 24 | 25 | dashboard = self._client.dashboard(dashboard_id) 26 | dashboard.update(**data) 27 | return dashboard.raw 28 | -------------------------------------------------------------------------------- /actions/update_dashboard.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: update_dashboard 3 | runner_type: python-script 4 | description: Update an existing JIRA dashboard. 5 | enabled: true 6 | entry_point: update_dashboard.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard to update. 11 | required: true 12 | name: 13 | type: string 14 | description: The name of the new dashboard. 15 | required: true 16 | description: 17 | type: string 18 | description: A description of the dashboard. 19 | required: false 20 | edit_permissions: 21 | type: array 22 | items: 23 | type: object 24 | required: false 25 | description: A list of permission objects to grant editing permissions. 26 | share_permissions: 27 | type: array 28 | items: 29 | type: object 30 | required: false 31 | description: A list of permissions objects to grant sharing permissions. 32 | -------------------------------------------------------------------------------- /actions/update_dashboard_automatic_refresh.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["UpdateJiraDashboardAutomaticRefreshAction"] 4 | 5 | 6 | class UpdateJiraDashboardAutomaticRefreshAction(BaseJiraAction): 7 | def run(self, id, minutes): 8 | response = self._client.update_dashboard_automatic_refresh_minutes(id, minutes) 9 | result = {"status_code": response.status_code, "response_text": response.text} 10 | return bool(response.status_code == 204), result 11 | -------------------------------------------------------------------------------- /actions/update_dashboard_automatic_refresh.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: update_dashboard_automatic_refresh 3 | runner_type: python-script 4 | description: Update how often an existing JIRA dashboard refreshes. 5 | enabled: true 6 | entry_point: update_dashboard_automatic_refresh.py 7 | parameters: 8 | id: 9 | type: string 10 | description: The ID of the Dashboard to update. 11 | required: true 12 | minutes: 13 | type: integer 14 | description: The refresh interval for the target dashboard in minutes. 15 | required: true 16 | -------------------------------------------------------------------------------- /actions/update_dashboard_item_property.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | 3 | __all__ = ["UpdateDashboardItemPropertyAction"] 4 | 5 | 6 | class UpdateDashboardItemPropertyAction(BaseJiraAction): 7 | def run(self, dashboard_id, item_id, property_key, value): 8 | dashboard_item_property = self._client.dashboard_item_property( 9 | dashboard_id, 10 | item_id, 11 | property_key, 12 | ) 13 | dashboard_item_property = dashboard_item_property.update( 14 | dashboard_id, item_id, value 15 | ) 16 | return dashboard_item_property.raw 17 | -------------------------------------------------------------------------------- /actions/update_dashboard_item_property.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: update_dashboard_item_property 3 | runner_type: python-script 4 | description: Update the property of an item (gadget) on a dashboard. 5 | enabled: true 6 | entry_point: update_dashboard_item_property.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard. 11 | required: true 12 | item_id: 13 | type: string 14 | description: The ID of the dashboard item (gadget). 15 | required: true 16 | property_key: 17 | type: string 18 | description: The item property key. 19 | required: true 20 | value: 21 | type: object 22 | description: The value of the property. 23 | required: true 24 | -------------------------------------------------------------------------------- /actions/update_field_value.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.formatters import fmt_field_value, to_issue_dict 3 | 4 | __all__ = [ 5 | 'UpdateFieldValue' 6 | ] 7 | 8 | 9 | class UpdateFieldValue(BaseJiraAction): 10 | def run(self, issue_key, field, value, notify): 11 | issue = self._client.issue(issue_key) 12 | issue.update( 13 | fields={field: fmt_field_value(field, value)}, 14 | notify=notify, 15 | ) 16 | return to_issue_dict(issue) 17 | -------------------------------------------------------------------------------- /actions/update_field_value.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: update_field_value 3 | runner_type: python-script 4 | description: Update a field in a particular JIRA issue. 5 | enabled: true 6 | entry_point: update_field_value.py 7 | parameters: 8 | issue_key: 9 | type: string 10 | description: Issue key (e.g. PROJECT-1000). 11 | required: true 12 | field: 13 | type: string 14 | description: the field name. 15 | required: true 16 | value: 17 | type: string 18 | description: the field text itself. 19 | required: true 20 | notify: 21 | type: boolean 22 | description: jira will send notifications (default is true) 23 | default: true 24 | required: false 25 | -------------------------------------------------------------------------------- /actions/update_gadget.py: -------------------------------------------------------------------------------- 1 | from lib.base import BaseJiraAction 2 | from lib.utils import remove_empty_attributes 3 | 4 | __all__ = ["UpdateGadgetAction"] 5 | 6 | 7 | class UpdateGadgetAction(BaseJiraAction): 8 | def run(self, dashboard_id, gadget_id, color=None, position=None, title=None): 9 | data = remove_empty_attributes( 10 | { 11 | "dashboard_id": dashboard_id, 12 | "color": color, 13 | "position": position, 14 | "title": title, 15 | } 16 | ) 17 | 18 | dashboard = self._client.dashboard(dashboard_id) 19 | gadget = next( 20 | gadget for gadget in dashboard.gadgets if str(gadget.id) == gadget_id 21 | ) 22 | gadget = gadget.update(**data) 23 | 24 | return gadget.raw 25 | -------------------------------------------------------------------------------- /actions/update_gadget.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: update_gadget 3 | runner_type: python-script 4 | description: Update a gadget on a dashboard. 5 | enabled: true 6 | entry_point: update_gadget.py 7 | parameters: 8 | dashboard_id: 9 | type: string 10 | description: The ID of the Dashboard to update the gadget on. 11 | required: true 12 | gadget_id: 13 | type: string 14 | description: The ID of the Gadget to update. 15 | required: true 16 | color: 17 | type: string 18 | description: The color of the gadget. 19 | default: blue 20 | enum: 21 | - blue 22 | - red 23 | - yellow 24 | - green 25 | - cyan 26 | - purple 27 | - gray 28 | - white 29 | required: false 30 | position: 31 | type: object 32 | description: | 33 | A dictionary (object) containing positioning information for the 34 | gadget like - {"column": 0, "row": 0} 35 | required: false 36 | title: 37 | type: string 38 | description: The title of the gadget. 39 | required: false 40 | -------------------------------------------------------------------------------- /config.schema.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | url: 3 | description: "URL of the JIRA instance (e.g. ``https://myproject.atlassian.net``)" 4 | type: "string" 5 | secret: false 6 | required: true 7 | verify: 8 | description: "Verify SSL certificate. Set to False to disable verification. Default True" 9 | type: boolean 10 | default: True 11 | auth_method: 12 | description: "Authentication method to use - oauth, basic or pat" 13 | type: "string" 14 | secret: false 15 | required: true 16 | default: "oauth" 17 | enum: 18 | - oauth 19 | - basic 20 | - pat 21 | - cookie 22 | - api_token 23 | username: 24 | description: "Username if using the basic, api_token, or cookie auth_method" 25 | type: "string" 26 | secret: false 27 | required: false 28 | password: 29 | description: "Password if using the basic or cookie auth_method" 30 | type: "string" 31 | secret: true 32 | required: false 33 | token: 34 | description: "PAT or API token" 35 | type: "string" 36 | secret: true 37 | required: false 38 | rsa_cert_file: 39 | description: "Path to a private key file, e.g. /home/vagrant/jira.pem" 40 | type: "string" 41 | secret: false 42 | required: false 43 | oauth_token: 44 | description: "OAuth token" 45 | type: "string" 46 | secret: true 47 | required: false 48 | oauth_secret: 49 | description: "OAuth secret" 50 | type: "string" 51 | secret: true 52 | required: false 53 | consumer_key: 54 | description: "Consumer key" 55 | type: "string" 56 | secret: true 57 | required: false 58 | poll_interval: 59 | description: "Polling interval - default 30s" 60 | type: "integer" 61 | secret: false 62 | required: false 63 | default: 30 64 | project: 65 | description: "Project to be used as default for actions that don't require or allow a project" 66 | type: "string" 67 | secret: false 68 | required: true 69 | validate: 70 | description: "If true it will validate your credentials first." 71 | type: boolean 72 | default: false 73 | required: false 74 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackStorm-Exchange/stackstorm-jira/486b27b151b423a72c04117cab3ebaf5247bccb4/icon.png -------------------------------------------------------------------------------- /jira.yaml.example: -------------------------------------------------------------------------------- 1 | --- 2 | url: "https://company.atlassian.net" 3 | rsa_cert_file: "/home/vagrant/jira.pem" 4 | auth_method: "oauth" 5 | oauth_token: "" 6 | oauth_secret: "" 7 | consumer_key: "" 8 | poll_interval: 30 9 | project: "MY_PROJECT" 10 | verify: True 11 | -------------------------------------------------------------------------------- /pack.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ref: jira 3 | name: jira 4 | description: Jira integrations 5 | keywords: 6 | - issues 7 | - ticket management 8 | - project management 9 | version: 3.2.0 10 | python_versions: 11 | - "3" 12 | author: StackStorm, Inc. 13 | email: info@stackstorm.com 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jira==3.8.0 2 | pyjwt==2.4.0 3 | -------------------------------------------------------------------------------- /sensors/jira_sensor.py: -------------------------------------------------------------------------------- 1 | # See ./requirements.txt for requirements. 2 | import os 3 | import base64 4 | from jira.client import JIRA 5 | 6 | from st2reactor.sensor.base import PollingSensor 7 | 8 | 9 | class JIRASensor(PollingSensor): 10 | ''' 11 | Sensor will monitor for any new projects created in JIRA and 12 | emit trigger instance when one is created. 13 | ''' 14 | def __init__(self, sensor_service, config=None, poll_interval=5): 15 | super(JIRASensor, self).__init__(sensor_service=sensor_service, 16 | config=config, 17 | poll_interval=poll_interval) 18 | 19 | self._jira_url = None 20 | # The Consumer Key created while setting up the "Incoming Authentication" in 21 | # JIRA for the Application Link. 22 | self._consumer_key = u'' 23 | self._rsa_key = None 24 | self._jira_client = None 25 | self._access_token = u'' 26 | self._access_secret = u'' 27 | self._projects_available = None 28 | self._poll_interval = 30 29 | self._project = None 30 | self._latest_id = None 31 | self._jql_query = None 32 | self._trigger_name = 'issues_tracker' 33 | self._trigger_pack = 'jira' 34 | self._trigger_ref = '.'.join([self._trigger_pack, self._trigger_name]) 35 | 36 | def _read_cert(self, file_path): 37 | with open(file_path) as f: 38 | return f.read() 39 | 40 | def setup(self): 41 | self._jira_url = self._config['url'] 42 | auth_method = self._config['auth_method'] 43 | 44 | if auth_method == 'oauth': 45 | rsa_cert_file = self._config['rsa_cert_file'] 46 | if not os.path.exists(rsa_cert_file): 47 | raise Exception('Cert file for JIRA OAuth not found at %s.' % rsa_cert_file) 48 | self._rsa_key = self._read_cert(rsa_cert_file) 49 | self._poll_interval = self._config.get('poll_interval', self._poll_interval) 50 | oauth_creds = { 51 | 'access_token': self._config['oauth_token'], 52 | 'access_token_secret': self._config['oauth_secret'], 53 | 'consumer_key': self._config['consumer_key'], 54 | 'key_cert': self._rsa_key 55 | } 56 | 57 | self._jira_client = JIRA(options={'server': self._jira_url}, 58 | oauth=oauth_creds) 59 | elif auth_method == 'basic': 60 | basic_creds = (self._config['username'], self._config['password']) 61 | self._jira_client = JIRA(options={'server': self._jira_url}, 62 | basic_auth=basic_creds) 63 | 64 | elif auth_method == 'pat': 65 | headers = JIRA.DEFAULT_OPTIONS["headers"].copy() 66 | headers["Authorization"] = f"Bearer {self._config['token']}" 67 | self._jira_client = JIRA(server=self._jira_url, options={"headers": headers}) 68 | 69 | elif auth_method == 'cookie': 70 | basic_creds = (self._config['username'], self._config['password']) 71 | self._jira_client = JIRA(server=self._jira_url, auth=basic_creds) 72 | 73 | elif auth_method == 'api_token': 74 | headers = JIRA.DEFAULT_OPTIONS["headers"].copy() 75 | b64_header = base64.b64encode(f"{self._config['username']}:{self._config['token']}" 76 | .encode()) 77 | headers["Authorization"] = f"Basic {b64_header.decode()}" 78 | self._jira_client = JIRA(server=self._jira_url, options={"headers": headers}) 79 | 80 | else: 81 | msg = ('You must set auth_method to either "oauth", ', 82 | '"basic", "pat", "api_token", or "cookie" in your Jira pack config file.') 83 | raise Exception(msg) 84 | 85 | if self._projects_available is None: 86 | self._projects_available = set() 87 | for proj in self._jira_client.projects(): 88 | self._projects_available.add(proj.key) 89 | self._project = self._config.get('project', None) 90 | if not self._project or self._project not in self._projects_available: 91 | raise Exception('Invalid project (%s) to track.' % self._project) 92 | 93 | self._jql_query = 'project={} ORDER BY id DESC'.format(self._project) 94 | latest_issue = self._jira_client.search_issues(self._jql_query, maxResults=1) 95 | if latest_issue: 96 | self._latest_id = int(latest_issue[0].id) 97 | self._update_jql(self._latest_id) 98 | 99 | def poll(self): 100 | self._detect_new_issues() 101 | 102 | def cleanup(self): 103 | pass 104 | 105 | def add_trigger(self, trigger): 106 | pass 107 | 108 | def update_trigger(self, trigger): 109 | pass 110 | 111 | def remove_trigger(self, trigger): 112 | pass 113 | 114 | def _update_jql(self, latest_id=None): 115 | jql = 'project={}'.format(self._project) 116 | if latest_id: 117 | jql = '{} AND id > {}'.format(jql, latest_id) 118 | self._jql_query = '{} ORDER BY id ASC'.format(jql) 119 | 120 | def _detect_new_issues(self): 121 | new_issues = self._jira_client.search_issues(self._jql_query, maxResults=50, startAt=0) 122 | for issue in new_issues: 123 | self._latest_id = int(issue.id) 124 | self._dispatch_issues_trigger(issue) 125 | self._update_jql(self._latest_id) 126 | 127 | def _dispatch_issues_trigger(self, issue): 128 | trigger = self._trigger_ref 129 | payload = {} 130 | payload['issue_name'] = issue.key 131 | payload['issue_url'] = issue.self 132 | payload['issue_browse_url'] = self._jira_url + '/browse/' + issue.key 133 | payload['project'] = self._project 134 | payload['created'] = issue.raw['fields']['created'] 135 | payload['assignee'] = issue.raw['fields']['assignee'] 136 | payload['fix_versions'] = issue.raw['fields']['fixVersions'] 137 | payload['issue_type'] = issue.raw['fields']['issuetype']['name'] 138 | self._sensor_service.dispatch(trigger, payload) 139 | -------------------------------------------------------------------------------- /sensors/jira_sensor.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | class_name: "JIRASensor" 3 | entry_point: "jira_sensor.py" 4 | description: "Sensor which monitors JIRA for new tickets" 5 | poll_interval: 30 6 | trigger_types: 7 | - 8 | name: "issues_tracker" 9 | description: "Trigger which indicates that a new issue has been created" 10 | payload_schema: 11 | type: "object" 12 | properties: 13 | project: 14 | type: "string" 15 | issue_name: 16 | type: "string" 17 | issue_url: 18 | type: "string" 19 | created: 20 | type: "string" 21 | assignee: 22 | type: "string" 23 | fix_versions: 24 | type: "string" 25 | issue_type: 26 | type: "string" 27 | -------------------------------------------------------------------------------- /sensors/jira_sensor_for_apiv2.py: -------------------------------------------------------------------------------- 1 | # See ./requirements.txt for requirements. 2 | import os 3 | import base64 4 | 5 | from jira.client import JIRA 6 | 7 | from st2reactor.sensor.base import PollingSensor 8 | 9 | 10 | class JIRASensorForAPIv2(PollingSensor): 11 | ''' 12 | Sensor will monitor for any new projects created in JIRA and 13 | emit trigger instance when one is created. 14 | ''' 15 | def __init__(self, sensor_service, config=None, poll_interval=5): 16 | super(JIRASensorForAPIv2, self).__init__(sensor_service=sensor_service, 17 | config=config, 18 | poll_interval=poll_interval) 19 | 20 | self._jira_url = None 21 | # The Consumer Key created while setting up the "Incoming Authentication" in 22 | # JIRA for the Application Link. 23 | self._consumer_key = u'' 24 | self._rsa_key = None 25 | self._jira_client = None 26 | self._access_token = u'' 27 | self._access_secret = u'' 28 | self._projects_available = None 29 | self._poll_interval = 30 30 | self._project = None 31 | self._latest_id = None 32 | self._jql_query = None 33 | self._trigger_name = 'issues_tracker_for_apiv2' 34 | self._trigger_pack = 'jira' 35 | self._trigger_ref = '.'.join([self._trigger_pack, self._trigger_name]) 36 | 37 | def _read_cert(self, file_path): 38 | with open(file_path) as f: 39 | return f.read() 40 | 41 | def setup(self): 42 | self._jira_url = self._config['url'] 43 | auth_method = self._config['auth_method'] 44 | 45 | if auth_method == 'oauth': 46 | rsa_cert_file = self._config['rsa_cert_file'] 47 | if not os.path.exists(rsa_cert_file): 48 | raise Exception('Cert file for JIRA OAuth not found at %s.' % rsa_cert_file) 49 | self._rsa_key = self._read_cert(rsa_cert_file) 50 | self._poll_interval = self._config.get('poll_interval', self._poll_interval) 51 | oauth_creds = { 52 | 'access_token': self._config['oauth_token'], 53 | 'access_token_secret': self._config['oauth_secret'], 54 | 'consumer_key': self._config['consumer_key'], 55 | 'key_cert': self._rsa_key 56 | } 57 | 58 | self._jira_client = JIRA(options={'server': self._jira_url}, 59 | oauth=oauth_creds) 60 | elif auth_method == 'basic': 61 | basic_creds = (self._config['username'], self._config['password']) 62 | self._jira_client = JIRA(options={'server': self._jira_url}, 63 | basic_auth=basic_creds) 64 | 65 | elif auth_method == 'pat': 66 | headers = JIRA.DEFAULT_OPTIONS["headers"].copy() 67 | headers["Authorization"] = f"Bearer {self._config['token']}" 68 | self._jira_client = JIRA(server=self._jira_url, options={"headers": headers}) 69 | 70 | elif auth_method == 'cookie': 71 | basic_creds = (self._config['username'], self._config['password']) 72 | self._jira_client = JIRA(server=self._jira_url, auth=basic_creds) 73 | 74 | elif auth_method == 'api_token': 75 | headers = JIRA.DEFAULT_OPTIONS["headers"].copy() 76 | b64_header = base64.b64encode(f"{self._config['username']}:{self._config['token']}" 77 | .encode()) 78 | headers["Authorization"] = f"Basic {b64_header.decode()}" 79 | self._jira_client = JIRA(server=self._jira_url, options={"headers": headers}) 80 | 81 | else: 82 | msg = ('You must set auth_method to either "oauth", ', 83 | '"basic", "pat", "api_token", or "cookie" in your Jira pack config file.') 84 | raise Exception(msg) 85 | 86 | if self._projects_available is None: 87 | self._projects_available = set() 88 | for proj in self._jira_client.projects(): 89 | self._projects_available.add(proj.key) 90 | self._project = self._config.get('project', None) 91 | if not self._project or self._project not in self._projects_available: 92 | raise Exception('Invalid project (%s) to track.' % self._project) 93 | 94 | self._jql_query = 'project={} ORDER BY id DESC'.format(self._project) 95 | latest_issue = self._jira_client.search_issues(self._jql_query, maxResults=1) 96 | if latest_issue: 97 | self._latest_id = int(latest_issue[0].id) 98 | self._update_jql(self._latest_id) 99 | 100 | def poll(self): 101 | self._detect_new_issues() 102 | 103 | def cleanup(self): 104 | pass 105 | 106 | def add_trigger(self, trigger): 107 | pass 108 | 109 | def update_trigger(self, trigger): 110 | pass 111 | 112 | def remove_trigger(self, trigger): 113 | pass 114 | 115 | def _update_jql(self, latest_id=None): 116 | jql = 'project={}'.format(self._project) 117 | if latest_id: 118 | jql = '{} AND id > {}'.format(jql, latest_id) 119 | self._jql_query = '{} ORDER BY id ASC'.format(jql) 120 | 121 | def _detect_new_issues(self): 122 | new_issues = self._jira_client.search_issues(self._jql_query, maxResults=50, startAt=0) 123 | for issue in new_issues: 124 | self._latest_id = int(issue.id) 125 | self._dispatch_issues_trigger(issue) 126 | self._update_jql(self._latest_id) 127 | 128 | def _dispatch_issues_trigger(self, issue): 129 | trigger = self._trigger_ref 130 | payload = {} 131 | payload['project'] = self._project 132 | payload['id'] = issue.id 133 | payload['expand'] = issue.raw.get('expand', '') 134 | payload['issue_key'] = issue.key 135 | payload['issue_url'] = issue.self 136 | payload['issue_browse_url'] = self._jira_url + '/browse/' + issue.key 137 | payload['fields'] = issue.raw.get('fields', {}) 138 | self._sensor_service.dispatch(trigger, payload) 139 | -------------------------------------------------------------------------------- /sensors/jira_sensor_for_apiv2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | class_name: "JIRASensorForAPIv2" 3 | entry_point: "jira_sensor_for_apiv2.py" 4 | description: "Sensor which monitors JIRA for new tickets" 5 | poll_interval: 30 6 | trigger_types: 7 | - 8 | name: "issues_tracker_for_apiv2" 9 | description: "Trigger which indicates that a new issue has been created" 10 | payload_schema: 11 | type: "object" 12 | properties: 13 | project: 14 | type: "string" 15 | id: 16 | type: "string" 17 | expand: 18 | type: "string" 19 | issue_key: 20 | type: "string" 21 | issue_url: 22 | type: "string" 23 | issue_browse_url: 24 | type: "string" 25 | fields: 26 | type: "object" 27 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackStorm-Exchange/stackstorm-jira/486b27b151b423a72c04117cab3ebaf5247bccb4/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/blank.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /tests/fixtures/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "This Board is a Test Board", 3 | "id": "11117", 4 | "isFavourite": true, 5 | "name": "Test Board", 6 | "popularity": 0, 7 | "self": "https://some.atlassian.net/rest/api/2/dashboard/11117", 8 | "sharePermissions": [ ], 9 | "editPermissions": [ ], 10 | "view": "/jira/dashboards/11117", 11 | "isWritable": true, 12 | "systemDashboard": false 13 | } -------------------------------------------------------------------------------- /tests/fixtures/dashboard_item_property.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "config", 3 | "value": { 4 | "filterId": "16416", 5 | "isConfigured": "true", 6 | "columnNames": "issuetype|issuekey|summary|priority|status", 7 | "isPopup": "false", 8 | "num": "5", 9 | "refresh": "15" 10 | } 11 | } -------------------------------------------------------------------------------- /tests/fixtures/dashboard_item_property_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "config", 3 | "self": "https://test.atlassian.net/rest/api/2/dashboard/11012/items/15916/properties/config" 4 | } -------------------------------------------------------------------------------- /tests/fixtures/full_auth_passwd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | url: "https://company.atlassian.net" 3 | auth_method: "basic" 4 | username: "user" 5 | password: "passwd" 6 | poll_interval: 30 7 | project: "MY_PROJECT" 8 | verify: True 9 | 10 | -------------------------------------------------------------------------------- /tests/fixtures/gadget.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 15917, 3 | "uri": "rest/gadgets/1.0/g/com.atlassian.jira.gadgets:filter-results-gadget/gadgets/filter-results-gadget.xml", 4 | "color": "cyan", 5 | "position": { 6 | "row": 0, 7 | "column": 1 8 | }, 9 | "title": "Test Gadget" 10 | } -------------------------------------------------------------------------------- /tests/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackStorm-Exchange/stackstorm-jira/486b27b151b423a72c04117cab3ebaf5247bccb4/tests/lib/__init__.py -------------------------------------------------------------------------------- /tests/lib/actions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | 4 | from st2tests.base import BaseActionTestCase 5 | 6 | __all__ = [ 7 | 'JIRABaseActionTestCase', 8 | ] 9 | 10 | 11 | class JIRABaseActionTestCase(BaseActionTestCase): 12 | __test__ = False 13 | 14 | def setUp(self): 15 | super(JIRABaseActionTestCase, self).setUp() 16 | 17 | self.full_auth_passwd_config = self.load_yaml('full_auth_passwd.yaml') 18 | self.blank_config = self.load_yaml('blank.yaml') 19 | 20 | def load_yaml(self, filename): 21 | return yaml.safe_load(self.get_fixture_content(filename)) 22 | 23 | def load_json_fixture(self, filename): 24 | return json.loads(self.get_fixture_content(filename)) 25 | -------------------------------------------------------------------------------- /tests/test_action_add_gadget.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from add_gadget import AddGadgetAction 3 | from jira.resources import DashboardGadget 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class AddGadgetTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = AddGadgetAction 11 | 12 | @mock.patch("requests.Session.request") 13 | @mock.patch("lib.base.JIRA.add_gadget_to_dashboard") 14 | def test_add_gadget(self, mocked_add_gadget, mocked_request): 15 | dashboard_id = 1 16 | 17 | action = self.get_action_instance(self.full_auth_passwd_config) 18 | mocked_add_gadget.return_value = DashboardGadget( 19 | {}, {}, self.load_json_fixture("gadget.json") 20 | ) 21 | result = action.run(dashboard_id) 22 | self.assertEqual(result["color"], "cyan") 23 | -------------------------------------------------------------------------------- /tests/test_action_copy_dashboard.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from copy_dashboard import CopyJiraDashboardAction 3 | from jira.resources import Dashboard 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class CopyDashboardTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = CopyJiraDashboardAction 11 | 12 | @mock.patch("requests.Session.request") 13 | @mock.patch("lib.base.JIRA.copy_dashboard") 14 | def test_copy_dashboard(self, mock_copy_dashboard, mock_request): 15 | board_name = "Test Board Copied" 16 | description = "This Board is a Test Board that has been copied" 17 | copy_data = {"name": board_name, "description": description} 18 | original_data = self.load_json_fixture("dashboard.json") 19 | 20 | action = self.get_action_instance(self.full_auth_passwd_config) 21 | 22 | original_data.update(copy_data) 23 | 24 | mock_copy_dashboard.return_value = Dashboard({}, {}, raw=original_data) 25 | result = action.run("id", **copy_data) 26 | self.assertEqual(result["name"], board_name) 27 | self.assertEqual(result["description"], description) 28 | -------------------------------------------------------------------------------- /tests/test_action_create_dashboard.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from create_dashboard import CreateJiraDashboardAction 3 | from jira.resources import Dashboard 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class CreateDashboardTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = CreateJiraDashboardAction 11 | 12 | @mock.patch("requests.Session.request") 13 | @mock.patch("lib.base.JIRA.create_dashboard") 14 | def test_create_dashboard(self, mock_create_dashboard, mock_request): 15 | board_name = "Test Board" 16 | description = "This Board is a Test Board" 17 | 18 | action = self.get_action_instance(self.full_auth_passwd_config) 19 | mock_create_dashboard.return_value = Dashboard( 20 | {}, {}, raw=self.load_json_fixture("dashboard.json") 21 | ) 22 | result = action.run(name="Test Board", description="The Funnest Board Around") 23 | self.assertEqual(result["name"], board_name) 24 | self.assertEqual(result["description"], description) 25 | -------------------------------------------------------------------------------- /tests/test_action_delete_dashboard.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from delete_dashboard import DeleteJiraDashboardAction 3 | 4 | from tests.lib.actions import JIRABaseActionTestCase 5 | 6 | 7 | class DeleteDashboardTests(JIRABaseActionTestCase): 8 | __test__ = True 9 | action_cls = DeleteJiraDashboardAction 10 | 11 | @mock.patch("requests.Session.request") 12 | @mock.patch("jira.resources.Dashboard.delete") 13 | def test_delete_dashboard(self, mocked_delete_dashboard, mocked_request): 14 | status_code = 204 15 | text = "" 16 | mocked_delete_dashboard.return_value = mock.MagicMock( 17 | status_code=status_code, text=text 18 | ) 19 | action = self.get_action_instance(self.full_auth_passwd_config) 20 | 21 | result, data = action.run("id") 22 | 23 | self.assertTrue(result) 24 | self.assertEqual(data["status_code"], status_code) 25 | self.assertEqual(data["response_text"], text) 26 | -------------------------------------------------------------------------------- /tests/test_action_delete_dashboard_item_property.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from delete_dashboard_item_property import DeleteDashboardItemPropertyAction 3 | from jira.resources import DashboardItemProperty 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class DeleteDashboardItemPropertyTest(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = DeleteDashboardItemPropertyAction 11 | 12 | @mock.patch("jira.resources.DashboardItemProperty.delete") 13 | @mock.patch("lib.base.JIRA.dashboard_item_property") 14 | @mock.patch("requests.Session.request") 15 | def test_delete_dashboard_item_property( 16 | self, 17 | mocked_request, 18 | mocked_dashboard_item_property, 19 | mocked_dashboard_item_property_delete, 20 | ): 21 | status_code = 204 22 | text = "" 23 | 24 | dashboard_item_property = DashboardItemProperty( 25 | {}, {}, self.load_json_fixture("dashboard_item_property.json") 26 | ) 27 | mocked_dashboard_item_property.return_value = dashboard_item_property 28 | mocked_dashboard_item_property_delete.return_value = mock.MagicMock( 29 | status_code=status_code, text=text 30 | ) 31 | 32 | action = self.get_action_instance(self.full_auth_passwd_config) 33 | result, data = action.run("1", "2", "config") 34 | 35 | self.assertTrue(result) 36 | self.assertEqual(data["status_code"], status_code) 37 | self.assertEqual(data["response_text"], text) 38 | -------------------------------------------------------------------------------- /tests/test_action_get_available_gadgets.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from get_available_gadgets import GetAvailableGadgetsAction 3 | from jira.resources import DashboardGadget 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class GetAvailableGadgetsTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = GetAvailableGadgetsAction 11 | 12 | @mock.patch("requests.Session.request") 13 | @mock.patch("lib.base.JIRA.all_dashboard_gadgets") 14 | def test_get_available_gadgets(self, mocked_gadgets, mocked_request): 15 | action = self.get_action_instance(self.full_auth_passwd_config) 16 | gadget = DashboardGadget({}, {}, self.load_json_fixture("gadget.json")) 17 | mocked_gadgets.return_value = [gadget] 18 | result = action.run() 19 | self.assertIsInstance(result, list) 20 | self.assertEqual(result[0], gadget.raw) 21 | -------------------------------------------------------------------------------- /tests/test_action_get_dashboard_gadgets.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from get_dashboard_gadgets import GetDashboardGadgetsAction 3 | from jira.resources import Dashboard, DashboardGadget 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class GetDashboardGadgetsTest(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = GetDashboardGadgetsAction 11 | 12 | @mock.patch("lib.base.JIRA.dashboard_gadgets") 13 | @mock.patch("requests.Session.request") 14 | def test_get_dashboard_gadgets(self, mocked_request, mocked_dashboard): 15 | dashboard = Dashboard({}, {}, raw=self.load_json_fixture("dashboard.json")) 16 | gadget = DashboardGadget({}, {}, raw=self.load_json_fixture("gadget.json")) 17 | 18 | dashboard.gadgets.append(gadget) 19 | mocked_dashboard.return_value = dashboard.gadgets 20 | 21 | action = self.get_action_instance(self.full_auth_passwd_config) 22 | result = action.run(dashboard.id) 23 | self.assertIsInstance(result, list) 24 | self.assertEqual(result[0], gadget.raw) 25 | -------------------------------------------------------------------------------- /tests/test_action_get_dashboard_item_property.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from get_dashboard_item_property import GetDashboardItemPropertyAction 3 | from jira.resources import DashboardItemProperty 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class GetDashboardItemPropertyTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = GetDashboardItemPropertyAction 11 | 12 | @mock.patch("requests.Session.request") 13 | @mock.patch("lib.base.JIRA.dashboard_item_property") 14 | def test_get_dashboard_item_property( 15 | self, mocked_dashboard_item_property, mocked_request 16 | ): 17 | action = self.get_action_instance(self.full_auth_passwd_config) 18 | item_property = DashboardItemProperty( 19 | {}, {}, self.load_json_fixture("dashboard_item_property_key.json") 20 | ) 21 | mocked_dashboard_item_property.return_value = item_property 22 | result = action.run("1", "2", "config") 23 | self.assertEqual(result, item_property.raw) 24 | -------------------------------------------------------------------------------- /tests/test_action_get_dashboard_item_property_keys.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from get_dashboard_item_property_keys import GetDashboardItemPropertyKeysAction 3 | from jira.resources import DashboardItemPropertyKey 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class GetDashboardItemPropertyKeysTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = GetDashboardItemPropertyKeysAction 11 | 12 | @mock.patch("requests.Session.request") 13 | @mock.patch("lib.base.JIRA.dashboard_item_property_keys") 14 | def test_get_dashboard_item_property_keys( 15 | self, mocked_dashboard_item_property_keys, mocked_request 16 | ): 17 | action = self.get_action_instance(self.full_auth_passwd_config) 18 | key = DashboardItemPropertyKey( 19 | {}, {}, self.load_json_fixture("dashboard_item_property_key.json") 20 | ) 21 | mocked_dashboard_item_property_keys.return_value = [key] 22 | result = action.run("1", "2") 23 | self.assertIsInstance(result, list) 24 | self.assertEqual(result[0], key.raw) 25 | -------------------------------------------------------------------------------- /tests/test_action_remove_gadget.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from jira.resources import Dashboard, DashboardGadget 3 | from remove_gadget import RemoveGadgetAction 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class RemoveGadgetTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = RemoveGadgetAction 11 | 12 | @mock.patch("lib.base.JIRA.dashboard") 13 | @mock.patch("requests.Session.request") 14 | @mock.patch("jira.resources.DashboardGadget.delete") 15 | def test_remove_gadget( 16 | self, mocked_delete_gadget, mocked_request, mocked_dashboard 17 | ): 18 | status_code = 204 19 | text = "" 20 | 21 | gadget = DashboardGadget({}, {}, raw=self.load_json_fixture("gadget.json")) 22 | dashboard = Dashboard({}, {}, raw=self.load_json_fixture("dashboard.json")) 23 | dashboard.gadgets.append(gadget) 24 | mocked_dashboard.return_value = dashboard 25 | 26 | mocked_delete_gadget.return_value = mock.MagicMock( 27 | status_code=status_code, text=text 28 | ) 29 | action = self.get_action_instance(self.full_auth_passwd_config) 30 | result, data = action.run(dashboard.id, str(gadget.id)) 31 | 32 | self.assertTrue(result) 33 | self.assertEqual(data["status_code"], status_code) 34 | self.assertEqual(data["response_text"], text) 35 | -------------------------------------------------------------------------------- /tests/test_action_run.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from jira.exceptions import JIRAError 4 | from run import ActionManager 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class RunTestCase(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = ActionManager 11 | 12 | @mock.patch('lib.base.JIRA') 13 | def test_run_without_exception(self, mock_jira): 14 | action = self.get_action_instance(self.full_auth_passwd_config) 15 | 16 | action._client.method.return_value = 'result' 17 | 18 | (is_success, value) = action.run(action='method') 19 | self.assertTrue(is_success) 20 | self.assertEqual(value, 'result') 21 | 22 | @mock.patch('lib.base.JIRA') 23 | def test_run_with_invalid_action(self, mock_jira): 24 | action = self.get_action_instance(self.full_auth_passwd_config) 25 | 26 | def side_effect(*args, **kwargs): 27 | raise AttributeError() 28 | 29 | action._client.not_implemented_method.side_effect = side_effect 30 | 31 | (is_success, value) = action.run(action='not_implemented_method') 32 | self.assertFalse(is_success) 33 | self.assertEqual(value, 'Action "not_implemented_method" is not implemented') 34 | 35 | @mock.patch('lib.base.JIRA') 36 | def test_run_with_jira_exception(self, mock_jira): 37 | action = self.get_action_instance(self.full_auth_passwd_config) 38 | 39 | def side_effect(*args, **kwargs): 40 | raise JIRAError('error message') 41 | 42 | action._client.method.side_effect = side_effect 43 | 44 | (is_success, value) = action.run(action='method') 45 | self.assertFalse(is_success) 46 | self.assertEqual(value, "JiraError HTTP None\n\ttext: error message\n\t") 47 | 48 | @mock.patch('lib.base.JIRA') 49 | def test_transition_name_to_id(self, mock_jira): 50 | action = self.get_action_instance(self.full_auth_passwd_config) 51 | 52 | def side_effect(*args, **kwargs): 53 | return [{'id': '11', 'name': 'Start'}, 54 | {'id': '21', 'name': 'Doing'}, 55 | {'id': '31', 'name': 'Close'}] 56 | 57 | action._client.transitions.side_effect = side_effect 58 | 59 | kwargs = {'issue': 'ISSUE-XX', 'transition_name': 'Doing'} 60 | transition_id = action.transition_name_to_id(**kwargs) 61 | self.assertEqual(transition_id, '21') 62 | 63 | kwargs = {'issue': 'ISSUE-XX', 'transition_name': 'Done'} 64 | transition_id = action.transition_name_to_id(**kwargs) 65 | self.assertEqual(transition_id, None) 66 | -------------------------------------------------------------------------------- /tests/test_action_set_dashboard_item_property.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from jira.resources import DashboardItemProperty 3 | from set_dashboard_item_property import SetDashboardItemPropertyAction 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class SetDashboardItemPropertyTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = SetDashboardItemPropertyAction 11 | 12 | @mock.patch("requests.Session.request") 13 | @mock.patch("lib.base.JIRA.set_dashboard_item_property") 14 | def test_get_dashboard_item_property( 15 | self, mocked_dashboard_item_property, mocked_request 16 | ): 17 | action = self.get_action_instance(self.full_auth_passwd_config) 18 | item_property = DashboardItemProperty( 19 | {}, {}, self.load_json_fixture("dashboard_item_property_key.json") 20 | ) 21 | mocked_dashboard_item_property.return_value = item_property 22 | value = {"num": "5"} 23 | result = action.run("1", "2", "config", value=value) 24 | self.assertEqual(result, item_property.raw) 25 | -------------------------------------------------------------------------------- /tests/test_action_update_dashboard.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from jira.resources import Dashboard 3 | from update_dashboard import UpdateJiraDashboardAction 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class UpdateDashboardTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = UpdateJiraDashboardAction 11 | 12 | @mock.patch("lib.base.JIRA.dashboard") 13 | @mock.patch("requests.Session.request") 14 | @mock.patch("jira.resources.Dashboard.update") 15 | def test_update_dashboard( 16 | self, mock_update_dashboard, mock_request, mocked_dashboard 17 | ): 18 | board_name = "Test Board Updated" 19 | description = "This Board is a Test Board that has been updated" 20 | update_data = {"name": board_name, "description": description} 21 | original_data = self.load_json_fixture("dashboard.json") 22 | 23 | action = self.get_action_instance(self.full_auth_passwd_config) 24 | original_dashboard = Dashboard({}, {}, raw=original_data) 25 | mocked_dashboard.return_value = original_dashboard 26 | 27 | original_data.update(update_data) 28 | 29 | mock_update_dashboard.return_value = Dashboard({}, {}, raw=original_data) 30 | result = action.run(original_dashboard.id, **update_data) 31 | self.assertEqual(result["name"], board_name) 32 | self.assertEqual(result["description"], description) 33 | -------------------------------------------------------------------------------- /tests/test_action_update_dashboard_automatic_refresh.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from update_dashboard_automatic_refresh import UpdateJiraDashboardAutomaticRefreshAction 3 | 4 | from tests.lib.actions import JIRABaseActionTestCase 5 | 6 | 7 | class UpdateJiraDashboardAutomaticRefreshActionDashboardTests(JIRABaseActionTestCase): 8 | __test__ = True 9 | action_cls = UpdateJiraDashboardAutomaticRefreshAction 10 | 11 | @mock.patch("requests.Session.request") 12 | @mock.patch("lib.base.JIRA.update_dashboard_automatic_refresh_minutes") 13 | def test_update_dashboard_automatic_refresh( 14 | self, mock_automatic_refresh, mock_request 15 | ): 16 | dashboard_id = 1 17 | minutes = 5 18 | status_code = 204 19 | text = "" 20 | mock_automatic_refresh.return_value = mock.MagicMock( 21 | status_code=status_code, text=text 22 | ) 23 | 24 | action = self.get_action_instance(self.full_auth_passwd_config) 25 | result, data = action.run(dashboard_id, minutes) 26 | 27 | self.assertTrue(result) 28 | self.assertEqual(data["status_code"], status_code) 29 | self.assertEqual(data["response_text"], text) 30 | -------------------------------------------------------------------------------- /tests/test_action_update_dashboard_property.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from jira.resources import DashboardItemProperty 3 | from update_dashboard_item_property import UpdateDashboardItemPropertyAction 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class UpdateDashboardItemPropertyTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = UpdateDashboardItemPropertyAction 11 | 12 | @mock.patch("jira.resources.DashboardItemProperty.update") 13 | @mock.patch("requests.Session.request") 14 | @mock.patch("lib.base.JIRA.dashboard_item_property") 15 | def test_set_dashboard_item_property( 16 | self, 17 | mocked_dashboard_item_property, 18 | mocked_request, 19 | mocked_dashboard_property_update, 20 | ): 21 | updated_count = 10 22 | updated_data = {"num": updated_count} 23 | action = self.get_action_instance(self.full_auth_passwd_config) 24 | 25 | original_data = self.load_json_fixture("dashboard_item_property_key.json") 26 | original_item_property = DashboardItemProperty({}, {}, original_data) 27 | mocked_dashboard_item_property.return_value = original_item_property 28 | 29 | original_data.update(updated_data) 30 | mocked_dashboard_property_update.return_value = DashboardItemProperty( 31 | {}, {}, original_data 32 | ) 33 | 34 | result = action.run("1", "2", "config", value=updated_data) 35 | self.assertEqual(result["num"], updated_count) 36 | -------------------------------------------------------------------------------- /tests/test_action_update_gadget.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from jira.resources import Dashboard, DashboardGadget 3 | from update_gadget import UpdateGadgetAction 4 | 5 | from tests.lib.actions import JIRABaseActionTestCase 6 | 7 | 8 | class UpdateGadgetTests(JIRABaseActionTestCase): 9 | __test__ = True 10 | action_cls = UpdateGadgetAction 11 | 12 | @mock.patch("jira.resources.DashboardGadget.update") 13 | @mock.patch("requests.Session.request") 14 | @mock.patch("lib.base.JIRA.dashboard") 15 | def test_update_gadget( 16 | self, mocked_dashboard, mocked_request, mocked_update_gadget 17 | ): 18 | updated_color = "blue" 19 | updated_title = "Updated Title" 20 | update_data = {"color": updated_color, "title": updated_title} 21 | 22 | original_gadget_data = self.load_json_fixture("gadget.json") 23 | 24 | action = self.get_action_instance(self.full_auth_passwd_config) 25 | original_gadget = DashboardGadget({}, {}, raw=original_gadget_data) 26 | dashboard = Dashboard({}, {}, raw=self.load_json_fixture("dashboard.json")) 27 | dashboard.gadgets.append(original_gadget) 28 | 29 | original_gadget_data.update(update_data) 30 | 31 | mocked_dashboard.return_value = dashboard 32 | mocked_update_gadget.return_value = DashboardGadget( 33 | {}, {}, raw=original_gadget_data 34 | ) 35 | 36 | result = action.run(dashboard.id, str(original_gadget.id), **update_data) 37 | self.assertEqual(result["color"], updated_color) 38 | self.assertEqual(result["title"], updated_title) 39 | -------------------------------------------------------------------------------- /tests/test_sensor_jira_sensor.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from st2tests.base import BaseSensorTestCase 4 | 5 | from jira_sensor import JIRASensor 6 | 7 | JIRA_URL = "https://ja.atlassian.com/" 8 | 9 | MOCK_PAYLOAD = { 10 | "issue_name": "ISSUEKEY-1", 11 | "issue_url": "https://ja.atlassian.com/api/hoge", 12 | "issue_browse_url": JIRA_URL + "/browse/ISSUEKEY-1", 13 | "project": "PROJECT", 14 | "created": "2018-11-12T11:20:54.000+0900", 15 | "assignee": "user01", 16 | "fix_versions": ["verA"], 17 | "issue_type": "task" 18 | } 19 | 20 | MOCK_TRIGGER = { 21 | "trace_context": None, 22 | "trigger": "jira.issues_tracker", 23 | "payload": MOCK_PAYLOAD 24 | } 25 | 26 | MOCK_ISSUE_RAW = { 27 | "id": '1', 28 | "fields": { 29 | "created": MOCK_PAYLOAD["created"], 30 | "assignee": MOCK_PAYLOAD["assignee"], 31 | "fixVersions": MOCK_PAYLOAD["fix_versions"], 32 | "issuetype": { 33 | "name": MOCK_PAYLOAD["issue_type"] 34 | } 35 | } 36 | } 37 | 38 | 39 | class JIRASensorTestCase(BaseSensorTestCase): 40 | sensor_cls = JIRASensor 41 | 42 | def test_poll(self): 43 | sensor = self.get_sensor_instance() 44 | sensor._jira_client = mock.Mock() 45 | sensor._jira_client.search_issues.return_value = [] 46 | sensor._issues_in_project = {} 47 | 48 | # no issues 49 | sensor.poll() 50 | self.assertEqual(self.get_dispatched_triggers(), []) 51 | 52 | # 1 new issue 53 | issue = mock.Mock() 54 | issue.raw = MOCK_ISSUE_RAW 55 | 56 | issue.id = MOCK_ISSUE_RAW["id"] 57 | issue.key = MOCK_PAYLOAD["issue_name"] 58 | issue.self = MOCK_PAYLOAD["issue_url"] 59 | sensor._project = MOCK_PAYLOAD["project"] 60 | sensor._jira_url = JIRA_URL 61 | sensor._jira_client.search_issues.return_value = [issue] 62 | 63 | sensor.poll() 64 | self.assertEqual(self.get_dispatched_triggers(), [MOCK_TRIGGER]) 65 | -------------------------------------------------------------------------------- /tests/test_sensor_jira_sensor_for_apiv2.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from st2tests.base import BaseSensorTestCase 4 | 5 | from jira_sensor_for_apiv2 import JIRASensorForAPIv2 6 | 7 | JIRA_URL = "http://jira.hoge.com/" 8 | PROJECT_NAME = "PROJECT" 9 | ISSUE_ID = "112" 10 | ISSUE_SELF = "http://jira.hoge.com/rest/api/2/issue/" + ISSUE_ID 11 | ISSUE_KEY = "ISSUEKEY-1" 12 | 13 | ISSUE = { 14 | "project": PROJECT_NAME, 15 | "id": ISSUE_ID, 16 | "expand": "html,editmeta,changelog", 17 | "fields": { 18 | "assignee": None, 19 | "creator": { 20 | "displayName": "user01@test", 21 | "name": "user-name" 22 | }, 23 | "issuetype": { 24 | "name": "task" 25 | }, 26 | "components": [ 27 | { 28 | "id": "421203", 29 | "name": "test_components" 30 | } 31 | ] 32 | } 33 | } 34 | 35 | MOCK_ISSUE_RAW = ISSUE.copy() 36 | MOCK_ISSUE_RAW["key"] = ISSUE_KEY 37 | MOCK_ISSUE_RAW["self"] = ISSUE_SELF 38 | 39 | PAYLOAD = ISSUE.copy() 40 | PAYLOAD["issue_key"] = ISSUE_KEY 41 | PAYLOAD["issue_url"] = ISSUE_SELF 42 | PAYLOAD["issue_browse_url"] = JIRA_URL + '/browse/' + ISSUE_KEY 43 | 44 | TRIGGER = { 45 | "trace_context": None, 46 | "trigger": "jira.issues_tracker_for_apiv2", 47 | "payload": PAYLOAD 48 | } 49 | 50 | 51 | class JIRASensorForAPIv2TestCase(BaseSensorTestCase): 52 | maxDiff = None 53 | sensor_cls = JIRASensorForAPIv2 54 | 55 | def test_poll(self): 56 | sensor = self.get_sensor_instance() 57 | sensor._jira_client = mock.Mock() 58 | sensor._jira_client.search_issues.return_value = [] 59 | sensor._issues_in_project = {} 60 | 61 | # no issues 62 | sensor.poll() 63 | self.assertEqual(self.get_dispatched_triggers(), []) 64 | 65 | # 1 new issue 66 | issue = mock.Mock() 67 | issue.raw = MOCK_ISSUE_RAW 68 | issue.id = ISSUE_ID 69 | issue.self = ISSUE_SELF 70 | issue.key = ISSUE_KEY 71 | 72 | sensor._project = PROJECT_NAME 73 | sensor._jira_url = JIRA_URL 74 | sensor._jira_client.search_issues.return_value = [issue] 75 | 76 | sensor.poll() 77 | payload = self.get_dispatched_triggers()[0]['payload'] 78 | self.assertEqual(payload, PAYLOAD) 79 | 80 | # 1 new issue 81 | issue = mock.Mock() 82 | issue.raw = MOCK_ISSUE_RAW 83 | issue.id = ISSUE_ID 84 | issue.self = ISSUE_SELF 85 | issue.key = ISSUE_KEY 86 | 87 | sensor._project = PROJECT_NAME 88 | sensor._jira_url = JIRA_URL 89 | sensor._jira_client.search_issues.return_value = [issue] 90 | 91 | sensor.poll() 92 | payload = self.get_dispatched_triggers()[0]['payload'] 93 | self.assertEqual(payload, PAYLOAD) 94 | --------------------------------------------------------------------------------