├── docker-compose.yml ├── plugin.yml ├── lib └── shared.bash ├── .github └── workflows │ └── tests.yml ├── LICENSE ├── README.md ├── hooks └── post-command └── tests └── post-command.bats /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | tests: 4 | image: "buildkite/plugin-tester" 5 | volumes: 6 | - ".:/plugin:ro" 7 | lint: 8 | image: "buildkite/plugin-linter" 9 | command: ["--id", "envato/aws-cloudfront-invalidation"] 10 | volumes: 11 | - ".:/plugin:ro" 12 | -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: AWS Cloudfront Invalidation 2 | description: A Buildkite plugin that invalidates AWS Cloudfront caches 3 | author: https://github.com/envato 4 | requirements: 5 | - aws 6 | - bash 7 | configuration: 8 | properties: 9 | distribution-id: 10 | type: string 11 | paths: 12 | type: [ string, array ] 13 | minimum: 1 14 | required: 15 | - distribution-id 16 | - paths 17 | -------------------------------------------------------------------------------- /lib/shared.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Shorthand for reading env config 4 | function plugin_read_config() { 5 | local var="BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_${1}" 6 | local default="${2:-}" 7 | echo "${!var:-$default}" 8 | } 9 | 10 | # Reads either a value or a list from plugin config 11 | function plugin_read_list() { 12 | prefix_read_list "BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_$1" 13 | } 14 | 15 | # Reads either a value or a list from the given env prefix 16 | function prefix_read_list() { 17 | local prefix="$1" 18 | local parameter="${prefix}_0" 19 | 20 | if [[ -n "${!parameter:-}" ]]; then 21 | local i=0 22 | local parameter="${prefix}_${i}" 23 | while [[ -n "${!parameter:-}" ]]; do 24 | echo "${!parameter}" 25 | i=$((i+1)) 26 | parameter="${prefix}_${i}" 27 | done 28 | elif [[ -n "${!prefix:-}" ]]; then 29 | echo "${!prefix}" 30 | fi 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tests 3 | on: [push, pull_request] 4 | jobs: 5 | plugin-tests: 6 | name: Tests 7 | runs-on: ubuntu-latest 8 | container: 9 | image: buildkite/plugin-tester:latest 10 | volumes: 11 | - "${{github.workspace}}:/plugin" 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: tests 15 | run: bats tests/ 16 | plugin-lint: 17 | name: Lint 18 | runs-on: ubuntu-latest 19 | container: 20 | image: buildkite/plugin-linter:latest 21 | volumes: 22 | - "${{github.workspace}}:/plugin" 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: lint 26 | run: lint --id envato/aws-cloudfront-invalidation 27 | plugin-shellcheck: 28 | name: Shellcheck 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Run ShellCheck 33 | uses: ludeeus/action-shellcheck@1.1.0 34 | with: 35 | check_together: 'yes' 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Envato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Cloudfront Invalidation Buildkite Plugin 2 | 3 | [![tests](https://github.com/envato/aws-cloudfront-invalidation-buildkite-plugin/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/envato/aws-cloudfront-invalidation-buildkite-plugin/actions/workflows/tests.yml) 4 | [![MIT License](https://img.shields.io/badge/License-MIT-brightgreen.svg)](LICENSE) 5 | 6 | A [Buildkite plugin] that invalidates AWS Cloudfront caches. 7 | 8 | ## Example 9 | 10 | ```yml 11 | steps: 12 | - plugins: 13 | - envato/aws-cloudfront-invalidation#v0.1.0: 14 | distribution-id: 15 | paths: 16 | - 17 | debug: true 18 | ``` 19 | 20 | ## Configuration 21 | 22 | ### `distribution-id` 23 | 24 | The id of the Cloudfront distribution to create an invalidation for. 25 | 26 | ### `paths` 27 | 28 | One or more [invalidation paths]. 29 | 30 | ### `debug` 31 | 32 | Adds the `--debug` flag to all AWS CLI commands, providing detailed output for troubleshooting. 33 | 34 | ## Development 35 | 36 | To run the tests: 37 | 38 | ```sh 39 | docker-compose run --rm tests 40 | ``` 41 | 42 | To run the [Buildkite Plugin Linter]: 43 | 44 | ```sh 45 | docker-compose run --rm lint 46 | ``` 47 | 48 | [Buildkite plugin]: https://buildkite.com/docs/agent/v3/plugins 49 | [Buildkite Plugin Linter]: https://github.com/buildkite-plugins/buildkite-plugin-linter 50 | [invalidation paths]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html#invalidation-specifying-objects-paths 51 | -------------------------------------------------------------------------------- /hooks/post-command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)" 5 | # shellcheck source=lib/shared.bash 6 | . "$DIR/../lib/shared.bash" 7 | 8 | if [[ ${BUILDKITE_COMMAND_EXIT_STATUS:-0} != '0' ]]; then 9 | echo 'Skipping Cloudfront invalidation because the command failed' 10 | exit 0 11 | fi 12 | 13 | distribution_id="$(plugin_read_config DISTRIBUTION_ID)" 14 | debug="$(plugin_read_config DEBUG false)" 15 | 16 | # Add the --debug flag to the AWS CLI command if debug is enabled 17 | aws_debug_flag="" 18 | if [[ "$debug" == "true" ]]; then 19 | aws_debug_flag="--debug" 20 | fi 21 | 22 | # This tries to create an invalidation with an empty path list 23 | # 24 | # - If we have permission to call create-invalidation for this distribution, 25 | # we'll receive an InvalidArgument error, and we can proceed with a real invalidation 26 | # - Otherwise, either an AccessDenied or NoSuchDistribution error are the most likely results, 27 | # in which case we exit with error 28 | if ! aws cloudfront create-invalidation --distribution-id "$distribution_id" --invalidation-batch 'Paths={Quantity=0,Items=[]},CallerReference=cloudfront-invalidation-buildkite-plugin' $aws_debug_flag 2>&1 | grep InvalidArgument > /dev/null; then 29 | echo "Unable to invalidate cloudfront cache: create-invalidation not possible for distribution ($distribution_id) with current credentials" 30 | exit 1 31 | fi 32 | 33 | paths=() 34 | while read -r line ; do 35 | [[ -n "$line" ]] && paths+=("$line") 36 | done <<< "$(plugin_read_list PATHS)" 37 | 38 | echo "~~~ :cloudfront: Creating Cloudfront invalidation for distribution $distribution_id on paths" "${paths[@]}" 39 | SLEEP_PERIOD=15 40 | PERIOD_LIMIT=480 41 | until aws cloudfront create-invalidation --distribution-id "$distribution_id" --paths "${paths[@]}" --query Invalidation.Id --output text $aws_debug_flag 42 | do 43 | if [ $SLEEP_PERIOD == $PERIOD_LIMIT ]; then 44 | echo "Maximum retries reached - giving up..." 45 | exit 1 46 | fi 47 | echo "Invalidation failed - retrying in ${SLEEP_PERIOD}" 48 | sleep ${SLEEP_PERIOD} 49 | SLEEP_PERIOD=$((SLEEP_PERIOD+SLEEP_PERIOD)) 50 | done 51 | -------------------------------------------------------------------------------- /tests/post-command.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | bats_load_library load.bash 4 | 5 | # Uncomment the following to get more detail on failures of stubs 6 | # export AWS_STUB_DEBUG=/dev/tty 7 | 8 | @test "Invalidates given distribution and 1 path" { 9 | export BUILDKITE_COMMAND_EXIT_STATUS=0 10 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_DISTRIBUTION_ID=test_id 11 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_PATHS=/something/* 12 | 13 | stub aws \ 14 | "cloudfront create-invalidation --distribution-id test_id --invalidation-batch 'Paths={Quantity=0,Items=[]},CallerReference=cloudfront-invalidation-buildkite-plugin' : echo InvalidArgument" \ 15 | "cloudfront create-invalidation --distribution-id test_id --paths /something/* --query Invalidation.Id --output text : echo cloudfront invalidated" 16 | 17 | run $PWD/hooks/post-command 18 | 19 | assert_success 20 | assert_output --partial "cloudfront invalidated" 21 | unstub aws 22 | } 23 | 24 | @test "Invalidates given distribution and 1 path in array" { 25 | export BUILDKITE_COMMAND_EXIT_STATUS=0 26 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_DISTRIBUTION_ID=test_id 27 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_PATHS_0=/something/* 28 | 29 | stub aws \ 30 | "cloudfront create-invalidation --distribution-id test_id --invalidation-batch 'Paths={Quantity=0,Items=[]},CallerReference=cloudfront-invalidation-buildkite-plugin' : echo InvalidArgument" \ 31 | "cloudfront create-invalidation --distribution-id test_id --paths /something/* --query Invalidation.Id --output text : echo cloudfront invalidated" 32 | 33 | run $PWD/hooks/post-command 34 | 35 | assert_success 36 | assert_output --partial "cloudfront invalidated" 37 | unstub aws 38 | } 39 | 40 | @test "Invalidates given distribution and 2 paths in array" { 41 | export BUILDKITE_COMMAND_EXIT_STATUS=0 42 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_DISTRIBUTION_ID=test_id 43 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_PATHS_0=/something/* 44 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_PATHS_1=/something-else/* 45 | 46 | stub aws \ 47 | "cloudfront create-invalidation --distribution-id test_id --invalidation-batch 'Paths={Quantity=0,Items=[]},CallerReference=cloudfront-invalidation-buildkite-plugin' : echo InvalidArgument" \ 48 | "cloudfront create-invalidation --distribution-id test_id --paths /something/* /something-else/* --query Invalidation.Id --output text : echo cloudfront invalidated" 49 | 50 | run $PWD/hooks/post-command 51 | 52 | assert_success 53 | assert_output --partial "cloudfront invalidated" 54 | unstub aws 55 | } 56 | 57 | @test "Doesn't attempt to invalidate if the step command fails" { 58 | export BUILDKITE_COMMAND_EXIT_STATUS=1 59 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_DISTRIBUTION_ID=test_id 60 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_PATHS=/something/* 61 | 62 | run $PWD/hooks/post-command 63 | 64 | assert_success 65 | } 66 | 67 | @test "Retries after unsuccessfully submitting a validation request" { 68 | export BUILDKITE_COMMAND_EXIT_STATUS=0 69 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_DISTRIBUTION_ID=test_id 70 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_PATHS_0=/something/* 71 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_PATHS_1=/something-else/* 72 | 73 | stub aws \ 74 | "cloudfront create-invalidation --distribution-id test_id --invalidation-batch 'Paths={Quantity=0,Items=[]},CallerReference=cloudfront-invalidation-buildkite-plugin' : echo InvalidArgument" \ 75 | "cloudfront create-invalidation --distribution-id test_id --paths /something/* /something-else/* --query Invalidation.Id --output text : return 1" \ 76 | "cloudfront create-invalidation --distribution-id test_id --paths /something/* /something-else/* --query Invalidation.Id --output text : echo cloudfront invalidated" 77 | stub sleep "15 : echo sleeping" 78 | 79 | run $PWD/hooks/post-command 80 | 81 | assert_success 82 | assert_output --partial "sleeping" 83 | assert_output --partial "cloudfront invalidated" 84 | unstub aws 85 | unstub sleep 86 | } 87 | 88 | @test "Stops executing if a credential problem is detected" { 89 | export BUILDKITE_COMMAND_EXIT_STATUS=0 90 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_DISTRIBUTION_ID=test_id 91 | 92 | stub aws "cloudfront create-invalidation --distribution-id test_id --invalidation-batch 'Paths={Quantity=0,Items=[]},CallerReference=cloudfront-invalidation-buildkite-plugin' : echo AccessDenied" \ 93 | 94 | run $PWD/hooks/post-command 95 | 96 | assert_failure 97 | unstub aws 98 | } 99 | 100 | @test "AWS CLI includes --debug flag when DEBUG=true" { 101 | export BUILDKITE_COMMAND_EXIT_STATUS=0 102 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_DEBUG=true 103 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_DISTRIBUTION_ID=test_id 104 | export BUILDKITE_PLUGIN_AWS_CLOUDFRONT_INVALIDATION_PATHS_0=/something/* 105 | 106 | stub aws \ 107 | "cloudfront create-invalidation --distribution-id test_id --invalidation-batch 'Paths={Quantity=0,Items=[]},CallerReference=cloudfront-invalidation-buildkite-plugin' --debug : echo 'InvalidArgument'" \ 108 | "cloudfront create-invalidation --distribution-id test_id --paths /something/* --query Invalidation.Id --output text --debug : echo 'cloudfront invalidated with --debug'" 109 | 110 | run $PWD/hooks/post-command 111 | 112 | assert_success 113 | assert_output --partial "--debug" 114 | unstub aws 115 | } 116 | --------------------------------------------------------------------------------