gnarails 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/gnarly.rb CHANGED
@@ -2,7 +2,7 @@
2
2
  # to look up files
3
3
  def source_paths
4
4
  Array(super) +
5
- [File.expand_path(File.dirname(__FILE__))]
5
+ [__dir__]
6
6
  end
7
7
 
8
8
  def create_gnarly_rails_app
@@ -16,14 +16,17 @@ def create_gnarly_rails_app
16
16
 
17
17
  add_gems
18
18
 
19
- run "bundle install"
19
+ run_bundle
20
20
 
21
21
  after_bundle do
22
22
  setup_testing
23
+ setup_binstubs
23
24
  setup_database
24
- setup_scss
25
+ setup_assets
25
26
  setup_gitignore
26
- setup_analysis
27
+ setup_linting
28
+ setup_pronto
29
+ setup_simplecov
27
30
  setup_environments
28
31
  setup_readme
29
32
  remove_dir "test"
@@ -35,28 +38,24 @@ end
35
38
 
36
39
  def add_gems
37
40
  gem_group :development, :test do
38
- gem 'axe-matchers'
39
- gem 'bullet'
40
- gem 'bundler-audit'
41
- gem 'capybara'
42
- gem 'dotenv-rails'
43
- gem 'factory_bot_rails'
44
- gem 'gnar-style', require: false
45
- gem 'launchy'
46
- gem 'lol_dba'
47
- gem 'okcomputer'
48
- gem 'pronto'
49
- gem 'pronto-brakeman', require: false
50
- gem 'pronto-rubocop', require: false
51
- gem 'pronto-scss', require: false
52
- gem 'pry-byebug'
53
- gem 'pry-rails'
54
- gem 'rspec-its'
55
- gem 'rspec-rails', '~> 3.7'
56
- gem 'scss_lint', require: false
57
- gem 'selenium-webdriver'
58
- gem 'shoulda-matchers'
59
- gem 'simplecov', require: false
41
+ gem "axe-core-capybara"
42
+ gem "axe-core-rspec"
43
+ gem "bullet"
44
+ gem "dotenv-rails"
45
+ gem "factory_bot_rails"
46
+ gem "gnar-style", require: false
47
+ gem "launchy"
48
+ gem "lol_dba"
49
+ gem "okcomputer"
50
+ gem "pronto"
51
+ gem "pronto-rubocop", require: false
52
+ gem "pry-byebug"
53
+ gem "pry-rails"
54
+ gem "rspec-its"
55
+ gem "rspec-rails", "~> 4"
56
+ gem "rubocop-rspec", require: false
57
+ gem "shoulda-matchers"
58
+ gem "simplecov", require: false
60
59
  end
61
60
  end
62
61
 
@@ -64,17 +63,26 @@ def setup_database
64
63
  remove_file "config/database.yml"
65
64
  copy_file "templates/database.yml", "config/database.yml"
66
65
  gsub_file "config/database.yml", "__application_name__", app_name
67
-
68
66
  gsub_file "Gemfile", /.*sqlite.*\n/, ""
69
67
  end
70
68
 
71
- def setup_scss
72
- run "mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss"
69
+ def setup_assets
70
+ run "yarn add esbuild-rails"
71
+ remove_file "esbuild.config.js"
72
+ copy_file "templates/esbuild.config.js", "esbuild.config.js"
73
+ run "npm set-script build 'node esbuild.config.js'"
73
74
  end
74
75
 
75
76
  def setup_gitignore
76
- remove_file ".gitignore"
77
- copy_file "templates/.gitignore", ".gitignore"
77
+ append_to_file ".gitignore" do
78
+ <<~GITIGNORE
79
+ # Ignore Byebug command history file.
80
+ .byebug_history
81
+
82
+ # Ignore output of simplecov
83
+ coverage
84
+ GITIGNORE
85
+ end
78
86
  end
79
87
 
80
88
  def setup_testing
@@ -115,7 +123,8 @@ end
115
123
  def system_tests_rails_helper_text
116
124
  <<~SYSTEM_TESTS
117
125
  require "capybara/rails"
118
- require "axe/rspec"
126
+ require "axe-rspec"
127
+ require "axe-capybara"
119
128
  require "selenium/webdriver"
120
129
  SYSTEM_TESTS
