Skip to content

Commit

Permalink
Introduce suspenders:ci generator
Browse files Browse the repository at this point in the history
Creates CI template to be run via [GitHub Actions][ga] based on a
[similar template][ci template] that will be generated in an upcoming
Release of Rails.

Raises if the application is not using PostgreSQL, since our CI template
assumes that adapter.

Because this generator can be run in an existing application, we add
conditional checks for some jobs. However, this generator is intended
to be run as part of our holistic `suspenders:install:web` which will be
introduced in #1152.

Once Rails is released to contain a CI template, we will need to
consider how we want to handle conflicts between its file and ours, but
for now, we do not need to worry about that.

[ga]: https://docs.github.com/en/actions
[ci template]: rails/rails#50508
  • Loading branch information
stevepolitodesign committed Mar 23, 2024
1 parent 52e99e9 commit 6904b03
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 0 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ Configures prerequisites. Currently Node.
bin/rails g suspenders:prerequisites
```

### CI

CI

```
bin/rails g suspenders:ci
```

## Contributing

See the [CONTRIBUTING] document.
Expand Down
50 changes: 50 additions & 0 deletions lib/generators/suspenders/ci_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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__)

def ci_files
empty_directory ".github/workflows"
template "ci.yml", ".github/workflows/ci.yml"
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
148 changes: 148 additions & 0 deletions lib/generators/templates/ci/ci.yml.tt
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions lib/suspenders/generators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
38 changes: 38 additions & 0 deletions test/generators/suspenders/ci_generator_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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 template" do
with_database "postgresql" do
run_generator

assert_file app_root(".github/workflows/ci.yml")
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.yml")
end
end
end

private

def restore_destination
remove_dir_if_exists ".github"
end
end
end
end
8 changes: 8 additions & 0 deletions test/suspenders/generators_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 12 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6904b03

Please sign in to comment.