├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── publish.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cfn-publish.config ├── docs └── deploy-to-aws.png ├── examples ├── amazon_chime.json ├── jira.json ├── slack_custom.json └── slack_simple.json ├── lambdas └── index.py ├── prod.txt ├── requirements.txt ├── template.yaml └── tests └── test_index.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: HieronymusLex 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: HieronymusLex 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. Also mention any alternative solutions or features you've considered. 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Publish Version 4 | on: 5 | release: 6 | types: [created, edited] 7 | jobs: 8 | publish: 9 | name: Publish Version 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Fetch Tags 14 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true 15 | - name: Configure AWS credentials 16 | uses: aws-actions/configure-aws-credentials@v1 17 | with: 18 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 19 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 20 | aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} 21 | aws-region: ${{ secrets.REGION }} 22 | - name: Set version 23 | id: version 24 | run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 25 | # Setup 26 | - name: Set up Python 3.8 27 | uses: actions/setup-python@v1 28 | with: 29 | python-version: 3.8 30 | # Cache 31 | - uses: actions/cache@v2 32 | with: 33 | path: ~/.cache/pip 34 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 35 | restore-keys: | 36 | ${{ runner.os }}-pip- 37 | # Setup 38 | - name: Install Python dependencies 39 | run: pip3 install -r requirements.txt 40 | # Package and Upload Archive 41 | - name: Install Lambda dependencies 42 | run: pip install -r prod.txt -t lambdas/ 43 | - name: Zip artefact 44 | run: zip -r $VERSION.zip lambdas/ template.yaml cfn-publish.config 45 | - name: Upload artefact 46 | run: aws s3 cp ./$VERSION.zip s3://$CFN_BUCKET/aws-codebuild-webhooks/$VERSION/aws-codebuild-webhooks.zip >/dev/null 2>&1 47 | env: 48 | CFN_BUCKET: ${{ secrets.CFN_BUCKET }} 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Release Version 4 | on: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | release: 10 | if: "! contains(toJSON(github.event.commits), '[skip release]')" 11 | name: Release Version 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true 16 | # Cache 17 | - uses: actions/cache@v2 18 | with: 19 | path: ~/.cache/pip 20 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 21 | restore-keys: | 22 | ${{ runner.os }}-pip- 23 | # Setup 24 | - name: Set up Python 3.8 25 | uses: actions/setup-python@v1 26 | with: 27 | python-version: 3.8 28 | - name: Install python dependencies 29 | run: pip3 install -r requirements.txt 30 | # Release 31 | - name: Set latest version in env variables 32 | id: latest-version 33 | run: echo "LATEST_VERSION=$(git describe --tags $(git rev-list --tags --max-count=1) | sed s/^v// 2> /dev/null || echo '0')" >> $GITHUB_ENV 34 | - name: Set new version in env variables 35 | id: this-version 36 | run: echo "THIS_VERSION=$((${LATEST_VERSION##*[^0-9]} + 1 ))" >> $GITHUB_ENV 37 | - name: Create Release 38 | id: create_release 39 | uses: actions/create-release@latest 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 42 | with: 43 | tag_name: v${{ env.THIS_VERSION }} 44 | release_name: Release v${{ env.THIS_VERSION }} 45 | body: | 46 | See the commits for a list of features included in this release 47 | draft: false 48 | prerelease: false 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Tests 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | types: 10 | - opened 11 | - edited 12 | - synchronize 13 | jobs: 14 | test: 15 | name: Run All Tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true 20 | # Cache 21 | - uses: actions/cache@v2 22 | with: 23 | path: ~/.cache/pip 24 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 25 | restore-keys: | 26 | ${{ runner.os }}-pip- 27 | # Setup 28 | - name: Set up Python 3.8 29 | uses: actions/setup-python@v1 30 | with: 31 | python-version: 3.8 32 | - name: Install python dependencies 33 | run: pip3 install -r requirements.txt 34 | - name: Run CFN Lint 35 | run: cfn-lint template.yaml 36 | - name: Run Python linting 37 | run: pylint --rcfile .pylintrc lambdas/index.py 38 | - name: Run Python Unit Tests 39 | run: python -m unittest discover tests 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packaged.yaml 2 | __pycache__/ 3 | .idea/ 4 | *.pyc 5 | venv/ 6 | .vscode/ 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | 4 | repos: 5 | # General 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v3.2.0 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | args: ['--unsafe'] 13 | - id: check-added-large-files 14 | - id: no-commit-to-branch 15 | args: ['--branch', 'master'] 16 | 17 | # Secrets 18 | - repo: https://github.com/awslabs/git-secrets 19 | rev: 80230afa8c8bdeac766a0fece36f95ffaa0be778 20 | hooks: 21 | - id: git-secrets 22 | 23 | # CloudFormation 24 | - repo: https://github.com/aws-cloudformation/cfn-python-lint 25 | rev: v0.40.0 26 | hooks: 27 | - id: cfn-python-lint 28 | name: AWS CloudFormation Linter 29 | files: \.(template)$ 30 | args: [--ignore-checks=W3002] 31 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=compat.py, __main__.py 13 | 14 | # Pickle collected data for later comparisons. 15 | persistent=yes 16 | 17 | # List of plugins (as comma separated values of python modules names) to load, 18 | # usually to register additional checkers. 19 | load-plugins= 20 | 21 | # Use multiple processes to speed up Pylint. 22 | jobs=1 23 | 24 | # Allow loading of arbitrary C extensions. Extensions are imported into the 25 | # active Python interpreter and may run arbitrary code. 26 | unsafe-load-any-extension=no 27 | 28 | # A comma-separated list of package or module names from where C extensions may 29 | # be loaded. Extensions are loading into the active Python interpreter and may 30 | # run arbitrary code 31 | extension-pkg-whitelist= 32 | 33 | # Allow optimization of some AST trees. This will activate a peephole AST 34 | # optimizer, which will apply various small optimizations. For instance, it can 35 | # be used to obtain the result of joining multiple strings with the addition 36 | # operator. Joining a lot of strings can lead to a maximum recursion error in 37 | # Pylint and this flag can prevent that. It has one side effect, the resulting 38 | # AST will be different than the one from reality. 39 | optimize-ast=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Enable the message, report, category or checker with the given id(s). You can 49 | # either give multiple identifier separated by comma (,) or put this option 50 | # multiple time. See also the "--disable" option for examples. 51 | #enable= 52 | 53 | # Disable the message, report, category or checker with the given id(s). You 54 | # can either give multiple identifiers separated by comma (,) or put this 55 | # option multiple times (only on the command line, not in the configuration 56 | # file where it should appear only once).You can also use "--disable=all" to 57 | # disable everything first and then reenable specific checks. For example, if 58 | # you want to run only the similarities checker, you can use "--disable=all 59 | # --enable=similarities". If you want to run only the classes checker, but have 60 | # no Warning level messages displayed, use"--disable=all --enable=classes 61 | # --disable=W" 62 | disable=W0107,W0201,R0913,R0902,E0401,C0103,E0611,R0914,W0613,E1101 63 | 64 | 65 | [REPORTS] 66 | 67 | # Set the output format. Available formats are text, parseable, colorized, msvs 68 | # (visual studio) and html. You can also give a reporter class, eg 69 | # mypackage.mymodule.MyReporterClass. 70 | output-format=text 71 | 72 | # Put messages in a separate file for each module / package specified on the 73 | # command line instead of printing them on stdout. Reports (if any) will be 74 | # written in a file name "pylint_global.[txt|html]". 75 | files-output=no 76 | 77 | # Tells whether to display a full report or only the messages 78 | reports=no 79 | 80 | # Python expression which should return a note less than 10 (10 is the highest 81 | # note). You have access to the variables errors warning, statement which 82 | # respectively contain the number of errors / warnings messages and the total 83 | # number of statements analyzed. This is used by the global evaluation report 84 | # (RP0004). 85 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 86 | 87 | # Template used to display messages. This is a python new-style format string 88 | # used to format the message information. See doc for all details 89 | #msg-template= 90 | 91 | 92 | [BASIC] 93 | 94 | # List of builtins function names that should not be used, separated by a comma 95 | bad-functions=apply,reduce 96 | 97 | # Good variable names which should always be accepted, separated by a comma 98 | good-names=e,i,j,k,n,ex,Run,_ 99 | 100 | # Bad variable names which should always be refused, separated by a comma 101 | bad-names=foo,bar,baz,toto,tutu,tata 102 | 103 | # Colon-delimited sets of names that determine each other's naming style when 104 | # the name regexes allow several styles. 105 | name-group= 106 | 107 | # Include a hint for the correct naming format with invalid-name 108 | include-naming-hint=yes 109 | 110 | # Regular expression matching correct function names 111 | function-rgx=[a-z_][a-z0-9_]{2,50}$ 112 | 113 | # Naming hint for function names 114 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 115 | 116 | # Regular expression matching correct variable names 117 | variable-rgx=[a-z_][a-z0-9_]{0,50}$ 118 | 119 | # Naming hint for variable names 120 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 121 | 122 | # Regular expression matching correct constant names 123 | const-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$ 124 | 125 | # Naming hint for constant names 126 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 127 | 128 | # Regular expression matching correct attribute names 129 | attr-rgx=[a-z_][a-z0-9_]{1,50}$ 130 | 131 | # Naming hint for attribute names 132 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 133 | 134 | # Regular expression matching correct argument names 135 | argument-rgx=[a-z_][a-z0-9_]{0,50}$ 136 | 137 | # Naming hint for argument names 138 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 139 | 140 | # Regular expression matching correct class attribute names 141 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 142 | 143 | # Naming hint for class attribute names 144 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 145 | 146 | # Regular expression matching correct inline iteration names 147 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 148 | 149 | # Naming hint for inline iteration names 150 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 151 | 152 | # Regular expression matching correct class names 153 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 154 | 155 | # Naming hint for class names 156 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 157 | 158 | # Regular expression matching correct module names 159 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 160 | 161 | # Naming hint for module names 162 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 163 | 164 | # Regular expression matching correct method names 165 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 166 | 167 | # Naming hint for method names 168 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 169 | 170 | # Regular expression which should only match function or class names that do 171 | # not require a docstring. 172 | no-docstring-rgx=.* 173 | 174 | # Minimum line length for functions/classes that require docstrings, shorter 175 | # ones are exempt. 176 | docstring-min-length=-1 177 | 178 | 179 | [FORMAT] 180 | 181 | # Maximum number of characters on a single line. 182 | max-line-length=190 183 | 184 | # Regexp for a line that is allowed to be longer than the limit. 185 | ignore-long-lines=^\s*(# )??$ 186 | 187 | # Allow the body of an if to be on the same line as the test if there is no 188 | # else. 189 | single-line-if-stmt=no 190 | 191 | # List of optional constructs for which whitespace checking is disabled 192 | no-space-check=trailing-comma,dict-separator 193 | 194 | # Maximum number of lines in a module 195 | max-module-lines=1000 196 | 197 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 198 | # tab). 199 | indent-string=' ' 200 | 201 | # Number of spaces of indent required inside a hanging or continued line. 202 | indent-after-paren=4 203 | 204 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 205 | expected-line-ending-format= 206 | 207 | 208 | [LOGGING] 209 | 210 | # Logging modules to check that the string format arguments are in logging 211 | # function parameter format 212 | logging-modules=logging 213 | 214 | 215 | [MISCELLANEOUS] 216 | 217 | # List of note tags to take in consideration, separated by a comma. 218 | notes=FIXME,XXX 219 | 220 | 221 | [SIMILARITIES] 222 | 223 | # Minimum lines number of a similarity. 224 | # Temp 500 until we merge initial_commit into shared codebase. 225 | min-similarity-lines=500 226 | 227 | # Ignore comments when computing similarities. 228 | ignore-comments=yes 229 | 230 | # Ignore docstrings when computing similarities. 231 | ignore-docstrings=yes 232 | 233 | # Ignore imports when computing similarities. 234 | ignore-imports=yes 235 | 236 | 237 | [SPELLING] 238 | 239 | # Spelling dictionary name. Available dictionaries: none. To make it working 240 | # install python-enchant package. 241 | spelling-dict= 242 | 243 | # List of comma separated words that should not be checked. 244 | spelling-ignore-words= 245 | 246 | # A path to a file that contains private dictionary; one word per line. 247 | spelling-private-dict-file= 248 | 249 | # Tells whether to store unknown words to indicated private dictionary in 250 | # --spelling-private-dict-file option instead of raising a message. 251 | spelling-store-unknown-words=no 252 | 253 | 254 | [TYPECHECK] 255 | 256 | # Tells whether missing members accessed in mixin class should be ignored. A 257 | # mixin class is detected if its name ends with "mixin" (case insensitive). 258 | ignore-mixin-members=yes 259 | 260 | # List of module names for which member attributes should not be checked 261 | # (useful for modules/projects where namespaces are manipulated during runtime 262 | # and thus existing member attributes cannot be deduced by static analysis 263 | ignored-modules=six.moves, 264 | 265 | # List of classes names for which member attributes should not be checked 266 | # (useful for classes with attributes dynamically set). 267 | ignored-classes=SQLObject 268 | 269 | # List of members which are set dynamically and missed by pylint inference 270 | # system, and so shouldn't trigger E0201 when accessed. Python regular 271 | # expressions are accepted. 272 | generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,md5,sha1,sha224,sha256,sha384,sha512 273 | 274 | 275 | [VARIABLES] 276 | 277 | # Tells whether we should check for unused import in __init__ files. 278 | init-import=no 279 | 280 | # A regular expression matching the name of dummy variables (i.e. expectedly 281 | # not used). 282 | dummy-variables-rgx=_|dummy|ignore 283 | 284 | # List of additional names supposed to be defined in builtins. Remember that 285 | # you should avoid to define new builtins when possible. 286 | additional-builtins= 287 | 288 | # List of strings which can identify a callback function by name. A callback 289 | # name must start or end with one of those strings. 290 | callbacks=cb_,_cb 291 | 292 | 293 | [CLASSES] 294 | 295 | # List of method names used to declare (i.e. assign) instance attributes. 296 | defining-attr-methods=__init__,__new__,setUp 297 | 298 | # List of valid names for the first argument in a class method. 299 | valid-classmethod-first-arg=cls 300 | 301 | # List of valid names for the first argument in a metaclass class method. 302 | valid-metaclass-classmethod-first-arg=mcs 303 | 304 | # List of member names, which should be excluded from the protected access 305 | # warning. 306 | exclude-protected=_asdict,_fields,_replace,_source,_make 307 | 308 | 309 | [DESIGN] 310 | 311 | # Maximum number of arguments for function / method 312 | max-args=5 313 | 314 | # Argument names that match this expression will be ignored. Default to name 315 | # with leading underscore 316 | ignored-argument-names=_.* 317 | 318 | # Maximum number of locals for function / method body 319 | max-locals=15 320 | 321 | # Maximum number of return / yield for function / method body 322 | max-returns=6 323 | 324 | # Maximum number of branch for function / method body 325 | max-branches=12 326 | 327 | # Maximum number of statements in function / method body 328 | max-statements=35 329 | 330 | # Maximum number of parents for a class (see R0901). 331 | max-parents=6 332 | 333 | # Maximum number of attributes for a class (see R0902). 334 | max-attributes=7 335 | 336 | # Minimum number of public methods for a class (see R0903). 337 | min-public-methods=0 338 | 339 | # Maximum number of public methods for a class (see R0904). 340 | max-public-methods=20 341 | 342 | 343 | [IMPORTS] 344 | 345 | # Deprecated modules which should not be used, separated by a comma 346 | deprecated-modules=regsub,TERMIOS,Bastion,rexec,UserDict 347 | 348 | # Create a graph of every (i.e. internal and external) dependencies in the 349 | # given file (report RP0402 must not be disabled) 350 | import-graph= 351 | 352 | # Create a graph of external dependencies in the given file (report RP0402 must 353 | # not be disabled) 354 | ext-import-graph= 355 | 356 | # Create a graph of internal dependencies in the given file (report RP0402 must 357 | # not be disabled) 358 | int-import-graph= 359 | 360 | [EXCEPTIONS] 361 | 362 | # Exceptions that will emit a warning when being caught. Defaults to 363 | # "Exception" 364 | overgeneral-exceptions=Exception 365 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeBuild Webhooks 2 | 3 | A solution for CodeBuild custom webhook notifications. Enables you to configure 4 | a list of HTTP endpoints which should be notified of CodeBuild state changes 5 | on a per CodeBuild project basis. 6 | 7 | ![Tests](https://github.com/aws-samples/aws-codebuild-webhooks/workflows/Tests/badge.svg?branch=master) 8 | 9 | ## Deployment 10 | 11 | The easiest way to deploy the solution is using the relevant Launch Stack button 12 | below. When launching the stack you will need to provide the ID of the 13 | KMS Key you'll be using to encrypt `SecureString` parameters in SSM. By default 14 | the solution will use the AWS Managed Key for SSM however you can change this 15 | when deploying if required by supplying a different KMS Key ID. 16 | 17 | |Region|Launch Template| 18 | |------|---------------| 19 | |**US East (N. Virginia)** (us-east-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-us-east-1.s3.us-east-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 20 | |**US East (Ohio)** (us-east-2) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-us-east-2.s3.us-east-2.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 21 | |**US West (Oregon)** (us-west-2) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-us-west-2.s3.us-west-2.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 22 | |**EU (Ireland)** (eu-west-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-eu-west-1.s3.eu-west-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 23 | |**Asia Pacific (Tokyo)** (ap-northeast-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 24 | |**Asia Pacific (Sydney)** (ap-southeast-2) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-2#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-ap-southeast-2.s3.ap-southeast-2.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 25 | 26 |
27 | More regions 28 | 29 | |Region|Launch Template| 30 | |------|---------------| 31 | |**US West (N. California)** (us-west-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-us-west-1.s3.us-west-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 32 | |**Asia Pacific (Hong Kong)** (ap-east-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-east-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-ap-east-1.s3.ap-east-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 33 | |**Asia Pacific (Mumbai)** (ap-south-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-south-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-ap-south-1.s3.ap-south-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 34 | |**Asia Pacific (Seoul)** (ap-northeast-2) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-2#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 35 | |**Asia Pacific (Singapore)** (ap-southeast-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-south-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-ap-southeast-1.s3.ap-southeast-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 36 | |**Canada (Central)** (ca-central-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ca-central-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-ca-central-1.s3.ca-central-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 37 | |**EU (London)** (eu-west-2) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-2#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-eu-west-2.s3.eu-west-2.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 38 | |**EU (Frankfurt)** (eu-west-3) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-3#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-eu-west-3.s3.eu-west-3.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 39 | |**EU (Stockholm)** (eu-north-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-north-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-eu-north-1.s3.eu-north-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 40 | |**South America (Sao Paulo)** (sa-east-1) | [![Launch the AWS CodeBuild Webhooks Stack with CloudFormation](docs/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=sa-east-1#/stacks/new?stackName=aws-codebuild-webhooks&templateURL=https://solution-builders-sa-east-1.s3.sa-east-1.amazonaws.com/aws-codebuild-webhooks/latest/template.yaml)| 41 |
42 | 43 | If you wish to deploy using CloudFormation via the CLI, clone this repo then 44 | run the following commands: 45 | 46 | ``` 47 | CFN_BUCKET=your-temp-bucket 48 | virtualenv venv 49 | source venv/bin/activate 50 | pip install -r prod.txt -t lambdas/ 51 | aws cloudformation package --output-template-file packaged.yaml --template-file template.yaml --s3-bucket $CFN_BUCKET 52 | aws cloudformation deploy \ 53 | --stack-name codebuild-webhooks \ 54 | --template-file packaged.yaml \ 55 | --capabilities CAPABILITY_IAM 56 | ``` 57 | 58 | ## Registering a Webhook 59 | To register a webhook, you need to create a new item in the CodeBuildWebhooks DDB table, 60 | created by this solution, with the following keys: 61 | 62 | - `project`: The name of the CodeBuild project for which your webhook should be invoked 63 | - `hook_url_param_name`: The name of the SSM parameter which contains the URL for your webhook. The 64 | param name must be prefixed with `/webhooks/` 65 | - `statuses`: The list of CodeBuild statuses which your webhook should respond to 66 | - `template` (optional): A template for the body of the request that will be made to your webhook. 67 | This should be a properly escaped JSON string. The `$PROJECT` and `$STATUS` placeholders can be used 68 | in your template which will be substituted at runtime. Additionally, any env vars on your CodeBuild 69 | job are available in your template by prefixing their name with `$`. 70 | - `hook_headers_param_name` (optional): The name of the SSM parameter which contains a 71 | JSON string containing key/value pairs for custom headers for your webhooks. This can be used 72 | for things such as Authorization headers. The param name must be prefixed with `webhooks/` 73 | 74 | ### Example 1: Creating a simple Slack channel webhook 75 | 1. Follow [Slack's Incoming Webhook Instructions] to create a webhook 76 | 2. Create a parameter in SSM containing the Webhook URL generated for you by Slack: 77 | ``` 78 | aws ssm put-parameter --cli-input-json '{ 79 | "Name": "/webhooks/your-slack-webhook-url", 80 | "Value": "url-from-slack", 81 | "Type": "SecureString", 82 | "Description": "Slack webhook URL for my project" 83 | }' 84 | ``` 85 | 3. Create an entry in the DDB webhooks table which uses the default template: 86 | ``` 87 | aws dynamodb put-item --table-name CodeBuildWebhooks --item file://examples/slack_simple.json 88 | ``` 89 | 90 | ### Example 2: Creating a customised Slack channel webhook 91 | The steps are the same as in [Example 1](#creating-a-simple-slack-channel-webhook) except you 92 | need to provide a custom template when registering the webhook in DDB. This example also makes use 93 | of Slack's flavour of markdown. Once you've substituted the relevant values in `examples/slack_custom.json`, 94 | run the following command: 95 | ``` 96 | aws dynamodb put-item --table-name CodeBuildWebhooks --item file://examples/slack_custom.json 97 | ``` 98 | 99 | You could also use [Slack Blocks](https://api.slack.com/block-kit) in your template and any 100 | environment variables from your CodeBuild job will be available for interpolation in your template. 101 | 102 | ### Example 3: Creating a Jira Issues webhook 103 | 1. Follow [Jira's Auth Instructions] to obtain a basic auth header 104 | 2. Create a parameter in SSM containing the Jira Issues API endpoint for your Jira instance: 105 | ``` 106 | aws ssm put-parameter --cli-input-json '{ 107 | "Name": "/webhooks/jira-issues-webhook-url", 108 | "Value": "https:///rest/api/latest/issue/", 109 | "Type": "String", 110 | "Description": "Jira issues Rest API URL" 111 | }' 112 | ``` 113 | 3. Create a parameter in SSM containing your basic auth headers as a JSON string: 114 | ``` 115 | aws ssm put-parameter --cli-input-json '{ 116 | "Name": "/webhooks/jira-basic-auth-headers", 117 | "Value": "{\"Authorization\": \"Basic \"}", 118 | "Type": "SecureString", 119 | "Description": "Jira basic auth headers for CodeBuild webhooks" 120 | } 121 | ``` 122 | 4. Create an entry in the DDB webhooks table which uses the default template, substituting values 123 | as required: 124 | ``` 125 | aws dynamodb put-item --table-name CodeBuildWebhooks --item file://examples/jira.json 126 | ``` 127 | 128 | ## Tests 129 | To execute tests, run: 130 | ``` 131 | python -m unittest discover tests 132 | ``` 133 | 134 | [Slack's Incoming Webhook Instructions]: https://slack.com/intl/en-gb/help/articles/115005265063 135 | [Jira's Auth Instructions]: https://developer.atlassian.com/cloud/jira/platform/jira-rest-api-basic-authentication/#supplying-basic-auth-headers 136 | 137 | ## Contributing 138 | 139 | Contributions are more than welcome. Please read the [code of conduct](CODE_OF_CONDUCT.md) and the [contributing guidelines](CONTRIBUTING.md). 140 | 141 | ## License 142 | 143 | This library is licensed under the MIT-0 License. See the LICENSE file. 144 | -------------------------------------------------------------------------------- /cfn-publish.config: -------------------------------------------------------------------------------- 1 | template=template.yaml 2 | bucket_name_prefix="solution-builders" 3 | acl="public-read" 4 | -------------------------------------------------------------------------------- /docs/deploy-to-aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-codebuild-webhooks/be18e3a7b7269f4d7d222d3956e3172228218990/docs/deploy-to-aws.png -------------------------------------------------------------------------------- /examples/amazon_chime.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": {"S": "your-codebuild-project-name"}, 3 | "hook_url_param_name": {"S": "/webhooks/your-chime-webhook-url-param"}, 4 | "statuses": {"L": [{"S": "SUCCEEDED"}, {"S": "FAILED"}]}, 5 | "template": {"S": "{\"Content\":\"$PROJECT build $STATUS :+1: \"}"} 6 | } 7 | -------------------------------------------------------------------------------- /examples/jira.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": {"S": "your-codebuild-project-name"}, 3 | "hook_url_param_name": {"S": "/webhooks/your-jira-webhook-issues-url-param"}, 4 | "hook_headers_param_name": {"S": "/webhooks/your-jira-basic-auth-headers-param"}, 5 | "statuses": {"L": [{"S": "FAILED"}]}, 6 | "template": {"S": "{\"fields\": {\"project\":{\"id\": \"10000\"},\"summary\": \"$PROJECT build failing\",\"description\": \"AWS CodeBuild project $PROJECT latest build $STATUS\",\"issuetype\":{\"id\": \"10004\"}}}"} 7 | } 8 | -------------------------------------------------------------------------------- /examples/slack_custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": {"S": "your-codebuild-project-name"}, 3 | "hook_url_param_name": {"S": "/webhooks/your-slack-webhook-url-param"}, 4 | "statuses": {"L": [{"S": "SUCCEEDED"}, {"S": "FAILED"}]}, 5 | "template": {"S": "{ \"username\": \"webhookbot\",\"text\": \"*$PROJECT* reached status $STATUS\",\"icon_emoji\": \":building_construction:\"}"} 6 | } 7 | -------------------------------------------------------------------------------- /examples/slack_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": {"S": "your-codebuild-project-name"}, 3 | "hook_url_param_name": {"S": "/webhooks/your-slack-webhook-url-param"}, 4 | "statuses": {"L": [{"S": "SUCCEEDED"}, {"S": "FAILED"}]} 5 | } -------------------------------------------------------------------------------- /lambdas/index.py: -------------------------------------------------------------------------------- 1 | """ 2 | CodeBuild status webhook notifier 3 | 4 | This Lambda is invoked in response to CodeBuild status changes. A list of interested hooks 5 | is obtained from DynamoDB using the build project name and a customisable message will 6 | be posted to each 7 | """ 8 | import os 9 | import logging 10 | import json 11 | from string import Template 12 | import boto3 13 | from boto3.dynamodb.conditions import Key 14 | from botocore.exceptions import ClientError 15 | import requests 16 | 17 | logger = logging.getLogger() 18 | logger.setLevel(os.environ.get("LOG_LEVEL", logging.INFO)) 19 | dynamodb = boto3.resource("dynamodb") 20 | ssm = boto3.client("ssm") 21 | table = dynamodb.Table(os.environ["TABLE_NAME"]) 22 | 23 | 24 | def lambda_handler(event, context): 25 | logger.info(event) 26 | project = event["detail"]["project-name"] 27 | status = event["detail"]["build-status"] 28 | env_vars = event["detail"]["additional-information"]["environment"]["environment-variables"] 29 | 30 | template_params = { 31 | "PROJECT": project, 32 | "STATUS": status, 33 | } 34 | 35 | template_params.update({i["name"]: i["value"] for i in env_vars}) 36 | logger.info("Template params: %s", template_params) 37 | 38 | hooks = get_hooks_for_project(project) 39 | logger.info("Obtained %s hooks for project %s", len(hooks), project) 40 | for i in hooks: 41 | try: 42 | hook_url_param = i["hook_url_param_name"] 43 | statuses = i["statuses"] 44 | message = compile_template(template_params, i.get("template")) 45 | logger.info("Compiled template: %s", message) 46 | if status in statuses: 47 | logger.info("Invoking hook with SSM param '%s' for status '%s'", hook_url_param, status) 48 | hook_url = get_ssm_param(hook_url_param) 49 | headers = None 50 | header_param_name = i.get("hook_headers_param_name", None) 51 | if header_param_name: 52 | headers = json.loads(get_ssm_param(header_param_name)) 53 | invoke_webhook(hook_url, message, headers) 54 | except ClientError as e: 55 | logger.error( 56 | "Unable to retrieve webhook url from Parameter Store for item %s", 57 | hook_url_param) 58 | logger.error(str(e)) 59 | except ValueError as e: 60 | logger.error( 61 | "Unable to post to webhook url from Parameter Store for item %s", 62 | hook_url_param) 63 | logger.error(str(e)) 64 | except KeyError as e: 65 | logger.error("Invalid DDB item. Verify your template is valid.") 66 | logger.error(str(e)) 67 | 68 | 69 | def get_hooks_for_project(project): 70 | response = table.query( 71 | KeyConditionExpression=Key("project").eq(project) 72 | ) 73 | 74 | return response["Items"] 75 | 76 | 77 | def get_ssm_param(param_name): 78 | response = ssm.get_parameter( 79 | Name=param_name, 80 | WithDecryption=True 81 | ) 82 | 83 | return response["Parameter"]["Value"] 84 | 85 | 86 | def invoke_webhook(webhook_url, payload, additional_headers=None): 87 | headers = {'Content-Type': 'application/json'} 88 | if additional_headers: 89 | headers.update(additional_headers) 90 | response = requests.post( 91 | webhook_url, data=payload, 92 | headers=headers 93 | ) 94 | if response.status_code != 200: 95 | raise ValueError( 96 | 'Webhook returned an error %s, the response is:\n%s' 97 | % (response.status_code, response.text) 98 | ) 99 | 100 | 101 | def compile_template(template_params, template=None): 102 | if not template: 103 | template = '{"text": "Build of *$PROJECT* reached status *$STATUS*"}' 104 | if not template_params: 105 | template_params = {} 106 | 107 | t = Template(template) 108 | 109 | return t.substitute(template_params) 110 | -------------------------------------------------------------------------------- /prod.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | boto3==1.18.64 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r prod.txt 2 | pylint==2.4.1 3 | mock==3.0.5 4 | cfn-lint==0.54.2 5 | awscli==1.20.64 6 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Invokes webhooks in response to CodeBuild state changes (uksb-1q5j83kkh) 4 | 5 | Parameters: 6 | SSMKeyId: 7 | Description: Key used to encrypt SSM parameters 8 | Type: String 9 | Default: alias/aws/ssm 10 | SSMPrefix: 11 | Description: Prefix used for SSM parameters 12 | Type: String 13 | Default: webhooks 14 | 15 | Resources: 16 | WebhookInvoker: 17 | Type: AWS::Serverless::Function 18 | Properties: 19 | Handler: index.lambda_handler 20 | Runtime: python3.7 21 | AutoPublishAlias: live 22 | CodeUri: ./lambdas/ 23 | Events: 24 | BuildEvent: 25 | Type: CloudWatchEvent 26 | Properties: 27 | Pattern: 28 | source: 29 | - "aws.codebuild" 30 | detail-type: 31 | - "CodeBuild Build State Change" 32 | Environment: 33 | Variables: 34 | TABLE_NAME: !Ref HooksTable 35 | Policies: 36 | - KMSDecryptPolicy: 37 | KeyId: !Ref SSMKeyId 38 | - DynamoDBCrudPolicy: 39 | TableName: !Ref HooksTable 40 | - Statement: 41 | - Action: "ssm:GetParameter" 42 | Effect: "Allow" 43 | Resource: !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${SSMPrefix}/*" 44 | 45 | HooksTable: 46 | Type: AWS::DynamoDB::Table 47 | UpdateReplacePolicy: Retain 48 | DeletionPolicy: Retain 49 | Properties: 50 | TableName: CodeBuildWebhooks 51 | BillingMode: PAY_PER_REQUEST 52 | AttributeDefinitions: 53 | - 54 | AttributeName: "project" 55 | AttributeType: "S" 56 | - 57 | AttributeName: "hook_url_param_name" 58 | AttributeType: "S" 59 | KeySchema: 60 | - 61 | AttributeName: "project" 62 | KeyType: "HASH" 63 | - 64 | AttributeName: "hook_url_param_name" 65 | KeyType: "RANGE" 66 | 67 | Outputs: 68 | HooksTable: 69 | Description: DDB table used to register CodeBuild webhooks 70 | Value: !Ref HooksTable 71 | -------------------------------------------------------------------------------- /tests/test_index.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lambda tests 3 | """ 4 | import unittest 5 | import os 6 | from mock import patch, ANY, Mock 7 | with patch.dict(os.environ, {'TABLE_NAME':'mytemp', 'AWS_DEFAULT_REGION': 'eu-west-1'}): 8 | from lambdas import index 9 | 10 | 11 | class TestHandler(unittest.TestCase): 12 | @patch("lambdas.index.get_hooks_for_project") 13 | @patch("lambdas.index.get_ssm_param") 14 | @patch("lambdas.index.invoke_webhook") 15 | @patch("lambdas.index.compile_template") 16 | def test_it_handles_standard_build_event(self, compile_template, invoke_webhook, get_ssm_param, get_hooks_for_project): 17 | get_hooks_for_project.return_value = [{"hook_url_param_name": "some_val", "statuses": ["SUCCEEDED"]}] 18 | get_ssm_param.return_value = "hook_url" 19 | index.lambda_handler(get_build_event(), {}) 20 | compile_template.assert_called_with({'PROJECT': 'my-sample-project', 'STATUS': 'SUCCEEDED'}, None) 21 | invoke_webhook.assert_called_with("hook_url", ANY, None) 22 | 23 | @patch("lambdas.index.get_hooks_for_project") 24 | @patch("lambdas.index.get_ssm_param") 25 | @patch("lambdas.index.invoke_webhook") 26 | @patch("lambdas.index.compile_template") 27 | def test_it_handles_custom_headers(self, compile_template, invoke_webhook, get_ssm_param, get_hooks_for_project): 28 | get_hooks_for_project.return_value = [{"hook_headers_param_name": "some_val", "hook_url_param_name": "some_other_val", "statuses": ["SUCCEEDED"]}] 29 | get_ssm_param.side_effect = ["hook_url", '{"Authorization": "Bearer abc123"}'] 30 | index.lambda_handler(get_build_event(), {}) 31 | compile_template.assert_called_with({'PROJECT': 'my-sample-project', 'STATUS': 'SUCCEEDED'}, None) 32 | invoke_webhook.assert_called_with("hook_url", ANY, {"Authorization": "Bearer abc123"}) 33 | 34 | 35 | class TestTemplater(unittest.TestCase): 36 | template_params = { 37 | "PROJECT": "test", 38 | "VERSION": 1, 39 | "STATUS": "SUCCEEDED", 40 | } 41 | 42 | def test_it_compiles_valid_templates(self): 43 | expected = "Some custom message including test and 1" 44 | actual = index.compile_template(self.template_params, "Some custom message including $PROJECT and $VERSION") 45 | self.assertEqual(expected, actual) 46 | 47 | def test_it_provides_default_template(self): 48 | expected = '{"text": "Build of *test* reached status *SUCCEEDED*"}' 49 | actual = index.compile_template(self.template_params) 50 | self.assertEqual(expected, actual) 51 | 52 | def test_it_throws_for_invalid_templates(self): 53 | self.assertRaises(KeyError, index.compile_template, self.template_params, "$invalid $vars $specified") 54 | 55 | 56 | class TestHooks(unittest.TestCase): 57 | @patch("lambdas.index.ssm") 58 | def test_it_retrieves_hook_details_from_ssm(self, mock_ssm): 59 | expected = "test" 60 | mock_ssm.get_parameter.return_value = {"Parameter": {"Value": expected}} 61 | actual = index.get_ssm_param("my_param") 62 | self.assertEqual(expected, actual) 63 | 64 | @patch("lambdas.index.table") 65 | def test_it_retrieves_hooks_from_dynamodb(self, mock_table): 66 | expected = ["foo"] 67 | mock_table.query.return_value = {"Items": expected} 68 | actual = index.get_hooks_for_project("my_project") 69 | self.assertEqual(expected, actual) 70 | 71 | @patch("lambdas.index.requests") 72 | def test_it_invokes_webhook_correctly(self, mock_request): 73 | webhook_url = "https://example.com" 74 | payload = "something" 75 | mock_request.post.return_value = Mock(status_code=200) 76 | index.invoke_webhook(webhook_url, payload) 77 | mock_request.post.assert_called_with( 78 | webhook_url, data=payload, 79 | headers={'Content-Type': 'application/json'} 80 | ) 81 | 82 | @patch("lambdas.index.requests") 83 | def test_it_throws_on_non_200_response(self, mock_request): 84 | webhook_url = "https://example.com" 85 | payload = "something" 86 | mock_request.post.return_value = Mock(status_code=401) 87 | self.assertRaises(ValueError, index.invoke_webhook, webhook_url, payload) 88 | 89 | 90 | def get_build_event(): 91 | return { 92 | "version": "0", 93 | "id": "c030038d-8c4d-6141-9545-00ff7b7153EX", 94 | "detail-type": "CodeBuild Build State Change", 95 | "source": "aws.codebuild", 96 | "account": "account-id", 97 | "time": "2017-09-01T16:14:28Z", 98 | "region": "us-west-2", 99 | "resources": [ 100 | "arn:aws:codebuild:us-west-2:account-id:build/my-sample-project:8745a7a9-c340-456a-9166-edf953571bEX" 101 | ], 102 | "detail": { 103 | "build-status": "SUCCEEDED", 104 | "project-name": "my-sample-project", 105 | "build-id": "arn:aws:codebuild:us-west-2:account-id:build/my-sample-project:8745a7a9-c340-456a-9166-edf953571bEX", 106 | "additional-information": { 107 | "artifact": { 108 | "md5sum": "da9c44c8a9a3cd4b443126e823168fEX", 109 | "sha256sum": "6ccc2ae1df9d155ba83c597051611c42d60e09c6329dcb14a312cecc0a8e39EX", 110 | "location": "arn:aws:s3:::codebuild-account-id-output-bucket/my-output-artifact.zip" 111 | }, 112 | "environment": { 113 | "image": "aws/codebuild/standard:2.0", 114 | "privileged-mode": False, 115 | "compute-type": "BUILD_GENERAL1_SMALL", 116 | "type": "LINUX_CONTAINER", 117 | "environment-variables": [] 118 | }, 119 | "timeout-in-minutes": 60, 120 | "build-complete": True, 121 | "initiator": "MyCodeBuildDemoUser", 122 | "build-start-time": "Sep 1, 2017 4:12:29 PM", 123 | "source": { 124 | "location": "codebuild-account-id-input-bucket/my-input-artifact.zip", 125 | "type": "S3" 126 | }, 127 | "logs": { 128 | "group-name": "/aws/codebuild/my-sample-project", 129 | "stream-name": "8745a7a9-c340-456a-9166-edf953571bEX", 130 | "deep-link": "https://console.aws.amazon.com/cloudwatch/home?region=us-west-2#logEvent:group=/aws/codebuild/my-sample-project;stream=8745a7a9-c340-456a-9166-edf953571bEX" 131 | }, 132 | "phases": [ 133 | { 134 | "phase-context": [], 135 | "start-time": "Sep 1, 2017 4:12:29 PM", 136 | "end-time": "Sep 1, 2017 4:12:29 PM", 137 | "duration-in-seconds": 0, 138 | "phase-type": "SUBMITTED", 139 | "phase-status": "SUCCEEDED" 140 | }, 141 | { 142 | "phase-context": [], 143 | "start-time": "Sep 1, 2017 4:12:29 PM", 144 | "end-time": "Sep 1, 2017 4:13:05 PM", 145 | "duration-in-seconds": 36, 146 | "phase-type": "PROVISIONING", 147 | "phase-status": "SUCCEEDED" 148 | }, 149 | { 150 | "phase-context": [], 151 | "start-time": "Sep 1, 2017 4:13:05 PM", 152 | "end-time": "Sep 1, 2017 4:13:10 PM", 153 | "duration-in-seconds": 4, 154 | "phase-type": "DOWNLOAD_SOURCE", 155 | "phase-status": "SUCCEEDED" 156 | }, 157 | { 158 | "phase-context": [], 159 | "start-time": "Sep 1, 2017 4:13:10 PM", 160 | "end-time": "Sep 1, 2017 4:13:10 PM", 161 | "duration-in-seconds": 0, 162 | "phase-type": "INSTALL", 163 | "phase-status": "SUCCEEDED" 164 | }, 165 | { 166 | "phase-context": [], 167 | "start-time": "Sep 1, 2017 4:13:10 PM", 168 | "end-time": "Sep 1, 2017 4:13:10 PM", 169 | "duration-in-seconds": 0, 170 | "phase-type": "PRE_BUILD", 171 | "phase-status": "SUCCEEDED" 172 | }, 173 | { 174 | "phase-context": [], 175 | "start-time": "Sep 1, 2017 4:13:10 PM", 176 | "end-time": "Sep 1, 2017 4:14:21 PM", 177 | "duration-in-seconds": 70, 178 | "phase-type": "BUILD", 179 | "phase-status": "SUCCEEDED" 180 | }, 181 | { 182 | "phase-context": [], 183 | "start-time": "Sep 1, 2017 4:14:21 PM", 184 | "end-time": "Sep 1, 2017 4:14:21 PM", 185 | "duration-in-seconds": 0, 186 | "phase-type": "POST_BUILD", 187 | "phase-status": "SUCCEEDED" 188 | }, 189 | { 190 | "phase-context": [], 191 | "start-time": "Sep 1, 2017 4:14:21 PM", 192 | "end-time": "Sep 1, 2017 4:14:21 PM", 193 | "duration-in-seconds": 0, 194 | "phase-type": "UPLOAD_ARTIFACTS", 195 | "phase-status": "SUCCEEDED" 196 | }, 197 | { 198 | "phase-context": [], 199 | "start-time": "Sep 1, 2017 4:14:21 PM", 200 | "end-time": "Sep 1, 2017 4:14:26 PM", 201 | "duration-in-seconds": 4, 202 | "phase-type": "FINALIZING", 203 | "phase-status": "SUCCEEDED" 204 | }, 205 | { 206 | "start-time": "Sep 1, 2017 4:14:26 PM", 207 | "phase-type": "COMPLETED" 208 | } 209 | ] 210 | }, 211 | "current-phase": "COMPLETED", 212 | "current-phase-context": "[]", 213 | "version": "1" 214 | } 215 | } 216 | --------------------------------------------------------------------------------