├── .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 | 
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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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 |
--------------------------------------------------------------------------------