├── lib ├── arrays.sh ├── git.sh └── config.sh ├── docker-compose.yml ├── update └── update.sh ├── unwrappr └── annotate.sh ├── plugin.yml ├── .github └── workflows │ └── tests.yml ├── tests ├── command.bats ├── annotate.bats └── update.bats ├── LICENSE ├── hooks └── command ├── commands ├── annotate.sh └── update.sh └── README.md /lib/arrays.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | function in_array() { 5 | local e 6 | for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done 7 | return 1 8 | } 9 | -------------------------------------------------------------------------------- /lib/git.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | function repo_from_origin() { 5 | local origin 6 | local without_prefix 7 | origin="$(git remote get-url origin)" 8 | without_prefix="${origin#*:}" 9 | echo "${without_prefix%.git}" 10 | } 11 | -------------------------------------------------------------------------------- /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/bundle-update"] 10 | volumes: 11 | - ".:/plugin:ro" 12 | -------------------------------------------------------------------------------- /update/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | cd /bundle_update 5 | 6 | if [ -n "$PRE_BUNDLE_UPDATE" ]; then 7 | echo "--- :shell: Running pre bundle update" 8 | eval "$PRE_BUNDLE_UPDATE" 9 | fi 10 | 11 | echo "+++ :bundler: Running bundle update" 12 | bundle update --jobs="$(nproc)" 13 | 14 | if [ -n "$POST_BUNDLE_UPDATE" ]; then 15 | echo "--- :shell: Running post bundle update" 16 | eval "$POST_BUNDLE_UPDATE" 17 | fi 18 | -------------------------------------------------------------------------------- /unwrappr/annotate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | echo "--- :bundler: Installing Unwrappr" 5 | cat <<\GEMS > Gemfile 6 | source 'https://rubygems.org/' 7 | gem 'unwrappr' 8 | GEMS 9 | bundle install --jobs="$(nproc)" 10 | 11 | echo "+++ :github: Annotating Github pull request" 12 | repository=$1 13 | pull_request=$2 14 | gemfile_lock_files=("${@:3}") 15 | 16 | if [[ ${#gemfile_lock_files[@]} -eq 0 ]]; then 17 | gemfile_lock_files+=("--lock-file" "Gemfile.lock") 18 | fi 19 | 20 | echo "Annotating https://github.com/${repository}/pull/${pull_request}" 21 | echo "Files: " "${gemfile_lock_files[@]}" 22 | echo 23 | 24 | bundle exec unwrappr annotate-pull-request \ 25 | --repo "${repository}" \ 26 | --pr "${pull_request}" \ 27 | "${gemfile_lock_files[@]}" 28 | -------------------------------------------------------------------------------- /lib/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | function plugin_read_config() { 5 | local var="BUILDKITE_PLUGIN_BUNDLE_UPDATE_${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_BUNDLE_UPDATE_$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 | -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: Bundle Update 2 | description: A Buildkite plugin that runs bundle update 3 | author: https://github.com/envato 4 | requirements: 5 | - docker 6 | configuration: 7 | properties: 8 | env: 9 | type: [ string, array ] 10 | minimum: 1 11 | gemfile-lock-files: 12 | type: [ string, array ] 13 | minimum: 1 14 | image: 15 | type: string 16 | post-bundle-update: 17 | type: string 18 | pre-bundle-update: 19 | type: string 20 | pull-request: 21 | type: integer 22 | minimum: 1 23 | pull-request-metadata-key: 24 | type: string 25 | repository: 26 | type: string 27 | update: 28 | type: boolean 29 | oneOf: 30 | - required: 31 | - annotate 32 | - required: 33 | - update 34 | dependencies: 35 | post-bundle-update: [ update ] 36 | pre-bundle-update: [ update ] 37 | pull-request: [ annotate ] 38 | repository: [ annotate ] 39 | -------------------------------------------------------------------------------- /.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: | 16 | cd /plugin/ 17 | bats tests/ 18 | plugin-lint: 19 | name: Lint 20 | runs-on: ubuntu-latest 21 | container: 22 | image: buildkite/plugin-linter:latest 23 | volumes: 24 | - "${{github.workspace}}:/plugin" 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: lint 28 | run: lint --id envato/bundle-update 29 | plugin-shellcheck: 30 | name: Shellcheck 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Run ShellCheck 35 | uses: ludeeus/action-shellcheck@1.1.0 36 | with: 37 | check_together: 'yes' 38 | 39 | -------------------------------------------------------------------------------- /tests/command.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load '/usr/local/lib/bats/load.bash' 4 | 5 | # Uncomment the following to get more detail on failures of stubs 6 | # export NPROC_STUB_DEBUG=/dev/tty 7 | # export DOCKER_STUB_DEBUG=/dev/tty 8 | # export GIT_STUB_DEBUG=/dev/tty 9 | # export BUILDKITE_AGENT_STUB_DEBUG=/dev/tty 10 | 11 | @test "Errors out when update and annotate parameter not provided" { 12 | stub nproc 13 | stub docker 14 | stub git 15 | stub buildkite-agent 16 | 17 | run $PWD/hooks/command 18 | 19 | assert_failure 20 | assert_output --partial "No update or annotate options were specified" 21 | } 22 | 23 | @test "Errors out when both update and annotate parameter are provided" { 24 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 25 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_ANNOTATE=true 26 | 27 | stub nproc 28 | stub docker 29 | stub git 30 | stub buildkite-agent 31 | 32 | run $PWD/hooks/command 33 | 34 | assert_failure 35 | assert_output --partial "Only one of update or annotate. More than one was used." 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /hooks/command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | PLUGIN_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)/.." 5 | 6 | # shellcheck source=lib/config.sh 7 | . "$PLUGIN_DIR/lib/config.sh" 8 | # shellcheck source=lib/arrays.sh 9 | . "$PLUGIN_DIR/lib/arrays.sh" 10 | # shellcheck source=lib/git.sh 11 | . "$PLUGIN_DIR/lib/git.sh" 12 | 13 | commands=('') 14 | 15 | [[ -n "$(plugin_read_config UPDATE)" ]] && commands+=("UPDATE") 16 | [[ -n "$(plugin_read_config ANNOTATE)" ]] && commands+=("ANNOTATE") 17 | 18 | # Check we've only got one command 19 | if [[ ${#commands[@]} -gt 2 ]] ; then 20 | echo "+++ Bundle Update plugin error" 21 | echo "Only one of update or annotate. More than one was used." 22 | exit 1 23 | fi 24 | 25 | # Dispatch to the command file 26 | if in_array "UPDATE" "${commands[@]}" ; then 27 | # shellcheck source=commands/update.sh 28 | . "$PLUGIN_DIR/commands/update.sh" 29 | elif in_array "ANNOTATE" "${commands[@]}" ; then 30 | # shellcheck source=commands/annotate.sh 31 | . "$PLUGIN_DIR/commands/annotate.sh" 32 | else 33 | echo "+++ Bundle Update plugin error" 34 | echo "No update or annotate options were specified" 35 | exit 1 36 | fi 37 | -------------------------------------------------------------------------------- /commands/annotate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | repository=$(plugin_read_config REPOSITORY "$(repo_from_origin)") 5 | pull_request=$(plugin_read_config PULL_REQUEST) 6 | if [[ -z "${pull_request}" ]]; then 7 | pull_request_metadata_key=$(plugin_read_config PULL_REQUEST_METADATA_KEY) 8 | pull_request=$(buildkite-agent meta-data get "${pull_request_metadata_key}") 9 | fi 10 | image=${BUILDKITE_PLUGIN_BUNDLE_UPDATE_IMAGE:-ruby} 11 | 12 | echo "--- :docker: Fetching the latest ${image} image" 13 | docker pull "${image}" 14 | 15 | echo "--- :docker: Launching ${image} image" 16 | args=( 17 | "--interactive" 18 | "--tty" 19 | "--rm" 20 | "--volume" "$PLUGIN_DIR/unwrappr:/unwrappr" 21 | "--workdir" "/annotate" 22 | "--env" "GITHUB_TOKEN" 23 | ) 24 | 25 | # check the list of Gemfiles to annotate, these are newline delimited 26 | gemfile_lock_files=() 27 | while IFS=$'\n' read -r gemfile_lock_file ; do 28 | [[ -n "${gemfile_lock_file:-}" ]] && gemfile_lock_files+=("--lock-file" "${gemfile_lock_file}") 29 | done <<< "$(printf '%s\n' "$(plugin_read_list GEMFILE_LOCK_FILES)")" 30 | 31 | docker run "${args[@]}" "${image}" /unwrappr/annotate.sh \ 32 | "${repository}" \ 33 | "${pull_request}" \ 34 | "${gemfile_lock_files[@]-}" 35 | -------------------------------------------------------------------------------- /commands/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | image=${BUILDKITE_PLUGIN_BUNDLE_UPDATE_IMAGE:-ruby:slim} 5 | pre_bundle_update=${BUILDKITE_PLUGIN_BUNDLE_UPDATE_PRE_BUNDLE_UPDATE:-""} 6 | post_bundle_update=${BUILDKITE_PLUGIN_BUNDLE_UPDATE_POST_BUNDLE_UPDATE:-""} 7 | 8 | echo "~~~ :docker: Fetching the latest ${image} image" 9 | docker pull "${image}" 10 | 11 | echo "~~~ :docker: Starting up ${image} container" 12 | args=( 13 | "--interactive" 14 | "--tty" 15 | "--rm" 16 | "--volume" "$PWD:/bundle_update" 17 | "--volume" "$PLUGIN_DIR/update:/update" 18 | "--workdir" "/bundle_update" 19 | "--env" "BUNDLE_APP_CONFIG=/tmp/bundle_app_config" 20 | "--env" "PRE_BUNDLE_UPDATE=${pre_bundle_update}" 21 | "--env" "POST_BUNDLE_UPDATE=${post_bundle_update}" 22 | ) 23 | while IFS='=' read -r name _ ; do 24 | if [[ $name =~ ^BUNDLE_ ]] ; then 25 | args+=( "--env" "${name}" ) 26 | fi 27 | done < <(env | sort) 28 | 29 | # append env vars provided in ENV, these are newline delimited 30 | while IFS=$'\n' read -r env ; do 31 | [[ -n "${env:-}" ]] && args+=("--env" "${env}") 32 | done <<< "$(printf '%s\n' "$(plugin_read_list ENV)")" 33 | 34 | docker run "${args[@]}" "${image}" /update/update.sh 35 | 36 | # check the list of Gemfiles for changes, these are newline delimited 37 | gemfile_lock_files=() 38 | while IFS=$'\n' read -r gemfile_lock_file ; do 39 | [[ -n "${gemfile_lock_file:-}" ]] && gemfile_lock_files+=("${gemfile_lock_file}") 40 | done <<< "$(printf '%s\n' "$(plugin_read_list GEMFILE_LOCK_FILES)")" 41 | 42 | if git diff-index --quiet HEAD -- "${gemfile_lock_files[@]-Gemfile.lock}"; then 43 | buildkite-agent annotate ":bundler: No gem updates found." --style info 44 | else 45 | buildkite-agent meta-data set bundle-update-plugin-changes true 46 | fi 47 | -------------------------------------------------------------------------------- /tests/annotate.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load '/usr/local/lib/bats/load.bash' 4 | 5 | # Uncomment the following to get more detail on failures of stubs 6 | # export DOCKER_STUB_DEBUG=/dev/tty 7 | # export GIT_STUB_DEBUG=/dev/tty 8 | # export BUILDKITE_AGENT_STUB_DEBUG=/dev/tty 9 | 10 | @test "Annotate runs unwrappr via Docker" { 11 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_ANNOTATE=true 12 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_REPOSITORY=envato/ruby-service 13 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_PULL_REQUEST=42 14 | 15 | stub docker \ 16 | "pull ruby : echo pulled image" \ 17 | "run --interactive --tty --rm --volume /plugin/hooks/../unwrappr:/unwrappr --workdir /annotate --env GITHUB_TOKEN ruby /unwrappr/annotate.sh envato/ruby-service 42 : echo pull request annotated" 18 | stub git 'remote get-url origin : echo "git@github.com:owner/project"' 19 | 20 | run $PWD/hooks/command 21 | 22 | assert_success 23 | assert_output --partial "pulled image" 24 | assert_output --partial "pull request annotated" 25 | unstub docker 26 | unstub git 27 | } 28 | 29 | @test "Annotate uses the current repository if not provided" { 30 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_ANNOTATE=true 31 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_PULL_REQUEST=42 32 | 33 | stub docker \ 34 | "pull ruby : echo pulled image" \ 35 | "run --interactive --tty --rm --volume /plugin/hooks/../unwrappr:/unwrappr --workdir /annotate --env GITHUB_TOKEN ruby /unwrappr/annotate.sh owner/project 42 : echo pull request annotated" 36 | stub git 'remote get-url origin : echo "git@github.com:owner/project"' 37 | 38 | run $PWD/hooks/command 39 | 40 | assert_success 41 | assert_output --partial "pulled image" 42 | assert_output --partial "pull request annotated" 43 | unstub docker 44 | unstub git 45 | } 46 | 47 | @test "Annotate obtains the pull request number from build metadata" { 48 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_ANNOTATE=true 49 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_REPOSITORY=envato/ruby-service 50 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_PULL_REQUEST_METADATA_KEY=pull-request 51 | 52 | stub docker \ 53 | "pull ruby : echo pulled image" \ 54 | "run --interactive --tty --rm --volume /plugin/hooks/../unwrappr:/unwrappr --workdir /annotate --env GITHUB_TOKEN ruby /unwrappr/annotate.sh envato/ruby-service 232 : echo pull request annotated" 55 | stub git 'remote get-url origin : echo "git@github.com:owner/project"' 56 | stub buildkite-agent "meta-data get pull-request : echo 232" 57 | 58 | run $PWD/hooks/command 59 | 60 | assert_success 61 | assert_output --partial "pulled image" 62 | assert_output --partial "pull request annotated" 63 | unstub docker 64 | unstub git 65 | unstub buildkite-agent 66 | } 67 | 68 | @test "Supports the image option" { 69 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_ANNOTATE=true 70 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_REPOSITORY=envato/ruby-service 71 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_PULL_REQUEST=42 72 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_IMAGE=my-image 73 | 74 | stub docker \ 75 | "pull my-image : echo pulled my-image" \ 76 | "run --interactive --tty --rm --volume /plugin/hooks/../unwrappr:/unwrappr --workdir /annotate --env GITHUB_TOKEN my-image /unwrappr/annotate.sh envato/ruby-service 42 : echo pull request annotated" 77 | stub git 'remote get-url origin : echo "git@github.com:owner/project"' 78 | 79 | run $PWD/hooks/command 80 | 81 | assert_success 82 | assert_output --partial "pulled my-image" 83 | assert_output --partial "pull request annotated" 84 | unstub docker 85 | unstub git 86 | } 87 | 88 | @test "Supports the gemfile-lock-files option" { 89 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_ANNOTATE=true 90 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_REPOSITORY=envato/ruby-service 91 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_PULL_REQUEST=42 92 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_GEMFILE_LOCK_FILES=Gemfile_v2.lock 93 | 94 | stub docker \ 95 | "pull ruby : echo pulled image" \ 96 | "run --interactive --tty --rm --volume /plugin/hooks/../unwrappr:/unwrappr --workdir /annotate --env GITHUB_TOKEN ruby /unwrappr/annotate.sh envato/ruby-service 42 --lock-file Gemfile_v2.lock : echo pull request annotated" 97 | stub git 'remote get-url origin : echo "git@github.com:owner/project"' 98 | 99 | run $PWD/hooks/command 100 | 101 | assert_success 102 | assert_output --partial "pulled image" 103 | assert_output --partial "pull request annotated" 104 | unstub docker 105 | unstub git 106 | } 107 | 108 | @test "Supports multiple gemfile lock files" { 109 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_ANNOTATE=true 110 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_REPOSITORY=envato/ruby-service 111 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_PULL_REQUEST=42 112 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_GEMFILE_LOCK_FILES_0=Gemfile.lock 113 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_GEMFILE_LOCK_FILES_1=Gemfile_v2.lock 114 | 115 | stub docker \ 116 | "pull ruby : echo pulled image" \ 117 | "run --interactive --tty --rm --volume /plugin/hooks/../unwrappr:/unwrappr --workdir /annotate --env GITHUB_TOKEN ruby /unwrappr/annotate.sh envato/ruby-service 42 --lock-file Gemfile.lock --lock-file Gemfile_v2.lock : echo pull request annotated" 118 | stub git 'remote get-url origin : echo "git@github.com:owner/project"' 119 | 120 | run $PWD/hooks/command 121 | 122 | assert_success 123 | assert_output --partial "pulled image" 124 | assert_output --partial "pull request annotated" 125 | unstub docker 126 | unstub git 127 | } 128 | -------------------------------------------------------------------------------- /tests/update.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load '/usr/local/lib/bats/load.bash' 4 | 5 | # Uncomment the following to get more detail on failures of stubs 6 | # export DOCKER_STUB_DEBUG=/dev/tty 7 | # export GIT_STUB_DEBUG=/dev/tty 8 | # export BUILDKITE_AGENT_STUB_DEBUG=/dev/tty 9 | 10 | @test "Runs the bundle update via Docker" { 11 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 12 | 13 | stub docker \ 14 | "pull ruby:slim : echo pulled image" \ 15 | "run --interactive --tty --rm --volume /plugin:/bundle_update --volume /plugin/hooks/../update:/update --workdir /bundle_update --env BUNDLE_APP_CONFIG=/tmp/bundle_app_config --env PRE_BUNDLE_UPDATE= --env POST_BUNDLE_UPDATE= ruby:slim /update/update.sh : echo bundle updated" 16 | stub git "diff-index --quiet HEAD -- Gemfile.lock : exit 1" 17 | stub buildkite-agent "meta-data set bundle-update-plugin-changes true : echo meta-data set" 18 | 19 | run $PWD/hooks/command 20 | 21 | assert_success 22 | assert_output --partial "pulled image" 23 | assert_output --partial "bundle updated" 24 | unstub docker 25 | unstub git 26 | unstub buildkite-agent 27 | } 28 | 29 | @test "Sets buildkite metadata when changes are found" { 30 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 31 | 32 | stub docker \ 33 | "pull ruby:slim : echo pulled image" \ 34 | "run --interactive --tty --rm --volume /plugin:/bundle_update --volume /plugin/hooks/../update:/update --workdir /bundle_update --env BUNDLE_APP_CONFIG=/tmp/bundle_app_config --env PRE_BUNDLE_UPDATE= --env POST_BUNDLE_UPDATE= ruby:slim /update/update.sh : echo bundle updated" 35 | stub git "diff-index --quiet HEAD -- Gemfile.lock : exit 1" 36 | stub buildkite-agent "meta-data set bundle-update-plugin-changes true : echo meta-data set" 37 | 38 | run $PWD/hooks/command 39 | 40 | assert_success 41 | assert_output --partial "meta-data set" 42 | unstub docker 43 | unstub git 44 | unstub buildkite-agent 45 | } 46 | 47 | @test "Supports the gemfile-lock-files option" { 48 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 49 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_GEMFILE_LOCK_FILES=Gemfile_v2.lock 50 | 51 | stub docker \ 52 | "pull ruby:slim : echo pulled image" \ 53 | "run --interactive --tty --rm --volume /plugin:/bundle_update --volume /plugin/hooks/../update:/update --workdir /bundle_update --env BUNDLE_APP_CONFIG=/tmp/bundle_app_config --env PRE_BUNDLE_UPDATE= --env POST_BUNDLE_UPDATE= ruby:slim /update/update.sh : echo bundle updated" 54 | stub git "diff-index --quiet HEAD -- Gemfile_v2.lock : exit 1" 55 | stub buildkite-agent "meta-data set bundle-update-plugin-changes true : echo meta-data set" 56 | 57 | run $PWD/hooks/command 58 | 59 | assert_success 60 | assert_output --partial "meta-data set" 61 | unstub docker 62 | unstub git 63 | unstub buildkite-agent 64 | } 65 | 66 | @test "Adds buildkite annotation, but no metadata, when no changes are found" { 67 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 68 | 69 | stub docker \ 70 | "pull ruby:slim : echo pulled image" \ 71 | "run --interactive --tty --rm --volume /plugin:/bundle_update --volume /plugin/hooks/../update:/update --workdir /bundle_update --env BUNDLE_APP_CONFIG=/tmp/bundle_app_config --env PRE_BUNDLE_UPDATE= --env POST_BUNDLE_UPDATE= ruby:slim /update/update.sh : echo bundle updated" 72 | stub git "diff-index --quiet HEAD -- Gemfile.lock : exit 0" 73 | stub buildkite-agent "annotate ':bundler: No gem updates found.' --style info : echo buildkite-annotation-added" 74 | 75 | run $PWD/hooks/command 76 | 77 | assert_success 78 | assert_output --partial "buildkite-annotation-added" 79 | unstub docker 80 | unstub git 81 | unstub buildkite-agent 82 | } 83 | 84 | @test "Supports the image option" { 85 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 86 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_IMAGE=my-image 87 | 88 | stub docker \ 89 | "pull my-image : echo pulled image" \ 90 | "run --interactive --tty --rm --volume /plugin:/bundle_update --volume /plugin/hooks/../update:/update --workdir /bundle_update --env BUNDLE_APP_CONFIG=/tmp/bundle_app_config --env PRE_BUNDLE_UPDATE= --env POST_BUNDLE_UPDATE= my-image /update/update.sh : echo bundle updated" 91 | stub git "diff-index --quiet HEAD -- Gemfile.lock : exit 1" 92 | stub buildkite-agent "meta-data set bundle-update-plugin-changes true : echo meta-data set" 93 | 94 | run $PWD/hooks/command 95 | 96 | assert_success 97 | assert_output --partial "pulled image" 98 | assert_output --partial "bundle updated" 99 | unstub docker 100 | unstub git 101 | unstub buildkite-agent 102 | } 103 | 104 | @test "Passes the pre-bundle-update command" { 105 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 106 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_PRE_BUNDLE_UPDATE=my-pre-bundle-update 107 | 108 | stub docker \ 109 | "pull ruby:slim : echo pulled image" \ 110 | "run --interactive --tty --rm --volume /plugin:/bundle_update --volume /plugin/hooks/../update:/update --workdir /bundle_update --env BUNDLE_APP_CONFIG=/tmp/bundle_app_config --env PRE_BUNDLE_UPDATE=my-pre-bundle-update --env POST_BUNDLE_UPDATE= ruby:slim /update/update.sh : echo bundle updated" 111 | stub git "diff-index --quiet HEAD -- Gemfile.lock : exit 1" 112 | stub buildkite-agent "meta-data set bundle-update-plugin-changes true : echo meta-data set" 113 | 114 | run $PWD/hooks/command 115 | 116 | assert_success 117 | assert_output --partial "pulled image" 118 | assert_output --partial "bundle updated" 119 | unstub docker 120 | unstub git 121 | unstub buildkite-agent 122 | } 123 | 124 | @test "Passes the post-bundle-update command" { 125 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 126 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_POST_BUNDLE_UPDATE=my-post-bundle-update 127 | 128 | stub docker \ 129 | "pull ruby:slim : echo pulled image" \ 130 | "run --interactive --tty --rm --volume /plugin:/bundle_update --volume /plugin/hooks/../update:/update --workdir /bundle_update --env BUNDLE_APP_CONFIG=/tmp/bundle_app_config --env PRE_BUNDLE_UPDATE= --env POST_BUNDLE_UPDATE=my-post-bundle-update ruby:slim /update/update.sh : echo bundle updated" 131 | stub git "diff-index --quiet HEAD -- Gemfile.lock : exit 1" 132 | stub buildkite-agent "meta-data set bundle-update-plugin-changes true : echo meta-data set" 133 | 134 | run $PWD/hooks/command 135 | 136 | assert_success 137 | assert_output --partial "pulled image" 138 | assert_output --partial "bundle updated" 139 | unstub docker 140 | unstub git 141 | unstub buildkite-agent 142 | } 143 | 144 | @test "Passes BUNDLE* environment variables" { 145 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 146 | export BUNDLE_RUBYGEMS__EXAMPLE__COM=secret1 147 | export BUNDLE_RUBYGEMS__EXAMPLE__NET=secret2 148 | export NOT_AS_BUNDLE_VAR=secret3 149 | 150 | stub docker \ 151 | "pull ruby:slim : echo pulled image" \ 152 | "run --interactive --tty --rm --volume /plugin:/bundle_update --volume /plugin/hooks/../update:/update --workdir /bundle_update --env BUNDLE_APP_CONFIG=/tmp/bundle_app_config --env PRE_BUNDLE_UPDATE= --env POST_BUNDLE_UPDATE= --env BUNDLE_RUBYGEMS__EXAMPLE__COM --env BUNDLE_RUBYGEMS__EXAMPLE__NET ruby:slim /update/update.sh : echo bundle updated" 153 | stub git "diff-index --quiet HEAD -- Gemfile.lock : exit 1" 154 | stub buildkite-agent "meta-data set bundle-update-plugin-changes true : echo meta-data set" 155 | 156 | run $PWD/hooks/command 157 | 158 | assert_success 159 | assert_output --partial "pulled image" 160 | assert_output --partial "bundle updated" 161 | unstub docker 162 | unstub git 163 | unstub buildkite-agent 164 | } 165 | 166 | @test "Passes environment variables" { 167 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_UPDATE=true 168 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_ENV_0="ZERO=0" 169 | export BUILDKITE_PLUGIN_BUNDLE_UPDATE_ENV_1="ONE=1" 170 | 171 | stub docker \ 172 | "pull ruby:slim : echo pulled image" \ 173 | "run --interactive --tty --rm --volume /plugin:/bundle_update --volume /plugin/hooks/../update:/update --workdir /bundle_update --env BUNDLE_APP_CONFIG=/tmp/bundle_app_config --env PRE_BUNDLE_UPDATE= --env POST_BUNDLE_UPDATE= --env ZERO=0 --env ONE=1 ruby:slim /update/update.sh : echo bundle updated" 174 | stub git "diff-index --quiet HEAD -- Gemfile.lock : exit 1" 175 | stub buildkite-agent "meta-data set bundle-update-plugin-changes true : echo meta-data set" 176 | 177 | run $PWD/hooks/command 178 | 179 | assert_success 180 | assert_output --partial "pulled image" 181 | assert_output --partial "bundle updated" 182 | unstub docker 183 | unstub git 184 | unstub buildkite-agent 185 | } 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bundle Update Buildkite Plugin 2 | 3 | [![tests](https://github.com/envato/bundle-update-buildkite-plugin/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/envato/bundle-update-buildkite-plugin/actions/workflows/tests.yml) 4 | [![MIT License](https://img.shields.io/badge/License-MIT-brightgreen.svg)](LICENSE) 5 | 6 | A [Buildkite plugin](https://buildkite.com/docs/agent/v3/plugins) that updates 7 | gem dependencies in your Ruby projects by running `bundle update`. 8 | 9 | ## Update 10 | 11 | This function runs `bundle update` from within a Docker container. 12 | 13 | ```yml 14 | steps: 15 | - label: ":bundler: Update" 16 | plugins: 17 | - envato/bundle-update#v0.9.1: 18 | update: true 19 | ``` 20 | 21 | By itself this function is quite useless, the resulting changes to the 22 | `Gemfile.lock` will simply sit in the Buildkite working directory. What we 23 | really want is for the changes to be committed back to the repository. For this 24 | we can make use of the [Git Commit Buildkite Plugin]. 25 | 26 | ```yml 27 | steps: 28 | - label: ":bundler: Update" 29 | plugins: 30 | - envato/bundle-update#v0.9.1: 31 | update: true 32 | - thedyrt/git-commit#v0.3.0: 33 | branch: "bundle-update/${BUILDKITE_BUILD_NUMBER}" 34 | message: "Bundle update - ${BUILDKITE_BUILD_URL}" 35 | create-branch: true 36 | user: 37 | name: "Bundle Update Bot" 38 | email: "bundle-update-bot@example.com" 39 | ``` 40 | 41 | One could then use the [Github Pull Request Buildkite Plugin] to a create pull 42 | request with these changes (if your project codebase is hosted on Github): 43 | 44 | ```yml 45 | - label: ":github: Open Pull Request" 46 | plugins: 47 | - envato/github-pull-request#v0.4.0: 48 | head: "bundle-update/${BUILDKITE_BUILD_NUMBER}" 49 | title: "Bundle update" 50 | body: "[Bundle update #${BUILDKITE_BUILD_NUMBER}](${BUILDKITE_BUILD_URL})" 51 | ``` 52 | 53 | By default the bundle update plugin will use the `ruby:slim` Docker image. But 54 | one can specify a Docker image, this way you can control which version of Ruby 55 | and Bundler will be used. If your project's gems require specific compile-time 56 | packages installed you'll need to choose an image that satisfies these 57 | constraints also. 58 | 59 | ```yml 60 | steps: 61 | - label: ":bundler: Update" 62 | plugins: 63 | - envato/bundle-update#v0.9.1: 64 | update: true 65 | image: "ruby:2.3.7-slim" 66 | ``` 67 | 68 | If the main build produces a Docker image artifact, it may be easiest to use that 69 | to run the bundle update, as it'll have all the compile-time dependencies 70 | installed. Here's an example obtaining the image from Amazon ECR: 71 | 72 | ```yml 73 | steps: 74 | - label: ":bundler: Update" 75 | plugins: 76 | - ecr#v1.1.4: 77 | login: true 78 | account_ids: 100000000000 79 | - envato/bundle-update#v0.9.1: 80 | update: true 81 | image: "100000000000.dkr.ecr.us-east-1.amazonaws.com/my-service:latest" 82 | ``` 83 | 84 | Bundler can be further configured by setting environment variables it 85 | understands. For instance, if you need to authenticate to access a private 86 | RubyGems server at https://rubygems.example.com, you can set your credentials in 87 | an environment variable named `BUNDLE_RUBYGEMS__EXAMPLE__COM`. (Please use a 88 | secure mechanism for setting private environment variables. For instance, the 89 | [AWS S3 Secrets Buildkite Plugin].) 90 | 91 | If bundle update produces changes to `Gemfile.lock` files, the 92 | `bundle-update-plugin-changes: true` key-value pair is added to the build 93 | metadata. This is helpful for triggering or cancelling later steps in the 94 | pipeline. 95 | 96 | ## Annotate 97 | 98 | Add comments to each gem change to a `Gemfile.lock` file in a Github pull 99 | request. These comments provide some context and are helpful to engineers when 100 | determining if the change in version is safe. 101 | 102 | This feature is implemented using the [unwrappr] library. 103 | 104 | ```yml 105 | steps: 106 | - label: ":rubygems: Annotate Gem Changes" 107 | plugins: 108 | - envato/bundle-update#v0.9.1: 109 | annotate: true 110 | pull-request: 42 111 | ``` 112 | 113 | By default the plugin uses the repository from the Buildkite pipeline 114 | configuration. However, this can be overridden by specifying the Github 115 | repository: 116 | 117 | ```yml 118 | steps: 119 | - label: ":rubygems: Annotate Gem Changes" 120 | plugins: 121 | - envato/bundle-update#v0.9.1: 122 | annotate: true 123 | pull-request: 42 124 | repository: "owner/project" 125 | ``` 126 | 127 | The pull request number can also be loaded from the build metadata. For instance, 128 | the [Github Pull Request Buildkite Plugin] saves the PR number with the key 129 | `github-pull-request-plugin-number` so it can be loaded like so: 130 | 131 | ```yml 132 | steps: 133 | - label: ":rubygems: Annotate Gem Changes" 134 | plugins: 135 | - envato/bundle-update#v0.9.1: 136 | annotate: true 137 | pull-request-metadata-key: "github-pull-request-plugin-number" 138 | ``` 139 | 140 | ## Installing Custom Dependencies 141 | 142 | When running a bundle update from within a docker container, there may or may not 143 | be the dependencies you require for the update to complete successfully. 144 | For example, compiling native extensions or access to a library from another package. 145 | 146 | In this case you have 2 options to help solve the problem. 147 | 148 | 1. Use a docker container which you have prebuilt (or sourced) with all the 149 | required dependencies. 150 | 151 | 2. You can specify a script location or shell command which will be executed prior to running the 152 | bundle update. Here you can install and configure the container as needed. 153 | 154 | ```yml 155 | steps: 156 | - label: ":bundler: Update" 157 | plugins: 158 | - envato/bundle-update#v0.9.1: 159 | update: true 160 | pre-bundle-update: .buildkite/scripts/pre-bundle-update 161 | ``` 162 | 163 | or a command 164 | 165 | ```yml 166 | steps: 167 | - label: ":bundler: Update" 168 | plugins: 169 | - envato/bundle-update#v0.9.1: 170 | update: true 171 | pre-bundle-update: "apk add --no-progress build-base" 172 | ``` 173 | 174 | ## Example Pipeline 175 | 176 | This is an example pipeline which ties everything together to produce nicely 177 | annotated bundle update pull requests. 178 | 179 | This pipeline requires two secrets: 180 | 181 | * Write access to the project GIT repository, by way of an [SSH Key][Github 182 | Deploy Key]. This write access is used for pushing up the bundle update 183 | commit. 184 | 185 | * Github API access, by populating the environment variable `GITHUB_TOKEN` with 186 | a personal access token providing `repo` access to the repository. This is 187 | used for opening the pull request and adding comments. 188 | 189 | It's recommended to use the [AWS S3 Secrets Buildkite Plugin] to provide these 190 | secrets. With this you can simply upload the `private_ssh_key` file and 191 | `environment` file (containing `GITHUB_TOKEN=`) to your S3 192 | secrets bucket. 193 | 194 | ```yml 195 | steps: 196 | 197 | - label: ":bundler: Update" 198 | plugins: 199 | - envato/bundle-update#v0.9.1: 200 | update: true 201 | image: "ruby:2.5" 202 | - thedyrt/git-commit#v0.3.0: 203 | branch: "bundle-update/${BUILDKITE_BUILD_NUMBER}" 204 | message: | 205 | Bundle update 206 | 207 | ${BUILDKITE_BUILD_URL} 208 | create-branch: true 209 | user: 210 | name: "Bundle Update Bot" 211 | email: "bundle-update-bot@example.com" 212 | - envato/stop-the-line#v0.1.0: 213 | unless: 214 | key: "bundle-update-plugin-changes" 215 | value: "true" 216 | style: "pass" 217 | 218 | - wait 219 | 220 | - label: ":github: Open Pull Request" 221 | plugins: 222 | - envato/github-pull-request#v0.4.0: 223 | head: "bundle-update/${BUILDKITE_BUILD_NUMBER}" 224 | title: "Bundle update" 225 | body: | 226 | Let's upgrade these dependencies for the long-term health and security of the system. 227 | 228 | A slight inconvenience now prevents a severe pain later. 229 | 230 | ([Bundle update #${BUILDKITE_BUILD_NUMBER}](${BUILDKITE_BUILD_URL})) 231 | labels: hygiene 232 | team-reviewers: a-team 233 | 234 | - wait 235 | 236 | - label: ":writing_hand: Annotate Changes" 237 | plugins: 238 | - envato/bundle-update#v0.9.1: 239 | annotate: true 240 | pull-request-metadata-key: github-pull-request-plugin-number 241 | ``` 242 | 243 | 1. Save this file to `.buildkite/pipeline.bundle-update.yml` and configure a 244 | dedicated Buildkite pipeline to load its steps from this location. 245 | 246 | 2. Configure the private SSH key and Github token as outlined above. 247 | 248 | 3. Edit the `.buildkite/pipeline.bundle-update.yml` file to use a Docker image 249 | supports your bundle of gems (and tweak the Git commit and pull request 250 | message contents to your liking). 251 | 252 | 4. Then use the Buildkite schedule feature to run the pipeline as often as your 253 | team desires. 254 | 255 | ## Configuration 256 | 257 | ### `update` 258 | 259 | Instruct the plugin to run `bundle update` on the project. 260 | 261 | ### `image` (optional) 262 | 263 | The Docker image to use. Checkout the [official Ruby 264 | builds](https://hub.docker.com/_/ruby/) at Docker Hub or build your own. 265 | 266 | Default: `ruby:slim` 267 | 268 | ### `gemfile-lock-files` (optional) 269 | 270 | The Gemfile lock files to check for changes post `bundle update` or to annotate. 271 | 272 | Default: `Gemfile.lock` 273 | 274 | ```yml 275 | steps: 276 | - name: ":bundler: Update" 277 | plugins: 278 | - envato/bundle-update#v0.9.1: 279 | update: true 280 | gemfile-lock-files: 281 | - Gemfile.lock 282 | - Gemfile_next.lock 283 | ``` 284 | 285 | ### `env` (optional, update only) 286 | 287 | The environment variables that get passed to the docker container. 288 | 289 | ```yml 290 | steps: 291 | - name: ":bundler: Update" 292 | plugins: 293 | - envato/bundle-update#v0.9.1: 294 | update: true 295 | env: 296 | - BUILDKITE_BUILD_NUMBER 297 | - MY_CUSTOM_ENV=llamas 298 | ``` 299 | 300 | Note how the values in the list can either be just a key (so the value is sourced from the environment) or a KEY=VALUE pair. 301 | 302 | ### `post-bundle-update` (optional, update only) 303 | 304 | A script or command to run inside the docker container after the bundle update. 305 | 306 | ### `pre-bundle-update` (optional, update only) 307 | 308 | The script or command to run inside the docker container prior to the bundle update. 309 | Used to install any dependencies that the bundle update needs if not already in 310 | the container. 311 | 312 | ### `annotate` 313 | 314 | Instruct the plugin to run annotate `Gemfile.lock` gem changes in a Github pull 315 | request. 316 | 317 | ### `pull-request` (optional, annotate only) 318 | 319 | The number of the Github pull request to annotate. This or 320 | `pull-request-metadata-key` needs to be provided for the `annotate` function. 321 | 322 | ### `pull-request-metadata-key` (optional, annotate only) 323 | 324 | The Buildkite metadata key to the Github pull request number. This or 325 | `pull-request` needs to be provided for the `annotate` function. 326 | 327 | ### `repository` (optional, annotate only) 328 | 329 | The Github repository. 330 | 331 | Default: pipeline configured repository 332 | 333 | ## Development 334 | 335 | To run the tests: 336 | 337 | ```sh 338 | docker-compose run --rm tests 339 | ``` 340 | 341 | To run the [Buildkite Plugin Linter](https://github.com/buildkite-plugins/buildkite-plugin-linter): 342 | 343 | ```sh 344 | docker-compose run --rm lint 345 | ``` 346 | 347 | [unwrappr]: https://github.com/envato/unwrappr 348 | [Github Deploy Key]: https://developer.github.com/v3/guides/managing-deploy-keys/#deploy-keys 349 | [Git Commit Buildkite Plugin]: https://github.com/thedyrt/git-commit-buildkite-plugin 350 | [Github Pull Request Buildkite Plugin]: https://github.com/envato/github-pull-request-buildkite-plugin 351 | [AWS S3 Secrets Buildkite Plugin]: https://github.com/buildkite/elastic-ci-stack-s3-secrets-hooks#uploading-secrets 352 | --------------------------------------------------------------------------------