diff --git a/NEWS.md b/NEWS.md index 7831295c6..1323a5fe5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,6 +16,7 @@ Unreleased * Introduce `suspenders:email` generator * Introduce `suspenders:testing` generator * Introduce `suspenders:prerequisites` generator +* Introduce `suspenders:ci` generator 20230113.0 (January, 13, 2023) diff --git a/README.md b/README.md index 4876d7085..f64559cdb 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,14 @@ Configures prerequisites. Currently Node. bin/rails g suspenders:prerequisites ``` +### CI + +Creates CI files for GitHub Actions. + +``` +bin/rails g suspenders:ci +``` + ## Contributing See the [CONTRIBUTING] document. diff --git a/lib/generators/suspenders/ci_generator.rb b/lib/generators/suspenders/ci_generator.rb new file mode 100644 index 000000000..eec4b0f48 --- /dev/null +++ b/lib/generators/suspenders/ci_generator.rb @@ -0,0 +1,52 @@ +module Suspenders + module Generators + class CiGenerator < Rails::Generators::Base + include Suspenders::Generators::DatabaseUnsupported + include Suspenders::Generators::Helpers + + source_root File.expand_path("../../templates/ci", __FILE__) + desc "Creates CI files for GitHub Actions" + + def ci_files + empty_directory ".github/workflows" + template "ci.yml", ".github/workflows/ci.yaml" + template "dependabot.yml", ".github/dependabot.yaml" + end + + private + + def scan_ruby? + has_gem? "bundler-audit" + end + + def scan_js? + File.exist?("bin/importmap") && using_node? + end + + def lint? + using_node? && has_gem?("standard") && has_yarn_script?("lint") + end + + def using_node? + File.exist? "package.json" + end + + def has_gem?(name) + Bundler.rubygems.find_name(name).any? + end + + def using_rspec? + File.exist? "spec" + end + + def has_yarn_script?(name) + return false if !using_node? + + content = File.read("package.json") + json = JSON.parse(content) + + json.dig("scripts", name) + end + end + end +end diff --git a/lib/generators/templates/ci/ci.yml.tt b/lib/generators/templates/ci/ci.yml.tt new file mode 100644 index 000000000..10d85a904 --- /dev/null +++ b/lib/generators/templates/ci/ci.yml.tt @@ -0,0 +1,148 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: +<%- if scan_ruby? -%> + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Scan for security vulnerabilities in Ruby dependencies + run: | + bin/rails bundle:audit:update + bin/rails bundle:audit +<% end -%> + +<%- if scan_js? -%> + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + + - name: Install modules + run: yarn install + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: | + bin/importmap audit + yarn audit +<% end -%> + +<%- if lint? -%> + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + + - name: Install modules + run: yarn install + + - name: Lint Ruby code for consistent style + run: bin/rails standard + + - name: Lint front-end code for consistent style + run: yarn lint +<% end -%> + + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 + + # redis: + # image: redis + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libvips postgresql-client libpq-dev + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + <%- if using_node? -%> + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + + - name: Install modules + run: yarn install + <%- end -%> + + - name: Run tests + env: + RAILS_ENV: test + DATABASE_URL: postgres://postgres:postgres@localhost:5432 + # REDIS_URL: redis://localhost:6379/0 + <%- if using_rspec? -%> + run: bin/rails db:setup spec + <%- else -%> + run: bin/rails db:setup test test:system + <%- end -%> + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + <%- if using_rspec? -%> + path: ${{ github.workspace }}/tmp/capybara + <%- else -%> + path: ${{ github.workspace }}/tmp/screenshots + <%- end -%> + if-no-files-found: ignore diff --git a/lib/generators/templates/ci/dependabot.yml b/lib/generators/templates/ci/dependabot.yml new file mode 100644 index 000000000..452ebb342 --- /dev/null +++ b/lib/generators/templates/ci/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/lib/suspenders/generators.rb b/lib/suspenders/generators.rb index cbefec2bf..a7228ae1a 100644 --- a/lib/suspenders/generators.rb +++ b/lib/suspenders/generators.rb @@ -44,5 +44,33 @@ def api_only_app? .match?(/^\s*config\.api_only\s*=\s*true/i) end end + + module DatabaseUnsupported + class Error < StandardError + def message + "This generator requires PostgreSQL" + end + end + + extend ActiveSupport::Concern + + included do + def raise_if_database_unsupported + if database_unsupported? + raise Suspenders::Generators::DatabaseUnsupported::Error + end + end + + private + + def database_unsupported? + configuration = File.read(Rails.root.join("config/database.yml")) + configuration = YAML.load(configuration, aliases: true) + adapter = configuration["default"]["adapter"] + + adapter != "postgresql" + end + end + end end end diff --git a/test/generators/suspenders/ci_generator_test.rb b/test/generators/suspenders/ci_generator_test.rb new file mode 100644 index 000000000..3d6f96011 --- /dev/null +++ b/test/generators/suspenders/ci_generator_test.rb @@ -0,0 +1,40 @@ +require "test_helper" +require "generators/suspenders/ci_generator" + +module Suspenders + module Generators + class CiGeneratorTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::CiGenerator + destination Rails.root + teardown :restore_destination + + test "generates CI files" do + with_database "postgresql" do + run_generator + + assert_file app_root(".github/workflows/ci.yaml") + assert_file app_root(".github/dependabot.yaml") + end + end + + test "raises if PostgreSQL is not the adapter" do + with_database "unsupported" do + assert_raises Suspenders::Generators::DatabaseUnsupported::Error, match: "This generator requires PostgreSQL" do + run_generator + + assert_no_file app_root(".github/workflows/ci.yaml") + assert_no_file app_root(".github/dependabot.yaml") + end + end + end + + private + + def restore_destination + remove_dir_if_exists ".github" + end + end + end +end diff --git a/test/suspenders/generators_test.rb b/test/suspenders/generators_test.rb index 3d7cccb62..5c2f56320 100644 --- a/test/suspenders/generators_test.rb +++ b/test/suspenders/generators_test.rb @@ -8,4 +8,12 @@ class APIAppUnsupportedTest < Suspenders::GeneratorsTest assert_equal expected, Suspenders::Generators::APIAppUnsupported::Error.new.message end end + + class DatabaseUnsupportedTest < Suspenders::GeneratorsTest + test "message returns a custom message" do + expected = "This generator requires PostgreSQL" + + assert_equal expected, Suspenders::Generators::DatabaseUnsupported::Error.new.message + end + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index b0670b416..c34022698 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -100,6 +100,18 @@ def with_test_suite(test_suite, &block) remove_dir_if_exists "spec" end + def with_database(database, &block) + backup_file "config/database.yml" + configuration = File.read app_root("config/database.yml") + configuration = YAML.load(configuration, aliases: true) + configuration["default"]["adapter"] = database + File.open(app_root("config/database.yml"), "w") { _1.write configuration.to_yaml } + + yield + ensure + restore_file "config/database.yml" + end + def backup_file(file) FileUtils.copy app_root(file), app_root("#{file}.bak") end