121
130
  end
@@ -188,27 +197,29 @@ def limit_test_logging
188
197
  end
189
198
  end
190
199
 
191
- def setup_analysis
192
- setup_linting
193
- setup_pronto
194
- setup_brakeman
195
- setup_simplecov
200
+ def setup_binstubs
201
+ remove_file "bin/setup"
202
+ copy_file "templates/bin/setup", "bin/setup"
203
+ run "chmod +x bin/setup"
204
+
205
+ copy_file "templates/bin/rspec", "bin/rspec"
206
+ run "chmod +x bin/rspec"
207
+
208
+ copy_file "templates/bin/rubocop", "bin/rubocop"
209
+ run "chmod +x bin/rubocop"
196
210
  end
197
211
 
198
212
  def setup_linting
199
213
  copy_file "templates/.rubocop.yml", ".rubocop.yml"
200
- copy_file "templates/.scss-lint.yml", ".scss-lint.yml"
201
214
  end
202
215
 
203
216
  def setup_pronto
204
217
  copy_file "templates/.pronto.yml", ".pronto.yml"
205
- copy_file "templates/bin/ci_pronto", "bin/ci_pronto"
206
- run "chmod +x bin/ci_pronto"
207
- end
208
218
 
209
- def setup_brakeman
210
- copy_file "templates/bin/brakeman", "bin/brakeman"
211
- run "chmod +x bin/brakeman"
219
+ copy_file "templates/bin/pronto", "bin/pronto"
220
+ run "chmod +x bin/pronto"
221
+
222
+ copy_file ".github/workflows/pronto.yml", ".github/workflows/pronto.yml"
212
223
  end
213
224
 
214
225
  def setup_simplecov
@@ -230,7 +241,7 @@ end
230
241
 
231
242
  def setup_environments
232
243
  setup_dotenv
233
- setup_ci
244
+ setup_github_workflows
234
245
  setup_docker
235
246
  setup_procfile
236
247
  configure_i18n
@@ -243,10 +254,12 @@ def setup_dotenv
243
254
  gsub_file ".env.test", "__application_name__", app_name
244
255
  end
245
256
 
246
- def setup_ci
247
- copy_file "templates/.circleci/config.yml", ".circleci/config.yml"
248
- gsub_file ".circleci/config.yml", "__ruby_version__", RUBY_VERSION
249
- gsub_file ".circleci/config.yml", "__application_name__", app_name
257
+ def setup_github_workflows
258
+ copy_file "templates/.github/workflows/run-tests.yml", ".github/workflows/run-tests.yml"
259
+ copy_file "templates/.github/workflows/brakeman.yml", ".github/workflows/brakeman.yml"
260
+
261
+ copy_file ".github/actions/test-rails/action.yml", ".github/actions/test-rails/action.yml"
262
+ copy_file ".github/workflows/bundler-audit.yml", ".github/workflows/bundler-audit.yml"
250
263
  end
251
264
 
252
265
  def setup_docker
@@ -305,16 +318,21 @@ def post_install_instructions
305
318
  puts "* Install ChromeDriver for default headless acceptance tests: brew cask install chromedriver"
306
319
  puts "* Follow the post-install instructions to set up circle to allow gnarbot to comment on PRs."
307
320
  puts " * https://github.com/TheGnarCo/gnarails#post-install"
321
+ puts "=========="
322
+ puts "* Make sure your package.json has the following scripts:"
323
+ puts "* \`\"build\": \"node esbuild.config.js\"\`"
324
+ puts "* \`\"build:css\": \"sass ./app/assets/stylesheets/application.sass.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules\"\`"
308
325
  end
309
326
 
310
327
  def format_ruby
311
- run "bundle exec rubocop --auto-correct"
328
+ run "bin/rubocop -A"
312
329
  end
313
330
 
314
331
  def completion_notification
315
332
  puts ""
316
333
  ascii_art
317
334
  post_install_instructions
335
+ puts ""
318
336
  end
319
337
 
320
338
  create_gnarly_rails_app
@@ -7,180 +7,52 @@ module Gnarails
7
7
 
8
8
  add_runtime_options!
9
9
 
