Skip to content

Commit 1e41e64

Browse files
Introduce suspenders:ci generator
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 --- **To Do** - [ ] Consider returning early and raising if the application is not using PostgreSQL.
1 parent 52e99e9 commit 1e41e64

File tree

7 files changed

+285
-0
lines changed

7 files changed

+285
-0
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Unreleased
1616
* Introduce `suspenders:email` generator
1717
* Introduce `suspenders:testing` generator
1818
* Introduce `suspenders:prerequisites` generator
19+
* Introduce `suspenders:ci` generator
1920

2021
20230113.0 (January, 13, 2023)
2122

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ Configures prerequisites. Currently Node.
188188
bin/rails g suspenders:prerequisites
189189
```
190190

191+
### CI
192+
193+
CI
194+
195+
```
196+
bin/rails g suspenders:ci
197+
```
198+
191199
## Contributing
192200

193201
See the [CONTRIBUTING] document.
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
module Suspenders
2+
module Generators
3+
class CiGenerator < Rails::Generators::Base
4+
include Suspenders::Generators::DatabaseUnsupported
5+
include Suspenders::Generators::Helpers
6+
7+
source_root File.expand_path("../../templates/ci", __FILE__)
8+
9+
def ci_files
10+
empty_directory ".github/workflows"
11+
template "ci.yml", ".github/workflows/ci.yml"
12+
end
13+
14+
private
15+
16+
def scan_ruby?
17+
has_gem? "bundler-audit"
18+
end
19+
20+
def scan_js?
21+
File.exist?("bin/importmap") && using_node?
22+
end
23+
24+
def lint?
25+
using_node? && has_gem?("standard") && has_yarn_script?("lint")
26+
end
27+
28+
def using_node?
29+
File.exist? "package.json"
30+
end
31+
32+
def has_gem?(name)
33+
Bundler.rubygems.find_name(name).any?
34+
end
35+
36+
def using_rspec?
37+
File.exist? "spec"
38+
end
39+
40+
def has_yarn_script?(name)
41+
return false if !using_node?
42+
43+
content = File.read("package.json")
44+
json = JSON.parse(content)
45+
46+
json.dig("scripts", name)
47+
end
48+
end
49+
end
50+
end

lib/generators/templates/ci/ci.yml.tt

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
jobs:
9+
<%- if scan_ruby? -%>
10+
scan_ruby:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Ruby
18+
uses: ruby/setup-ruby@v1
19+
with:
20+
ruby-version: .ruby-version
21+
bundler-cache: true
22+
23+
- name: Scan for security vulnerabilities in Ruby dependencies
24+
run: |
25+
bin/rails bundle:audit:update
26+
bin/rails bundle:audit
27+
<% end -%>
28+
29+
<%- if scan_js? -%>
30+
scan_js:
31+
runs-on: ubuntu-latest
32+
33+
steps:
34+
- name: Checkout code
35+
uses: actions/checkout@v4
36+
37+
- name: Set up Ruby
38+
uses: ruby/setup-ruby@v1
39+
with:
40+
ruby-version: .ruby-version
41+
bundler-cache: true
42+
43+
- name: Setup Node
44+
uses: actions/setup-node@v4
45+
with:
46+
node-version-file: .node-version
47+
48+
- name: Install modules
49+
run: yarn install
50+
51+
- name: Scan for security vulnerabilities in JavaScript dependencies
52+
run: |
53+
bin/importmap audit
54+
yarn audit
55+
<% end -%>
56+
57+
<%- if lint? -%>
58+
lint:
59+
runs-on: ubuntu-latest
60+
steps:
61+
- name: Checkout code
62+
uses: actions/checkout@v4
63+
64+
- name: Set up Ruby
65+
uses: ruby/setup-ruby@v1
66+
with:
67+
ruby-version: .ruby-version
68+
bundler-cache: true
69+
70+
- name: Setup Node
71+
uses: actions/setup-node@v4
72+
with:
73+
node-version-file: .node-version
74+
75+
- name: Install modules
76+
run: yarn install
77+
78+
- name: Lint Ruby code for consistent style
79+
run: bin/rails standard
80+
81+
- name: Lint front-end code for consistent style
82+
run: yarn lint
83+
<% end -%>
84+
85+
test:
86+
runs-on: ubuntu-latest
87+
88+
services:
89+
postgres:
90+
image: postgres
91+
env:
92+
POSTGRES_USER: postgres
93+
POSTGRES_PASSWORD: postgres
94+
ports:
95+
- 5432:5432
96+
options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3
97+
98+
# redis:
99+
# image: redis
100+
# ports:
101+
# - 6379:6379
102+
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
103+
104+
steps:
105+
- name: Install packages
106+
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libvips postgresql-client libpq-dev
107+
108+
- name: Checkout code
109+
uses: actions/checkout@v4
110+
111+
- name: Set up Ruby
112+
uses: ruby/setup-ruby@v1
113+
with:
114+
ruby-version: .ruby-version
115+
bundler-cache: true
116+
117+
<%- if using_node? -%>
118+
- name: Setup Node
119+
uses: actions/setup-node@v4
120+
with:
121+
node-version-file: .node-version
122+
123+
- name: Install modules
124+
run: yarn install
125+
<%- end -%>
126+
127+
- name: Run tests
128+
env:
129+
RAILS_ENV: test
130+
DATABASE_URL: postgres://postgres:postgres@localhost:5432
131+
# REDIS_URL: redis://localhost:6379/0
132+
<%- if using_rspec? -%>
133+
run: bin/rails db:setup spec
134+
<%- else -%>
135+
run: bin/rails db:setup test test:system
136+
<%- end -%>
137+
138+
- name: Keep screenshots from failed system tests
139+
uses: actions/upload-artifact@v4
140+
if: failure()
141+
with:
142+
name: screenshots
143+
<%- if using_rspec? -%>
144+
path: ${{ github.workspace }}/tmp/capybara
145+
<%- else -%>
146+
path: ${{ github.workspace }}/tmp/screenshots
147+
<%- end -%>
148+
if-no-files-found: ignore

lib/suspenders/generators.rb

+28
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,33 @@ def api_only_app?
4444
.match?(/^\s*config\.api_only\s*=\s*true/i)
4545
end
4646
end
47+
48+
module DatabaseUnsupported
49+
class Error < StandardError
50+
def message
51+
"This generator requires PostgreSQL"
52+
end
53+
end
54+
55+
extend ActiveSupport::Concern
56+
57+
included do
58+
def raise_if_database_unsupported
59+
if database_unsupported?
60+
raise Suspenders::Generators::DatabaseUnsupported::Error
61+
end
62+
end
63+
64+
private
65+
66+
def database_unsupported?
67+
configuration = File.read(Rails.root.join("config/database.yml"))
68+
configuration = YAML.load(configuration, aliases: true)
69+
adapter = configuration["default"]["adapter"]
70+
71+
adapter != "postgresql"
72+
end
73+
end
74+
end
4775
end
4876
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require "test_helper"
2+
require "generators/suspenders/ci_generator"
3+
4+
module Suspenders
5+
module Generators
6+
class CiGeneratorTest < Rails::Generators::TestCase
7+
include Suspenders::TestHelpers
8+
9+
tests Suspenders::Generators::CiGenerator
10+
destination Rails.root
11+
teardown :restore_destination
12+
13+
test "generates CI template" do
14+
with_database "postgresql" do
15+
run_generator
16+
17+
assert_file app_root(".github/workflows/ci.yml")
18+
end
19+
end
20+
21+
test "raises if PostgreSQL is not the adapter" do
22+
with_database "unsupported" do
23+
assert_raises Suspenders::Generators::DatabaseUnsupported::Error, match: "This generator requires PostgreSQL" do
24+
run_generator
25+
26+
assert_no_file app_root(".github/workflows/ci.yml")
27+
end
28+
end
29+
end
30+
31+
private
32+
33+
def restore_destination
34+
remove_dir_if_exists ".github"
35+
end
36+
end
37+
end
38+
end

test/test_helper.rb

+12
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ def with_test_suite(test_suite, &block)
100100
remove_dir_if_exists "spec"
101101
end
102102

103+
def with_database(database, &block)
104+
backup_file "config/database.yml"
105+
configuration = File.read app_root("config/database.yml")
106+
configuration = YAML.load(configuration, aliases: true)
107+
configuration["default"]["adapter"] = database
108+
File.open(app_root("config/database.yml"), "w") { _1.write configuration.to_yaml }
109+
110+
yield
111+
ensure
112+
restore_file "config/database.yml"
113+
end
114+
103115
def backup_file(file)
104116
FileUtils.copy app_root(file), app_root("#{file}.bak")
105117
end

0 commit comments

Comments
 (0)