diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9d0b5be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + ruby-version: ['3.2', '3.3', '3.4', '4.0'] + rails-version: ['7.0', '7.1', '7.2', '8.1'] + + name: Ruby ${{ matrix.ruby-version }} / Rails ${{ matrix.rails-version }} + + env: + RAILS_VERSION: ${{ matrix.rails-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby ${{ matrix.ruby-version }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + cache-version: rails-${{ matrix.rails-version }} + + - name: Fix default gem directory permissions + run: | + gem_dir=$(ruby -e 'puts Gem.default_dir') + if [ -d "$gem_dir/gems" ]; then + chmod +t "$gem_dir/gems" 2>/dev/null || true + fi + + - name: Run tests + run: bundle exec rspec diff --git a/.gitignore b/.gitignore index 5c249f1..ec3eef7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ .rspec_status Gemfile.lock +/**/CLAUDE.md +mise.toml diff --git a/Gemfile b/Gemfile index 013bdf1..edfad56 100644 --- a/Gemfile +++ b/Gemfile @@ -6,3 +6,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in bankai.gemspec gemspec + +if ENV['RAILS_VERSION'] + gem 'rails', "~> #{ENV['RAILS_VERSION']}.0" +end diff --git a/lib/bankai/generator.rb b/lib/bankai/generator.rb index 335c410..49018da 100644 --- a/lib/bankai/generator.rb +++ b/lib/bankai/generator.rb @@ -82,7 +82,7 @@ def setup_dotfiles def generate_default run('bundle binstubs bundler') - Bundler.with_unbundled_env do + Bundler.with_original_env do generate('bankai:testing') unless options[:skip_rspec] generate('bankai:ci', options.api? ? '--api' : '') generate('bankai:json') @@ -109,7 +109,7 @@ def self.banner protected def rails_command(command, command_options = {}) - Bundler.with_unbundled_env { super } + Bundler.with_original_env { super } end # rubocop:disable Naming/AccessorMethodName diff --git a/lib/bankai/helper.rb b/lib/bankai/helper.rb index b372ec6..82f816e 100644 --- a/lib/bankai/helper.rb +++ b/lib/bankai/helper.rb @@ -10,7 +10,7 @@ def pg? end def mysql? - gemfile.match?(/gem .mysql2./) + gemfile.match?(/gem .(mysql2|trilogy)./) end def capistrano? diff --git a/lib/bankai/version.rb b/lib/bankai/version.rb index 27a00be..c2755c7 100644 --- a/lib/bankai/version.rb +++ b/lib/bankai/version.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module Bankai - VERSION = '0.13.4' - RUBY_VERSION = '2.6.7' + VERSION = '0.14.0' RAILS_VERSION = '7.0.0' RUBOCOP_VERSION = '1.24.1' CAPISTRANO_VERSION = '3.16.0' diff --git a/spec/bankai/generator_spec.rb b/spec/bankai/generator_spec.rb new file mode 100644 index 0000000..c915f29 --- /dev/null +++ b/spec/bankai/generator_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'tmpdir' +require 'fileutils' +require 'open3' + +RSpec.describe Bankai::Generator, :slow do + before(:all) do + @tmpdir = Dir.mktmpdir('bankai_test_') + @project_path = File.join(@tmpdir, 'testapp') + + gem_root = File.expand_path('../..', __dir__) + bankai_bin = File.join(gem_root, 'exe', 'bankai') + + output, status = Open3.capture2e( + 'ruby', bankai_bin, 'testapp', + '--database=sqlite3', + '--skip-rspec', + "--path=#{gem_root}", + chdir: @tmpdir + ) + unless status.success? + diag = "Ruby: #{RUBY_VERSION}, Bundler: #{Bundler::VERSION}\n" + diag += "GEM_HOME: #{ENV['GEM_HOME']}\n" + diag += "Gem.default_dir: #{Gem.default_dir}\n" + diag += "BUNDLE_GEMFILE: #{ENV['BUNDLE_GEMFILE']}\n" + diag += "Gemfile.lock exists: #{File.exist?(File.join(@project_path, 'Gemfile.lock'))}\n" + diag += ".bundle/config exists: #{File.exist?(File.join(@project_path, '.bundle', 'config'))}\n" + raise "Generator failed (exit #{status.exitstatus}):\n\n--- Diagnostics ---\n#{diag}\n--- Output (last 80 lines) ---\n#{output.lines.last(80).join}" + end + end + + after(:all) do + FileUtils.remove_entry(@tmpdir) if @tmpdir + end + + def project_file(*path) + File.join(@project_path, *path) + end + + def read_project_file(*path) + File.read(project_file(*path)) + end + + describe 'Gemfile' do + subject(:gemfile) { read_project_file('Gemfile') } + + it 'includes rails' do + expect(gemfile).to match(/gem ['"]rails['"]/) + end + + it 'includes database adapter' do + expect(gemfile).to match(/gem ['"]sqlite3['"]/) + end + + it 'includes bankai' do + expect(gemfile).to match(/gem ['"]bankai['"]/) + end + end + + describe 'static files' do + it 'generates README.md' do + expect(File).to exist(project_file('README.md')) + end + + it 'generates .gitignore' do + expect(File).to exist(project_file('.gitignore')) + end + + it 'generates .env.example' do + expect(File).to exist(project_file('.env.example')) + end + + it 'generates .ctags' do + expect(File).to exist(project_file('.ctags')) + end + + it 'generates .gitlab-ci.yml' do + expect(File).to exist(project_file('.gitlab-ci.yml')) + end + + it 'generates .overcommit.yml' do + expect(File).to exist(project_file('.overcommit.yml')) + end + + it 'generates rack_mini_profiler initializer' do + expect(File).to exist(project_file('config', 'initializers', 'rack_mini_profiler.rb')) + end + + it 'generates oj initializer' do + expect(File).to exist(project_file('config', 'initializers', 'oj.rb')) + end + end + + describe 'configuration injection' do + it 'configures generators with rspec in application.rb' do + content = read_project_file('config', 'application.rb') + expect(content).to include('test_framework :rspec') + end + + it 'configures quiet assets in application.rb' do + content = read_project_file('config', 'application.rb') + expect(content).to include('config.assets.quiet = true') + end + + it 'configures puma-dev host in development.rb' do + content = read_project_file('config', 'environments', 'development.rb') + expect(content).to include('.test') + end + + it 'configures Bullet in development.rb' do + content = read_project_file('config', 'environments', 'development.rb') + expect(content).to include('Bullet.enable') + end + + it 'configures letter_opener in development.rb' do + content = read_project_file('config', 'environments', 'development.rb') + expect(content).to include('letter_opener') + end + + it 'clears db/seeds.rb' do + expect(read_project_file('db', 'seeds.rb')).to be_empty + end + end + + describe 'directory structure' do + %w[ + spec/lib/.keep + spec/controllers/.keep + spec/helpers/.keep + spec/support/matchers/.keep + ].each do |path| + it "creates #{path}" do + expect(File).to exist(project_file(path)) + end + end + end +end diff --git a/spec/bankai/helper_spec.rb b/spec/bankai/helper_spec.rb new file mode 100644 index 0000000..2f49f4f --- /dev/null +++ b/spec/bankai/helper_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +RSpec.describe Bankai::Helper do + let(:test_instance) do + klass = Class.new do + include Bankai::Helper + + attr_accessor :destination_root + + public :pg?, :mysql?, :capistrano? + end + klass.new + end + + before do + test_instance.destination_root = '/tmp/fake_app' + end + + describe '#pg?' do + it 'returns true when Gemfile contains pg' do + allow(File).to receive(:read).with('/tmp/fake_app/Gemfile').and_return("gem 'pg'") + expect(test_instance.pg?).to be true + end + + it 'returns false when Gemfile contains mysql2' do + allow(File).to receive(:read).with('/tmp/fake_app/Gemfile').and_return("gem 'mysql2'") + expect(test_instance.pg?).to be false + end + end + + describe '#mysql?' do + it 'returns true when Gemfile contains mysql2' do + allow(File).to receive(:read).with('/tmp/fake_app/Gemfile').and_return("gem 'mysql2'") + expect(test_instance.mysql?).to be true + end + + it 'returns true when Gemfile contains trilogy' do + allow(File).to receive(:read).with('/tmp/fake_app/Gemfile').and_return("gem 'trilogy'") + expect(test_instance.mysql?).to be true + end + + it 'returns false when Gemfile contains pg' do + allow(File).to receive(:read).with('/tmp/fake_app/Gemfile').and_return("gem 'pg'") + expect(test_instance.mysql?).to be false + end + end + + describe '#capistrano?' do + it 'returns true when Gemfile contains capistrano' do + allow(File).to receive(:read).with('/tmp/fake_app/Gemfile').and_return("gem 'capistrano'") + expect(test_instance.capistrano?).to be true + end + + it 'returns false when Gemfile does not contain capistrano' do + allow(File).to receive(:read).with('/tmp/fake_app/Gemfile').and_return("gem 'pg'") + expect(test_instance.capistrano?).to be false + end + end +end diff --git a/spec/bankai/version_spec.rb b/spec/bankai/version_spec.rb new file mode 100644 index 0000000..379503b --- /dev/null +++ b/spec/bankai/version_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.describe Bankai do + describe 'VERSION' do + it 'is defined' do + expect(Bankai::VERSION).not_to be_nil + end + + it 'follows semver format' do + expect(Bankai::VERSION).to match(/\A\d+\.\d+\.\d+\z/) + end + end + + describe 'RAILS_VERSION' do + it 'is defined' do + expect(Bankai::RAILS_VERSION).not_to be_nil + end + + it 'follows semver format' do + expect(Bankai::RAILS_VERSION).to match(/\A\d+\.\d+\.\d+\z/) + end + end + + describe 'RUBOCOP_VERSION' do + it 'is defined' do + expect(Bankai::RUBOCOP_VERSION).not_to be_nil + end + + it 'follows semver format' do + expect(Bankai::RUBOCOP_VERSION).to match(/\A\d+\.\d+\.\d+\z/) + end + end + + describe 'CAPISTRANO_VERSION' do + it 'is defined' do + expect(Bankai::CAPISTRANO_VERSION).not_to be_nil + end + + it 'follows semver format' do + expect(Bankai::CAPISTRANO_VERSION).to match(/\A\d+\.\d+\.\d+\z/) + end + end +end diff --git a/spec/bankai_spec.rb b/spec/bankai_spec.rb deleted file mode 100644 index b9126fe..0000000 --- a/spec/bankai_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Bankai do - it 'has a version number' do - expect(Bankai::VERSION).not_to be nil - end -end diff --git a/templates/gitlab-ci.yml.erb b/templates/gitlab-ci.yml.erb index fb96503..ba278fb 100644 --- a/templates/gitlab-ci.yml.erb +++ b/templates/gitlab-ci.yml.erb @@ -8,10 +8,6 @@ stages: variables: RAILS_ENV: test -<%- unless options.api? -%> - NODE_VERSION: 12.13.1 -<%- end -%> - BUNDLER_VERSION: 2.2.26 <%- if pg? -%> POSTGRES_DB: <%= app_name %> POSTGRES_PASSWORD: postgres @@ -26,14 +22,7 @@ variables: <%- end -%> .install_ruby_gems: &install_ruby_gems - - gem install bundler -v ${BUNDLER_VERSION} - bundle install --path vendor -<% unless options.api? %> -.install_nodejs: &install_nodejs - - curl -SLO https://nodejs.org/dist/v$NODE_VERSION/node-v${NODE_VERSION}-linux-x64.tar.xz && tar -xJf node-v${NODE_VERSION}-linux-x64.tar.xz -C /usr/local --strip-components=1; - - curl -o- -L https://yarnpkg.com/install.sh | bash - - export PATH=$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH -<% end -%> .common: before_script: @@ -44,14 +33,8 @@ variables: key: files: - Gemfile.lock -<%- unless options.api? -%> - - yarn.lock -<%- end -%> paths: - vendor/ruby -<% unless options.api? %> - - node_modules -<% end -%> rubocop: extends: .common @@ -62,14 +45,11 @@ rubocop: - if: $CI_MERGE_REQUEST_ID brakeman: - image: registry.gitlab.com/gitlab-org/security-products/analyzers/brakeman:2 + extends: .common stage: lint allow_failure: true script: - - /analyzer run - artifacts: - reports: - sast: gl-sast-report.json + - bundle exec brakeman -q --no-pager rules: - if: $CI_MERGE_REQUEST_ID @@ -77,9 +57,7 @@ bundler-audit: extends: .common stage: lint script: - - gem install bundler-audit - - bundle audit --update - - bundle audit + - bundle exec bundler-audit --update rules: - if: $CI_MERGE_REQUEST_ID - if: $CI_PIPELINE_SOURCE == 'schedule' @@ -95,36 +73,17 @@ bundler-leak: rules: - if: $CI_MERGE_REQUEST_ID - if: $CI_PIPELINE_SOURCE == 'schedule' -<% unless options.api? %> -yarn-audit: - extends: .common - stage: lint - before_script: - - *install_nodejs - script: - - yarn audit - rules: - - if: $CI_MERGE_REQUEST_ID - - if: $CI_PIPELINE_SOURCE == 'schedule' - allow_failure: true -<% end -%> rspec: extends: .common stage: test - before_script: - - *install_ruby_gems -<%- unless options.api? -%> - - *install_nodejs - - yarn install -<%- end -%> services: <%- if pg? -%> - - postgres:12-alpine + - postgres:16-alpine <%- end -%> <%- if mysql? -%> - - name: mysql:5.7 - command: ['mysqld', '--character-set-server=utf8', '--collation-server=utf8_unicode_ci'] + - name: mysql:8.0 + command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'] <%- end -%> script: - bundle exec rake db:migrate