10
- WEBPACKS = %w[react vue angular elm stimulus].freeze
11
-
12
- method_option :ruby,
13
- type: :string,
14
- aliases: "-r",
15
- default: Thor::Util.ruby_command,
16
- desc: "Path to the Ruby binary of your choice", banner: "PATH"
17
-
18
- method_option :skip_namespace,
19
- type: :boolean,
20
- default: false,
21
- desc: "Skip namespace (affects only isolated applications)"
22
-
23
- method_option :skip_yarn,
24
- type: :boolean,
25
- default: false,
26
- desc: "Don't use Yarn for managing JavaScript dependencies"
27
-
28
- method_option :skip_gemfile,
29
- type: :boolean,
30
- default: false,
31
- desc: "Don't create a Gemfile"
32
-
33
- method_option :skip_git,
34
- type: :boolean,
35
- aliases: "-G",
36
- default: false,
37
- desc: "Skip .gitignore file"
38
-
39
- method_option :skip_keeps,
40
- type: :boolean,
41
- default: false,
42
- desc: "Skip source control .keep files"
43
-
44
- method_option :skip_action_mailer,
45
- type: :boolean,
46
- aliases: "-M",
47
- default: false,
48
- desc: "Skip Action Mailer files"
49
-
50
- method_option :skip_active_record,
51
- type: :boolean,
52
- aliases: "-O",
53
- default: false,
54
- desc: "Skip Active Record files"
55
-
56
- method_option :skip_active_storage,
57
- type: :boolean,
58
- default: false,
59
- desc: "Skip Active Storage files"
60
-
61
- method_option :skip_puma,
62
- type: :boolean,
63
- aliases: "-P",
64
- default: false,
65
- desc: "Skip Puma related files"
66
-
67
- method_option :skip_action_cable,
68
- type: :boolean,
69
- aliases: "-C",
70
- default: false,
71
- desc: "Skip Action Cable files"
72
-
73
- method_option :skip_sprockets,
74
- type: :boolean,
75
- aliases: "-S",
76
- default: false,
77
- desc: "Skip Sprockets files"
78
-
79
- method_option :skip_spring,
80
- type: :boolean,
81
- default: false,
82
- desc: "Don't install Spring application preloader"
83
-
84
- method_option :skip_listen,
85
- type: :boolean,
86
- default: false,
87
- desc: "Don't generate configuration that depends on the listen gem"
88
-
89
- method_option :skip_coffee,
90
- type: :boolean,
91
- default: false,
92
- desc: "Don't use CoffeeScript"
93
-
94
- method_option :skip_javascript,
95
- type: :boolean,
96
- aliases: "-J",
97
- default: false,
98
- desc: "Skip JavaScript files"
99
-
100
- method_option :skip_turbolinks,
101
- type: :boolean,
102
- default: false,
103
- desc: "Skip turbolinks gem"
104
-
105
- method_option :skip_test,
106
- type: :boolean,
107
- aliases: "-T",
108
- default: false,
109
- desc: "Skip test files"
110
-
111
- method_option :skip_system_test,
112
- type: :boolean,
113
- default: false,
114
- desc: "Skip system test files"
115
-
116
- method_option :skip_bootsnap,
117
- type: :boolean,
118
- default: false,
119
- desc: "Skip bootsnap gem"
120
-
121
- method_option :dev,
122
- type: :boolean,
123
- default: false,
124
- desc: "Setup the application with Gemfile pointing to your Rails checkout"
125
-
126
- method_option :edge,
127
- type: :boolean,
128
- default: false,
129
- desc: "Setup the application with Gemfile pointing to Rails repository"
130
-
131
- method_option :rc,
132
- type: :string,
133
- default: nil,
134
- desc: "Path to file containing extra configuration options for rails command"
135
-
136
- method_option :no_rc,
137
- type: :boolean,
138
- default: false,
139
- desc: "Skip loading of extra configuration options from .railsrc file"
140
-
141
- method_option :api,
142
- type: :boolean,
143
- desc: "Preconfigure smaller stack for API only apps"
144
-
145
- method_option :skip_bundle,
146
- type: :boolean,
147
- aliases: "-B",
148
- default: false,
149
- desc: "Don't run bundle install"
150
-
151
- method_option :webpack,
152
- type: :string,
153
- default: nil,
154
- desc: "Preconfigure for app-like JavaScript with Webpack (options: #{WEBPACKS.join('/')})"
155
-
156
10
  desc "new APP_PATH [options]", "generate a gnarly rails app"
