├── .buildkite ├── pipeline.yml └── template.yml ├── .dockerignore ├── .env-sample ├── .gitignore ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── License.md ├── Rakefile ├── Readme.md ├── docker-compose.yml ├── knapsack_rspec_report.json ├── package-lock.json ├── package.json ├── renovate.json ├── scripts └── ci │ └── parallel_specs.sh └── spec ├── eighteenth_spec.rb ├── eighth_spec.rb ├── eleventh_spec.rb ├── fifteenth_spec.rb ├── fifth_spec.rb ├── first_spec.rb ├── fourteenth_spec.rb ├── fourth_spec.rb ├── nineteenth_spec.rb ├── nineth_spec.rb ├── second_spec.rb ├── seventeenth_spec.rb ├── seventh_spec.rb ├── sixteenth_spec.rb ├── sixth_spec.rb ├── spec_helper.rb ├── tenth_spec.rb ├── third_spec.rb ├── thirteenth_spec.rb ├── twelth_spec.rb └── twentieth_spec.rb /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: ":rspec:" 3 | command: "scripts/ci/parallel_specs.sh" 4 | artifact_paths: "log/**/*" 5 | env: 6 | RAILS_ENV: test 7 | plugins: 8 | - docker-compose#v5.4.0: 9 | run: app 10 | parallelism: 20 11 | agents: 12 | queue: $BUILDKITE_AGENT_META_DATA_QUEUE 13 | -------------------------------------------------------------------------------- /.buildkite/template.yml: -------------------------------------------------------------------------------- 1 | # Used by the 'Add to Buildkite' button in the readme 2 | name: "Rails Parallel Docker Example" 3 | steps: 4 | - command: "buildkite-agent pipeline upload" 5 | label: ":pipeline:" 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | public/assets 3 | log/* 4 | tmp/* 5 | -------------------------------------------------------------------------------- /.env-sample: -------------------------------------------------------------------------------- 1 | SOME_KEY=value -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:latest 2 | 3 | EXPOSE 5000 4 | 5 | ENV RAILS_ENV=test 6 | 7 | # Add official postgresql apt deb source 8 | RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ 9 | && wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ 10 | && apt-get update \ 11 | && apt-get install -y postgresql-client-10 12 | 13 | # Node, needed for asset pipeline 14 | RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - \ 15 | && apt-get update \ 16 | && apt-get install -y nodejs \ 17 | && npm install -q -g npm 18 | 19 | # Add the wait-for-it.sh script for waiting on dependent containers 20 | RUN curl https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh > /usr/local/bin/wait-for-it.sh \ 21 | && chmod +x /usr/local/bin/wait-for-it.sh 22 | 23 | WORKDIR /app 24 | 25 | # Install Rubygems first 26 | ADD Gemfile Gemfile.lock /app/ 27 | RUN gem install bundler \ 28 | && bundle install -j 32 29 | 30 | # Install npm libraries next 31 | ADD package.json package-lock.json /app/ 32 | RUN npm install 33 | 34 | # Now add the rest of your code 35 | ADD . /app/ 36 | 37 | # We want to precompile the assets into the Docker image, unless it's 38 | # development when you're just using live asset compilation 39 | RUN if [ "$RAILS_ENV" != "development" ]; then rm -rf public/assets && rake assets:precompile; else true; fi 40 | 41 | # Clean up 42 | RUN apt-get clean \ 43 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /app/log/* 44 | 45 | CMD ["rails", "server", "-p", "5000"] 46 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org/' 2 | 3 | gem 'rspec' 4 | gem 'knapsack' 5 | gem 'puma' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.3) 5 | knapsack (1.14.0) 6 | rake 7 | nio4r (2.7.3) 8 | puma (5.6.9) 9 | nio4r (~> 2.0) 10 | rake (13.0.1) 11 | rspec (3.6.0) 12 | rspec-core (~> 3.6.0) 13 | rspec-expectations (~> 3.6.0) 14 | rspec-mocks (~> 3.6.0) 15 | rspec-core (3.6.0) 16 | rspec-support (~> 3.6.0) 17 | rspec-expectations (3.6.0) 18 | diff-lcs (>= 1.2.0, < 2.0) 19 | rspec-support (~> 3.6.0) 20 | rspec-mocks (3.6.0) 21 | diff-lcs (>= 1.2.0, < 2.0) 22 | rspec-support (~> 3.6.0) 23 | rspec-support (3.6.0) 24 | 25 | PLATFORMS 26 | ruby 27 | 28 | DEPENDENCIES 29 | knapsack 30 | puma 31 | rspec 32 | 33 | BUNDLED WITH 34 | 1.15.1 35 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Buildkite Pty Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do 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. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | task :reset do 3 | puts "This is a fake db:reset" 4 | end 5 | end 6 | 7 | namespace :assets do 8 | task :precompile do 9 | puts "This is a fake assets:precompile" 10 | end 11 | end 12 | 13 | require 'knapsack' 14 | 15 | Knapsack.load_tasks 16 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Buildkite Rails Docker Parallel Example 2 | 3 | [![Add to Buildkite](https://buildkite.com/button.svg)](https://buildkite.com/new) 4 | 5 | This repository is an example of how to easily run a large number of parallel testing agents for a [Rails](https://rubyonrails.org/) application using [Buildkite](https://buildkite.com/), [Docker](https://www.docker.com) and [Knapsack](https://github.com/ArturT/knapsack). This approach works well with Docker available autoscaling environments like the [Buildkite AWS Stack](https://buildkite.com/buildkite/buildkite-aws-stack). 6 | 7 | This repository doesn't actually include a Rails sample application, but has the same CI scripts, `Dockerfile` and `docker-compose.yml` you would use for a production application. For a non-Docker based setup, see the [Rails Parallel Example](https://github.com/buildkite/rails-parallel-example). 8 | 9 | Files to note: 10 | 11 | * [docker-compose.yml](docker-compose.yml) - sets up the app to be able to run via [Buildkite’s Docker Compose support](https://buildkite.com/docs/guides/docker-containerized-builds) 12 | * [Dockerfile](Dockerfile) - setups up the entire Ruby environment, including a waiter script to help wait for Posgtres, Redis and Memcache to become available 13 | * [.buildkite/pipeline.yml](.buildkite/pipeline.yml) - the pipeline config showing how to have parallel steps 14 | * [scripts/ci/parallel_specs.sh](scripts/ci/parallel_specs.sh) - runs the specs in parallel using Knapsack 15 | 16 | ## How does it work? 17 | 18 | In the [pipeline configuration file](.buildkite/pipeline.yml) the `parallelism` property for the test step is set to 20. When a build, the step will appear 20 times in the pipeline, each with different environment variables exposed so you can divvy up your test suite accordingly. You can set the parallelism as high as you need to bring down your build times. 19 | 20 | See the [parallelizing builds guide](https://buildkite.com/docs/guides/parallelizing-builds) for more information to create parallelized and distributed builds with Buildkite. 21 | 22 | See the [Containerized Builds with Docker guide](https://buildkite.com/docs/guides/docker-containerized-builds) for information on how to use Docker Compose with Buildkite pipelines. 23 | 24 | Consult the [Knapsack documentation](https://github.com/ArturT/knapsack) for configuring your database and dependent services to support running parallel steps on the one machine. 25 | 26 | ## Knapsack Pro 27 | 28 | See [Knapsack Pro version of this example pipeline](https://github.com/KnapsackPro/buildkite-rails-docker-parallel-example-with-knapsack_pro) for how to perform dynamic splits/allocation of test nodes. 29 | 30 | ## Running locally 31 | 32 | To run the specs locally on your development machine, you can run: 33 | 34 | ```bash 35 | docker-compose run app scripts/ci/parallel_specs.sh 36 | ``` 37 | 38 | ## License 39 | 40 | See [Licence.md](Licence.md) (MIT) 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: . 6 | links: 7 | - db:db 8 | - redis:redis 9 | - memcache:memcache 10 | # This mounts the current Buildkite build into /app, ensuring any 11 | # generated files and artifacts are available to the buildkite-agent on 12 | # the host machine (outside of the Docker Container) 13 | volumes: 14 | - "./:/app" 15 | env_file: 16 | - ".env-sample" 17 | environment: 18 | PGHOST: db 19 | PGUSER: postgres 20 | REDIS_URL: redis://redis 21 | MEMCACHE_SERVERS: memcache 22 | # CI envs 23 | CI: 24 | RAILS_ENV: 25 | # CI envs For Knapsack 26 | BUILDKITE_PARALLEL_JOB: 27 | BUILDKITE_PARALLEL_JOB_COUNT: 28 | BUILDKITE_COMMIT: 29 | BUILDKITE_BRANCH: 30 | BUILDKITE_REPO: 31 | BUILDKITE_BUILD_ID: 32 | BUILDKITE_BUILD_NUMBER: 33 | ports: 34 | - "5000:5000" 35 | entrypoint: wait-for-it.sh db:5432 -s -- wait-for-it.sh redis:6379 -s -- wait-for-it.sh memcache:11211 -s -- 36 | 37 | db: 38 | image: postgres 39 | 40 | redis: 41 | image: redis 42 | 43 | memcache: 44 | image: tutum/memcached 45 | -------------------------------------------------------------------------------- /knapsack_rspec_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec/eighteenth_spec.rb": 0.0002357959747314453, 3 | "spec/eighth_spec.rb": 1.5020370483398438e-05, 4 | "spec/eleventh_spec.rb": 3.910064697265625e-05, 5 | "spec/fifteenth_spec.rb": 2.6226043701171875e-05, 6 | "spec/fifth_spec.rb": 3.1948089599609375e-05, 7 | "spec/first_spec.rb": 1.5735626220703125e-05, 8 | "spec/fourteenth_spec.rb": 1.1920928955078125e-05, 9 | "spec/fourth_spec.rb": 1.1205673217773438e-05, 10 | "spec/nineteenth_spec.rb": 1.2159347534179688e-05, 11 | "spec/nineth_spec.rb": 1.1920928955078125e-05, 12 | "spec/second_spec.rb": 1.0967254638671875e-05, 13 | "spec/seventeenth_spec.rb": 1.1920928955078125e-05, 14 | "spec/seventh_spec.rb": 1.2159347534179688e-05, 15 | "spec/sixteenth_spec.rb": 1.0967254638671875e-05, 16 | "spec/sixth_spec.rb": 1.2159347534179688e-05, 17 | "spec/tenth_spec.rb": 1.0013580322265625e-05, 18 | "spec/third_spec.rb": 1.0967254638671875e-05, 19 | "spec/thirteenth_spec.rb": 1.0967254638671875e-05, 20 | "spec/twelth_spec.rb": 1.0967254638671875e-05, 21 | "spec/twentieth_spec.rb": 1.0013580322265625e-05 22 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rails-parallel-example", 3 | "lockfileVersion": 1 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rails-parallel-example", 3 | "description": "...", 4 | "README": "...", 5 | "repository": "...", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "minor": { "automerge": true }, 6 | "patch": { "automerge": true }, 7 | "requiredStatusChecks": null, 8 | "prHourlyLimit": 0 9 | } 10 | -------------------------------------------------------------------------------- /scripts/ci/parallel_specs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | rake db:reset 6 | 7 | rake knapsack:rspec 8 | -------------------------------------------------------------------------------- /spec/eighteenth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Eighteenth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/eighth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Eighth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/eleventh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Eleventh spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/fifteenth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Fifteenth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/fifth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Fifth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/first_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "First spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/fourteenth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Fourteenth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/fourth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Fourth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/nineteenth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Nineteenth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/nineth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Nineth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/second_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Secondrequire 'spec_helper' 2 | 3 | spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/seventeenth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Seventeenth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/seventh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Seventh spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/sixteenth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Sixteenth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/sixth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Sixth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'knapsack' 4 | 5 | Knapsack::Adapters::RSpecAdapter.bind 6 | -------------------------------------------------------------------------------- /spec/tenth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Tenth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/third_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Third spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/thirteenth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Thirteenth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/twelth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Twelth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end -------------------------------------------------------------------------------- /spec/twentieth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe "Twentieth spec" do 4 | it "runs in parallel" do 5 | expect(1).to eql(1) 6 | end 7 | end --------------------------------------------------------------------------------