diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f1ac244c2..211e28835 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,10 +9,9 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Set up Ruby 2.7 + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '2.7' bundler-cache: true - name: rubocop run: | @@ -25,7 +24,7 @@ jobs: fail-fast: false matrix: ruby_version: - - '2.7' + - '3.0' services: db: @@ -68,7 +67,7 @@ jobs: fail-fast: false matrix: ruby_version: - - '2.7' + - '3.0' services: db: @@ -107,9 +106,10 @@ jobs: fail-fast: false matrix: ruby_version: - - '2.7' - '3.0' - '3.1' + - '3.2' + - '3.3' services: redis: @@ -139,8 +139,6 @@ jobs: - uses: actions/checkout@v1 - name: Set up Ruby uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.0' - name: Run setup script run: | git config --global user.email "you@example.com" @@ -149,7 +147,7 @@ jobs: export SHIPIT_GEM_PATH="${PWD}" mkdir /tmp/new-app cd /tmp/new-app - gem install rails -v '~> 7.0.2' --no-document + gem install rails -v '~> 7.1.1' --no-document rails new shipit --skip-action-cable --skip-turbolinks --skip-action-mailer --skip-active-storage --skip-webpack-install --skip-action-mailbox --skip-action-text -m "${SHIPIT_GEM_PATH}/template.rb" env: SHIPIT_EDGE: "1" diff --git a/.rubocop.yml b/.rubocop.yml index 979b3f24a..da85c1c48 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,5 @@ AllCops: - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.0 Exclude: - tmp/* - bin/* diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000..75a22a26a --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.0.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 61bbaf0c0..679df9436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Unreleased +# 0.39.0 + +* Minimum Ruby version is now Ruby 3.0 +* Upgraded to Rails 7.1.1 +* Upgraded Octokit to 5.6.1 (#1327) +* Migrate from legacy Rails secrets to credentials (#1326) + * Rails secrets were [deprecated in Rails 7.1](https://github.com/rails/rails/pull/48472) + * [Guide on credentials](https://guides.rubyonrails.org/security.html#custom-credentials) +* For deployments, `allow_concurrency` defaults to the same value as `force`. If wanted, it can be set separately by passing the intended value for `allow_concurrency` to `build_deploy` method + +# 0.38.0 + +* Convert `commit_deployment_statuses.github_id` to bigint (#1312) +* Allow to lock and archive stacks from the API (#1282) +* Hide API tokens from the user interface after the initial creation (#1298) + +# 0.37.0 + +* Suppress progress output for git checkout (#1278) +* Make API refresh action match non-API refresh action for stacks (#1277) + # 0.36.1 * Fix compatibility with Rails 7.0.3.1 (YAML serialized fields). (#1273) diff --git a/Gemfile b/Gemfile index e6c52fb4c..b12bdc854 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source 'https://rubygems.org' gemspec gem 'sqlite3' +gem 'ejson-rails', require: 'ejson/rails/skip_secrets' group :ci do gem 'mysql2' @@ -25,5 +26,4 @@ end group :development, :test do gem 'byebug' gem 'pry' - gem 'mini_racer' end diff --git a/Gemfile.lock b/Gemfile.lock index 50b6deeed..f1021ac8a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - shipit-engine (0.36.1) + shipit-engine (0.39.0) active_model_serializers (~> 0.9.3) ansi_stream (~> 0.0.6) autoprefixer-rails (~> 6.4.1) @@ -12,11 +12,11 @@ PATH gemoji (~> 2.1) jquery-rails (~> 4.4) lodash-rails (~> 4.17) - octokit (~> 4.20) + octokit (~> 5.6.0) omniauth-github (~> 1.4) paquito pubsubstub (~> 0.2.0) - rails (~> 7.0.0) + rails (~> 7.1.1) rails-timeago (~> 2.13.0) rails_autolink (~> 1.1.6) rake @@ -33,73 +33,82 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (7.0.3.1) - actionpack (= 7.0.3.1) - activesupport (= 7.0.3.1) + actioncable (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.3.1) - actionpack (= 7.0.3.1) - activejob (= 7.0.3.1) - activerecord (= 7.0.3.1) - activestorage (= 7.0.3.1) - activesupport (= 7.0.3.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.3.1) - actionpack (= 7.0.3.1) - actionview (= 7.0.3.1) - activejob (= 7.0.3.1) - activesupport (= 7.0.3.1) + actionmailer (7.1.3.4) + actionpack (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activesupport (= 7.1.3.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.3.1) - actionview (= 7.0.3.1) - activesupport (= 7.0.3.1) - rack (~> 2.0, >= 2.2.0) + rails-dom-testing (~> 2.2) + actionpack (7.1.3.4) + actionview (= 7.1.3.4) + activesupport (= 7.1.3.4) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.3.1) - actionpack (= 7.0.3.1) - activerecord (= 7.0.3.1) - activestorage (= 7.0.3.1) - activesupport (= 7.0.3.1) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.3.4) + actionpack (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.3.1) - activesupport (= 7.0.3.1) + actionview (7.1.3.4) + activesupport (= 7.1.3.4) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - active_model_serializers (0.9.8) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + active_model_serializers (0.9.9) activemodel (>= 3.2) concurrent-ruby (~> 1.0) - activejob (7.0.3.1) - activesupport (= 7.0.3.1) + activejob (7.1.3.4) + activesupport (= 7.1.3.4) globalid (>= 0.3.6) - activemodel (7.0.3.1) - activesupport (= 7.0.3.1) - activerecord (7.0.3.1) - activemodel (= 7.0.3.1) - activesupport (= 7.0.3.1) - activestorage (7.0.3.1) - actionpack (= 7.0.3.1) - activejob (= 7.0.3.1) - activerecord (= 7.0.3.1) - activesupport (= 7.0.3.1) + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) + timeout (>= 0.4.0) + activestorage (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activesupport (= 7.1.3.4) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.3.1) + activesupport (7.1.3.4) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) @@ -111,6 +120,8 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + base64 (0.2.0) + bigdecimal (3.1.8) builder (3.2.4) byebug (11.1.3) coderay (1.1.3) @@ -123,16 +134,22 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.10) + concurrent-ruby (1.3.1) + connection_pool (2.4.1) crack (0.4.5) rexml crass (1.0.6) + date (3.3.4) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - digest (3.1.0) docile (1.4.0) + drb (2.2.1) + ejson (1.4.1) + ejson-rails (0.2.1) + ejson + railties (>= 5.2) equalizer (0.0.11) - erubi (1.10.0) + erubi (1.12.0) execjs (2.8.1) explicit-parameters (0.4.1) actionpack (>= 6.0) @@ -140,7 +157,7 @@ GEM virtus (~> 1.0) faker (2.20.0) i18n (>= 1.8.11, < 2) - faraday (1.10.0) + faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -155,7 +172,7 @@ GEM faraday-em_http (1.0.0) faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) - faraday-http-cache (2.4.0) + faraday-http-cache (2.5.0) faraday (>= 0.8) faraday-httpclient (1.0.1) faraday-multipart (1.0.4) @@ -167,71 +184,68 @@ GEM faraday-retry (1.0.3) ffi (1.15.5) gemoji (2.1.0) - globalid (1.0.0) - activesupport (>= 5.0) + globalid (1.2.1) + activesupport (>= 6.1) hashdiff (1.0.1) hashie (5.0.0) - i18n (1.11.0) + i18n (1.14.5) concurrent-ruby (~> 1.0) ice_nine (0.11.2) - jquery-rails (4.5.0) + io-console (0.7.2) + irb (1.13.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jwt (2.4.1) - libv8-node (16.10.0.0-arm64-darwin) - libv8-node (16.10.0.0-x86_64-darwin) - libv8-node (16.10.0.0-x86_64-linux) + jwt (2.7.1) lodash-rails (4.17.21) railties (>= 3.1) - loofah (2.18.0) + loofah (2.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (1.0.2) + net-imap + net-pop + net-smtp + marcel (1.0.4) method_source (1.0.0) - mini_mime (1.1.2) - mini_racer (0.6.2) - libv8-node (~> 16.10.0.0) - minitest (5.16.2) - mocha (1.13.0) - msgpack (1.5.3) + mini_mime (1.1.5) + minitest (5.23.1) + mocha (2.1.0) + ruby2_keywords (>= 0.0.5) + msgpack (1.7.1) multi_xml (0.6.0) - multipart-post (2.2.3) + multipart-post (2.3.0) + mutex_m (0.2.0) mysql2 (0.5.3) - net-imap (0.2.3) - digest + net-imap (0.4.12) + date net-protocol - strscan - net-pop (0.1.1) - digest + net-pop (0.1.2) net-protocol + net-protocol (0.2.2) timeout - net-protocol (0.1.3) - timeout - net-smtp (0.3.1) - digest + net-smtp (0.5.0) net-protocol - timeout - nio4r (2.5.8) - nokogiri (1.13.7-arm64-darwin) + nio4r (2.7.3) + nokogiri (1.16.5-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.7-x86_64-darwin) + nokogiri (1.16.5-x86_64-linux) racc (~> 1.4) - nokogiri (1.13.7-x86_64-linux) - racc (~> 1.4) - oauth2 (2.0.6) + oauth2 (2.0.9) faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) multi_xml (~> 0.5) - rack (>= 1.2, < 3) - rash_alt (>= 0.4, < 1) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0) version_gem (~> 1.1) - octokit (4.25.1) + octokit (5.6.1) faraday (>= 1, < 3) sawyer (~> 0.9) - omniauth (1.9.1) + omniauth (1.9.2) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) omniauth-github (1.4.0) @@ -240,7 +254,7 @@ GEM omniauth-oauth2 (1.7.3) oauth2 (>= 1.4, < 3) omniauth (>= 1.9, < 3) - paquito (0.6.1) + paquito (0.10.0) msgpack (>= 1.5.2) parallel (1.21.0) parser (3.1.1.0) @@ -249,57 +263,72 @@ GEM pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) + psych (5.1.2) + stringio public_suffix (4.0.6) pubsubstub (0.2.2) rack redis (~> 4.0) - racc (1.6.0) - rack (2.2.4) - rack-test (2.0.2) + racc (1.8.0) + rack (2.2.9) + rack-session (1.0.2) + rack (< 3) + rack-test (2.1.0) rack (>= 1.3) - rails (7.0.3.1) - actioncable (= 7.0.3.1) - actionmailbox (= 7.0.3.1) - actionmailer (= 7.0.3.1) - actionpack (= 7.0.3.1) - actiontext (= 7.0.3.1) - actionview (= 7.0.3.1) - activejob (= 7.0.3.1) - activemodel (= 7.0.3.1) - activerecord (= 7.0.3.1) - activestorage (= 7.0.3.1) - activesupport (= 7.0.3.1) + rackup (1.0.0) + rack (< 3) + webrick + rails (7.1.3.4) + actioncable (= 7.1.3.4) + actionmailbox (= 7.1.3.4) + actionmailer (= 7.1.3.4) + actionpack (= 7.1.3.4) + actiontext (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activemodel (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) bundler (>= 1.15.0) - railties (= 7.0.3.1) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + railties (= 7.1.3.4) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) rails-timeago (2.13.0) actionpack (>= 3.1) activesupport (>= 3.1) - rails_autolink (1.1.6) - rails (> 3.1) - railties (7.0.3.1) - actionpack (= 7.0.3.1) - activesupport (= 7.0.3.1) - method_source + rails_autolink (1.1.8) + actionview (> 3.1) + activesupport (> 3.1) + railties (> 3.1) + railties (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.0.6) - rash_alt (0.4.12) - hashie (>= 3.4) - redis (4.7.1) + rake (13.2.1) + rdoc (6.7.0) + psych (>= 4.0.0) + redis (4.8.1) redis-objects (1.7.0) redis regexp_parser (2.2.1) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) - rexml (3.2.5) + reline (0.5.8) + io-console (~> 0.5) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) + rexml (3.2.8) + strscan (>= 3.0.9) rubocop (1.18.3) parallel (~> 1.10) parser (>= 3.0.0.0) @@ -336,6 +365,9 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -344,7 +376,8 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) spy (1.0.2) - sqlite3 (1.4.2) + sqlite3 (1.7.3-arm64-darwin) + sqlite3 (1.7.3-x86_64-linux) state_machines (0.5.0) state_machines-activemodel (0.8.0) activemodel (>= 5.1) @@ -352,18 +385,19 @@ GEM state_machines-activerecord (0.8.0) activerecord (>= 5.1) state_machines-activemodel (>= 0.8.0) - strscan (3.0.3) - thor (1.2.1) + stringio (3.1.0) + strscan (3.1.0) + thor (1.3.1) thread_safe (0.3.6) - tilt (2.0.10) - timeout (0.3.0) - tzinfo (2.0.4) + tilt (2.2.0) + timeout (0.4.1) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.1.0) validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix - version_gem (1.1.0) + version_gem (1.1.3) virtus (1.0.5) axiom-types (~> 0.1) coercible (~> 1.0) @@ -373,20 +407,20 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - websocket-driver (0.7.5) + webrick (1.8.1) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.6.0) + zeitwerk (2.6.15) PLATFORMS - arm64-darwin-21 - x86_64-darwin-20 + arm64-darwin x86_64-linux DEPENDENCIES byebug + ejson-rails faker - mini_racer mocha mysql2 pg @@ -400,4 +434,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.3.8 + 2.4.18 diff --git a/README.md b/README.md index 8bde27a95..872aee5b7 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ This guide aims to help you [set up](#installation-and-setup), [use](#using-ship **II. USING SHIPIT** * [Adding stacks](#adding-stacks) -* [Working on stacks](#working-on-stacks), -* [Configuring stacks](#configuring-stacks). +* [Working on stacks](#working-on-stacks) +* [Configuring stacks](#configuring-stacks) **III. REFERENCE** @@ -357,6 +357,11 @@ For example: fetch: curl --silent https://app.example.com/services/ping/version ``` + +**Note:** Currently, deployments in emergency mode are configured to occur concurrently via [the `build_deploy` method](https://github.com/Shopify/shipit-engine/blob/main/app/models/shipit/stack.rb), +whose `allow_concurrency` keyword argument defaults to `force`, where `force` is true when emergency mode is enabled. +If you'd like to separate these two from one another, override this method as desired in your service. +