157
11
  long_desc <<-LONGDESC
158
12
  `gnarails new NAME` will create a new rails application called NAME,
159
13
  pre-built with the same helpful default configuration you can expect from
160
14
  any rails project built by The Gnar Company.
161
15
 
162
- By default, we pass arguments to `rails new` that skip test unit
163
- generation and use postgres instead of sqlite as the default database.
16
+ By default, we pass arguments to `rails new` that:
17
+ - skip test unit (We'll install rspec later)
18
+ - use postgres over SQLlite,
19
+ - use Propshaft,
20
+ - Bundle CSS with cssbundling (using sass)
21
+ - bundle JS with esbuild
164
22
 
165
23
  You should also be able to pass any other arguments you would expect to
166
- be able to when generating a new rails app.
24
+ be able to when generating a new rails app. Use `rails -h` for more
25
+ information.
167
26
  LONGDESC
168
27
  def new(name)
169
- Kernel.system "rails new #{name} #{cli_options(options)}"
28
+ Kernel.system command(name, options)
170
29
  end
171
30
 
31
+ DEFAULT_OPTIONS = [
32
+ "--asset-pipeline=propshaft",
33
+ "--skip-test-unit",
34
+ "--css=sass",
35
+ "--javascript=esbuild",
36
+ "--database=postgresql",
37
+ ].freeze
38
+
172
39
  no_tasks do
40
+ def command(name, options)
41
+ "rails new #{name} #{cli_options(options)}"
42
+ end
43
+
173
44
  def cli_options(options)
174
- options_string = "-m #{Gnarails.template_file} --skip-test-unit --database=postgresql"
45
+ options_string = "-m #{Gnarails.template_file} " + DEFAULT_OPTIONS.join(" ")
175
46
  options.each_with_object(options_string) do |(k, v), str|
176
47
  str << cli_option(k, v)
177
48
  end
178
49
  end
179
50
 
180
51
  def cli_option(key, value)
181
- if value == false
52
+ case value
53
+ when false
182
54
  ""
183
- elsif value == true
55
+ when true
184
56
  " --#{key}"
185
57
  else
186
58
  " --#{key}=#{value}"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gnarails
2
- VERSION = "3.0.0"
4
+ VERSION = "3.0.1"
3
5
  end
@@ -0,0 +1,22 @@
1
+ name: Brakeman
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v3
10
+
11
+ - name: Setup Ruby
12
+ uses: ruby/setup-ruby@v1
13
+ with:
14
+ bundler-cache: true
15
+
16
+ - name: Brakeman
17
+ run: |
18
+ gem install brakeman --no-document
19
+ brakeman --exit-on-warn --separate-models -o tmp/brakeman.html -o tmp/brakeman.text .
20
+ brakeman_exit_code=$?
21
+ cat tmp/brakeman.text
22
+ exit $brakeman_exit_code
@@ -0,0 +1,26 @@
1
+ name: Run Tests
2
+ on: [push]
3
+
4
+ jobs:
5
+ run-tests:
6
+ runs-on: ubuntu-latest
7
+ env:
8
+ RAILS_ENV: test
9
+ DATABASE_PASSWORD: password
10
+ services:
11
+ postgres:
12
+ image: postgres:14
13
+ env:
14
+ POSTGRES_PASSWORD: password
15
+ ports: ["5432:5432"]
16
+ options:
17
+ --health-cmd pg_isready
18
+ --health-interval 10s
19
+ --health-timeout 5s
20
+ --health-retries 5
21
+ steps:
22
+ - name: Checkout code
23
+ uses: actions/checkout@v3
24
+
25
+ - name: Test
26
+ uses: ./.github/actions/test-rails
data/templates/README.md CHANGED
@@ -4,10 +4,9 @@
4
4
 
5
5
  ```sh
6
6
  $ bundle install
7
- $ bundle exec rake db:create
8
- $ bundle exec rake db:migrate
9
- $ bundle exec rake db:seed
10
- $ bundle exec rails s
7
+ $ yarn install
8
+ $ bin/setup
9
+ $ bin/dev
11
10
  $ open http://localhost:3000
12
11
  ```
13
12
 
@@ -64,7 +63,7 @@ $ docker-compose up
64
63
 
65
64
  To run rspec:
66
65
  ```sh
67
- $ docker-compose run web bundle exec rspec
66
+ $ docker-compose run web ./bin/rspec
68
67
  ```
69
68
 
70
69
  ### Using pry with Docker
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'pronto' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("bundle", __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("pronto", "pronto")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("bundle", __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("bundle", __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ require "fileutils"
3
+
4
+ # path to your application root.
5
+ APP_ROOT = File.expand_path("..", __dir__)
6
+
7
+ def system!(*args)
8
+ system(*args) || abort("\n== Command #{args} failed ==")
9
+ end
10
+
11
+ FileUtils.chdir APP_ROOT do
12
+ # This script is a way to set up or update your development environment automatically.
13
+ # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14
+ # Add necessary setup steps to this file.
15
+
16
+ puts "== Installing dependencies =="
17
+ system! "gem install bundler --conservative"
18
+ system("bundle check") || system!("bundle install")
19
+
20
+ system! "yarn install --pure-lockfile"
21
+
22
+ puts "\n== Preparing database =="
23
+ system! "bin/rails db:prepare"
24
+
25
+ puts "\n== Removing old logs and tempfiles =="
26
+ system! "bin/rails log:clear tmp:clear"
27
+
28
+ puts "\n== Restarting application server =="
29
+ system! "bin/rails restart"
30
+ end
@@ -4,7 +4,7 @@ services:
4
4
  image: postgres
5
5
  web:
6
6
  build: .
7
- command: bash -c 'rm -f tmp/pids/server.pid && bundle exec rails s --port 3000 --binding 0.0.0.0'
7
+ command: bash -c 'rm -f tmp/pids/server.pid && bin/dev --binding 0.0.0.0'
8
8
  volumes:
9
9
  - .:/app
10
10
  ports:
@@ -0,0 +1,11 @@
1
+ const path = require('path')
2
+ const rails = require('esbuild-rails')
3
+
4
+ require('esbuild').build({
5
+ entryPoints: ['application.js'],
6
+ bundle: true,
7
+ outdir: path.join(process.cwd(), 'app/assets/builds'),
8
+ absWorkingDir: path.join(process.cwd(), 'app/javascript'),
9
+ watch: process.argv.includes('--watch'),
10
+ plugins: [rails()],
11
+ }).catch(() => process.exit(1))
@@ -4,8 +4,8 @@
4
4
  <title>RailsTestApp</title>
5
5
  <%= csrf_meta_tags %>
6
6
 
7
- <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
8
- <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
7
+ <%= stylesheet_link_tag "application" %>
8
+ <%= javascript_include_tag "application", defer: true %>
9
9
  </head>
10
10
 
11
11
  <body>
@@ -5,17 +5,17 @@ RSpec.feature "Viewing all job postings", type: :system do
5
5
  create_list :job_posting, 1
6
6
  visit job_postings_path
7
7
 
8
- expect(page).to be_accessible
8
+ expect(page).to be_axe_clean
9
9
  end
10
10
 
11
11
  scenario "N+1 query proteection" do
12
12
  job_posting = create :job_posting
13
- job_posting.comments.create(body: "first comment")
14
- job_posting.comments.create(body: "second comment")
13
+ job_posting.comments.create!(body: "first comment")
14
+ job_posting.comments.create!(body: "second comment")
15
15
 
16
16
  another_posting = create :job_posting
17
- another_posting.comments.create(body: "third comment")
18
- another_posting.comments.create(body: "fourth comment")
17
+ another_posting.comments.create!(body: "third comment")
18
+ another_posting.comments.create!(body: "fourth comment")
19
19
 
20
20
  expect { visit job_postings_path }
21
21
  .to raise_error Bullet::Notification::UnoptimizedQueryError
@@ -25,10 +25,6 @@ RSpec.feature "Viewing all job postings", type: :system do
25
25
  posting = create :job_posting
26
26
  visit job_postings_path
27
27
 
28
- expect(has_job_posting?(posting)).to be true
29
- end
30
-
31
- def has_job_posting?(posting)
32
28
  within(".job-posting-#{posting.id}") do
33
29
  expect(page).to have_css("td.posting-title", text: posting.title)
34
30
  end