Kubernetes

**kubernetes** allows to specify a Kubernetes namespace and context to deploy to. @@ -610,7 +615,7 @@ review:

Shell commands timeout

-All the shell commands can take an optional `timeout` parameter to limit their duration: +All the shell commands can take an optional `timeout` parameter. This is the value in seconds that a command can be inactive before Shipit will terminate the task. ```yml deploy: @@ -641,7 +646,7 @@ Your deploy scripts have access to the following environment variables: * `GITHUB_REPO_OWNER`: The GitHub username of the repository owner for the current deploy/task. * `EMAIL`: Email of the user that triggered the deploy/task (if available) * `ENVIRONMENT`: The stack environment (e.g `production` / `staging`) -* `BRANCH`: The stack branch (e.g `master`) +* `BRANCH`: The stack branch (e.g `main`) * `LAST_DEPLOYED_SHA`: The git SHA of the last deployed commit * `DIFF_LINK`: URL to the diff on GitHub. * `TASK_ID`: ID of the task that is running diff --git a/app/controllers/shipit/api/base_controller.rb b/app/controllers/shipit/api/base_controller.rb index b294e1374..d7df8157a 100644 --- a/app/controllers/shipit/api/base_controller.rb +++ b/app/controllers/shipit/api/base_controller.rb @@ -93,8 +93,8 @@ def not_found(_error) render(status: :not_found, json: { status: '404', error: 'Not Found' }) end - def conflict(_error) - render(status: :conflict, json: { status: '409', error: 'Conflict' }) + def conflict(error) + render(status: :conflict, json: { status: '409', error: error.message }) end end end diff --git a/app/controllers/shipit/api/deploys_controller.rb b/app/controllers/shipit/api/deploys_controller.rb index d80dedbfb..3ccb3f077 100644 --- a/app/controllers/shipit/api/deploys_controller.rb +++ b/app/controllers/shipit/api/deploys_controller.rb @@ -11,6 +11,7 @@ def index params do requires :sha, String, length: { in: 6..40 } accepts :force, Boolean, default: false + accepts :allow_concurrency, Boolean accepts :require_ci, Boolean, default: false accepts :env, Hash, default: {} end @@ -18,7 +19,10 @@ def create commit = stack.commits.by_sha(params.sha) || param_error!(:sha, 'Unknown revision') param_error!(:force, "Can't deploy a locked stack") if !params.force && stack.locked? param_error!(:require_ci, "Commit is not deployable") if params.require_ci && !commit.deployable? - deploy = stack.trigger_deploy(commit, current_user, env: params.env, force: params.force) + + allow_concurrency = params.allow_concurrency.nil? ? params.force : params.allow_concurrency + deploy = stack.trigger_deploy(commit, current_user, env: params.env, force: params.force, + allow_concurrency: allow_concurrency) render_resource(deploy, status: :accepted) end end diff --git a/app/controllers/shipit/api/stacks_controller.rb b/app/controllers/shipit/api/stacks_controller.rb index 9d5b1ab14..ffcec1215 100644 --- a/app/controllers/shipit/api/stacks_controller.rb +++ b/app/controllers/shipit/api/stacks_controller.rb @@ -46,9 +46,13 @@ def create accepts :ignore_ci, Boolean accepts :merge_queue_enabled, Boolean accepts :continuous_deployment, Boolean + accepts :archived, Boolean end def update - stack.update(params) + stack.update(update_params) + + update_archived + render_resource(stack) end @@ -62,6 +66,8 @@ def destroy end def refresh + RefreshStatusesJob.perform_later(stack_id: stack.id) + RefreshCheckRunsJob.perform_later(stack_id: stack.id) GithubSyncJob.perform_later(stack_id: stack.id) render_resource(stack, status: :accepted) end @@ -76,6 +82,26 @@ def stack @stack ||= stacks.from_param!(params[:id]) end + def update_archived + if key?(:archived) + if params[:archived] + stack.archive!(nil) + elsif stack.archived? + stack.unarchive! + end + end + end + + def key?(key) + params.to_h.key?(key) + end + + def update_params + params.select do |key, _| + %i(environment branch deploy_url ignore_ci merge_queue_enabled continuous_deployment).include?(key) + end + end + def repository @repository ||= Repository.find_or_create_by(owner: repo_owner, name: repo_name) end diff --git a/app/helpers/shipit/api_clients_helper.rb b/app/helpers/shipit/api_clients_helper.rb new file mode 100644 index 000000000..1b55fc169 --- /dev/null +++ b/app/helpers/shipit/api_clients_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Shipit + module ApiClientsHelper + def api_client_token(api_client) + if api_client.created_at >= 5.minutes.ago && current_user == api_client.creator + api_client.authentication_token + else + "#{api_client.authentication_token[0..5]}************************" + end + end + end +end diff --git a/app/jobs/shipit/background_job.rb b/app/jobs/shipit/background_job.rb index ee7d69e91..0426792c5 100644 --- a/app/jobs/shipit/background_job.rb +++ b/app/jobs/shipit/background_job.rb @@ -5,8 +5,14 @@ class << self attr_accessor :timeout end + DEFAULT_RETRY_TIME_IN_SECONDS = 30 + # Write actions can sometimes fail intermittently, particulary for large and/or busy repositories - retry_on(Octokit::BadGateway, Octokit::InternalServerError) + retry_on(Octokit::ServerError) + + rescue_from(Octokit::TooManyRequests, Octokit::AbuseDetected) do |exception| + retry_job wait: exception.response_headers.fetch("Retry-After", DEFAULT_RETRY_TIME_IN_SECONDS).to_i.seconds + end def perform(*) with_timeout do diff --git a/app/jobs/shipit/continuous_delivery_job.rb b/app/jobs/shipit/continuous_delivery_job.rb index de5c1b4b3..46a292f01 100644 --- a/app/jobs/shipit/continuous_delivery_job.rb +++ b/app/jobs/shipit/continuous_delivery_job.rb @@ -8,7 +8,10 @@ class ContinuousDeliveryJob < BackgroundJob def perform(stack) return unless stack.continuous_deployment? - return if stack.active_task? + + # checks if there are any tasks running, including concurrent tasks + return if stack.occupied? + stack.trigger_continuous_delivery end end diff --git a/app/jobs/shipit/create_release_statuses_job.rb b/app/jobs/shipit/create_release_statuses_job.rb index dbd3a681f..d632eb4ef 100644 --- a/app/jobs/shipit/create_release_statuses_job.rb +++ b/app/jobs/shipit/create_release_statuses_job.rb @@ -4,6 +4,7 @@ class CreateReleaseStatusesJob < BackgroundJob include BackgroundJob::Unique queue_as :default + on_duplicate :drop def perform(commit) commit.release_statuses.to_be_created.each(&:create_status_on_github!) diff --git a/app/models/shipit/api_client.rb b/app/models/shipit/api_client.rb index 27459c3ec..18f5b73c7 100644 --- a/app/models/shipit/api_client.rb +++ b/app/models/shipit/api_client.rb @@ -8,7 +8,7 @@ class ApiClient < Record validates :creator, :name, presence: true - serialize :permissions, Shipit.serialized_column(:permissions, type: Array) + serialize :permissions, coder: Shipit.serialized_column(:permissions, type: Array) PERMISSIONS = %w( read:stack write:stack diff --git a/app/models/shipit/commit.rb b/app/models/shipit/commit.rb index a1417cc5b..9e152b2ed 100644 --- a/app/models/shipit/commit.rb +++ b/app/models/shipit/commit.rb @@ -168,10 +168,26 @@ def create_status_from_github!(github_status) end end - def refresh_check_runs! + def paginated_check_runs response = stack.handle_github_redirections do - stack.github_api.check_runs(github_repo_name, sha) + stack.github_api.check_runs(github_repo_name, sha, per_page: 100) + end + + return response if stack.github_api.last_response.rels[:next].nil? + + loop do + page = stack.handle_github_redirections do + stack.github_api.get(stack.github_api.last_response.rels[:next].href) + end + response.check_runs.concat(page.check_runs) + break if stack.github_api.last_response.rels[:next].nil? end + + response + end + + def refresh_check_runs! + response = paginated_check_runs response.check_runs.each do |check_run| create_or_update_check_run_from_github!(check_run) end diff --git a/app/models/shipit/commit_deployment_status.rb b/app/models/shipit/commit_deployment_status.rb index 85bde2b35..6be2693f5 100644 --- a/app/models/shipit/commit_deployment_status.rb +++ b/app/models/shipit/commit_deployment_status.rb @@ -48,7 +48,6 @@ def create_status_on_github(client) client.create_deployment_status( commit_deployment.api_url, status, - accept: 'application/vnd.github.flash-preview+json', target_url: url_helpers.stack_deploy_url(stack, task), description: description.truncate(DESCRIPTION_CHARACTER_LIMIT_ON_GITHUB), environment_url: stack.deploy_url, diff --git a/app/models/shipit/delivery.rb b/app/models/shipit/delivery.rb index 141c87f8b..9f1fef9ae 100644 --- a/app/models/shipit/delivery.rb +++ b/app/models/shipit/delivery.rb @@ -9,7 +9,7 @@ class Delivery < Record validates :url, presence: true, url: { no_local: true, allow_blank: true } validates :content_type, presence: true - serialize :response_headers, SafeJSON + serialize :response_headers, coder: SafeJSON after_commit :purge_old_deliveries, on: :create diff --git a/app/models/shipit/deploy_spec/file_system.rb b/app/models/shipit/deploy_spec/file_system.rb index 8e3d0f84a..3708b9df3 100644 --- a/app/models/shipit/deploy_spec/file_system.rb +++ b/app/models/shipit/deploy_spec/file_system.rb @@ -101,11 +101,23 @@ def load_config end def shipit_file_names_in_priority_order - ["#{app_name}.#{@env}.yml", "#{app_name}.yml", "shipit.#{@env}.yml", "shipit.yml"].uniq + [ + "#{app_name}.#{@env}.yml", + ".shipit/#{app_name}.#{@env}.yml", + + "#{app_name}.yml", + ".shipit/#{app_name}.yml", + + "shipit.#{@env}.yml", + ".shipit/#{@env}.yml", + + "shipit.yml", + ".shipit/shipit.yml", + ].uniq end def bare_shipit_filenames - ["#{app_name}.yml", "shipit.yml"].uniq + ["#{app_name}.yml", "shipit.yml", ".shipit/#{app_name}.yml", ".shipit/shipit.yml"].uniq end def config_file_path diff --git a/app/models/shipit/hook.rb b/app/models/shipit/hook.rb index 9aa6a4e1f..5b20ddeda 100644 --- a/app/models/shipit/hook.rb +++ b/app/models/shipit/hook.rb @@ -87,7 +87,7 @@ def signature validates :content_type, presence: true, inclusion: { in: CONTENT_TYPES.keys } validates :events, presence: true, subset: { of: EVENTS } - serialize :events, Shipit::CSVSerializer + serialize :events, coder: Shipit::CSVSerializer scope :global, -> { where(stack_id: nil) } scope :scoped_to, ->(stack) { where(stack_id: stack.id) } diff --git a/app/models/shipit/pull_request.rb b/app/models/shipit/pull_request.rb index 0083ad9c0..8e993af82 100644 --- a/app/models/shipit/pull_request.rb +++ b/app/models/shipit/pull_request.rb @@ -11,7 +11,7 @@ class PullRequest < Record has_many :pull_request_assignments has_many :assignees, class_name: :User, through: :pull_request_assignments, source: :user - serialize :labels, Shipit.serialized_column(:labels, type: Array) + serialize :labels, coder: Shipit.serialized_column(:labels, type: Array) after_create_commit :emit_create_hooks after_update_commit :emit_update_hooks diff --git a/app/models/shipit/stack.rb b/app/models/shipit/stack.rb index 8e93a8e6b..23767f254 100644 --- a/app/models/shipit/stack.rb +++ b/app/models/shipit/stack.rb @@ -101,7 +101,7 @@ def sync_github_if_necessary validates :lock_reason, length: { maximum: 4096 } - serialize :cached_deploy_spec, DeploySpec + serialize :cached_deploy_spec, coder: DeploySpec delegate( :provisioning_handler_name, :find_task_definition, @@ -150,14 +150,14 @@ def trigger_task(definition_id, user, env: nil, force: false) task end - def build_deploy(until_commit, user, env: nil, force: false) + def build_deploy(until_commit, user, env: nil, force: false, allow_concurrency: force) since_commit = last_deployed_commit.presence || commits.first deploys.build( user_id: user.id, until_commit: until_commit, since_commit: since_commit, env: filter_deploy_envs(env&.to_h || {}), - allow_concurrency: force, + allow_concurrency: allow_concurrency, ignored_safeties: force || !until_commit.deployable?, max_retries: retries_on_deploy, ) @@ -226,8 +226,12 @@ def schedule_merges def next_commit_to_deploy commits_to_deploy = commits.order(id: :asc).newer_than(last_deployed_commit).reachable.preload(:statuses) - commits_to_deploy = commits_to_deploy.limit(maximum_commits_per_deploy) if maximum_commits_per_deploy - commits_to_deploy.to_a.reverse.find(&:deployable?) + if maximum_commits_per_deploy + commits_with_max_applied = commits_to_deploy.limit(maximum_commits_per_deploy) + deployable_commits(commits_with_max_applied) || deployable_commits(commits_to_deploy) + else + deployable_commits(commits_to_deploy) + end end def deployed_too_recently? @@ -453,6 +457,14 @@ def active_task @active_task ||= tasks.current end + def occupied? + !!occupied + end + + def occupied + @occupied ||= tasks.active.last + end + def locked? lock_reason.present? end @@ -620,6 +632,10 @@ def emit_lock_hooks private + def deployable_commits(commits) + commits.to_a.reverse.find(&:deployable?) + end + def clear_cache remove_instance_variable(:@active_task) if defined?(@active_task) end diff --git a/app/models/shipit/task.rb b/app/models/shipit/task.rb index f2b6f0f05..c0baa4688 100644 --- a/app/models/shipit/task.rb +++ b/app/models/shipit/task.rb @@ -3,7 +3,11 @@ module Shipit class Task < Record include DeferredTouch - ConcurrentTaskRunning = Class.new(StandardError) + class ConcurrentTaskRunning < StandardError + def message + "A task is already running." + end + end PRESENCE_CHECK_TIMEOUT = 30 ACTIVE_STATUSES = %w(pending running aborting).freeze @@ -54,8 +58,8 @@ def new end end - serialize :definition, TaskDefinition - serialize :env, Shipit.serialized_column(:env, coder: EnvHash) + serialize :definition, coder: TaskDefinition + serialize :env, coder: Shipit.serialized_column(:env, coder: EnvHash) scope :success, -> { where(status: 'success') } scope :completed, -> { where(status: COMPLETED_STATUSES) } diff --git a/app/models/shipit/task_execution_strategy/default.rb b/app/models/shipit/task_execution_strategy/default.rb index 72e52554d..3cd2998b9 100644 --- a/app/models/shipit/task_execution_strategy/default.rb +++ b/app/models/shipit/task_execution_strategy/default.rb @@ -70,7 +70,7 @@ def checkout_repository @task.acquire_git_cache_lock do @task.ping unless @commands.fetched?(@task.until_commit).tap(&:run).success? - capture!(@commands.fetch) + capture!(@commands.fetch_commit(@task.until_commit)) end end end @@ -94,7 +94,7 @@ def capture!(command) @task.write(line) end finished_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @task.write("pid: #{command.pid} finished in: #{finished_at - started_at} seconds\n") + @task.write("pid: #{command.pid} finished in: #{(finished_at - started_at).round(3)} seconds\n") command.success? end diff --git a/app/models/shipit/user.rb b/app/models/shipit/user.rb index 0e283e143..c94c29dfa 100644 --- a/app/models/shipit/user.rb +++ b/app/models/shipit/user.rb @@ -95,6 +95,8 @@ def refresh_from_github! update!(github_user: Shipit.github.api.user(github_id)) rescue Octokit::NotFound identify_renamed_user! + rescue Octokit::Forbidden + Rails.logger.info("User #{name}, github_id #{github_id} has forbidden access to their GitHub, likely deleted.") end def github_user=(github_user) diff --git a/app/views/shipit/_variables.html.erb b/app/views/shipit/_variables.html.erb index c87397154..a43dd6c93 100644 --- a/app/views/shipit/_variables.html.erb +++ b/app/views/shipit/_variables.html.erb @@ -9,7 +9,7 @@ <% if variable.select %> <%= field.select variable.name, options_for_select([["Please select...", { disabled: "disabled" }]] + variable.select, variable.default || "Please select...") %> <% else %> - <%= field.text_field variable.name, value: variable.default %> + <%= field.text_field variable.name, value: params[variable.name].presence || variable.default %> <% end %> <%= field.label variable.name, variable.title || variable.name %>

diff --git a/app/views/shipit/api_clients/show.html.erb b/app/views/shipit/api_clients/show.html.erb index 0bcfe40f5..3c515c90d 100644 --- a/app/views/shipit/api_clients/show.html.erb +++ b/app/views/shipit/api_clients/show.html.erb @@ -10,7 +10,7 @@

Authentication token:

- <%= @api_client.authentication_token %> + <%= api_client_token(@api_client) %>
diff --git a/app/views/shipit/merge_status/failure.html.erb b/app/views/shipit/merge_status/failure.html.erb index dcd0a258e..2fe738881 100644 --- a/app/views/shipit/merge_status/failure.html.erb +++ b/app/views/shipit/merge_status/failure.html.erb @@ -15,7 +15,7 @@ <%= link_to @stack.to_param, stack_url(@stack), target: '_blank', rel: 'noopener' %> - master branch is failing! + main branch is failing! <%= render 'commit_count_warning' if display_commit_count_warning?(params[:commits].to_i) %> diff --git a/app/views/shipit/missing_settings.html.erb b/app/views/shipit/missing_settings.html.erb index 8c874f0c5..59acfb32f 100644 --- a/app/views/shipit/missing_settings.html.erb +++ b/app/views/shipit/missing_settings.html.erb @@ -22,7 +22,7 @@

Config: - <% if Rails.application.secrets.github.present? %> + <% if Rails.application.credentials.github.present? %> Success! <% else %> diff --git a/config/secrets.development.example.yml b/config/secrets.development.example.yml index 90829c9f8..ef996717d 100644 --- a/config/secrets.development.example.yml +++ b/config/secrets.development.example.yml @@ -1,7 +1,7 @@ host: 'localhost:3000' redis_url: 'redis://127.0.0.1:6379/0' -# For creating an app see: https://github.com/Shopify/shipit-engine/blob/master/docs/setup.md#creating-the-github-app +# For creating an app see: https://github.com/Shopify/shipit-engine/blob/main/docs/setup.md#creating-the-github-app # Can be obtained there: https://github.com/settings/apps # Set the "Authorization callback URL" as `/github/auth/github/callback` diff --git a/config/secrets.development.shopify.yml b/config/secrets.development.shopify.yml index f13fa971c..52e9ff6c5 100644 --- a/config/secrets.development.shopify.yml +++ b/config/secrets.development.shopify.yml @@ -1,7 +1,7 @@ host: 'shipit-engine.myshopify.io' redis_url: 'redis://shipit-engine.railgun:6379' -# For creating an app see: https://github.com/Shopify/shipit-engine/blob/master/docs/setup.md#creating-the-github-app +# For creating an app see: https://github.com/Shopify/shipit-engine/blob/main/docs/setup.md#creating-the-github-app github: somegithuborg: diff --git a/contrib/browser-extension/README.md b/contrib/browser-extension/README.md index 642015dbe..5a4a5452f 100644 --- a/contrib/browser-extension/README.md +++ b/contrib/browser-extension/README.md @@ -1,8 +1,8 @@ # Shipit Browser Extension -If you have ever pushed to a broken `master` because you forgot to check CI/Slack then we have the extension for you. +If you have ever pushed to a broken `main` because you forgot to check CI/Slack then we have the extension for you. -This extension will alert you on GitHub's pull request page if `master` is broken, so that you can avoid the embarassment of having merged on red master. +This extension will alert you on GitHub's pull request page if `main` is broken, so that you can avoid the embarassment of having merged on red master. ## Installation diff --git a/db/migrate/20230703181143_change_commit_deployment_statuses_github_id_to_big_int.rb b/db/migrate/20230703181143_change_commit_deployment_statuses_github_id_to_big_int.rb new file mode 100644 index 000000000..a450890b3 --- /dev/null +++ b/db/migrate/20230703181143_change_commit_deployment_statuses_github_id_to_big_int.rb @@ -0,0 +1,5 @@ +class ChangeCommitDeploymentStatusesGithubIdToBigInt < ActiveRecord::Migration[7.0] + def change + change_column :commit_deployment_statuses, :github_id, :bigint + end +end diff --git a/dev.yml b/dev.yml index 3327f5df7..2dfd802fa 100644 --- a/dev.yml +++ b/dev.yml @@ -5,10 +5,9 @@ name: shipit-engine type: rails up: - - homebrew: + - packages: - sqlite - - ruby: - version: 2.7.5 + - ruby - isogun - bundler: without: ci diff --git a/docs/setup.md b/docs/setup.md index 0a907a9aa..ffbfc1727 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -14,8 +14,8 @@ In the future we'd like to provide it fully packaged inside a Docker container, Shipit provides you with a Rails template. To bootstrap your Shipit installation: -1. If you don't have Rails installed, run this command: `gem install rails -v 7.0` -2. Run this command: `rails _7.0_ new shipit --skip-action-cable --skip-turbolinks --skip-action-mailer --skip-active-storage --skip-webpack-install --skip-action-mailbox --skip-action-text -m https://raw.githubusercontent.com/Shopify/shipit-engine/master/template.rb` +1. If you don't have Rails installed, run this command: `gem install rails -v 7.1` +2. Run this command: `rails _7.1_ new shipit --skip-action-cable --skip-turbolinks --skip-action-mailer --skip-active-storage --skip-webpack-install --skip-action-mailbox --skip-action-text -m https://raw.githubusercontent.com/Shopify/shipit-engine/main/template.rb` ## Creating the GitHub App diff --git a/lib/shipit.rb b/lib/shipit.rb index 885f6b53f..8360dbb0a 100644 --- a/lib/shipit.rb +++ b/lib/shipit.rb @@ -291,7 +291,7 @@ def revision_file end def secrets - Rails.application.secrets + Rails.application.credentials end end diff --git a/lib/shipit/engine.rb b/lib/shipit/engine.rb index 0d90d124c..b1279137f 100644 --- a/lib/shipit/engine.rb +++ b/lib/shipit/engine.rb @@ -21,7 +21,7 @@ class Engine < ::Rails::Engine Shipit::Engine.routes.default_url_options[:host] = Shipit.host Pubsubstub.redis_url = Shipit.redis_url.to_s - Rails.application.secrets.deep_symbolize_keys! + Rails.application.credentials.deep_symbolize_keys! app.config.assets.paths << Emoji.images_path app.config.assets.precompile += %w( diff --git a/lib/shipit/github_app.rb b/lib/shipit/github_app.rb index a8ee5bfba..ae27297e0 100644 --- a/lib/shipit/github_app.rb +++ b/lib/shipit/github_app.rb @@ -103,7 +103,6 @@ def fetch_new_token ) do response = new_client(bearer_token: authentication_payload).create_app_installation_access_token( installation_id, - accept: 'application/vnd.github.machine-man-preview+json', ) token = Token.from_github(response) raise AuthenticationFailed if token.blank? diff --git a/lib/shipit/octokit_check_runs.rb b/lib/shipit/octokit_check_runs.rb index 09d75a016..4b13172b1 100644 --- a/lib/shipit/octokit_check_runs.rb +++ b/lib/shipit/octokit_check_runs.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module OctokitCheckRuns def check_runs(repo, sha, options = {}) - paginate("#{Octokit::Repository.path(repo)}/commits/#{sha}/check-runs", options.reverse_merge( - accept: 'application/vnd.github.antiope-preview+json', - )) + paginate("#{Octokit::Repository.path(repo)}/commits/#{sha}/check-runs", options) end end diff --git a/lib/shipit/octokit_iterator.rb b/lib/shipit/octokit_iterator.rb index c6c96f0b6..3a113cb1b 100644 --- a/lib/shipit/octokit_iterator.rb +++ b/lib/shipit/octokit_iterator.rb @@ -16,6 +16,8 @@ def each(&block) response = @response loop do + return unless response.present? + response.data.each(&block) return unless response.rels[:next] response = response.rels[:next].get diff --git a/lib/shipit/stack_commands.rb b/lib/shipit/stack_commands.rb index 0e37e3315..252e93956 100644 --- a/lib/shipit/stack_commands.rb +++ b/lib/shipit/stack_commands.rb @@ -13,6 +13,16 @@ def env super.merge(@stack.env) end + def fetch_commit(commit) + create_directories + if valid_git_repository?(@stack.git_path) + git('fetch', 'origin', '--quiet', '--tags', commit.sha, env: env, chdir: @stack.git_path) + else + @stack.clear_git_cache! + git_clone(@stack.repo_git_url, @stack.git_path, branch: @stack.branch, env: env, chdir: @stack.deploys_path) + end + end + def fetch create_directories if valid_git_repository?(@stack.git_path) @@ -48,12 +58,12 @@ def fetch_deployed_revision end def build_cacheable_deploy_spec - with_temporary_working_directory do |dir| + with_temporary_working_directory(recursive: false) do |dir| DeploySpec::FileSystem.new(dir, @stack.environment).cacheable end end - def with_temporary_working_directory(commit: nil) + def with_temporary_working_directory(commit: nil, recursive: true) commit ||= @stack.last_deployed_commit.presence || @stack.commits.reachable.last if !commit || !fetched?(commit).tap(&:run).success? @@ -64,15 +74,24 @@ def with_temporary_working_directory(commit: nil) end end + git_args = [] + git_args << '--recursive' if recursive Dir.mktmpdir do |dir| git( 'clone', @stack.git_path, @stack.repo_name, - '--recursive', '--origin', 'cache', + *git_args, '--origin', 'cache', chdir: dir ).run! git_dir = File.join(dir, @stack.repo_name) - git('-c', 'advice.detachedHead=false', 'checkout', commit.sha, chdir: git_dir).run! if commit + git( + '-c', + 'advice.detachedHead=false', + 'checkout', + '--quiet', + commit.sha, + chdir: git_dir + ).run! if commit yield Pathname.new(git_dir) end end diff --git a/lib/shipit/task_commands.rb b/lib/shipit/task_commands.rb index 522efe2cf..c7a011bc8 100644 --- a/lib/shipit/task_commands.rb +++ b/lib/shipit/task_commands.rb @@ -2,7 +2,7 @@ # rubocop:disable Lint/MissingSuper module Shipit class TaskCommands < Commands - delegate :fetch, :fetched?, to: :stack_commands + delegate :fetch_commit, :fetch, :fetched?, to: :stack_commands def initialize(task) @task = task @@ -47,7 +47,14 @@ def env end def checkout(commit) - git('-c', 'advice.detachedHead=false', 'checkout', commit.sha, chdir: @task.working_directory) + git( + '-c', + 'advice.detachedHead=false', + 'checkout', + '--quiet', + commit.sha, + chdir: @task.working_directory + ) end def clone diff --git a/lib/shipit/version.rb b/lib/shipit/version.rb index c3d2009a2..528579f6b 100644 --- a/lib/shipit/version.rb +++ b/lib/shipit/version.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true module Shipit - VERSION = '0.36.1' + VERSION = '0.39.0' end diff --git a/script/cibuild b/script/cibuild index 3ed1e1a50..12d7377ac 100755 --- a/script/cibuild +++ b/script/cibuild @@ -38,8 +38,8 @@ case $SUITE in git config --global user.email "anonymous@example.com" git config --global user.name "Anonymous" - gem install rails -v 7.0 - rails _7.0_ new shipit -m "${TEMPLATE}" \ + gem install rails -v 7.1.1 + rails _7.1.1_ new shipit -m "${TEMPLATE}" \ --skip-action-cable \ --skip-turbolinks \ --skip-action-mailer \ diff --git a/shipit-engine.gemspec b/shipit-engine.gemspec index 3ede910da..9eed3b91d 100644 --- a/shipit-engine.gemspec +++ b/shipit-engine.gemspec @@ -17,6 +17,8 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,db,lib,vendor}/**/*", "LICENSE", "Rakefile", "README.md"] s.test_files = Dir["test/**/*"] - Dir["test/dummy/tmp/**/*"] - Dir["test/dummy/log/**/*"] + s.required_ruby_version = '>= 3.0.0' + s.add_dependency('active_model_serializers', '~> 0.9.3') s.add_dependency('ansi_stream', '~> 0.0.6') s.add_dependency('autoprefixer-rails', '~> 6.4.1') @@ -27,10 +29,10 @@ Gem::Specification.new do |s| s.add_dependency('gemoji', '~> 2.1') s.add_dependency('jquery-rails', '~> 4.4') s.add_dependency('lodash-rails', '~> 4.17') - s.add_dependency('octokit', '~> 4.20') + s.add_dependency('octokit', '~> 5.6.0') s.add_dependency('omniauth-github', '~> 1.4') s.add_dependency('pubsubstub', '~> 0.2.0') - s.add_dependency('rails', '~> 7.0.0') + s.add_dependency('rails', '~> 7.1.1') s.add_dependency('rails-timeago', '~> 2.13.0') s.add_dependency('rails_autolink', '~> 1.1.6') s.add_dependency('rake') diff --git a/template.rb b/template.rb index 607b043e0..2f80764ff 100644 --- a/template.rb +++ b/template.rb @@ -1,10 +1,10 @@ # Template for rails new app # Run this like `rails new shipit -m template.rb` -if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7') - raise Thor::Error, "You need at least Ruby 2.7 to install shipit" +if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.0') + raise Thor::Error, "You need at least Ruby 3.0 to install shipit" end -if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new('7.0') - raise Thor::Error, "You need Rails 7.0 to install shipit" +if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new('7.1') + raise Thor::Error, "You need Rails 7.1 to install shipit" end route %(mount Shipit::Engine, at: '/') @@ -124,7 +124,7 @@ end CODE -inject_into_file 'config/application.rb', after: "load_defaults 7.0\n" do +inject_into_file 'config/application.rb', after: "load_defaults 7.1\n" do "\n config.active_job.queue_adapter = :sidekiq\n" end @@ -151,5 +151,5 @@ ) end - say("Read https://github.com/Shopify/shipit-engine/blob/master/docs/setup.md for the details on how to create the App and update config/secrets.yml", Thor::Shell::Color::GREEN, true) + say("Read https://github.com/Shopify/shipit-engine/blob/main/docs/setup.md for the details on how to create the App and update config/secrets.yml", Thor::Shell::Color::GREEN, true) end diff --git a/test/controllers/api/base_controller_test.rb b/test/controllers/api/base_controller_test.rb index a37d0211a..348514a46 100644 --- a/test/controllers/api/base_controller_test.rb +++ b/test/controllers/api/base_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class BaseControllerTest < ActionController::TestCase + class BaseControllerTest < ApiControllerTestCase test "authentication is required" do get :index assert_response :unauthorized diff --git a/test/controllers/api/ccmenu_controller_test.rb b/test/controllers/api/ccmenu_controller_test.rb index 76c695c72..ba3dbdf2a 100644 --- a/test/controllers/api/ccmenu_controller_test.rb +++ b/test/controllers/api/ccmenu_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class CCMenuControllerTest < ActionController::TestCase + class CCMenuControllerTest < ApiControllerTestCase setup do authenticate! @stack = shipit_stacks(:shipit) diff --git a/test/controllers/api/commits_controller_test.rb b/test/controllers/api/commits_controller_test.rb index 607deea37..793216eae 100644 --- a/test/controllers/api/commits_controller_test.rb +++ b/test/controllers/api/commits_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class CommitsControllerTest < ActionController::TestCase + class CommitsControllerTest < ApiControllerTestCase setup do @stack = shipit_stacks(:shipit) authenticate! diff --git a/test/controllers/api/deploys_controller_test.rb b/test/controllers/api/deploys_controller_test.rb index 770281582..c7ebbf02a 100644 --- a/test/controllers/api/deploys_controller_test.rb +++ b/test/controllers/api/deploys_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class DeploysControllerTest < ActionController::TestCase + class DeploysControllerTest < ApiControllerTestCase setup do authenticate! @user = shipit_users(:walrus) @@ -81,6 +81,7 @@ class DeploysControllerTest < ActionController::TestCase end assert_response :conflict + assert_json 'error', 'A task is already running.' end test "#create refuses to deploy unsuccessful commits if the require_ci flag is passed" do @@ -119,6 +120,29 @@ class DeploysControllerTest < ActionController::TestCase assert_response :accepted assert_json 'status', 'pending' end + + test "#create uses allow_concurrency param when provided" do + @stack.update!(lock_reason: 'Something broken') + + assert_difference -> { @stack.deploys.count }, 1 do + post :create, params: { stack_id: @stack.to_param, sha: @commit.sha, force: 'true', allow_concurrency: 'false' } + end + assert_response :accepted + assert_json 'status', 'pending' + refute @stack.deploys.last.allow_concurrency + end + + test "#create defaults allow_concurrency to force param when not provided" do + @stack.update!(lock_reason: 'Something broken') + expected_force = true + + assert_difference -> { @stack.deploys.count }, 1 do + post :create, params: { stack_id: @stack.to_param, sha: @commit.sha, force: expected_force } + end + assert_response :accepted + assert_json 'status', 'pending' + assert_equal expected_force, @stack.deploys.last.allow_concurrency + end end end end diff --git a/test/controllers/api/hooks_controller_test.rb b/test/controllers/api/hooks_controller_test.rb index 2d19780c8..a577f6a05 100644 --- a/test/controllers/api/hooks_controller_test.rb +++ b/test/controllers/api/hooks_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class HooksControllerTest < ActionController::TestCase + class HooksControllerTest < ApiControllerTestCase setup do authenticate! @stack = shipit_stacks(:shipit) diff --git a/test/controllers/api/locks_controller_test.rb b/test/controllers/api/locks_controller_test.rb index 3ba27867f..faf47aeb1 100644 --- a/test/controllers/api/locks_controller_test.rb +++ b/test/controllers/api/locks_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class LocksControllerTest < ActionController::TestCase + class LocksControllerTest < ApiControllerTestCase setup do authenticate! @stack = shipit_stacks(:shipit) diff --git a/test/controllers/api/merge_requests_controller_test.rb b/test/controllers/api/merge_requests_controller_test.rb index 59f7a8d59..7ed252cdd 100644 --- a/test/controllers/api/merge_requests_controller_test.rb +++ b/test/controllers/api/merge_requests_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class MergeRequestsControllerTest < ActionController::TestCase + class MergeRequestsControllerTest < ApiControllerTestCase setup do @stack = shipit_stacks(:shipit) @merge_request = shipit_merge_requests(:shipit_pending) diff --git a/test/controllers/api/outputs_controller_test.rb b/test/controllers/api/outputs_controller_test.rb index a4a730e40..a2c158e93 100644 --- a/test/controllers/api/outputs_controller_test.rb +++ b/test/controllers/api/outputs_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class OutputsControllerTest < ActionController::TestCase + class OutputsControllerTest < ApiControllerTestCase setup do @stack = shipit_stacks(:shipit) authenticate! diff --git a/test/controllers/api/release_statuses_controller_test.rb b/test/controllers/api/release_statuses_controller_test.rb index bccd076c0..7af530290 100644 --- a/test/controllers/api/release_statuses_controller_test.rb +++ b/test/controllers/api/release_statuses_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class ReleaseStatusesControllerTest < ActionController::TestCase + class ReleaseStatusesControllerTest < ApiControllerTestCase setup do authenticate! @stack = shipit_stacks(:shipit_canaries) diff --git a/test/controllers/api/rollback_controller_test.rb b/test/controllers/api/rollback_controller_test.rb index 60988db22..0f9af7be8 100644 --- a/test/controllers/api/rollback_controller_test.rb +++ b/test/controllers/api/rollback_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class RollbacksControllerTest < ActionController::TestCase + class RollbacksControllerTest < ApiControllerTestCase setup do authenticate! @user = shipit_users(:walrus) diff --git a/test/controllers/api/stacks_controller_test.rb b/test/controllers/api/stacks_controller_test.rb index 4148f5316..0586d221a 100644 --- a/test/controllers/api/stacks_controller_test.rb +++ b/test/controllers/api/stacks_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class StacksControllerTest < ActionController::TestCase + class StacksControllerTest < ApiControllerTestCase setup do authenticate! @stack = shipit_stacks(:shipit) @@ -114,6 +114,57 @@ class StacksControllerTest < ActionController::TestCase refute @stack.continuous_deployment end + test "#update does not perform archive when key is not provided" do + refute_predicate @stack, :archived? + refute_predicate @stack, :locked? + + patch :update, params: { id: @stack.to_param } + + @stack.reload + refute_predicate @stack, :archived? + refute_predicate @stack, :locked? + end + + test "#update does not perform unarchive when key is not provided" do + @stack.archive!(shipit_users(:walrus)) + assert_predicate @stack, :locked? + assert_predicate @stack, :archived? + + patch :update, params: { id: @stack.to_param } + + @stack.reload + assert_predicate @stack, :locked? + assert_predicate @stack, :archived? + end + + test "#update allows to archive the stack" do + refute_predicate @stack, :archived? + refute_predicate @stack, :locked? + + patch :update, params: { id: @stack.to_param, archived: true } + + @stack.reload + assert_predicate @stack, :locked? + assert_predicate @stack, :archived? + assert_instance_of AnonymousUser, @stack.lock_author + assert_equal "Archived", @stack.lock_reason + end + + test "#update allows to unarchive the stack" do + @stack.archive!(shipit_users(:walrus)) + assert_predicate @stack, :locked? + assert_predicate @stack, :archived? + + patch :update, params: { id: @stack.to_param, archived: false } + + @stack.reload + refute_predicate @stack, :archived? + refute_predicate @stack, :locked? + assert_nil @stack.locked_since + assert_nil @stack.lock_reason + assert_instance_of AnonymousUser, @stack.lock_author + end + test "#index returns a list of stacks" do stack = Stack.last get :index @@ -214,6 +265,15 @@ class StacksControllerTest < ActionController::TestCase end assert_response :accepted end + + test "#refresh queues a RefreshStatusesJob and RefreshCheckRunsJob" do + assert_enqueued_with(job: RefreshStatusesJob, args: [stack_id: @stack.id]) do + assert_enqueued_with(job: RefreshCheckRunsJob, args: [stack_id: @stack.id]) do + post :refresh, params: { id: @stack.to_param } + end + end + assert_response :accepted + end end end end diff --git a/test/controllers/api/tasks_controller_test.rb b/test/controllers/api/tasks_controller_test.rb index 4c0701378..4649e413d 100644 --- a/test/controllers/api/tasks_controller_test.rb +++ b/test/controllers/api/tasks_controller_test.rb @@ -3,7 +3,7 @@ module Shipit module Api - class TasksControllerTest < ActionController::TestCase + class TasksControllerTest < ApiControllerTestCase setup do @stack = shipit_stacks(:shipit) @user = shipit_users(:walrus) diff --git a/test/controllers/stacks_controller_test.rb b/test/controllers/stacks_controller_test.rb index 2a7c2fd15..7b8912937 100644 --- a/test/controllers/stacks_controller_test.rb +++ b/test/controllers/stacks_controller_test.rb @@ -10,7 +10,7 @@ class StacksControllerTest < ActionController::TestCase end test "validates that Shipit.github is present" do - Rails.application.secrets.stubs(:github).returns(nil) + Rails.application.credentials.stubs(:github).returns(nil) get :index assert_select "#github_app .missing" assert_select ".missing", count: 1 diff --git a/test/controllers/tasks_controller_test.rb b/test/controllers/tasks_controller_test.rb index adbd9355c..ff5fc08c2 100644 --- a/test/controllers/tasks_controller_test.rb +++ b/test/controllers/tasks_controller_test.rb @@ -11,9 +11,16 @@ class TasksControllerTest < ActionController::TestCase session[:user_id] = shipit_users(:walrus).id end - test "tasks defined in the shipit.yml can be displayed" do + test "tasks defined in the shipit.yml can be displayed with default variable values" do get :new, params: { stack_id: @stack, definition_id: @definition.id } assert_response :ok + assert_select 'input[name="task[env][FOO]"][value="1"]' + end + + test "it is possible to provide a default value override for a task" do + get :new, params: { stack_id: @stack, definition_id: @definition.id, FOO: '42' } + assert_response :ok + assert_select 'input[name="task[env][FOO]"][value="42"]' end test "tasks defined in the shipit.yml can't be triggered if the stack is being deployed" do diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index 99c43aac8..9a9a61c14 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -17,6 +17,7 @@ module Shipit class Application < Rails::Application - config.load_defaults 7.0 + config.load_defaults 7.1 + config.active_record.encryption.support_sha1_for_non_deterministic_encryption = true end end diff --git a/test/dummy/config/initializers/0_load_development_secrets.rb b/test/dummy/config/initializers/0_load_development_secrets.rb index 863be78d5..f3b83652b 100644 --- a/test/dummy/config/initializers/0_load_development_secrets.rb +++ b/test/dummy/config/initializers/0_load_development_secrets.rb @@ -2,8 +2,8 @@ if local_secrets.exist? secrets = YAML.load(local_secrets.read).deep_symbolize_keys if Rails.env.development? - Rails.application.secrets.deep_merge!(secrets) + Rails.application.credentials.deep_merge!(secrets) elsif Rails.env.test? - Rails.application.secrets.merge!(redis_url: secrets[:redis_url]) + Rails.application.credentials.merge!(redis_url: secrets[:redis_url]) end end diff --git a/test/dummy/config/secrets.development.json b/test/dummy/config/secrets.development.json new file mode 100644 index 000000000..2c22e2b66 --- /dev/null +++ b/test/dummy/config/secrets.development.json @@ -0,0 +1,3 @@ +{ + "secret_key_base": "s3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3t" +} \ No newline at end of file diff --git a/test/dummy/config/secrets.test.json b/test/dummy/config/secrets.test.json new file mode 100644 index 000000000..a08afdab4 --- /dev/null +++ b/test/dummy/config/secrets.test.json @@ -0,0 +1,21 @@ +{ + "host": "shipit.com", + "secret_key_base": "s3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3t", + "github_api": { + "token": "t0k3n" + }, + "github": { + "domain": null, + "app_id": 42, + "installation_id": 43, + "bot_login": "shipit[bot]", + "webhook_secret": null, + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA7iUQC2uUq/gtQg0gxtyaccuicYgmq1LUr1mOWbmwM1Cv63+S\n73qo8h87FX+YyclY5fZF6SMXIys02JOkImGgbnvEOLcHnImCYrWs03msOzEIO/pG\nM0YedAPtQ2MEiLIu4y8htosVxeqfEOPiq9kQgFxNKyETzjdIA9q1md8sofuJUmPv\nibacW1PecuAMnn+P8qf0XIDp7uh6noB751KvhCaCNTAPtVE9NZ18OmNG9GOyX/pu\npQHIrPgTpTG6KlAe3r6LWvemzwsMtuRGU+K+KhK9dFIlSE+v9rA32KScO8efOh6s\nGu3rWorV4iDu14U62rzEfdzzc63YL94sUbZxbwIDAQABAoIBADLJ8r8MxZtbhYN1\nu0zOFZ45WL6v09dsBfITvnlCUeLPzYUDIzoxxcBFittN6C744x3ARS6wjimw+EdM\nTZALlCSb/sA9wMDQzt7wchhz9Zh2H5RzDu+2f54sjDh38KqancdT8PO2fAFGxX/b\nqicOVyeZB9gv6MJtJc20olBbuXAeBNfcDABF9oxF+0i+Ssg7B4VXiqgcjtGbr/Og\nqRll7AqyTArVx2xEcVfZxeZ4zGnigzcJq4te7yYpxzwk+RxblkPh54Yt4WxZ+8DI\nRsn3r6ajlpwzpwvsJFU2Txq7xBTzGQMFmy/Pnjk83kP2cogxB2+tRyjITGqTwD8b\ngg9PFCkCgYEA+7u8A0l0Cz6p0SI6c7ftVePVRiIhpawWN7og/wEmI6zUjm/3rA+R\nhrhaVKuOD8QF/HdDsqTck5gjGAjTmJz6r33/cl1Tz+pr62znsrB4r0yMKvQbKN81\nWGaWOsi2+ZXqLNv5h5wpUF0MTKlXHeKnwP5kuEvGwVn6WURFCh6PhLMCgYEA8i5e\nJjulJVGyd5HuoY3xyO7E6DjidsqRnVRq+hYpORjnHvTmSwe4+tH4ha2p9Kv2Y6k3\nC1NYY/fSMQoYCCRaYyJleI+la/9tsZqAmtms4ZB8KhFmPHf9fW75i6G0xKWyZ8K+\nE2Ft/UaEiM282593cguV6+Kt5uExnyPxLLK4FlUCgYEAwRJ/JGI8/7bjFkTTYheq\nj5q75BufhOrU6471acAe2XPgXxLfefdC3Xodxh0CS3NESBvNL4Ikr4sbN37lk4Kq\n/th7iOKtuqUIeru/hZy2I3VpeDRbdGCmEJQ2GwYA2LKztg5Nd0Y9paaIHXAwIfrK\nQUqcQ4HTAk8ZpUeoUBeaaeMCgYANLmbjb9WiPVsYVPIHCwHA7PX8qbPxwT7BsGmO\nKQyfVfKmZa/vH4F67Vi4deZNMdrcO8aKMEQcVM2065a5QrlEsgeR00eupB1lUEJ1\nqylUsZeAdqf43JMIc7TTW77KATa/nQLZbTEeWus1wvTngztuEqFbUGAks9cOkVc8\nFpIcbQKBgQDVIL8gPLmn0f+4oLF8MBC+oxtKpz14X5iJ1saGFkzW5I+nIEskpS0S\nqtirnTCnJFGdCrFwctnxiuiCmyGwpBYdjIfHyvYAHnqAtMnESzCUyeSFZiquVW5W\nMvbMmDPoV27XOHU9kIq6NXtfrkpufiyo6/VEYWozXalxKLNuqLYfPQ==\n-----END RSA PRIVATE KEY-----\n", + "oauth": { + "id": "Iv1.bf2c2c45b449bfd9", + "secret": "ef694cd6e45223075d78d138ef014049052665f1", + "teams": null + } + }, + "redis_url": "redis://127.0.0.1:6379/7" +} \ No newline at end of file diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 2cea45691..22bbfaffb 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_11_03_154121) do +ActiveRecord::Schema.define(version: 2023_07_03_181143) do create_table "api_clients", force: :cascade do |t| t.text "permissions", limit: 65535 @@ -42,7 +42,7 @@ create_table "commit_deployment_statuses", force: :cascade do |t| t.integer "commit_deployment_id" t.string "status" - t.integer "github_id" + t.bigint "github_id" t.string "api_url" t.datetime "created_at", null: false t.datetime "updated_at", null: false diff --git a/test/helpers/api_helper.rb b/test/helpers/api_helper.rb index 909969dfa..45665a007 100644 --- a/test/helpers/api_helper.rb +++ b/test/helpers/api_helper.rb @@ -8,3 +8,16 @@ def authenticate!(client = @client || :spy) request.headers['Authorization'] = "Basic #{Base64.encode64(client.authentication_token)}" end end + +module Shipit + class ApiControllerTestCase < ActionController::TestCase + private + + def process(_action, **kwargs) + if kwargs[:method] != "GET" + kwargs[:as] ||= :json + end + super + end + end +end diff --git a/test/jobs/shipit/background_job_test.rb b/test/jobs/shipit/background_job_test.rb new file mode 100644 index 000000000..3c8be33b6 --- /dev/null +++ b/test/jobs/shipit/background_job_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +require 'test_helper' + +module Shipit + class BackgroundJobTest < ActiveSupport::TestCase + setup do + @stack = shipit_stacks(:shipit) + @last_commit = @stack.commits.last + @job = CacheDeploySpecJob.new + @user = shipit_users(:walrus) + end + + test "#perform retries on Octokit secondary rate limit exceptions" do + freeze_time do + Octokit::Forbidden.any_instance.expects(:response_headers) + .returns({ "Retry-After" => 45 }) + + Shipit.github.api.expects(:user).with(@user.github_id).raises(Octokit::TooManyRequests) + + assert_enqueued_with(job: BackgroundStubJob, at: Time.now + 45.seconds) do + BackgroundStubJob.perform_now(@user) + end + end + end + + class BackgroundStubJob < BackgroundJob + queue_as :default + + def perform(user) + Shipit.github.api.user(user.github_id) + end + end + end +end diff --git a/test/models/commit_deployment_status_test.rb b/test/models/commit_deployment_status_test.rb index 8bcbc5ab7..9d75fce59 100644 --- a/test/models/commit_deployment_status_test.rb +++ b/test/models/commit_deployment_status_test.rb @@ -15,7 +15,6 @@ class CommitDeploymentStatusTest < ActiveSupport::TestCase @author.github_api.class.any_instance.expects(:create_deployment_status).with( @deployment.api_url, 'in_progress', - accept: "application/vnd.github.flash-preview+json", target_url: "http://shipit.com/shopify/shipit-engine/production/deploys/#{@task.id}", description: "walrus triggered the deploy of shopify/shipit-engine/production to #{@deployment.short_sha}", environment_url: "https://shipit.shopify.com", diff --git a/test/models/commits_test.rb b/test/models/commits_test.rb index 0965167e5..55449bdc5 100644 --- a/test/models/commits_test.rb +++ b/test/models/commits_test.rb @@ -353,10 +353,11 @@ class CommitsTest < ActiveSupport::TestCase completed_at: Time.now, started_at: Time.now - 1.minute, ) - response = mock( + response = stub(rels: {}, data: mock( check_runs: [check_run], - ) - Shipit.github.api.expects(:check_runs).with(@stack.github_repo_name, @commit.sha).returns(response) + )) + Shipit.github.api.expects(:check_runs).with(@stack.github_repo_name, @commit.sha, per_page: 100).returns(response.data) + Shipit.github.api.expects(:last_response).returns(response) assert_difference -> { @commit.check_runs.count }, 1 do @commit.refresh_check_runs! diff --git a/test/models/merge_request_test.rb b/test/models/merge_request_test.rb index 249d6a5c6..a0140d4a6 100644 --- a/test/models/merge_request_test.rb +++ b/test/models/merge_request_test.rb @@ -125,7 +125,7 @@ class MergeRequestTest < ActiveSupport::TestCase created_at: 1.day.ago, )]) - Shipit.github.api.expects(:check_runs).with(@stack.github_repo_name, head_sha).returns(stub( + response = stub(rels: {}, data: stub( check_runs: [stub( id: 123456, name: 'check run', @@ -140,6 +140,8 @@ class MergeRequestTest < ActiveSupport::TestCase )] )) + Shipit.github.api.expects(:last_response).returns(response) + Shipit.github.api.expects(:check_runs).with(@stack.github_repo_name, head_sha, per_page: 100).returns(response.data) merge_request.refresh! assert_predicate merge_request, :mergeable? @@ -243,7 +245,10 @@ class MergeRequestTest < ActiveSupport::TestCase end test "status transitions emit hooks" do - job = assert_enqueued_with(job: EmitEventJob) do + expected_args = ->(job_args) do + job_args.first[:event] == 'merge' + end + job = assert_enqueued_with(job: EmitEventJob, args: expected_args) do @pr.reject!('merge_conflict') end params = job.arguments.first diff --git a/test/models/shipit/stacks_test.rb b/test/models/shipit/stack_test.rb similarity index 94% rename from test/models/shipit/stacks_test.rb rename to test/models/shipit/stack_test.rb index dbfe5bb9a..a65e82efe 100644 --- a/test/models/shipit/stacks_test.rb +++ b/test/models/shipit/stack_test.rb @@ -3,7 +3,7 @@ require 'securerandom' module Shipit - class StacksTest < ActiveSupport::TestCase + class StackTest < ActiveSupport::TestCase def setup @stack = shipit_stacks(:shipit) @expected_base_path = Rails.root.join('data', 'stacks', @stack.to_param).to_s @@ -278,6 +278,36 @@ def self.deliver(event, stack, payload) end end + test "#active_task? is false if stack has a concurrent deploy in active state" do + @stack.trigger_deploy(shipit_commits(:third), AnonymousUser.new, force: true) + refute @stack.active_task? + end + + test "#occupied? is false if stack has no deploy in either pending or running state" do + @stack.deploys.active.destroy_all + refute @stack.occupied? + end + + test "#occupied? is false if stack has no deploy at all" do + @stack.deploys.destroy_all + refute @stack.occupied? + end + + test "occupied? is true if stack has a concurrent deploy in active state" do + @stack.trigger_deploy(shipit_commits(:third), AnonymousUser.new, force: true) + assert @stack.occupied? + end + + test "occupied? is true if stack has a deploy in pending state" do + @stack.trigger_deploy(shipit_commits(:third), AnonymousUser.new) + assert @stack.occupied? + end + + test "#occupied? is true if a rollback is ongoing" do + shipit_deploys(:shipit_complete).trigger_rollback(AnonymousUser.new) + assert @stack.occupied? + end + test "#deployable? returns true if the stack is not locked, not awaiting provision, and is not deploying" do @stack.deploys.destroy_all @stack.update!(lock_reason: nil, awaiting_provision: false) @@ -674,12 +704,12 @@ def self.deliver(event, stack, payload) assert_equal shipit_commits(:fifth), @stack.next_commit_to_deploy end - test "#next_commit_to_deploy respects the deploy.max_commits directive" do + test "#next_commit_to_deploy respects the deploy.max_commits directive given the commit is deployable" do @stack.tasks.destroy_all - fifth_commit = shipit_commits(:third) - fifth_commit.statuses.create!(stack_id: @stack.id, state: 'success', context: 'ci/travis') - assert_predicate fifth_commit, :deployable? + third_commit = shipit_commits(:third) + third_commit.statuses.create!(stack_id: @stack.id, state: 'success', context: 'ci/travis') + assert_predicate third_commit, :deployable? assert_equal shipit_commits(:third), @stack.next_commit_to_deploy @@ -687,6 +717,19 @@ def self.deliver(event, stack, payload) assert_equal shipit_commits(:third), @stack.next_commit_to_deploy end + test "#next_commit_to_deploy deploys the first deployable commit when deploy.max_commits directive fails to find a deployable commit" do + @stack.tasks.destroy_all + + third_commit = shipit_commits(:third) + third_commit.statuses.create!(stack_id: @stack.id, state: 'success', context: 'ci/travis') + assert_predicate third_commit, :deployable? + + assert_equal shipit_commits(:third), @stack.next_commit_to_deploy + + @stack.expects(:maximum_commits_per_deploy).returns(1).at_least_once + assert_equal shipit_commits(:third), @stack.next_commit_to_deploy + end + test "setting #lock_reason also sets #locked_since" do assert_predicate @stack.locked_since, :nil? diff --git a/test/models/users_test.rb b/test/models/users_test.rb index 1cbe57296..b62083c05 100644 --- a/test/models/users_test.rb +++ b/test/models/users_test.rb @@ -203,6 +203,14 @@ class UsersTest < ActiveSupport::TestCase assert_equal 'george@cyclim.se', user.email end + test "#refresh_from_github! logs deleted users" do + Shipit.github.api.expects(:user).with(@user.github_id).raises(Octokit::Forbidden) + + Rails.logger.expects(:info).with("User #{@user.name}, github_id #{@user.github_id} has forbidden access to their GitHub, likely deleted.") + + @user.refresh_from_github! + end + test "#github_api uses the user's access token" do assert_equal @user.github_access_token, @user.github_api.access_token end diff --git a/test/test_helper.rb b/test/test_helper.rb index a5ba4068e..6c75d7f97 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,7 +23,7 @@ # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) + ActiveSupport::TestCase.fixture_paths << File.expand_path("../fixtures", __FILE__) ActiveSupport::TestCase.fixtures(:all) end @@ -71,7 +71,7 @@ class TestCase end end - ActiveRecord::Migration.check_pending! + ActiveRecord::Migration.check_all_pending! # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # diff --git a/test/unit/deploy_commands_test.rb b/test/unit/deploy_commands_test.rb index 5113706d1..d839ea501 100644 --- a/test/unit/deploy_commands_test.rb +++ b/test/unit/deploy_commands_test.rb @@ -21,6 +21,100 @@ def setup StackCommands.stubs(git_version: Gem::Version.new('1.8.4.3')) end + test "#fetch_commit calls git fetch if repository cache already exist" do + @stack.git_path.stubs(:exist?).returns(true) + @stack.git_path.stubs(:empty?).returns(false) + + command = @commands.fetch_commit(@deploy.until_commit) + + assert_equal %W(git fetch origin --quiet --tags #{@deploy.until_commit.sha}), command.args + end + + test "#fetch_commit calls git fetch in git_path directory if repository cache already exist" do + @stack.git_path.stubs(:exist?).returns(true) + @stack.git_path.stubs(:empty?).returns(false) + + command = @commands.fetch_commit(@deploy.until_commit) + + assert_equal @stack.git_path.to_s, command.chdir + end + + test "#fetch_commit calls git clone if repository cache do not exist" do + @stack.git_path.stubs(:exist?).returns(false) + + command = @commands.fetch_commit(@deploy.until_commit) + + expected = %W(git clone --quiet --single-branch --recursive --branch master #{@stack.repo_git_url} #{@stack.git_path}) + assert_equal expected, command.args.map(&:to_s) + end + + test "#fetch_commit calls git clone if repository cache is empty" do + @stack.git_path.stubs(:exist?).returns(true) + @stack.git_path.stubs(:empty?).returns(true) + + command = @commands.fetch_commit(@deploy.until_commit) + + expected = %W(git clone --quiet --single-branch --recursive --branch master #{@stack.repo_git_url} #{@stack.git_path}) + assert_equal expected, command.args + end + + test "#fetch_commit calls git clone if repository cache corrupt" do + @stack.git_path.stubs(:exist?).returns(true) + @stack.git_path.stubs(:empty?).returns(false) + StackCommands.any_instance.expects(:git_cmd_succeeds?) + .with(@stack.git_path) + .returns(false) + + command = @commands.fetch_commit(@deploy.until_commit) + + expected = %W(git clone --quiet --single-branch --recursive --branch master #{@stack.repo_git_url} #{@stack.git_path}) + assert_equal expected, command.args + end + + test "#fetch_commit clears a corrupted git stash before cloning" do + @stack.expects(:clear_git_cache!) + @stack.git_path.stubs(:exist?).returns(true) + @stack.git_path.stubs(:empty?).returns(false) + StackCommands.any_instance.expects(:git_cmd_succeeds?) + .with(@stack.git_path) + .returns(false) + + command = @commands.fetch_commit(@deploy.until_commit) + + expected = %W(git clone --quiet --single-branch --recursive --branch master #{@stack.repo_git_url} #{@stack.git_path}) + assert_equal expected, command.args + end + + test "#fetch_commit does not use --single-branch if git is outdated" do + @stack.git_path.stubs(:exist?).returns(false) + StackCommands.stubs(git_version: Gem::Version.new('1.7.2.30')) + + command = @commands.fetch_commit(@deploy.until_commit) + + expected = %W(git clone --quiet --recursive --branch master #{@stack.repo_git_url} #{@stack.git_path}) + assert_equal expected, command.args.map(&:to_s) + end + + test "#fetch_commit calls git fetch in base_path directory if repository cache do not exist" do + @stack.git_path.stubs(:exist?).returns(false) + + command = @commands.fetch_commit(@deploy.until_commit) + + assert_equal @stack.deploys_path.to_s, command.chdir + end + + test "#fetch_commit merges Shipit.env in ENVIRONMENT" do + Shipit.stubs(:env).returns("SPECIFIC_CONFIG" => 5) + command = @commands.fetch_commit(@deploy.until_commit) + assert_equal '5', command.env["SPECIFIC_CONFIG"] + end + + test "#env uses the correct Github token for a stack" do + Shipit.github(organization: 'shopify').stubs(:token).returns('aS3cr3Tt0kEn') + command = @commands.fetch_commit(@deploy.until_commit) + assert_equal 'aS3cr3Tt0kEn', command.env["GITHUB_TOKEN"] + end + test "#fetch calls git fetch if repository cache already exist" do @stack.git_path.stubs(:exist?).returns(true) @stack.git_path.stubs(:empty?).returns(false) @@ -109,12 +203,6 @@ def setup assert_equal '5', command.env["SPECIFIC_CONFIG"] end - test "#env uses the correct Github token for a stack" do - Shipit.github(organization: 'shopify').stubs(:token).returns('aS3cr3Tt0kEn') - command = @commands.fetch - assert_equal 'aS3cr3Tt0kEn', command.env["GITHUB_TOKEN"] - end - test "#clone clones the repository cache into the working directory" do commands = @commands.clone assert_equal 2, commands.size @@ -134,7 +222,11 @@ def setup test "#checkout checks out the deployed commit" do command = @commands.checkout(@deploy.until_commit) - assert_equal ['git', '-c', 'advice.detachedHead=false', 'checkout', @deploy.until_commit.sha], command.args + checkout_args = [ + 'git', '-c', 'advice.detachedHead=false', 'checkout', '--quiet', + @deploy.until_commit.sha, + ] + assert_equal checkout_args, command.args end test "#checkout checks out the deployed commit from the working directory" do diff --git a/test/unit/github_app_test.rb b/test/unit/github_app_test.rb index 5beda14d5..4e2aab2eb 100644 --- a/test/unit/github_app_test.rb +++ b/test/unit/github_app_test.rb @@ -202,7 +202,7 @@ def app(extra_config = {}) end def default_config - Rails.application.secrets.github.deep_dup + Rails.application.credentials.github.deep_dup end end end