├── .gitignore
├── .overcommit.yml
├── 10-STEPS-TO-PROD.md
├── CHANGELOG.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── bin
└── railsmaker
├── lib
├── railsmaker.rb
└── railsmaker
│ ├── generators
│ ├── app_generator.rb
│ ├── auth_generator.rb
│ ├── base_generator.rb
│ ├── concerns
│ │ └── gsub_validation.rb
│ ├── litestream_generator.rb
│ ├── mailjet_generator.rb
│ ├── opentelemetry_generator.rb
│ ├── plausible_generator.rb
│ ├── plausible_instrumentation_generator.rb
│ ├── registry_generator.rb
│ ├── sentry_generator.rb
│ ├── server_command_generator.rb
│ ├── signoz_generator.rb
│ ├── signoz_opentelemetry_generator.rb
│ ├── templates
│ │ ├── app
│ │ │ ├── credentials.example.yml
│ │ │ └── main_index.html.erb
│ │ ├── auth
│ │ │ └── app
│ │ │ │ └── controllers
│ │ │ │ └── omniauth_callbacks_controller.rb
│ │ ├── litestream
│ │ │ └── litestream.yml.erb
│ │ ├── opentelemetry
│ │ │ └── lograge.rb.erb
│ │ ├── shell_scripts
│ │ │ ├── _port_check.sh
│ │ │ ├── plausible.sh.erb
│ │ │ ├── registry.sh.erb
│ │ │ ├── signoz.sh.erb
│ │ │ └── signoz_opentelemetry.sh.erb
│ │ └── ui
│ │ │ ├── app
│ │ │ ├── assets
│ │ │ │ └── images
│ │ │ │ │ ├── og-image.webp
│ │ │ │ │ ├── plausible-screenshot.png
│ │ │ │ │ └── signoz-screenshot.png
│ │ │ ├── controllers
│ │ │ │ ├── demo_controller.rb
│ │ │ │ └── pages_controller.rb
│ │ │ ├── helpers
│ │ │ │ └── seo_helper.rb
│ │ │ ├── javascript
│ │ │ │ └── controllers
│ │ │ │ │ ├── flash_controller.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── scroll_fade_controller.js
│ │ │ └── views
│ │ │ │ ├── clearance_mailer
│ │ │ │ ├── change_password.html.erb
│ │ │ │ └── change_password.text.erb
│ │ │ │ ├── demo
│ │ │ │ ├── analytics.html.erb
│ │ │ │ ├── index.html.erb
│ │ │ │ └── support.html.erb
│ │ │ │ ├── layouts
│ │ │ │ ├── _navbar.html.erb
│ │ │ │ └── application.html.erb
│ │ │ │ ├── main
│ │ │ │ └── index.html.erb
│ │ │ │ ├── pages
│ │ │ │ ├── privacy.html.erb
│ │ │ │ └── terms.html.erb
│ │ │ │ ├── passwords
│ │ │ │ ├── create.html.erb
│ │ │ │ ├── edit.html.erb
│ │ │ │ └── new.html.erb
│ │ │ │ ├── sessions
│ │ │ │ └── new.html.erb
│ │ │ │ ├── shared
│ │ │ │ ├── _auth_layout.html.erb
│ │ │ │ ├── _flash.html.erb
│ │ │ │ ├── _footer.html.erb
│ │ │ │ └── _structured_data.html.erb
│ │ │ │ └── users
│ │ │ │ └── new.html.erb
│ │ │ ├── config
│ │ │ └── sitemap.rb
│ │ │ └── public
│ │ │ ├── icon.png
│ │ │ ├── icon.svg
│ │ │ └── robots.txt
│ └── ui_generator.rb
│ └── version.rb
├── railsmaker-core.gemspec
└── test
├── fixtures
├── .kamal
│ └── secrets
├── Dockerfile
├── Gemfile
├── app
│ ├── assets
│ │ └── tailwind
│ │ │ └── application.css
│ ├── controllers
│ │ └── application_controller.rb
│ ├── credentials.example.yml
│ ├── mailers
│ │ └── application_mailer.rb
│ └── views
│ │ ├── layouts
│ │ └── application.html.erb
│ │ └── main
│ │ └── index.html.erb
├── auth
│ └── user.rb
├── config
│ ├── deploy.yml
│ ├── environment.rb
│ ├── environments
│ │ └── production.rb
│ ├── initializers
│ │ ├── clearance.rb
│ │ ├── devise.rb
│ │ ├── lograge.rb
│ │ └── sentry.rb
│ └── routes.rb
└── db
│ └── migrate
│ └── add_omniauth_to_users.rb
├── lib
└── railsmaker
│ └── generators
│ ├── app_generator_test.rb
│ ├── auth_generator_test.rb
│ ├── litestream_generator_test.rb
│ ├── mailjet_generator_test.rb
│ ├── opentelemetry_generator_test.rb
│ ├── plausible_instrumentation_generator_test.rb
│ ├── sentry_generator_test.rb
│ └── ui_generator_test.rb
├── support
└── generator_helper.rb
└── test_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | /.config
4 | /coverage/
5 | /InstalledFiles
6 | /pkg/
7 | /spec/reports/
8 | /spec/examples.txt
9 | /test/tmp/
10 | /test/version_tmp/
11 | /tmp/
12 | .DS_Store
13 |
14 | # SimpleCov generated files
15 | /coverage
16 |
17 | # rspec failure tracking
18 | .rspec_status
19 |
20 | # Environment normalization
21 | /.bundle/
22 | /vendor/bundle
23 | /lib/bundler/man/
24 |
25 | Gemfile.lock
26 | bun.lock
27 | /node_modules/
28 | package.json
29 |
--------------------------------------------------------------------------------
/.overcommit.yml:
--------------------------------------------------------------------------------
1 | PreCommit:
2 | BundleOutdated:
3 | enabled: true
4 | description: 'Check for outdated dependencies'
5 | required: true
6 | command: ['bundle', 'outdated', '--strict']
7 | PrePush:
8 | Minitest:
9 | enabled: true
10 |
--------------------------------------------------------------------------------
/10-STEPS-TO-PROD.md:
--------------------------------------------------------------------------------
1 | # 10 Steps To Prod
2 |
3 | A guide for deploying a Rails application with observability, analytics, DaisyUI and much more using [RailsMaker](https://railsmaker.com).
4 |
5 | ## Table of Contents
6 |
7 | 1. [Introduction](#1-introduction)
8 | 2. [Pre-requisites](#2-pre-requisites)
9 | 3. [Setting Environment Variables](#3-setting-environment-variables)
10 | 4. [Remote Servers Setup](#4-remote-servers-setup)
11 | - [4.1. Install Docker & Add SSH Key](#41-install-docker--add-ssh-key)
12 | - [4.2. Metrics, tracing and logs (Signoz)](#42-metrics-tracing-and-logs-signoz)
13 | - [4.3. Analytics (Plausible)](#43-analytics-plausible)
14 | - [4.4. Private Docker Registry](#44-private-docker-registry)
15 | 5. [Generate the Rails app](#5-generate-the-rails-app)
16 | 6. [Add Rails Credentials](#6-add-rails-credentials)
17 | 7. [Deployment](#7-deployment)
18 | 8. [Add OpenTelemetry](#8-add-opentelemetry)
19 | 9. [DNS Configuration](#9-dns-configuration)
20 | 10. [Verification & Testing](#10-verification--testing)
21 |
22 | ## 1. Introduction
23 |
24 | Welcome to the **RailsMaker** tutorial! In this guide, you'll learn how to:
25 |
26 | - Deploy a Rails app on two cloud servers (one for the Rails app, one for analytics/metrics).
27 | - Integrate **analytics** (via Plausible) and **metrics/traces/logs** (via Signoz and OpenTelemetry).
28 | - Configure DNS, environment variables, and secure secrets.
29 | - Verify that everything is working end-to-end.
30 |
31 | > **Goal**: By the end, you'll have a live Rails app with observability tools in place, DB backups, and more.
32 |
33 | ## 2. Pre-requisites
34 |
35 | Before diving into commands, ensure you have:
36 |
37 | 1. **Local Tools**:
38 | - Ruby 3.x (via `rbenv` or `rvm`)
39 | - Bundler: `gem install bundler`
40 | - Bun: [Install guide](https://bun.sh)
41 | - Git
42 | - Build essentials:
43 | - Ubuntu/Debian: `sudo apt install build-essential libyaml-dev`
44 | - macOS: `xcode-select --install`
45 | - Docker
46 |
47 | 2. **RailsMaker** installed: `gem install railsmaker-core`
48 |
49 | 3. **A Domain Name**
50 |
51 | 4. **2 Servers** (Ubuntu recommended):
52 | - Server A for your Rails App (min 2GB RAM)
53 | - Server B for Metrics/Analytics (min 4GB RAM)
54 |
55 | 5. *(Optional)* **S3 Bucket** for DB backups
56 |
57 | 6. *(Optional)* Signed up for:
58 | - **Google Cloud** (OAuth2 sign in)
59 | - **Sentry** (error tracking)
60 | - **Mailjet** (email service)
61 | - **Cloudflare** (DNS/CDN)
62 | - **OpsGenie** (alerts)
63 |
64 | > **Note**: You can skip the optional services if you don't need them
65 |
66 | ## 3. Setting Environment Variables
67 |
68 | Next, you'll need to **export** certain environment variables on your local machine (where you'll run deployment commands).
69 |
70 | Example (bash/zsh):
71 |
72 | export KAMAL_REGISTRY_PASSWORD="registry_password"
73 |
74 | # optional
75 | export LITESTREAM_ACCESS_KEY_ID="access_key_id"
76 | export LITESTREAM_SECRET_ACCESS_KEY="secret_access_key"
77 | export LITESTREAM_BUCKET="bucket_url"
78 | export LITESTREAM_REGION="region"
79 |
80 | > Keep these credentials **private** and avoid committing them to git. The default setup uses ENV vars for simplicity but you can adjust your Kamal secrets file to use other methods.
81 |
82 | ## 4. Remote Servers Setup
83 |
84 | Now, let's prepare your servers. We'll assume you have SSH access (user + password or existing SSH key) to each VPS.
85 |
86 | ### 4.1. Install Docker & Add SSH Key
87 |
88 | 1. **Install Docker** on each server:
89 | - Ubuntu example:
90 |
91 | curl -fsSL https://get.docker.com -o get-docker.sh
92 | sudo sh get-docker.sh
93 |
94 | - For other distros, see [Docker Docs](https://docs.docker.com/engine/install/).
95 |
96 | 2. **Add your SSH key** (for passwordless access):
97 |
98 | ssh-copy-id user@YOUR_SERVER_IP
99 |
100 | This ensures RailsMaker commands can run via SSH smoothly.
101 |
102 | ### 4.2. Metrics, tracing and logs (Signoz)
103 |
104 | To set up **Signoz** (for observability) on your Metrics/Analytics server:
105 |
106 | ```bash
107 | railsmaker remote signoz --ssh-host=METRICS_SERVER_IP
108 | ```
109 |
110 | > ⏳ The Signoz query service will take a while to pull images and spin up, be patient
111 |
112 | ### 4.3. Analytics (Plausible)
113 |
114 | For **Plausible Analytics** on the same or another server:
115 |
116 | ```bash
117 | railsmaker remote plausible \
118 | --ssh-host=METRICS_SERVER_IP \
119 | --analytics-host=analytics.example.com
120 | ```
121 |
122 | > Make sure `--analytics-host` is the domain/subdomain you plan to use for your analytics dashboard
123 |
124 | ### 4.4. Private Docker Registry
125 |
126 | To set up your own private Docker registry with authentication and HTTPS:
127 |
128 | ```bash
129 | railsmaker remote registry \
130 | --ssh-host=REGISTRY_SERVER_IP \
131 | --registry-host=registry.example.com \
132 | --registry-username=admin \
133 | --registry-password=your_secure_password
134 | ```
135 |
136 | After installation:
137 | 1. Configure DNS:
138 | - Add an A record for `registry.example.com` pointing to your server
139 |
140 | 2. Update your Kamal deployment config:
141 | ```yaml
142 | # config/deploy.yml
143 | registry:
144 | server: registry.example.com
145 | username: admin
146 | password:
147 | - KAMAL_REGISTRY_PASSWORD
148 | ```
149 |
150 | 3. Set the registry password in your environment:
151 | ```bash
152 | export KAMAL_REGISTRY_PASSWORD=your_secure_password
153 | ```
154 |
155 | > **Note**: Using your own registry can significantly speed up deployments and provide more control over your container images.
156 |
157 | ## 5. Generate the Rails app
158 |
159 | First, use `railsmaker` to generate the Rails app:
160 |
161 | ```bash
162 | railsmaker new --name railsmaker-sample \
163 | --docker docker-username \
164 | --ip 12.123.123.1 \
165 | --domain example.com \
166 | --analytics analytics.example.com \
167 | --bucketname sample-railsmaker
168 | ```
169 | > **Important**: You can skip integrations, see `railsmaker new --help` to see all available options and skip unwanted integrations
170 |
171 | Alternatively, you can use the wizard to enter the details interactively:
172 |
173 | ```bash
174 | railsmaker new:wizard
175 | ```
176 |
177 | The generator will create the app in the `--name` directory. It will execute bundler and migrations for you so we are ready to start the server:
178 |
179 | **Start server locally**:
180 |
181 | ```bash
182 | cd railsmaker-sample
183 |
184 | ./bin/dev
185 | ```
186 |
187 | Visit `http://localhost:3000` to confirm your app works.
188 |
189 | ## 6. Add Rails Credentials
190 |
191 | Ensure your **Rails credentials** (like `secret_key_base`) are properly set through the credentials.yml file:
192 |
193 | ```bash
194 | EDITOR="vim" bin/rails credentials:edit
195 | ```
196 |
197 | See the `credentials.example.yml` file for reference of all possible credentials.
198 |
199 | > **Security Reminder**: Never commit your master key to a public repo.
200 |
201 | ## 7. Deployment
202 |
203 | You're almost there. Now run:
204 |
205 | ```bash
206 | kamal setup
207 | ```
208 |
209 | - This may create or update necessary containers (including limestream if you have configured a S3 bucket).
210 | - If you get a timeout, simply re-run the command or check for connectivity issues.
211 |
212 | ## 8. Add OpenTelemetry
213 |
214 | If you want distributed tracing and logs to be sent to your Signoz server:
215 |
216 | ```bash
217 | railsmaker remote signoz:opentelemetry \
218 | --ssh-host=APP_SERVER_IP \
219 | --signoz-server-host=METRICS_SERVER_IP \
220 | --hostname=rails-apps
221 | ```
222 |
223 | This configures your Rails app to send traces/spans/logs to Signoz through a sidecar container.
224 |
225 | > Make sure APP_SERVER_IP is the IP of your Rails app server. This has to be run **only once** if you host several apps on the same server
226 |
227 | ## 9. DNS Configuration
228 |
229 | 1. Log into your DNS provider (e.g., Cloudflare or your registrar).
230 | 2. Create **A records** pointing your domain (e.g. `myapp.com`) and subdomain (e.g. `analytics.myapp.com`) to the correct server IPs.
231 | 3. If using Cloudflare's proxy, ensure it's set to "Full" for the app and analytics domains.
232 |
233 | ## 10. Verification & Testing
234 |
235 | 1. **Visit Your App**
236 | - In your browser, go to `https://myapp.com` (or your chosen domain).
237 | - Confirm the Rails app is live.
238 | 2. **Check Analytics**
239 | - Go to your Plausible dashboard (e.g., `https://analytics.myapp.com`).
240 | - See if it's tracking page views or real-time visitors.
241 | 3. **Review Metrics**
242 | - Access Signoz at `http://METRICS_SERVER_IP:3301` (or your chosen domain).
243 | - Check for application traces, metrics, or logs to ensure all data is flowing.
244 |
245 | ## Congrats!
246 |
247 | You've deployed a Rails application with analytics and metrics using RailsMaker in just a few steps.
248 |
249 | Now you are ready to start bringing your ideas to life with a solid foundation!
250 |
251 | This project is PWYW, if it helped you ship faster, consider:
252 |
253 | [](https://buymeacoffee.com/sgerov)
254 |
255 | **Happy Deploying** 🚀
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [0.0.4] - 2025-02-18
9 |
10 | ### Added
11 | - Upgrade Tailwind and DaisyUI to 5.0.0
12 | - Added promo for DaisyUITemplates.com
13 |
14 | ## [0.0.3] - 2025-02-16
15 |
16 | ### Fixed
17 | - Docker registry URL is now configurable from the start
18 |
19 | ## [0.0.2] - 2025-02-13
20 |
21 | ### Added
22 | - Private Docker Registry support with authentication and HTTPS
23 |
24 | ### Fixed
25 | - DNS configuration instructions for Cloudflare users
26 | - Port conflict detection before installation
27 | - Remote script error handling
28 |
29 | ## [0.0.1] - 2025-02-12
30 |
31 | ### Added
32 | - Initial release
33 | - Rails 8 app generator
34 | - Tailwind CSS + DaisyUI integration
35 | - Kamal deployment configuration
36 | - Plausible Analytics integration
37 | - SigNoz monitoring setup
38 | - OpenTelemetry instrumentation
39 | - Litestream backup support
40 | - Authentication with Clearance
41 | - Google OAuth support
42 | - Mailjet email service integration
43 | - Sentry error tracking
44 | - Basic documentation and examples
45 |
46 | [0.0.4]: https://github.com/sgerov/railsmaker/compare/v0.0.3...v0.0.4
47 | [0.0.3]: https://github.com/sgerov/railsmaker/compare/v0.0.2...v0.0.3
48 | [0.0.2]: https://github.com/sgerov/railsmaker/compare/v0.0.1...v0.0.2
49 | [0.0.1]: https://github.com/sgerov/railsmaker/releases/tag/v0.0.1
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | gemspec
6 |
7 | # dev tools
8 | gem 'debug'
9 | gem 'git'
10 | gem 'minitest'
11 | gem 'minitest-reporters'
12 | gem 'mocha'
13 | gem 'overcommit'
14 | gem 'rake'
15 | gem 'rubocop'
16 | gem 'simplecov', require: false
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015-present Sava Gerov
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://rubygems.org/gems/railsmaker-core)
2 | [](https://buymeacoffee.com/sgerov)
3 | [](https://railsmaker.com)
4 | [](https://github.com/sgerov/railsmaker-sample)
5 | [](./10-STEPS-TO-PROD.md)
6 | [](https://daisyuitemplates.com/)
7 |
8 | # 📦 Railsmaker
9 |
10 | Ship your MVP in hours, not weeks • Zero config needed • Save 20+ dev hours
11 |
12 | ## ⚡ Why Railsmaker?
13 | - **Ship Faster**: From zero to production in 15 minutes
14 | - **Growth Ready**: Built-in analytics, SEO, and monitoring
15 | - **Own Your Data**: Fully self-hosted, full control, full flexibility
16 | - **Cost Efficient**: You decide how much you want to spend
17 | - **DX Focused**: Modern stack, zero configuration
18 |
19 | ## ✨ Features
20 |
21 | #### Growth & Analytics
22 | - **Privacy-focused**: Self-hosted Plausible, Signoz, and Docker Registry
23 | - **SEO**: Auto-optimized meta-tags & sitemaps
24 | - **Performance**: Lightning-fast ~50ms page loads
25 | - **Mobile First**: Instant responsive layouts
26 |
27 | #### Developer Experience
28 | - **UI**: Latest TailwindCSS 4 + DaisyUI 5
29 | - **Auth**: Battle-tested Clearance + OmniAuth
30 | - **Storage**: SQLite + Litestream
31 | - **Email**: Production-ready Mailjet integration
32 | - **Modern Stack**: Rails 8, Ruby 3.2, Hotwire magic
33 |
34 | #### Infrastructure
35 | - **Monitoring**: Full SigNoz & Sentry integration
36 | - **Deploy**: One-command Kamal deployments, self-hosted Registry support
37 | - **Observability**: Enterprise-grade OpenTelemetry + Lograge
38 | - **Scale-ready**: Global CDN support, multi-environment
39 |
40 | ## 🚀 Setup
41 |
42 | ### Prerequisites
43 | - Ruby 3.x (`rbenv` or `rvm` recommended)
44 | - Bundler: `gem install bundler`
45 | - Bun: [Install guide](https://bun.sh)
46 | - Git
47 | - Dev tools:
48 | - Ubuntu/Debian: `sudo apt install build-essential libyaml-dev`
49 | - macOS: `xcode-select --install`
50 | - Docker (for analytics & monitoring)
51 |
52 | ### 1. Bootstrapping your app
53 |
54 | #### A. Set Required Environment Variables
55 |
56 | ```bash
57 | # Docker registry access (required)
58 | export KAMAL_REGISTRY_PASSWORD="docker-registry-password"
59 |
60 | # Litestream backup configuration (optional)
61 | export LITESTREAM_ACCESS_KEY_ID="access-key"
62 | export LITESTREAM_SECRET_ACCESS_KEY="secret-access-key"
63 | export LITESTREAM_BUCKET="https://eu2.yourbucketendpoint.com/"
64 | export LITESTREAM_REGION="eu2"
65 | ```
66 |
67 | #### B. Install and Deploy
68 |
69 | ```bash
70 | gem install railsmaker-core
71 |
72 | # Interactive wizard (2 minutes)
73 | railsmaker new:wizard
74 |
75 | # Deploy to any cloud
76 | kamal setup
77 | ```
78 |
79 | If you have chosen to include litestream keep in mind that the corresponding kamal accessory will also be deployed.
80 |
81 | ### 2. Setting up Monitoring (Optional)
82 |
83 | #### A. Install SigNoz Server
84 | ```bash
85 | railsmaker remote signoz \
86 | --ssh-host=monitor.example.com \
87 | --ssh-user=deploy
88 | ```
89 |
90 | #### B. Add OpenTelemetry Collector to apps server
91 | ```bash
92 | railsmaker remote signoz:opentelemetry \
93 | --ssh-host=app.example.com \
94 | --ssh-user=deploy \
95 | --signoz-host=monitor.example.com \
96 | --hostname=my-production-apps
97 | ```
98 |
99 | ### 3. Setting up Analytics (Optional)
100 | ```bash
101 | railsmaker remote plausible \
102 | --ssh-host=analytics.example.com \
103 | --ssh-user=deploy \
104 | --analytics-host=plausible.example.com
105 | ```
106 |
107 | ### 4. Setting up Private Docker Registry (Optional)
108 | ```bash
109 | railsmaker remote registry \
110 | --ssh-host=192.168.1.10 \
111 | --ssh-user=deploy \
112 | --registry-host=registry.example.com \
113 | --registry-username=admin \
114 | --registry-password=secret
115 | ```
116 |
117 | After setting up your registry:
118 | 1. Create an A record for `registry.example.com` pointing to your server
119 | 2. Update your Kamal config to use your private registry (unless you already used `-r` option):
120 | ```yaml
121 | # config/deploy.yml
122 | registry:
123 | server: registry.example.com
124 | username: admin
125 | password:
126 | - KAMAL_REGISTRY_PASSWORD
127 | ```
128 |
129 | ### Verification
130 |
131 | - SigNoz Dashboard: `https://monitor.example.com:3301`
132 | - Plausible Analytics: `https://analytics.example.com`
133 | - Docker Registry: `https://registry.example.com`
134 | - Your App: `https://app.example.com`
135 |
136 | > **Note**: All services are tested on Ubuntu 24.04 and macOS 15.2.
137 |
138 | **For a more detailed guide, check out [10 Steps To Prod](./10-STEPS-TO-PROD.md).**
139 |
140 | ### Environment Requirements
141 |
142 | - **SigNoz Server**: 2 CPU, 4GB RAM minimum
143 | - **Plausible**: 1 CPU, 2GB RAM minimum
144 | - **App Server**: 1 CPU, 2GB RAM minimum
145 |
146 | You can decide how to split the services between your servers (e.g. SigNoz & Plausible on a separate server from the app or apps).
147 |
148 | ### Database Recovery
149 |
150 | In case of DB failure, follow these steps to recover your data:
151 |
152 | 1. Stop the application:
153 | ```bash
154 | kamal app stop
155 | ```
156 |
157 | 2. Remove existing database files:
158 | ```bash
159 | kamal app exec /bin/sh -i
160 | rm -rf ./storage/*
161 | exit
162 | ```
163 |
164 | 3. Recover files and set proper ownership to files:
165 | ```bash
166 | kamal restore-db-app
167 | kamal restore-db-cache
168 | kamal restore-db-queue
169 | kamal restore-db-cable
170 | kamal restore-db-ownership
171 | ```
172 |
173 | 4. Restart Litestream to initiate recovery:
174 | ```bash
175 | kamal accessory reboot litestream
176 | ```
177 |
178 | 5. Start the application:
179 | ```bash
180 | kamal app boot
181 | ```
182 |
183 | ### Managing Docker Services
184 |
185 | After deploying services with `railsmaker remote` commands, you can manage them using standard Docker commands:
186 |
187 | ```bash
188 | # Navigate to the service directory
189 | cd ~/SERVICE_DIRECTORY
190 |
191 | # Common commands for all services
192 | docker compose ps # List containers
193 | docker compose logs -f # View logs
194 | docker compose restart # Restart all containers
195 | docker compose down # Stop all containers
196 | docker compose up -d # Start all containers
197 | ```
198 |
199 | Service directories:
200 | - Plausible Analytics: `~/plausible-ce`
201 | - SigNoz Server: `~/signoz/deploy/docker`
202 | - SigNoz Server Collector: `~/signoz/deploy/docker/generator/infra`
203 | - OpenTelemetry Collector (other servers): `~/signoz-opentelemetry/deploy/docker/generator/infra`
204 |
205 | > **Note**: Replace `~` with the absolute path if using sudo or running commands as another user.
206 |
207 | ### Cloudflare DNS
208 |
209 | If you are relying on Cloudflare, make sure you set-up SSL/TLS to Full for your application and analytics.
210 |
211 | ## Support
212 |
213 | This project is **pay-what-you-want**. If it helps you ship faster:
214 |
215 | [](https://buymeacoffee.com/sgerov)
216 |
217 | *Give it a try at [railsmaker.com](https://railsmaker.com)*
218 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest/test_task'
4 |
5 | Minitest::TestTask.create # named test, sensible defaults
6 |
7 | task default: :test
8 |
--------------------------------------------------------------------------------
/bin/railsmaker:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'railsmaker'
5 | require 'thor'
6 |
7 | module RailsMaker
8 | class BaseCLI < Thor
9 | include Thor::Actions
10 |
11 | no_commands do
12 | def colorize(str, color)
13 | case color
14 | when :green then "\e[32m#{str}\e[0m"
15 | when :red then "\e[31m#{str}\e[0m"
16 | when :blue then "\e[34m#{str}\e[0m"
17 | when :gray then "\e[90m#{str}\e[0m"
18 | else str
19 | end
20 | end
21 |
22 | def status_indicator(value)
23 | if value == true || (value.is_a?(String) && !value.empty?)
24 | colorize('●', :green) + ' Enabled'
25 | else
26 | colorize('○', :red) + ' Disabled'
27 | end
28 | end
29 |
30 | def print_summary(title, sections)
31 | max_label_width = sections.values.flatten(1).map { |item| item[0].length }.max
32 |
33 | say "\n#{colorize("═══ #{title} ═══", :blue)}\n\n"
34 |
35 | sections.each do |section_title, items|
36 | say colorize(section_title.upcase, :gray)
37 | say colorize('─' * section_title.length, :gray)
38 |
39 | items.each do |label, value, format = :default|
40 | formatted_value = case format
41 | when :status then status_indicator(value)
42 | when :highlight then colorize(value, :blue)
43 | else value.to_s
44 | end
45 |
46 | say " #{label.ljust(max_label_width)} #{formatted_value}"
47 | end
48 | say "\n"
49 | end
50 | end
51 |
52 | def to_generator_args(options)
53 | options.flat_map { |key, value| ["--#{key}", value.to_s] }
54 | end
55 | end
56 | end
57 |
58 | class RemoteCLI < BaseCLI
59 | map 'signoz:opentelemetry' => :signoz_opentelemetry
60 |
61 | desc 'signoz SERVER', 'Install SigNoz observability server on a remote machine'
62 | long_desc <<-LONGDESC
63 | Installs SigNoz server on a remote machine. SigNoz is a full-stack open-source APM#{' '}
64 | and observability tool.
65 |
66 | Examples:
67 | # Install using SSH key authentication
68 | $ railsmaker remote signoz --ssh-host=192.168.1.30 --ssh-user=deploy --key-path=~/.ssh/id_rsa
69 |
70 | # Install using password authentication (will prompt for password)
71 | $ railsmaker remote signoz --ssh-host=192.168.1.30 --ssh-user=deploy
72 |
73 | # Force reinstallation of existing instance
74 | $ railsmaker remote signoz --ssh-host=192.168.1.30 --ssh-user=deploy --force
75 | LONGDESC
76 | method_option :ssh_host, type: :string, required: true, desc: 'SSH host (e.g., 123.456.789.0)'
77 | method_option :ssh_user, type: :string, default: 'root', desc: 'SSH user with sudo access'
78 | method_option :key_path, type: :string, desc: 'Path to SSH key (if not provided, will use password auth)'
79 | method_option :force, type: :boolean, default: false, desc: 'Force reinstallation if already installed'
80 | def signoz
81 | print_remote_summary('SigNoz Installation', options)
82 | RailsMaker::Generators::SignozGenerator.start(to_generator_args(options))
83 | end
84 |
85 | desc 'signoz:opentelemetry', 'Install SigNoz OpenTelemetry collector on a remote machine'
86 | long_desc <<-LONGDESC
87 | Installs and configures the OpenTelemetry collector on a remote machine to send metrics,#{' '}
88 | traces, and logs to your SigNoz server.
89 |
90 | The collector will be configured to:
91 | • Collect Docker container logs
92 | • Forward metrics and traces
93 | • Auto-discover running services
94 |
95 | Examples:
96 | # Basic installation
97 | $ railsmaker remote signoz:opentelemetry --ssh-host=192.168.1.40 --ssh-user=deploy \
98 | --signoz-server-host=192.168.1.30
99 |
100 | # With custom hostname for better identification
101 | $ railsmaker remote signoz:opentelemetry --ssh-host=192.168.1.40 --ssh-user=deploy \
102 | --signoz-server-host=192.168.1.30 --hostname=production-app-1
103 | LONGDESC
104 | method_option :ssh_host, type: :string, required: true, desc: 'SSH host where collector will be installed'
105 | method_option :signoz_server_host, type: :string, required: true, desc: 'Host where SigNoz server is running'
106 | method_option :ssh_user, type: :string, default: 'root', desc: 'SSH user with sudo access'
107 | method_option :hostname, type: :string, default: 'rails-apps-1', desc: 'Custom hostname identifier of the server'
108 | method_option :key_path, type: :string, desc: 'Path to SSH key (if not provided, will use password auth)'
109 | method_option :force, type: :boolean, default: false, desc: 'Force reinstallation if already installed'
110 | def signoz_opentelemetry
111 | print_remote_summary('SigNoz OpenTelemetry Installation', options)
112 | RailsMaker::Generators::SignozOpentelemetryGenerator.start(to_generator_args(options))
113 | end
114 |
115 | desc 'plausible', 'Install Plausible Analytics on a remote server'
116 | long_desc <<-LONGDESC
117 | Installs Plausible Analytics on a remote machine. Plausible is a lightweight,#{' '}
118 | open-source alternative to Google Analytics.
119 |
120 | Examples:
121 | # Basic installation
122 | $ railsmaker remote plausible --ssh-host=192.168.1.20 --ssh-user=deploy \
123 | --analytics-host=plausible.example.com
124 |
125 | # With custom SSH key
126 | $ railsmaker remote plausible --ssh-host=192.168.1.20 --ssh-user=deploy \
127 | --analytics-host=plausible.example.com --key-path=~/.ssh/analytics_key
128 | LONGDESC
129 | method_option :ssh_host, type: :string, required: true, desc: 'SSH host where Plausible will be installed'
130 | method_option :analytics_host, type: :string, required: true, desc: 'Domain where Plausible will be accessible'
131 | method_option :ssh_user, type: :string, default: 'root', desc: 'SSH user with sudo access'
132 | method_option :key_path, type: :string, desc: 'Path to SSH key (if not provided, will use password auth)'
133 | method_option :force, type: :boolean, default: false, desc: 'Force reinstallation if already installed'
134 | def plausible
135 | print_remote_summary('Plausible Installation', options)
136 | RailsMaker::Generators::PlausibleGenerator.start(to_generator_args(options))
137 | end
138 |
139 | desc 'registry', 'Install private Docker Registry on a remote server'
140 | long_desc <<-LONGDESC
141 | Installs and configures a private Docker Registry with authentication and HTTPS#{' '}
142 | (via Caddy) on a remote machine.
143 |
144 | Examples:
145 | # Basic installation
146 | $ railsmaker remote registry --ssh-host=192.168.1.10 --ssh-user=deploy \
147 | --registry-host=registry.example.com --registry-username=admin --registry-password=secret
148 |
149 | # With custom SSH key
150 | $ railsmaker remote registry --ssh-host=192.168.1.10 --ssh-user=deploy \
151 | --registry-host=registry.example.com --registry-username=admin \
152 | --registry-password=secret --key-path=~/.ssh/registry_key
153 | LONGDESC
154 | method_option :ssh_host, type: :string, required: true, desc: 'SSH host where Registry will be installed'
155 | method_option :registry_host, type: :string, required: true, desc: 'Domain where Registry will be accessible'
156 | method_option :registry_username, type: :string, required: true, desc: 'Username for Registry authentication'
157 | method_option :registry_password, type: :string, required: true, desc: 'Password for Registry authentication'
158 | method_option :ssh_user, type: :string, default: 'root', desc: 'SSH user with sudo access'
159 | method_option :key_path, type: :string, desc: 'Path to SSH key (if not provided, will use password auth)'
160 | method_option :force, type: :boolean, default: false, desc: 'Force reinstallation if already installed'
161 | def registry
162 | print_remote_summary('Docker Registry Installation', options)
163 | RailsMaker::Generators::RegistryGenerator.start(to_generator_args(options))
164 | end
165 |
166 | private
167 |
168 | def print_remote_summary(title, opts)
169 | sections = {
170 | 'Connection Details' => [
171 | ['SSH Host', opts[:ssh_host], :highlight],
172 | ['SSH User', opts[:ssh_user], :highlight]
173 | ]
174 | }
175 |
176 | if opts[:signoz_server_host]
177 | sections['Configuration'] = [
178 | ['SigNoz Host', opts[:signoz_server_host], :highlight],
179 | ['Custom Hostname', opts[:hostname], :highlight]
180 | ]
181 | end
182 |
183 | if opts[:analytics_host]
184 | sections['Configuration'] = [
185 | ['Analytics Host', opts[:analytics_host], :highlight]
186 | ]
187 | end
188 |
189 | print_summary(title, sections)
190 | end
191 | end
192 |
193 | class CLI < BaseCLI
194 | map 'new:wizard' => :new_wizard
195 |
196 | desc 'new', 'Generate a new Rails application with integrated features'
197 | method_option :name, type: :string, required: true, aliases: '-n', desc: 'Application name'
198 | method_option :docker, type: :string, required: true, aliases: '-d', desc: 'Docker username'
199 | method_option :ip, type: :string, required: true, aliases: '-i', desc: 'Server IP address'
200 | method_option :domain, type: :string, required: true, aliases: '-D', desc: 'Domain name'
201 | method_option :auth, type: :boolean, default: true, desc: 'Include authentication'
202 | method_option :mailjet, type: :boolean, default: true, desc: 'Configure Mailjet for email'
203 | method_option :bucketname, type: :string, desc: 'Enable litestream backups (provide your BUCKETNAME)'
204 | method_option :opentelemetry, type: :boolean, default: true, desc: 'Configure OpenTelemetry'
205 | method_option :analytics, type: :string, desc: 'Set up Plausible Analytics (provide your ANALYTICS_DOMAIN)'
206 | method_option :sentry, type: :boolean, default: true, desc: 'Configure Sentry error tracking'
207 | method_option :ui, type: :boolean, default: true, desc: 'Include UI assets'
208 | method_option :registry_url, type: :string, aliases: '-r', desc: 'Custom Docker registry URL'
209 | def new
210 | self.destination_root = File.expand_path(options[:name], Dir.pwd)
211 | say "Generating new Rails application: #{options[:name]}", :yellow
212 | generate_application(options)
213 | end
214 |
215 | desc 'new:wizard', 'Launch an interactive wizard for generating a Rails application'
216 | def new_wizard
217 | say 'Welcome to the RailsMaker wizard!', :blue
218 | wizard_options = collect_wizard_options
219 | self.destination_root = File.expand_path(wizard_options[:name], Dir.pwd)
220 | say "Generating new Rails application: #{wizard_options[:name]}", :yellow
221 | generate_application(wizard_options)
222 | end
223 |
224 | desc 'remote', 'Manage remote services'
225 | subcommand 'remote', RailsMaker::RemoteCLI
226 |
227 | def self.exit_on_failure?
228 | true
229 | end
230 |
231 | private
232 |
233 | def cleanup_on_failure
234 | return unless File.directory?(destination_root)
235 | return unless $!.to_s.include?('Failed to generate app')
236 |
237 | say_status 'cleanup', "Removing directory: #{destination_root}", :yellow
238 | FileUtils.rm_rf(destination_root)
239 | end
240 |
241 | def generate_application(opts)
242 | print_app_summary(opts)
243 | return unless yes?('Do you want to proceed with the installation? (y/N)')
244 |
245 | begin
246 | generate_components(opts)
247 | say 'Successfully generated RailsMaker template 🎉', :green
248 | rescue StandardError => e
249 | cleanup_on_failure
250 | raise
251 | end
252 | end
253 |
254 | def print_app_summary(opts)
255 | sections = {
256 | 'Application Settings' => [
257 | ['Name:', opts[:name], :highlight],
258 | ['Docker:', opts[:docker], :highlight],
259 | ['IP:', opts[:ip], :highlight],
260 | ['Domain:', opts[:domain], :highlight],
261 | ['Registry:', opts[:registry_url] || 'Docker Hub', :highlight]
262 | ],
263 | 'Features & Integrations' => [
264 | ['Authentication', opts[:auth], :status],
265 | ['Mailjet Email', opts[:mailjet], :status],
266 | ['Litestream Backups', opts[:bucketname], :status],
267 | ['OpenTelemetry', opts[:opentelemetry], :status],
268 | ['Plausible Analytics', opts[:analytics], :status],
269 | ['Sentry Error Tracking', opts[:sentry], :status],
270 | ['UI Components', opts[:ui], :status]
271 | ]
272 | }
273 |
274 | print_summary('Configuration Summary', sections)
275 | print_warnings(opts)
276 | end
277 |
278 | def print_warnings(opts)
279 | warnings = []
280 | if !opts[:ui] && opts[:analytics]
281 | warnings << "- Analytics was enabled but won't be installed because UI is disabled"
282 | end
283 |
284 | if !opts[:ui] && opts[:auth]
285 | warnings << "- Authentication was enabled but won't be installed because UI is disabled"
286 | end
287 |
288 | return unless warnings.any?
289 |
290 | say "\nWarnings:", :yellow
291 | warnings.each { |warning| say warning, :yellow }
292 | end
293 |
294 | def generate_components(opts)
295 | generator_args = to_generator_args(opts)
296 |
297 | RailsMaker::Generators::AppGenerator.start(generator_args)
298 |
299 | if opts[:ui]
300 | RailsMaker::Generators::AuthGenerator.start(generator_args) if opts[:auth]
301 | RailsMaker::Generators::UiGenerator.start(generator_args)
302 | RailsMaker::Generators::PlausibleInstrumentationGenerator.start(generator_args) if opts[:analytics]
303 | end
304 |
305 | RailsMaker::Generators::MailjetGenerator.start(generator_args) if opts[:mailjet]
306 | RailsMaker::Generators::LitestreamGenerator.start(generator_args) if opts[:bucketname]
307 | RailsMaker::Generators::OpentelemetryGenerator.start(generator_args) if opts[:opentelemetry]
308 | RailsMaker::Generators::SentryGenerator.start(generator_args) if opts[:sentry]
309 | end
310 |
311 | def collect_wizard_options
312 | wizard_options = {
313 | name: options[:name],
314 | docker: options[:docker],
315 | ip: options[:ip],
316 | domain: options[:domain],
317 | auth: options[:auth],
318 | mailjet: options[:mailjet],
319 | bucketname: options[:bucketname],
320 | opentelemetry: options[:opentelemetry],
321 | analytics: options[:analytics],
322 | sentry: options[:sentry],
323 | ui: options[:ui],
324 | registry_url: options[:registry_url]
325 | }
326 |
327 | # Only ask for values that weren't provided via command line
328 | while wizard_options[:name].to_s.empty?
329 | wizard_options[:name] = ask('Application name:')
330 | say 'Application name is required', :red if wizard_options[:name].empty?
331 | end
332 |
333 | while wizard_options[:docker].to_s.empty?
334 | wizard_options[:docker] = ask('Docker username:')
335 | say 'Docker username is required', :red if wizard_options[:docker].empty?
336 | end
337 |
338 | while wizard_options[:ip].to_s.empty?
339 | wizard_options[:ip] = ask('Server IP address:')
340 | say 'Server IP address is required', :red if wizard_options[:ip].empty?
341 | end
342 |
343 | while wizard_options[:domain].to_s.empty?
344 | wizard_options[:domain] = ask('Domain name:')
345 | say 'Domain name is required', :red if wizard_options[:domain].empty?
346 | end
347 |
348 | wizard_options[:auth] = yes?('Include authentication? (y/N)') if wizard_options[:auth].nil?
349 | wizard_options[:mailjet] = yes?('Configure Mailjet for email? (y/N)') if wizard_options[:mailjet].nil?
350 | wizard_options[:bucketname] ||= ask('Litestream bucketname: Provide BUCKET_NAME (leave blank to skip):')
351 | if wizard_options[:opentelemetry].nil?
352 | wizard_options[:opentelemetry] =
353 | yes?('Configure OpenTelemetry for metrics? (y/N)')
354 | end
355 | wizard_options[:analytics] ||= ask('Plausible Analytics: Provide ANALYTICS_DOMAIN (leave blank to skip):')
356 | wizard_options[:sentry] = yes?('Configure Sentry error tracking? (y/N)') if wizard_options[:sentry].nil?
357 | wizard_options[:ui] = yes?('Include UI assets? (y/N)') if wizard_options[:ui].nil?
358 |
359 | wizard_options[:bucketname] = nil if wizard_options[:bucketname] && wizard_options[:bucketname].empty?
360 | wizard_options[:analytics] = nil if wizard_options[:analytics] && wizard_options[:analytics].empty?
361 |
362 | # Add registry URL prompt if not provided
363 | wizard_options[:registry_url] ||= ask('Docker registry URL (leave blank for Docker Hub):')
364 | wizard_options[:registry_url] = nil if wizard_options[:registry_url].empty?
365 |
366 | wizard_options
367 | end
368 | end
369 | end
370 |
371 | begin
372 | RailsMaker::CLI.start
373 | rescue StandardError => e
374 | puts "\nError: #{e.message}"
375 | puts e.backtrace if ENV['DEBUG']
376 | puts "\nIf you need help, please create an issue at: https://github.com/sgerov/railsmaker/issues"
377 | exit 1
378 | end
379 |
--------------------------------------------------------------------------------
/lib/railsmaker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Rails gems
4 | require 'rails/generators'
5 | require 'rails/generators/rails/app/app_generator'
6 |
7 | # Concerns
8 | require 'railsmaker/generators/concerns/gsub_validation'
9 |
10 | # Local generators
11 | require 'railsmaker/generators/base_generator'
12 | require 'railsmaker/generators/server_command_generator'
13 |
14 | # App generators
15 | require 'railsmaker/generators/app_generator'
16 | require 'railsmaker/generators/plausible_generator'
17 | require 'railsmaker/generators/signoz_generator'
18 | require 'railsmaker/generators/signoz_opentelemetry_generator'
19 | require 'railsmaker/generators/opentelemetry_generator'
20 | require 'railsmaker/generators/plausible_instrumentation_generator'
21 | require 'railsmaker/generators/sentry_generator'
22 | require 'railsmaker/generators/auth_generator'
23 | require 'railsmaker/generators/ui_generator'
24 | require 'railsmaker/generators/mailjet_generator'
25 | require 'railsmaker/generators/litestream_generator'
26 | require 'railsmaker/generators/registry_generator'
27 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/app_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class AppGenerator < BaseGenerator
6 | source_root File.expand_path('templates/app', __dir__)
7 |
8 | class_option :name, type: :string, required: true, desc: 'Name of the application'
9 | class_option :docker, type: :string, required: true, desc: 'Docker username'
10 | class_option :ip, type: :string, required: true, desc: 'Server IP address'
11 | class_option :domain, type: :string, required: true, desc: 'Domain name'
12 | class_option :ui, type: :boolean, default: false, desc: 'Include UI assets?'
13 | class_option :registry_url, type: :string, desc: 'Custom Docker registry URL (e.g., registry.digitalocean.com)'
14 |
15 | def check_required_env_vars
16 | super(%w[
17 | KAMAL_REGISTRY_PASSWORD
18 | ])
19 | end
20 |
21 | def generate_app
22 | self.destination_root = File.expand_path(options[:name], current_dir)
23 |
24 | if !in_minitest? && File.directory?(destination_root)
25 | say_status 'error', "Directory '#{options[:name]}' already exists", :red
26 | raise BaseGeneratorError, 'Directory already exists'
27 | end
28 |
29 | say('Creating new Rails app')
30 | rails_args = [options[:name]]
31 | rails_args << '--javascript=bun' if options[:ui]
32 | Rails::Generators::AppGenerator.start(rails_args)
33 |
34 | setup_frontend if options[:ui]
35 |
36 | validate_gsub_strings([
37 | {
38 | file: 'config/deploy.yml',
39 | patterns: ['your-user', "web:\n - 192.168.0.1", 'ssl: true', 'app.example.com']
40 | }
41 | ])
42 |
43 | setup_kamal
44 |
45 | say('Modifying ApplicationController to allow all browsers (mobile)')
46 | comment_lines 'app/controllers/application_controller.rb', /allow_browser versions: :modern/
47 |
48 | say('Generating main controller with a landing page')
49 | generate :controller, 'main'
50 |
51 | if options[:ui]
52 | copy_file 'main_index.html.erb', 'app/views/main/index.html.erb'
53 | else
54 | create_file 'app/views/main/index.html.erb', "
Welcome to #{options[:name]} "
55 | end
56 |
57 | copy_file 'credentials.example.yml', 'config/credentials.example.yml'
58 |
59 | route "root 'main#index'"
60 | rescue StandardError => e
61 | say_status 'error', "Failed to generate app: #{e.message} in #{destination_root}", :red
62 | raise BaseGeneratorError, e.message
63 | end
64 |
65 | def git_commit
66 | git add: '.', commit: %(-m 'Initial railsmaker commit')
67 |
68 | say 'Successfully created Rails app with RailsMaker', :green
69 | end
70 |
71 | private
72 |
73 | def setup_frontend
74 | say('Adding Tailwind CSS')
75 | gem 'tailwindcss-rails', '~> 4.2.0'
76 |
77 | say('Installing gems')
78 | run 'bundle install --quiet'
79 |
80 | say('Setting up Tailwind')
81 | run 'bun add -d tailwindcss@4.0.12 @tailwindcss/cli@4.0.12'
82 | rails_command 'tailwindcss:install'
83 |
84 | say('Installing DaisyUI')
85 | run 'bun add -d daisyui@5.0.0'
86 |
87 | validate_gsub_strings([
88 | {
89 | file: 'app/views/layouts/application.html.erb',
90 | patterns: ['', '']
91 | },
92 | {
93 | file: 'app/assets/tailwind/application.css',
94 | patterns: ['@import "tailwindcss";']
95 | },
96 | {
97 | file: 'Dockerfile',
98 | patterns: ['bun.lockb', 'RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile',
99 | '# Precompiling assets for production']
100 | },
101 | {
102 | file: 'app/views/layouts/application.html.erb',
103 | patterns: []
104 | }
105 | ])
106 |
107 | inject_into_file 'app/assets/tailwind/application.css', after: '@import "tailwindcss";' do
108 | <<~RUBY
109 |
110 |
111 | @plugin "daisyui" {
112 | themes: light --default, dark --prefersdark, cupcake;
113 | }
114 | RUBY
115 | end
116 | gsub_file 'app/views/layouts/application.html.erb', '', ''
117 | gsub_file 'app/views/layouts/application.html.erb', '',
118 | ''
119 |
120 | say('Dockerfile: fixing legacy bun.lockb')
121 | gsub_file 'Dockerfile', 'bun.lockb', 'bun.lock'
122 |
123 | say('Dockerfile: fixing for Apple Silicon amd64 emulation')
124 | gsub_file 'Dockerfile',
125 | 'RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile',
126 | 'RUN TAILWINDCSS_INSTALL_DIR=node_modules/.bin SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile'
127 | inject_into_file 'Dockerfile', before: /# Precompiling assets for production/ do
128 | <<~RUBY
129 | # Ensure bun handles tailwind node bash binary
130 | RUN ln -s /usr/local/bun/bin/bun /usr/local/bun/bin/node\n
131 | RUBY
132 | end
133 | end
134 |
135 | def setup_kamal
136 | say('Configuring Kamal')
137 |
138 | inject_into_file 'config/deploy.yml', after: 'service: railsmaker' do
139 | "\ndeploy_timeout: 60 # to avoid timeout on first deploy"
140 | end
141 | gsub_file 'config/deploy.yml', 'your-user', options[:docker]
142 | gsub_file 'config/deploy.yml', "web:\n - 192.168.0.1", "web:\n hosts:\n - #{options[:ip]}"
143 | gsub_file 'config/deploy.yml', 'app.example.com', options[:domain]
144 | inject_into_file 'config/deploy.yml', after: 'ssl: true' do
145 | "\n forward_headers: true"
146 | end
147 |
148 | return unless options[:registry_url]
149 |
150 | uncomment_lines 'config/deploy.yml', 'server: registry.digitalocean.com'
151 | gsub_file 'config/deploy.yml', 'registry.digitalocean.com / ghcr.io / ...', options[:registry_url]
152 | end
153 |
154 | def current_dir
155 | Dir.pwd
156 | end
157 |
158 | def in_minitest?
159 | defined?(Minitest)
160 | end
161 | end
162 | end
163 | end
164 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/auth_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class AuthGenerator < BaseGenerator
6 | source_root File.expand_path('templates/auth', __dir__)
7 |
8 | def add_gems
9 | gem_group :default do
10 | gem 'clearance', '~> 2.9.3'
11 | gem 'omniauth', '~> 2.1.2'
12 | gem 'omniauth-google-oauth2', '~> 1.2.1'
13 | gem 'omniauth-rails_csrf_protection', '~> 1.0.2'
14 | end
15 |
16 | run 'bundle install'
17 | end
18 |
19 | def setup_clearance
20 | generate 'clearance:install'
21 | rake 'db:migrate'
22 | generate 'clearance:views'
23 | end
24 |
25 | def configure_clearance
26 | gsub_file 'config/initializers/clearance.rb',
27 | 'config.mailer_sender = "reply@example.com"',
28 | 'config.mailer_sender = Rails.application.credentials.dig(:app, :mailer_sender)'
29 |
30 | inject_into_file 'config/initializers/clearance.rb', after: 'Clearance.configure do |config|' do
31 | "\n config.redirect_url = \"/demo\""
32 | end
33 | end
34 |
35 | def setup_omniauth
36 | create_file 'config/initializers/omniauth.rb' do
37 | <<~RUBY
38 | # frozen_string_literal: true
39 |
40 | Rails.application.config.middleware.use OmniAuth::Builder do
41 | provider :google_oauth2,
42 | Rails.application.credentials.dig(:google_oauth, :client_id),
43 | Rails.application.credentials.dig(:google_oauth, :client_secret),
44 | {
45 | scope: "email",
46 | prompt: "select_account"
47 | }
48 | end
49 |
50 | OmniAuth.config.allowed_request_methods = %i[get]
51 | RUBY
52 | end
53 |
54 | generate 'migration', 'AddOmniauthToUsers provider:string uid:string'
55 | inject_into_file Dir['db/migrate/*add_omniauth_to_users.rb'].first,
56 | after: "add_column :users, :uid, :string\n" do
57 | <<-RUBY
58 | add_index :users, [:provider, :uid], unique: true
59 | RUBY
60 | end
61 |
62 | inject_into_file 'app/models/user.rb', after: "include Clearance::User\n" do
63 | <<-RUBY
64 |
65 | def self.from_omniauth(auth)
66 | where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
67 | user.email = auth.info.email
68 | user.password = SecureRandom.hex(10)
69 | end
70 | end
71 | RUBY
72 | end
73 |
74 | rake 'db:migrate'
75 | end
76 |
77 | def add_omniauth_controller
78 | template 'app/controllers/omniauth_callbacks_controller.rb',
79 | 'app/controllers/omniauth_callbacks_controller.rb'
80 | end
81 |
82 | def update_routes
83 | route <<~RUBY
84 | # OmniAuth callback routes
85 | get "auth/:provider/callback", to: "omniauth_callbacks#google_oauth2", constraints: { provider: "google_oauth2" }
86 | get 'auth/failure', to: 'omniauth_callbacks#failure'
87 | RUBY
88 | end
89 |
90 | def git_commit
91 | git add: '.', commit: %(-m 'Add authentication with Clearance and OmniAuth')
92 |
93 | say 'Successfully added authentication with Clearance and OmniAuth', :green
94 | end
95 | end
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/base_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class BaseGenerator < Rails::Generators::Base
6 | class BaseGeneratorError < StandardError; end
7 |
8 | include Rails::Generators::Actions
9 | include GsubValidation
10 |
11 | def self.default_command_options
12 | { abort_on_failure: true }
13 | end
14 |
15 | protected
16 |
17 | def run(*args)
18 | options = args.extract_options!
19 | options = self.class.default_command_options.merge(options)
20 | super(*args, options.merge(force: true)) # do not ask for confirmation on overrides
21 | end
22 |
23 | private
24 |
25 | def check_required_env_vars(required_vars)
26 | missing_vars = required_vars.reject { |var| ENV[var].present? }
27 |
28 | return if missing_vars.empty?
29 |
30 | say "\nError: Missing required environment variables:", :red
31 | missing_vars.each { |var| say " - #{var}", :red }
32 | say "\nPlease set these environment variables before continuing:", :red
33 | missing_vars.each { |var| say " export #{var}=your-value", :yellow }
34 |
35 | raise BaseGeneratorError, 'Missing required environment variables'
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/concerns/gsub_validation.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | module GsubValidation
6 | def validate_gsub_strings(validations)
7 | validations.each do |validation|
8 | file_path = File.join(destination_root, validation.fetch(:file))
9 |
10 | unless File.exist?(file_path)
11 | raise "Required file not found: #{validation.fetch(:file)}. Maybe a dependency changed?"
12 | end
13 |
14 | content = File.read(file_path)
15 | validation.fetch(:patterns).each do |pattern|
16 | unless content.include?(pattern)
17 | raise "Expected to find '#{pattern}' in #{validation.fetch(:file)} but didn't. Maybe a dependency changed?"
18 | end
19 | end
20 | end
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/litestream_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class LitestreamGenerator < BaseGenerator
6 | source_root File.expand_path('templates/litestream', __dir__)
7 |
8 | class_option :bucketname, type: :string, required: true, desc: 'Litestream bucketname'
9 | class_option :name, type: :string, required: true, desc: 'Application name for volume and bucket naming'
10 | class_option :ip, type: :string, required: true, desc: 'Server IP address'
11 |
12 | def check_required_env_vars
13 | super(%w[
14 | LITESTREAM_ACCESS_KEY_ID
15 | LITESTREAM_SECRET_ACCESS_KEY
16 | LITESTREAM_ENDPOINT
17 | LITESTREAM_REGION
18 | ])
19 | end
20 |
21 | def create_litestream_config
22 | template 'litestream.yml.erb', 'config/litestream.yml'
23 | end
24 |
25 | def add_kamal_secrets
26 | inject_into_file '.kamal/secrets', after: "RAILS_MASTER_KEY=$(cat config/master.key)\n" do
27 | <<~YAML
28 |
29 | # Litestream credentials for S3-compatible storage
30 | LITESTREAM_ACCESS_KEY_ID=$LITESTREAM_ACCESS_KEY_ID
31 | LITESTREAM_SECRET_ACCESS_KEY=$LITESTREAM_SECRET_ACCESS_KEY
32 | LITESTREAM_ENDPOINT=$LITESTREAM_ENDPOINT
33 | LITESTREAM_REGION=$LITESTREAM_REGION
34 | LITESTREAM_BUCKET_NAME=#{options[:bucketname]}
35 | YAML
36 | end
37 | end
38 |
39 | def add_to_deployment
40 | validations = [
41 | {
42 | file: 'config/deploy.yml',
43 | patterns: ["bin/rails dbconsole\"\n", "arch: amd64\n"]
44 | }
45 | ]
46 |
47 | validate_gsub_strings(validations)
48 |
49 | inject_into_file 'config/deploy.yml', after: "arch: amd64\n" do
50 | <<~YAML
51 |
52 | accessories:
53 | litestream:
54 | image: litestream/litestream:0.3
55 | host: #{options[:ip]}
56 | volumes:
57 | - "#{options[:name].underscore}_storage:/rails/storage"
58 | files:
59 | - config/litestream.yml:/etc/litestream.yml
60 | cmd: replicate -config /etc/litestream.yml
61 | env:
62 | secret:
63 | - LITESTREAM_ACCESS_KEY_ID
64 | - LITESTREAM_SECRET_ACCESS_KEY
65 | - LITESTREAM_ENDPOINT
66 | - LITESTREAM_REGION
67 | - LITESTREAM_BUCKET_NAME
68 | YAML
69 | end
70 |
71 | inject_into_file 'config/deploy.yml', after: "bin/rails dbconsole\"\n" do
72 | <<-YAML
73 | restore-db-app: accessory exec litestream "restore -if-replica-exists -config /etc/litestream.yml /rails/storage/production.sqlite3"
74 | restore-db-cache: accessory exec litestream "restore -if-replica-exists -config /etc/litestream.yml /rails/storage/production_cache.sqlite3"
75 | restore-db-queue: accessory exec litestream "restore -if-replica-exists -config /etc/litestream.yml /rails/storage/production_queue.sqlite3"
76 | restore-db-cable: accessory exec litestream "restore -if-replica-exists -config /etc/litestream.yml /rails/storage/production_cable.sqlite3"
77 | restore-db-ownership: server exec "sudo chown -R 1000:1000 /var/lib/docker/volumes/#{options[:name].underscore}_storage/_data/"
78 | YAML
79 | end
80 | end
81 |
82 | def git_commit
83 | git add: '.', commit: %(-m 'Add Litestream configuration')
84 |
85 | say 'Successfully added Litestream configuration', :green
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/mailjet_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class MailjetGenerator < BaseGenerator
6 | source_root File.expand_path('templates/mailjet', __dir__)
7 |
8 | class_option :name, type: :string, required: true, desc: 'Name of the service for email sender'
9 | class_option :domain, type: :string, required: true, desc: 'Host domain for the application'
10 |
11 | def initialize(*args)
12 | super
13 | @name = options[:name]
14 | @domain = options[:domain]
15 | end
16 |
17 | def add_gem
18 | gem_group :default do
19 | gem 'mailjet', '~> 1.8'
20 | end
21 |
22 | run 'bundle install'
23 | end
24 |
25 | def create_initializer
26 | create_file 'config/initializers/mailjet.rb' do
27 | <<~RUBY
28 | # frozen_string_literal: true
29 |
30 | Mailjet.configure do |config|
31 | config.api_key = Rails.application.credentials.dig(:mailjet, :api_key)
32 | config.secret_key = Rails.application.credentials.dig(:mailjet, :secret_key)
33 | config.default_from = Rails.application.credentials.dig(:app, :mailer_sender)
34 | config.api_version = "v3.1"
35 | end
36 | RUBY
37 | end
38 | end
39 |
40 | def configure_mailer
41 | environment(nil, env: 'production') do
42 | <<~RUBY
43 | # Mailjet API configuration
44 | config.action_mailer.delivery_method = :mailjet_api
45 | RUBY
46 | end
47 |
48 | gsub_file 'app/mailers/application_mailer.rb',
49 | /default from: .+$/,
50 | 'default from: Rails.application.credentials.dig(:app, :mailer_sender)'
51 |
52 | gsub_file 'config/environments/production.rb',
53 | /config\.action_mailer\.default_url_options = \{ host: .+\}/,
54 | 'config.action_mailer.default_url_options = { host: Rails.application.credentials.dig(:app, :host) }'
55 | end
56 |
57 | def git_commit
58 | git add: '.', commit: %(-m 'Add Mailjet configuration')
59 |
60 | say 'Successfully added Mailjet configuration', :green
61 | end
62 |
63 | private
64 |
65 | attr_reader :name, :domain
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/opentelemetry_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class OpentelemetryGenerator < BaseGenerator
6 | source_root File.expand_path('templates/opentelemetry', __dir__)
7 |
8 | class_option :name, type: :string, required: true, desc: 'Name of the application'
9 |
10 | def add_kamal_config
11 | validations = [
12 | {
13 | file: 'config/deploy.yml',
14 | patterns: [
15 | "web:\n",
16 | "SOLID_QUEUE_IN_PUMA: true\n"
17 | ]
18 | }
19 | ]
20 |
21 | validate_gsub_strings(validations)
22 |
23 | inject_into_file 'config/deploy.yml', after: "web:\n" do
24 | <<-YAML
25 | options:
26 | "add-host": host.docker.internal:host-gateway
27 | YAML
28 | end
29 |
30 | inject_into_file 'config/deploy.yml', after: "SOLID_QUEUE_IN_PUMA: true\n" do
31 | <<-YAML
32 | # OpenTelemetry env vars
33 | OTEL_EXPORTER: otlp
34 | OTEL_SERVICE_NAME: #{options[:name]}
35 | OTEL_EXPORTER_OTLP_ENDPOINT: http://host.docker.internal:4318
36 | YAML
37 | end
38 | end
39 |
40 | def add_gems
41 | gem_group :default do
42 | gem 'opentelemetry-sdk', '~> 1.6.0'
43 | gem 'opentelemetry-exporter-otlp', '~> 0.29.1'
44 | gem 'opentelemetry-instrumentation-all', '~> 0.72.0'
45 |
46 | gem 'lograge', '~> 0.14.0'
47 | gem 'logstash-event', '~> 1.2.02'
48 | end
49 |
50 | run 'bundle install'
51 | end
52 |
53 | def configure_opentelemetry
54 | environment_file = 'config/environment.rb'
55 |
56 | prepend_to_file environment_file, "require 'opentelemetry/sdk'\n"
57 | inject_into_file environment_file, before: 'Rails.application.initialize!' do
58 | <<~RUBY
59 |
60 | OpenTelemetry::SDK.configure do |c|
61 | c.use_all unless Rails.env.development? || Rails.env.test?
62 | end
63 |
64 | RUBY
65 | end
66 | end
67 |
68 | def setup_lograge
69 | template 'lograge.rb.erb', 'config/initializers/lograge.rb'
70 | end
71 |
72 | def git_commit
73 | git add: '.', commit: %(-m 'Add OpenTelemetry')
74 |
75 | say 'Successfully added OpenTelemetry configuration', :green
76 | end
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/plausible_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class PlausibleGenerator < ServerCommandGenerator
6 | source_root File.expand_path('templates/shell_scripts', __dir__)
7 |
8 | class_option :analytics_host, type: :string, required: true,
9 | desc: 'Domain where Plausible Analytics will be hosted'
10 |
11 | def initialize(*args)
12 | super
13 | @analytics_host = options[:analytics_host]
14 | end
15 |
16 | private
17 |
18 | def script_name
19 | 'plausible'
20 | end
21 |
22 | def check_path
23 | '~/plausible-ce'
24 | end
25 |
26 | def title
27 | "Installing Plausible Analytics on remote server #{options[:ssh_user]}@#{options[:ssh_host]}"
28 | end
29 |
30 | attr_reader :analytics_host
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/plausible_instrumentation_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class PlausibleInstrumentationGenerator < BaseGenerator
6 | class_option :domain, type: :string, required: true, desc: 'Domain of your application'
7 | class_option :analytics, type: :string, required: true, desc: 'Domain where Plausible is hosted'
8 |
9 | def add_plausible_script
10 | content = <<~HTML.indent(4)
11 | <%# Plausible Analytics %>
12 |
13 |
14 | HTML
15 |
16 | gsub_file 'app/views/layouts/application.html.erb',
17 | %r{<%# Plausible Analytics %>.*?\s*}m,
18 | content.strip.to_s
19 | end
20 |
21 | def git_commit
22 | git add: '.', commit: %(-m 'Add Plausible Analytics')
23 |
24 | say 'Successfully added Plausible Analytics', :green
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/registry_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class RegistryGenerator < ServerCommandGenerator
6 | source_root File.expand_path('templates/shell_scripts', __dir__)
7 |
8 | class_option :registry_host, type: :string, required: true,
9 | desc: 'Domain where Docker Registry will be hosted'
10 | class_option :registry_username, type: :string, required: true,
11 | desc: 'Username for registry authentication'
12 | class_option :registry_password, type: :string, required: true,
13 | desc: 'Password for registry authentication'
14 |
15 | def initialize(*args)
16 | super
17 | @registry_host = options[:registry_host]
18 | @registry_username = options[:registry_username]
19 | @registry_password = options[:registry_password]
20 | end
21 |
22 | private
23 |
24 | def script_name
25 | 'registry'
26 | end
27 |
28 | def check_path
29 | '~/docker-registry'
30 | end
31 |
32 | def title
33 | "Installing Docker Registry on remote server #{options[:ssh_user]}@#{options[:ssh_host]}"
34 | end
35 |
36 | attr_reader :registry_host, :registry_username, :registry_password
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/sentry_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class SentryGenerator < BaseGenerator
6 | def add_gems
7 | gem_group :default do
8 | gem 'sentry-ruby', '~> 5.22.3'
9 | gem 'sentry-rails', '~> 5.22.3'
10 | end
11 |
12 | run 'bundle install'
13 | end
14 |
15 | def generate_sentry_initializer
16 | generate 'sentry'
17 |
18 | validations = [
19 | {
20 | file: 'config/initializers/sentry.rb',
21 | patterns: [
22 | 'Sentry.init'
23 | ]
24 | }
25 | ]
26 |
27 | validate_gsub_strings(validations)
28 | end
29 |
30 | def configure_sentry
31 | gsub_file 'config/initializers/sentry.rb', /Sentry\.init.*end\n/m do
32 | <<~RUBY
33 | Sentry.init do |config|
34 | config.dsn = Rails.application.credentials.dig(:sentry_dsn)
35 | config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
36 | end
37 | RUBY
38 | end
39 | end
40 |
41 | def git_commit
42 | git add: '.', commit: %(-m 'Add Sentry')
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/server_command_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'English'
4 | require 'fileutils'
5 | require 'base64'
6 | require 'securerandom'
7 |
8 | module RailsMaker
9 | module Generators
10 | class ServerCommandGenerator < BaseGenerator
11 | source_root File.expand_path('templates/shell_scripts', __dir__)
12 |
13 | class_option :ssh_host, type: :string, required: true, desc: 'SSH host'
14 | class_option :ssh_user, type: :string, required: true, desc: 'SSH user'
15 | class_option :key_path, type: :string, desc: 'Path to SSH private key (optional)'
16 | class_option :force, type: :boolean, default: false,
17 | desc: 'Force installation even if already installed'
18 |
19 | def execute_script
20 | return unless ssh_available?
21 |
22 | say "⚠️ WARNING: This command will SSH into #{options[:ssh_user]}@#{options[:ssh_host]} and install software.",
23 | :yellow
24 | return unless yes?('Do you want to proceed? (y/N)')
25 |
26 | # Generate random suffix for tmp files
27 | random_suffix = SecureRandom.hex(8)
28 |
29 | # Store the port check filename for use in templates
30 | @port_check_filename = "port_check.sh.#{random_suffix}"
31 |
32 | remote_files_content = remote_files.map do |file|
33 | tmp_filename = "#{file[:filename]}.#{random_suffix}"
34 | template file[:template], "/tmp/#{tmp_filename}"
35 | content = File.read("/tmp/#{tmp_filename}")
36 | FileUtils.rm("/tmp/#{tmp_filename}")
37 | [tmp_filename, Base64.strict_encode64(content)]
38 | end.to_h
39 |
40 | file_commands = remote_files_content.map do |filename, content|
41 | [
42 | "echo '#{content}' | base64 -d > /tmp/#{filename}",
43 | filename.end_with?("install_script.sh.#{random_suffix}") ? "chmod +x /tmp/#{filename}" : nil
44 | ]
45 | end.flatten.compact
46 |
47 | execute_remote_commands(
48 | [
49 | *file_commands,
50 | "/tmp/install_script.sh.#{random_suffix}",
51 | *remote_files_content.keys.map { |filename| "rm /tmp/#{filename}" }
52 | ],
53 | title: title,
54 | check_path: check_path,
55 | force: options[:force]
56 | )
57 | end
58 |
59 | protected
60 |
61 | def script_name
62 | raise NotImplementedError, 'Subclasses must implement #script_name'
63 | end
64 |
65 | def check_path
66 | nil
67 | end
68 |
69 | def title
70 | "Executing #{script_name} script"
71 | end
72 |
73 | def config_files
74 | []
75 | end
76 |
77 | def remote_files
78 | [
79 | *config_files,
80 | {
81 | template: '_port_check.sh',
82 | filename: 'port_check.sh'
83 | },
84 | {
85 | template: "#{script_name}.sh.erb",
86 | filename: 'install_script.sh'
87 | }
88 | ]
89 | end
90 |
91 | def install_commands
92 | [
93 | '/tmp/install_script.sh',
94 | *remote_files.map { |file| "rm /tmp/#{file[:filename]}" }
95 | ]
96 | end
97 |
98 | private
99 |
100 | def ssh_available?
101 | return true if system('which ssh', out: File::NULL)
102 |
103 | say_status 'error', 'SSH client not found. Please install SSH first.', :red
104 | false
105 | end
106 |
107 | def ssh_destination
108 | "#{options[:ssh_user]}@#{options[:ssh_host]}"
109 | end
110 |
111 | def ssh_options
112 | opts = [
113 | 'StrictHostKeyChecking=accept-new',
114 | 'ConnectTimeout=10'
115 | ]
116 | opts.map { |opt| "-o #{opt}" }.join(' ')
117 | end
118 |
119 | def installation_exists?(check_path)
120 | return false unless check_path
121 |
122 | "[ -d #{check_path} ]"
123 | end
124 |
125 | def execute_remote_commands(commands, options = {})
126 | if options[:key_path] && !File.exist?(File.expand_path(options[:key_path]))
127 | say_status 'error', "SSH key not found: #{options[:key_path]}", :red
128 | raise BaseGeneratorError, 'SSH key not found'
129 | end
130 |
131 | title = options[:title] || 'Executing remote commands'
132 | say_status 'start', title, :blue
133 |
134 | script_content = []
135 |
136 | if options[:check_path] && !options[:force]
137 | script_content << <<~SHELL
138 | if #{installation_exists?(options[:check_path])}; then
139 | echo "Installation already exists at #{options[:check_path]}"
140 | echo "Use --force to reinstall"
141 | exit 0
142 | fi
143 | SHELL
144 | end
145 |
146 | script_content += commands.map do |cmd|
147 | <<~SHELL
148 | echo "→ Executing: #{cmd}"
149 | if ! #{cmd}; then
150 | echo "✗ Command failed: #{cmd}"
151 | raise BaseGeneratorError, "Command failed: #{cmd}"
152 | fi
153 | SHELL
154 | end
155 |
156 | script_content << 'exit 0'
157 |
158 | ssh_cmd = ['ssh']
159 | ssh_cmd << "-i #{options[:key_path]}" if options[:key_path]
160 | ssh_cmd << ssh_options
161 | ssh_cmd << ssh_destination
162 | ssh_cmd << "'#{script_content.join("\n")}'"
163 |
164 | success = system(ssh_cmd.join(' '))
165 |
166 | if success
167 | say_status 'success', 'All commands completed successfully', :green
168 | else
169 | say_status 'error', "Command failed with exit status #{$CHILD_STATUS.exitstatus}", :red
170 | case $CHILD_STATUS.exitstatus
171 | when 255
172 | say ' → Could not connect to the server. Please check:', :red
173 | say ' • SSH host and user are correct'
174 | say ' • Your authentication credentials are valid'
175 | say ' • Server is reachable and SSH port (22) is open'
176 | when 126, 127
177 | say ' → Command not found or not executable', :red
178 | say ' • Ensure all required software is installed on the remote server'
179 | end
180 | exit $CHILD_STATUS.exitstatus
181 | end
182 | end
183 | end
184 | end
185 | end
186 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/signoz_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class SignozGenerator < ServerCommandGenerator
6 | source_root File.expand_path('templates/shell_scripts', __dir__)
7 |
8 | class_option :signoz_version, type: :string, default: 'v0.71.0',
9 | desc: 'Version of SigNoz to install'
10 |
11 | def initialize(*args)
12 | super
13 | @signoz_version = options[:signoz_version]
14 | end
15 |
16 | private
17 |
18 | def script_name
19 | 'signoz'
20 | end
21 |
22 | def check_path
23 | '~/signoz'
24 | end
25 |
26 | def title
27 | "Installing SigNoz on remote server #{options[:ssh_user]}@#{options[:ssh_host]}"
28 | end
29 |
30 | attr_reader :signoz_version
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/signoz_opentelemetry_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class SignozOpentelemetryGenerator < ServerCommandGenerator
6 | source_root File.expand_path('templates/shell_scripts', __dir__)
7 |
8 | class_option :signoz_server_host, type: :string, required: true, desc: 'Host where SigNoz is running'
9 | class_option :hostname, type: :string, desc: 'Override the server hostname'
10 | class_option :signoz_version, type: :string, default: 'v0.71.0',
11 | desc: 'Version of SigNoz to install'
12 |
13 | def initialize(*args)
14 | super
15 | @signoz_server_host = options[:signoz_server_host]
16 | @hostname = options[:hostname]
17 | @signoz_version = options[:signoz_version]
18 | end
19 |
20 | private
21 |
22 | def script_name
23 | 'signoz_opentelemetry'
24 | end
25 |
26 | def check_path
27 | '~/signoz-opentelemetry'
28 | end
29 |
30 | def title
31 | "Installing SigNoz OpenTelemetry client on remote server #{options[:ssh_user]}@#{options[:ssh_host]}"
32 | end
33 |
34 | attr_reader :signoz_server_host, :hostname, :signoz_version
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/app/credentials.example.yml:
--------------------------------------------------------------------------------
1 | # Needed credentials.yml. To edit yours:
2 | # VISUAL="vim" bin/rails credentials:edit
3 |
4 | secret_key_base: "your-secret-key-base"
5 | google_oauth:
6 | client_id: "your-client-id"
7 | client_secret: "your-client-secret"
8 | sentry_dsn: "your-sentry-dsn"
9 | mailjet:
10 | api_key: "your-api-key"
11 | secret_key: "your-secret-key"
12 | app:
13 | host: "your-host"
14 | mailer_sender: "YourApp "
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/app/main_index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
15 |
20 |
21 |
8
22 |
23 |
24 |
27 |
28 |
8 Items
29 |
Subtotal: $999
30 |
31 | View cart
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
Hello there
64 |
65 | Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi exercitationem
66 | quasi. In deleniti eaque aut repudiandae et a id nisi.
67 |
68 |
Get Started
69 |
70 |
71 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/auth/app/controllers/omniauth_callbacks_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class OmniauthCallbacksController < ApplicationController
4 | def google_oauth2
5 | user = User.from_omniauth(request.env['omniauth.auth'])
6 |
7 | if user.persisted?
8 | sign_in user
9 | redirect_to demo_url, notice: 'Successfully signed in with Google!'
10 | else
11 | redirect_to sign_in_url, alert: 'Failed to sign in with Google.'
12 | end
13 | end
14 |
15 | def failure
16 | redirect_to sign_in_url, alert: 'Failed to sign in with Google.'
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/litestream/litestream.yml.erb:
--------------------------------------------------------------------------------
1 | access-key-id: ${LITESTREAM_ACCESS_KEY_ID}
2 | secret-access-key: ${LITESTREAM_SECRET_ACCESS_KEY}
3 |
4 | dbs:
5 | - path: /rails/storage/production.sqlite3
6 | replicas:
7 | - type: s3
8 | bucket: ${LITESTREAM_BUCKET_NAME}
9 | path: db_backup/production
10 | endpoint: ${LITESTREAM_ENDPOINT}
11 | region: ${LITESTREAM_REGION}
12 | - path: /rails/storage/production_cache.sqlite3
13 | replicas:
14 | - type: s3
15 | bucket: ${LITESTREAM_BUCKET_NAME}
16 | path: db_backup/production_cache
17 | endpoint: ${LITESTREAM_ENDPOINT}
18 | region: ${LITESTREAM_REGION}
19 | - path: /rails/storage/production_queue.sqlite3
20 | replicas:
21 | - type: s3
22 | bucket: ${LITESTREAM_BUCKET_NAME}
23 | path: db_backup/production_queue
24 | endpoint: ${LITESTREAM_ENDPOINT}
25 | region: ${LITESTREAM_REGION}
26 | - path: /rails/storage/production_cable.sqlite3
27 | replicas:
28 | - type: s3
29 | bucket: ${LITESTREAM_BUCKET_NAME}
30 | path: db_backup/production_cable
31 | endpoint: ${LITESTREAM_ENDPOINT}
32 | region: ${LITESTREAM_REGION}
33 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/opentelemetry/lograge.rb.erb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.lograge.enabled = true
3 | config.lograge.formatter = Lograge::Formatters::Logstash.new
4 | config.lograge.custom_options = lambda do |event|
5 | {
6 | params: event.payload[:params].except("controller", "action")
7 | }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/shell_scripts/_port_check.sh:
--------------------------------------------------------------------------------
1 | check_port() {
2 | if lsof -i ":$1" >/dev/null 2>&1; then
3 | echo "Error: Port $1 is already in use"
4 | return 1
5 | fi
6 | return 0
7 | }
8 |
9 | check_required_ports() {
10 | local ports="$1"
11 | local ports_in_use=""
12 |
13 | echo "🔍 Checking if required ports are available..."
14 |
15 | for port in $ports; do
16 | if ! check_port "$port"; then
17 | ports_in_use="$ports_in_use $port"
18 | fi
19 | done
20 |
21 | if [ ! -z "$ports_in_use" ]; then
22 | echo "❌ Cannot proceed. The following ports are already in use:$ports_in_use"
23 | echo "Please free up these ports before continuing"
24 | exit 1
25 | fi
26 |
27 | echo "✅ All required ports are available"
28 | }
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/shell_scripts/plausible.sh.erb:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | if ! command -v docker &> /dev/null; then
5 | echo "Error: Docker is not installed. Please install Docker first."
6 | echo "Visit https://docs.docker.com/get-docker/ for installation instructions."
7 | exit 1
8 | fi
9 |
10 | if ! command -v docker compose &> /dev/null; then
11 | echo "Error: Docker Compose is not installed. Please install Docker Compose first."
12 | echo "Visit https://docs.docker.com/compose/install/ for installation instructions."
13 | exit 1
14 | fi
15 |
16 | if ! command -v git &> /dev/null; then
17 | echo "Error: Git is not installed. Please install Git first."
18 | echo "Visit https://git-scm.com/downloads for installation instructions."
19 | exit 1
20 | fi
21 |
22 | # Source port checking functions
23 | . /tmp/<%= @port_check_filename %>
24 |
25 | # Check required ports
26 | check_required_ports "80 443"
27 |
28 | echo "📝 Cloning Plausible repository..."
29 | git clone -b v2.1.4 --single-branch https://github.com/plausible/community-edition plausible-ce
30 | cd plausible-ce
31 |
32 | echo "⚙️ Configuring environment..."
33 | touch .env
34 | echo "BASE_URL=https://<%= analytics_host %>" >> .env
35 | echo "SECRET_KEY_BASE=$(openssl rand -base64 48)" >> .env
36 | echo "HTTP_PORT=80" >> .env
37 | echo "HTTPS_PORT=443" >> .env
38 |
39 | echo "🔧 Creating docker-compose override..."
40 | cat > compose.override.yml << 'EOF'
41 | services:
42 | plausible:
43 | ports:
44 | - 80:80
45 | - 443:443
46 | EOF
47 |
48 | echo "🚀 Starting Plausible services... (it may take a while)"
49 | docker compose up -d --quiet-pull
50 |
51 | echo "✨ Verifying services..."
52 | if docker compose ps --status running | grep -q "plausible"; then
53 | echo "✅ Plausible Analytics is running successfully!"
54 | else
55 | echo "❌ Error: Plausible Analytics failed to start. Please check the logs with 'docker compose logs'"
56 | exit 1
57 | fi
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/shell_scripts/registry.sh.erb:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | if ! command -v docker &> /dev/null; then
5 | echo "Error: Docker is not installed. Please install Docker first."
6 | echo "Visit https://docs.docker.com/get-docker/ for installation instructions."
7 | exit 1
8 | fi
9 |
10 | if ! command -v docker compose &> /dev/null; then
11 | echo "Error: Docker Compose is not installed. Please install Docker Compose first."
12 | echo "Visit https://docs.docker.com/compose/install/ for installation instructions."
13 | exit 1
14 | fi
15 |
16 | # Source port checking functions
17 | . /tmp/<%= @port_check_filename %>
18 |
19 | # Check required ports
20 | check_required_ports "80 443 5000"
21 |
22 | echo "📝 Creating Docker Registry installation directory..."
23 | mkdir -p ~/docker-registry/{auth,data,caddy_data,caddy_config}
24 | cd ~/docker-registry
25 |
26 | echo "⚙️ Creating authentication credentials..."
27 | docker run --rm httpd:2.4-alpine htpasswd -Bbn "<%= registry_username %>" "<%= registry_password %>" > auth/htpasswd
28 |
29 | echo "🔧 Creating docker-compose configuration..."
30 | cat > docker-compose.yml << 'EOF'
31 | version: '3'
32 |
33 | services:
34 | registry:
35 | image: registry:2
36 | restart: always
37 | ports:
38 | - "5000:5000"
39 | environment:
40 | REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
41 | REGISTRY_AUTH: htpasswd
42 | REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
43 | REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
44 | volumes:
45 | - ./data:/var/lib/registry
46 | - ./auth:/auth
47 |
48 | caddy:
49 | image: caddy:2
50 | restart: always
51 | ports:
52 | - "80:80"
53 | - "443:443"
54 | volumes:
55 | - ./Caddyfile:/etc/caddy/Caddyfile
56 | - ./caddy_data:/data
57 | - ./caddy_config:/config
58 | EOF
59 |
60 | echo "📄 Creating Caddy configuration..."
61 | cat > Caddyfile << EOF
62 | <%= registry_host %> {
63 | reverse_proxy registry:5000
64 | }
65 | EOF
66 |
67 | echo "🚀 Starting Docker Registry services..."
68 | docker compose up -d --quiet-pull
69 |
70 | echo "✨ Verifying services..."
71 | if docker compose ps --status running | grep -q "registry"; then
72 | echo "✅ Docker Registry is running successfully!"
73 | echo
74 | echo "➡️ IMPORTANT: Configure DNS:"
75 | echo " 1. Create an A record for <%= registry_host %> pointing to this server"
76 | echo
77 | echo "➡️ Login to your registry with:"
78 | echo " docker login <%= registry_host %>"
79 | echo " Username: <%= registry_username %>"
80 | echo " Password: <%= registry_password %>"
81 | else
82 | echo "❌ Error: Docker Registry failed to start. Please check the logs with 'docker compose logs'"
83 | exit 1
84 | fi
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/shell_scripts/signoz.sh.erb:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | if ! command -v docker &> /dev/null; then
5 | echo "Error: Docker is not installed. Please install Docker first."
6 | echo "Visit https://docs.docker.com/get-docker/ for installation instructions."
7 | exit 1
8 | fi
9 |
10 | if ! command -v docker compose &> /dev/null; then
11 | echo "Error: Docker Compose is not installed. Please install Docker Compose first."
12 | echo "Visit https://docs.docker.com/compose/install/ for installation instructions."
13 | exit 1
14 | fi
15 |
16 | if ! command -v git &> /dev/null; then
17 | echo "Error: Git is not installed. Please install Git first."
18 | echo "Visit https://git-scm.com/downloads for installation instructions."
19 | exit 1
20 | fi
21 |
22 | # Source port checking functions
23 | . /tmp/<%= @port_check_filename %>
24 |
25 | # Check required ports
26 | check_required_ports "80 443 3301 4317 4318"
27 |
28 | echo "📝 Cloning SigNoz repository..."
29 | git clone -b main https://github.com/SigNoz/signoz.git --depth 1 --branch <%= signoz_version %>
30 |
31 | cd signoz/deploy/
32 |
33 | echo "🚀 Starting SigNoz services... (it may take a while)"
34 | docker compose -f docker/docker-compose.yaml up -d --remove-orphans --quiet-pull
35 |
36 | echo "✨ Verifying services..."
37 | if docker compose -f docker/docker-compose.yaml ps --status running | grep -q "query-service"; then
38 | echo "✅ SigNoz server is running successfully!"
39 | else
40 | echo "❌ Error: SigNoz server failed to start. Please check the logs with 'docker compose logs'"
41 | exit 1
42 | fi
43 |
44 | docker compose -f docker/generator/infra/docker-compose.yaml up -d --remove-orphans --quiet-pull
45 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/shell_scripts/signoz_opentelemetry.sh.erb:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | if ! command -v docker &> /dev/null; then
5 | echo "Error: Docker is not installed. Please install Docker first."
6 | echo "Visit https://docs.docker.com/get-docker/ for installation instructions."
7 | exit 1
8 | fi
9 |
10 | if ! command -v docker compose &> /dev/null; then
11 | echo "Error: Docker Compose is not installed. Please install Docker Compose first."
12 | echo "Visit https://docs.docker.com/compose/install/ for installation instructions."
13 | exit 1
14 | fi
15 |
16 | if ! command -v git &> /dev/null; then
17 | echo "Error: Git is not installed. Please install Git first."
18 | echo "Visit https://git-scm.com/downloads for installation instructions."
19 | exit 1
20 | fi
21 |
22 | # Source port checking functions
23 | . /tmp/<%= @port_check_filename %>
24 |
25 | # Check required ports
26 | check_required_ports "4317 4318"
27 |
28 | git clone -b main https://github.com/SigNoz/signoz.git --depth 1 signoz-opentelemetry --branch <%= signoz_version %>
29 |
30 | cd signoz-opentelemetry/deploy/
31 |
32 | echo "📝 Configuring SigNoz OpenTelemetry..."
33 | sed -i "s|SIGNOZ_COLLECTOR_ENDPOINT=http://host.docker.internal:4317|SIGNOZ_COLLECTOR_ENDPOINT=http://<%= options[:signoz_server_host] %>:4317|" docker/generator/infra/docker-compose.yaml
34 | sed -i "s|host.name=signoz-host|host.name=<%= options[:hostname] || 'signoz-host' %>|" docker/generator/infra/docker-compose.yaml
35 | sed -i '/external: true/d' docker/generator/infra/docker-compose.yaml
36 |
37 | # Add docker group permissions to the otel-agent service
38 | DOCKER_GID=$(getent group docker | cut -d: -f3)
39 | sed -i "/image: otel\/opentelemetry-collector-contrib/a\ group_add:\n - ${DOCKER_GID}" docker/generator/infra/docker-compose.yaml
40 |
41 | # Uncomment the ports section
42 | sed -i 's/# ports:/ports:/' docker/generator/infra/docker-compose.yaml
43 | sed -i 's/# - / - /' docker/generator/infra/docker-compose.yaml
44 | sed -i 's/# - / - /' docker/generator/infra/docker-compose.yaml
45 |
46 | echo "🚀 Starting SigNoz OpenTelemetry services..."
47 | docker compose -f docker/generator/infra/docker-compose.yaml up -d --remove-orphans --quiet-pull
48 |
49 | echo "✨ Verifying services..."
50 | if docker compose -f docker/generator/infra/docker-compose.yaml ps --status running | grep -q "otel-agent"; then
51 | echo "✅ SigNoz OpenTelemetry collector is running successfully!"
52 | else
53 | echo "❌ Error: SigNoz OpenTelemetry collector failed to start. Please check the logs with 'docker compose logs'"
54 | exit 1
55 | fi
56 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/assets/images/og-image.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgerov/railsmaker/94fdc9f76fe71251924e6fe23bd94806930dd4e6/lib/railsmaker/generators/templates/ui/app/assets/images/og-image.webp
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/assets/images/plausible-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgerov/railsmaker/94fdc9f76fe71251924e6fe23bd94806930dd4e6/lib/railsmaker/generators/templates/ui/app/assets/images/plausible-screenshot.png
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/assets/images/signoz-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgerov/railsmaker/94fdc9f76fe71251924e6fe23bd94806930dd4e6/lib/railsmaker/generators/templates/ui/app/assets/images/signoz-screenshot.png
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/controllers/demo_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DemoController < ApplicationController
4 | before_action :require_login
5 |
6 | def index; end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/controllers/pages_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PagesController < ApplicationController
4 | end
5 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/helpers/seo_helper.rb:
--------------------------------------------------------------------------------
1 | module SeoHelper
2 | def meta_title(title = nil)
3 | content_for(:title) { title } if title.present?
4 | content_for?(:title) ? content_for(:title) : 'RailsMaker - Ship Rails Apps in 15 Minutes'
5 | end
6 |
7 | def meta_description(desc = nil)
8 | content_for(:description) { desc } if desc.present?
9 | content_for?(:description) ? content_for(:description) : 'A fully self-hosted modern Rails template with authentication, analytics and observability. Ship production-ready apps in minutes, not weeks.'
10 | end
11 |
12 | def meta_image(image = nil)
13 | content_for(:og_image) { image } if image.present?
14 | content_for?(:og_image) ? content_for(:og_image) : asset_url('og-image.webp')
15 | end
16 |
17 | def meta_keywords(keywords = nil)
18 | content_for(:keywords) { keywords } if keywords.present?
19 | content_for?(:keywords) ? content_for(:keywords) : 'rails 8, ruby on rails, daisyui, tailwind, web development, starter template, template'
20 | end
21 |
22 | def meta_tags
23 | {
24 | title: meta_title,
25 | description: meta_description,
26 | image: meta_image,
27 | keywords: meta_keywords,
28 | canonical: request.original_url,
29 | author: 'RailsMaker',
30 | robots: 'index, follow',
31 | type: 'website',
32 | twitter_card: 'summary_large_image',
33 | twitter_site: '@sgerov',
34 | og_site_name: 'RailsMaker',
35 | article_published_time: Time.current.iso8601,
36 | article_modified_time: Time.current.iso8601
37 | }
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/javascript/controllers/flash_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus"
2 |
3 | // Connects to data-controller="flash"
4 | export default class extends Controller {
5 | connect() {
6 | setTimeout(() => {
7 | this.element.remove()
8 | }, 5000)
9 | }
10 |
11 | dismiss() {
12 | this.element.remove()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/javascript/controllers/index.js:
--------------------------------------------------------------------------------
1 | // This file is auto-generated by ./bin/rails stimulus:manifest:update
2 | // Run that command whenever you add a new controller or create them with
3 | // ./bin/rails generate stimulus controllerName
4 |
5 | import { application } from "./application"
6 |
7 | import FlashController from "./flash_controller"
8 | application.register("flash", FlashController)
9 |
10 | import ScrollFadeController from "./scroll_fade_controller"
11 | application.register("scroll-fade", ScrollFadeController)
12 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/javascript/controllers/scroll_fade_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus"
2 |
3 | export default class extends Controller {
4 | static values = {
5 | offset: Number
6 | }
7 |
8 | connect() {
9 | this.onScroll = this.onScroll.bind(this)
10 | window.addEventListener('scroll', this.onScroll)
11 | this.onScroll()
12 | }
13 |
14 | disconnect() {
15 | window.removeEventListener('scroll', this.onScroll)
16 | }
17 |
18 | onScroll() {
19 | if (window.scrollY > this.offsetValue) {
20 | this.element.classList.add('opacity-0', 'pointer-events-none')
21 | this.element.classList.remove('opacity-100')
22 | } else {
23 | this.element.classList.add('opacity-100')
24 | this.element.classList.remove('opacity-0', 'pointer-events-none')
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/clearance_mailer/change_password.html.erb:
--------------------------------------------------------------------------------
1 | <%= t(".opening") %>
2 |
3 |
4 | <%= link_to t(".link_text", default: "Change my password"),
5 | url_for([@user, :password, action: :edit, token: @user.confirmation_token]) %>
6 |
7 |
8 | <%= t(".closing") %>
9 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/clearance_mailer/change_password.text.erb:
--------------------------------------------------------------------------------
1 | <%= t(".opening") %>
2 |
3 | <%= url_for([@user, :password, action: :edit, token: @user.confirmation_token]) %>
4 |
5 | <%= t(".closing") %>
6 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/demo/analytics.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
Analytics
11 |
Track your application metrics
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
30 |
Total Page Views
31 |
25.6K
32 |
33 |
34 |
35 |
36 | 21% more than last month
37 |
38 |
39 |
40 |
41 |
48 |
Unique Visitors
49 |
2.6K
50 |
51 |
52 |
53 |
54 | 14% more than last month
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
Traffic Sources
68 |
69 | Last 7 days
70 | Last 30 days
71 | Last 90 days
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
Direct Traffic
80 |
81 |
45%
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
Social Media
91 |
92 |
30%
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
Referral
102 |
103 |
25%
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
Page Performance
116 |
Live Data
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
Homepage
125 |
126 |
127 | Views:
128 | 12,543
129 |
130 |
131 | Avg. Time:
132 | 2m 15s
133 |
134 |
135 |
136 |
137 |
138 |
139 |
Features
140 |
141 |
142 | Views:
143 | 8,234
144 |
145 |
146 | Avg. Time:
147 | 1m 45s
148 |
149 |
150 |
151 |
152 |
153 |
154 |
Pricing
155 |
156 |
157 | Views:
158 | 6,158
159 |
160 |
161 | Avg. Time:
162 | 3m 12s
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | Page
173 | Views
174 | Avg. Time
175 | Bounce Rate
176 |
177 |
178 |
179 |
180 |
181 |
182 | Homepage
183 |
184 | 12,543
185 | 2m 15s
186 | 32%
187 |
188 |
189 |
190 |
191 | Features
192 |
193 | 8,234
194 | 1m 45s
195 | 28%
196 |
197 |
198 |
199 |
200 | Pricing
201 |
202 | 6,158
203 | 3m 12s
204 | 25%
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/demo/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
Dashboard
12 |
Welcome back, Team
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | New Project
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
Active Projects
33 |
12
34 |
35 |
40 |
41 |
↑ 8% from last month
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
Team Members
50 |
24
51 |
52 |
57 |
58 |
↑ 2 new this week
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
Tasks Completed
67 |
84%
68 |
69 |
74 |
75 |
↑ 12% improvement
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
Time Tracked
84 |
164h
85 |
86 |
91 |
92 |
On track with estimates
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
Current Tasks
105 |
106 | All Projects
107 | Website Redesign
108 | Mobile App
109 | API Integration
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | Task
119 | Due Date
120 | Status
121 | Assigned
122 |
123 |
124 |
125 |
126 |
127 | Homepage Redesign
128 | Update hero section and CTA
129 |
130 |
131 | Sep 25
132 | Overdue
133 |
134 | In Progress
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | User Authentication
153 | Implement OAuth flow
154 |
155 |
156 | Sep 28
157 | On Track
158 |
159 | Completed
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | API Documentation
171 | Update endpoint references
172 |
173 |
174 | Sep 30
175 | On Track
176 |
177 | Not Started
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
Homepage Redesign
196 |
In Progress
197 |
198 |
Update hero section and CTA
199 |
200 |
201 |
Sep 25
202 |
Overdue
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
User Authentication
224 |
Completed
225 |
226 |
Implement OAuth flow
227 |
228 |
229 |
Sep 28
230 |
On Track
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
API Documentation
245 |
Not Started
246 |
247 |
Update endpoint references
248 |
249 |
250 |
Sep 30
251 |
On Track
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
Recent Activity
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
Alex completed Homepage Design
280 |
2 hours ago
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
Sarah commented on API Integration
292 |
5 hours ago
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
Mike created New Project
304 |
1 day ago
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/demo/support.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
Support
11 |
Get help with your application
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | How do I get started?
25 |
26 |
27 |
Getting started is easy! Follow these steps:
28 |
29 | Sign up for an account
30 | Complete your profile setup
31 | Follow our quick setup guide
32 | Start building your first project
33 |
34 |
35 |
36 |
37 |
38 |
39 | What payment methods do you accept?
40 |
41 |
42 |
We accept all major payment methods including:
43 |
44 | Credit/Debit Cards (Visa, Mastercard, Amex)
45 | PayPal
46 | Bank Transfers (ACH)
47 | Wire Transfers (for annual plans)
48 |
49 |
50 |
51 |
52 |
53 |
54 | Can I cancel my subscription?
55 |
56 |
57 |
Yes, you can cancel your subscription at any time. Here's what you need to know:
58 |
59 | No long-term contracts required
60 | Cancel directly from your account settings
61 | Access continues until the end of your billing period
62 | Data export available after cancellation
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
81 |
82 |
Contact Support
83 |
Our team typically responds within 24 hours
84 |
85 |
86 |
87 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/layouts/_navbar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link_to demo_path, class: 'flex items-center gap-2 px-2 py-1 rounded-lg group' do %>
4 |
11 |
12 | RailsMaker
13 |
14 | <% end %>
15 |
16 |
17 |
18 |
45 |
46 |
47 |
48 |
49 | <% if signed_in? %>
50 |
51 |
52 |
58 |
59 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
New feature available
69 |
Check out our latest analytics dashboard
70 |
2 hours ago
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
System Update
79 |
Performance improvements and bug fixes
80 |
5 hours ago
81 |
82 |
83 |
84 |
85 |
View all notifications
86 |
87 |
88 |
89 | <% end %>
90 |
91 |
92 |
93 |
98 |
99 | <% if signed_in? %>
100 |
103 |
114 |
115 |
116 | <%= link_to sign_out_path, class: "flex gap-2 text-error", data: { turbo_method: :delete } do %>
117 |
118 |
119 |
120 |
121 |
122 | Sign out
123 | <% end %>
124 |
125 | <% else %>
126 |
127 | <%= link_to sign_in_path, class: "flex gap-2" do %>
128 |
129 |
130 |
131 |
132 |
133 | Sign in
134 | <% end %>
135 |
136 | <% end %>
137 |
138 |
139 |
140 |
141 |
142 | <%# Mobile Bottom Dock Navigation - only shown on mobile %>
143 |
144 |
145 | <%= link_to demo_path,
146 | class: "#{current_page?(demo_path) ? 'dock-active' : ''} relative",
147 | disabled: signed_in? ? nil : 'disabled',
148 | data: { tooltip: signed_in? ? nil : "Please sign in first" } do %>
149 |
150 |
151 |
152 |
153 |
154 |
155 |
Dashboard
156 | <% end %>
157 |
158 | <%= link_to analytics_path,
159 | class: "#{current_page?(analytics_path) ? 'dock-active' : ''}",
160 | disabled: signed_in? ? nil : 'disabled',
161 | data: { tooltip: signed_in? ? nil : "Please sign in first" } do %>
162 |
163 |
Analytics
164 | <% end %>
165 |
166 | <%= link_to support_path,
167 | class: "#{current_page?(support_path) ? 'dock-active' : ''}",
168 | disabled: signed_in? ? nil : 'disabled',
169 | data: { tooltip: signed_in? ? nil : "Please sign in first" } do %>
170 |
171 |
Support
172 | <% end %>
173 |
174 | <% if signed_in? %>
175 | <%= link_to sign_out_path, data: { turbo_method: :delete } do %>
176 |
177 |
178 |
179 |
180 |
181 |
Sign out
182 | <% end %>
183 | <% else %>
184 | <%= link_to sign_in_path do %>
185 |
186 |
187 |
188 |
189 |
Sign in
190 | <% end %>
191 | <% end %>
192 |
193 |
194 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%%= meta_title %>
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | <%%= csrf_meta_tags %>
29 | <%%= csp_meta_tag %>
30 |
31 | <%%= yield :head %>
32 |
33 | <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
34 | <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
35 |
36 |
37 |
38 |
39 |
40 | <%# Includes all stylesheet files in app/assets/stylesheets %>
41 | <%%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
42 | <%%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
43 | <% if plausible_enabled %>
44 | <%%# Plausible Analytics %>
45 |
46 |
47 | <% end %>
48 | <% if sentry_enabled %>
49 | <%%= Sentry.get_trace_propagation_meta.html_safe if defined?(Sentry) %>
50 | <% end %>
51 |
52 |
53 |
54 | <%%= render "layouts/navbar" unless current_page?(root_path) %>
55 |
56 | <%%= render "shared/flash" %>
57 | <%%= yield %>
58 | <%%= render "shared/footer" %>
59 |
60 | <%%= render 'shared/structured_data' %>
61 |
62 |
63 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/pages/privacy.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
Privacy Policy
11 |
Last updated: <%= Time.current.strftime("%B %d, %Y") %>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
1. Information We Collect
19 |
We collect information you provide directly to us when you:
20 |
21 | Create an account
22 | Use our services
23 | Contact us for support
24 | Subscribe to our newsletters
25 |
26 |
27 |
2. How We Use Your Information
28 |
We use the information we collect to:
29 |
30 | Provide, maintain, and improve our services
31 | Process your transactions
32 | Send you technical notices and support messages
33 | Communicate with you about products, services, and events
34 | Protect against fraudulent or illegal activity
35 |
36 |
37 |
3. Information Sharing
38 |
We do not sell or rent your personal information to third parties. We may share your information with:
39 |
40 | Service providers who assist in our operations
41 | Professional advisers
42 | Law enforcement when required by law
43 |
44 |
45 |
4. Data Security
46 |
We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction.
47 |
48 |
5. Your Rights
49 |
You have the right to:
50 |
51 | Access your personal information
52 | Correct inaccurate data
53 | Request deletion of your data
54 | Object to our processing of your data
55 | Export your data
56 |
57 |
58 |
6. Contact Us
59 |
If you have questions about this Privacy Policy, please contact us at:
60 |
Email: privacy@railsmaker.com
61 |
62 |
63 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/pages/terms.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
Terms and Conditions
11 |
Last updated: <%= Time.current.strftime("%B %d, %Y") %>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
1. Agreement to Terms
19 |
By accessing or using RailsMaker, you agree to be bound by these Terms and Conditions and our Privacy Policy. If you disagree with any part of the terms, you do not have permission to access the service.
20 |
21 |
2. Use License
22 |
We grant you a limited, non-exclusive, non-transferable, and revocable license to use our service for your personal or business use, subject to these Terms and any separate agreements with us.
23 |
24 |
3. User Accounts
25 |
When you create an account with us, you must provide accurate, complete, and current information. You are responsible for safeguarding your account credentials and for any activities under your account.
26 |
27 |
4. Acceptable Use
28 |
You agree not to:
29 |
30 | Use the service for any illegal purpose
31 | Violate any laws in your jurisdiction
32 | Infringe upon any intellectual property rights
33 | Transmit harmful code or materials
34 | Attempt to gain unauthorized access to our systems
35 |
36 |
37 |
5. Intellectual Property
38 |
The service and its original content, features, and functionality are owned by RailsMaker and are protected by international copyright, trademark, and other intellectual property laws.
39 |
40 |
6. Termination
41 |
We may terminate or suspend your account and access to the service immediately, without prior notice, for conduct that we believe violates these Terms or is harmful to other users, us, or third parties, or for any other reason.
42 |
43 |
7. Limitation of Liability
44 |
In no event shall RailsMaker, its directors, employees, partners, agents, suppliers, or affiliates be liable for any indirect, incidental, special, consequential, or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses.
45 |
46 |
8. Changes to Terms
47 |
We reserve the right to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice prior to any new terms taking effect.
48 |
49 |
9. Contact Us
50 |
If you have any questions about these Terms, please contact us at:
51 |
Email: legal@railsmaker.com
52 |
53 |
54 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/passwords/create.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
<%= t(".title", default: "Reset Password") %>
5 |
<%= t(".description") %>
6 | <%= link_to t("layouts.application.sign_in"), sign_in_path, class: "btn btn-primary" %>
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/passwords/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :auth_header do %>
2 | Set new password
3 |
4 | Please choose a strong password for your account
5 |
6 | <% end %>
7 |
8 | <%= render layout: 'shared/auth_layout' do %>
9 | <%= form_for :password_reset,
10 | url: [@user, :password, token: @user.confirmation_token],
11 | html: { method: :put, class: "space-y-6" } do |form| %>
12 |
13 | <%= form.label :password, class: "label font-medium" %>
14 | <%= form.password_field :password, class: "input input-bordered w-full" %>
15 |
16 |
17 |
18 | <%= form.submit "Update password", class: "btn btn-primary w-full" %>
19 |
20 | <% end %>
21 | <% end %>
22 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/passwords/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :auth_header do %>
2 | Reset your password
3 |
4 | Enter your email address and we'll send you instructions
5 |
6 | <% end %>
7 |
8 | <%= render layout: 'shared/auth_layout' do %>
9 | <%= form_for :password, url: passwords_path, class: "space-y-6" do |form| %>
10 |
11 | <%= form.label :email, class: "label font-medium" %>
12 | <%= form.email_field :email, class: "input input-bordered w-full", placeholder: "you@example.com" %>
13 |
14 |
15 |
16 | <%= form.submit "Send reset instructions", class: "btn btn-primary w-full" %>
17 |
18 | <% end %>
19 |
20 |
21 |
22 | Remember your password?
23 | <%= link_to "Back to sign in", sign_in_path, class: "link link-hover font-medium" %>
24 |
25 |
26 | <% end %>
27 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :auth_header do %>
2 | Welcome back!
3 |
4 | This is a demo app. Sign in to continue
5 |
6 | <% end %>
7 |
8 | <%= render layout: 'shared/auth_layout' do %>
9 | <%= form_for :session, url: session_path, class: "space-y-6" do |form| %>
10 |
11 | <%= form.email_field :email, class: "input input-lg input-bordered w-full", placeholder: "you@example.com" %>
12 |
13 |
14 |
15 | <%= form.password_field :password, class: "input input-lg input-bordered w-full", placeholder: "Password" %>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
Demo account: test@test.com / 123123123
23 |
24 |
25 |
26 | <%= form.submit t(".submit", default: "Sign in"), class: "btn btn-primary w-full" %>
27 |
28 |
29 | or
30 |
31 |
39 | <% end %>
40 |
41 |
42 | <% if Clearance.configuration.allow_sign_up? %>
43 | <%= link_to t(".sign_up"), sign_up_path, class: "text-sm link link-hover" %>
44 | <% end %>
45 | <% if Clearance.configuration.allow_password_reset? %>
46 | <%= link_to t(".forgot_password"), new_password_path, class: "text-sm link link-hover" %>
47 | <% end %>
48 |
49 | <% end %>
50 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/shared/_auth_layout.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link_to root_path, class: "inline-block" do %>
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | <% end %>
13 | <%= yield :auth_header %>
14 |
15 |
16 |
17 |
18 | <%= yield %>
19 |
20 |
21 |
22 | <%= yield :auth_footer %>
23 |
24 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/shared/_flash.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% flash.each do |type, message| %>
3 |
6 | <% if type == 'notice' %>
7 |
8 |
9 |
10 | <% else %>
11 |
12 |
13 |
14 | <% end %>
15 |
<%= message %>
16 |
×
17 |
18 | <% end %>
19 |
20 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/shared/_footer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= link_to root_path, class: 'flex items-center gap-2 group' do %>
8 |
9 | RailsMaker
10 |
11 | <% end %>
12 |
13 |
14 | Launch your next idea in hours, not weeks. Self-host your app with sensible defaults including authentication,
15 | modern UI components, and production-ready infrastructure meant for quick iteration.
16 |
17 |
18 | <%= link_to "https://www.linkedin.com/in/savagerov/", class: "btn btn-ghost btn-sm", target: "_blank" do %>
19 |
20 |
21 |
22 | <% end %>
23 | <%= link_to "https://github.com/sgerov/railsmaker", class: "btn btn-ghost btn-sm", target: "_blank" do %>
24 |
25 |
26 |
27 | <% end %>
28 |
29 |
30 |
31 |
32 |
33 |
34 |
© <%= Time.current.year %> RailsMaker. All rights reserved.
35 |
36 | <%= link_to "https://gerovlabs.com/", target: "_blank" do %>
37 | gerovlabs.com
38 | <% end %>
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/shared/_structured_data.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/app/views/users/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :auth_header do %>
2 | Create your account
3 |
4 | Start building amazing applications today
5 |
6 | <% end %>
7 |
8 | <%= render layout: 'shared/auth_layout' do %>
9 | <%= form_for @user, class: "space-y-6" do |form| %>
10 |
11 | <%= form.email_field :email, class: "input input-lg input-bordered w-full", placeholder: "you@example.com" %>
12 |
13 |
14 |
15 | <%= form.password_field :password, class: "input input-lg input-bordered w-full", placeholder: "password" %>
16 |
17 |
18 | <% if @user.errors.any? %>
19 |
20 |
21 |
22 |
23 |
<%= @user.errors.full_messages.to_sentence %>
24 |
25 | <% end %>
26 |
27 |
28 | <%= form.submit "Create account", class: "btn btn-primary w-full" %>
29 |
30 |
31 | or continue with
32 |
33 |
41 | <% end %>
42 |
43 |
44 |
45 | Already have an account?
46 | <%= link_to t(".sign_in"), sign_in_path, class: "link link-hover font-medium" %>
47 |
48 |
49 | <% end %>
50 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/config/sitemap.rb:
--------------------------------------------------------------------------------
1 | SitemapGenerator::Sitemap.default_host = "https://#{Rails.application.credentials.dig(:app, :host)}"
2 |
3 | SitemapGenerator::Sitemap.create do
4 | # Home page with high priority and frequent updates
5 | add root_path,
6 | changefreq: 'daily',
7 | priority: 1.0,
8 | lastmod: Time.current
9 |
10 | # Main feature pages
11 | add demo_path,
12 | changefreq: 'weekly',
13 | priority: 0.9,
14 | lastmod: Time.current
15 | add analytics_path,
16 | changefreq: 'weekly',
17 | priority: 0.8,
18 | lastmod: Time.current
19 | add support_path,
20 | changefreq: 'weekly',
21 | priority: 0.8,
22 | lastmod: Time.current
23 |
24 | # Legal pages with lower update frequency
25 | add privacy_path,
26 | changefreq: 'monthly',
27 | priority: 0.6,
28 | lastmod: Time.current
29 | add terms_path,
30 | changefreq: 'monthly',
31 | priority: 0.6,
32 | lastmod: Time.current
33 | end
34 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgerov/railsmaker/94fdc9f76fe71251924e6fe23bd94806930dd4e6/lib/railsmaker/generators/templates/ui/public/icon.png
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/public/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/templates/ui/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
3 | User-agent: *
4 | Allow: /
5 | Sitemap: https://<%= host %>/sitemap.xml.gz
6 |
7 | Disallow: /auth/
8 | Disallow: /rails/
9 |
--------------------------------------------------------------------------------
/lib/railsmaker/generators/ui_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | module Generators
5 | class UiGenerator < BaseGenerator
6 | source_root File.expand_path('templates/ui', __dir__)
7 |
8 | class_option :domain, type: :string, required: true, desc: 'Host domain for the application'
9 | class_option :name, type: :string, required: true, desc: 'Name of the application'
10 | class_option :analytics, type: :string, desc: 'Domain where Plausible is hosted'
11 | class_option :sentry, type: :boolean, default: false, desc: 'Wether Sentry is enabled'
12 |
13 | attr_reader :domain, :name, :host, :app_name, :plausible_enabled, :sentry_enabled
14 |
15 | def initialize(*args)
16 | super
17 |
18 | @host = options[:domain]
19 | @app_name = options[:name]
20 | @plausible_enabled = options[:analytics].present?
21 | @sentry_enabled = options[:sentry]
22 | end
23 |
24 | def add_seo_capabilities
25 | gem 'sitemap_generator', '6.3.0'
26 |
27 | rails_command 'sitemap:install'
28 |
29 | inject_into_file 'Dockerfile', after: "./bin/rails assets:precompile\n" do
30 | "\n# Generate sitemap\nRUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails sitemap:refresh\n"
31 | end
32 | end
33 |
34 | def copy_views
35 | directory 'app/views', 'app/views', force: true
36 | directory 'app/assets/images', 'app/assets/images', force: true
37 | directory 'app/controllers', 'app/controllers', force: true
38 | directory 'app/helpers', 'app/helpers', force: true
39 | directory 'app/javascript', 'app/javascript', force: true
40 | directory 'public', 'public', force: true
41 |
42 | template 'app/views/layouts/application.html.erb', 'app/views/layouts/application.html.erb', force: true
43 | template 'config/sitemap.rb', 'config/sitemap.rb', force: true
44 | template 'public/robots.txt', 'public/robots.txt', force: true
45 | template 'app/views/shared/_structured_data.html.erb', 'app/views/shared/_structured_data.html.erb', force: true
46 | end
47 |
48 | def add_routes
49 | route <<~ROUTES
50 | # Demo pages
51 | get "demo", to: "demo#index", as: :demo
52 | get "analytics", to: "demo#analytics", as: :analytics
53 | get "support", to: "demo#support", as: :support
54 |
55 | # Static pages
56 | get "privacy", to: "pages#privacy", as: :privacy
57 | get "terms", to: "pages#terms", as: :terms
58 | ROUTES
59 | end
60 |
61 | def git_commit
62 | git add: '.', commit: %(-m 'Add sample UI layer')
63 |
64 | say 'Successfully added sample UI layer', :green
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/railsmaker/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RailsMaker
4 | VERSION = '0.0.4'
5 | end
6 |
--------------------------------------------------------------------------------
/railsmaker-core.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/railsmaker/version'
4 |
5 | Gem::Specification.new do |s|
6 | s.name = 'railsmaker-core'
7 | s.version = RailsMaker::VERSION
8 | s.executables = ['railsmaker']
9 | s.summary = 'Rails 8 app generator with Tailwind, DaisyUI, and Kamal'
10 | s.description = 'A comprehensive Rails 8 application generator that sets up a modern stack including Tailwind CSS, DaisyUI, Kamal deployment, monitoring, authentication, S3 backups,and more.'
11 | s.metadata = {
12 | 'homepage_uri' => 'https://railsmaker.com',
13 | 'source_code_uri' => 'https://github.com/sgerov/railsmaker',
14 | 'bug_tracker_uri' => 'https://github.com/sgerov/railsmaker/issues',
15 | 'documentation_uri' => 'https://github.com/sgerov/railsmaker/blob/main/10-STEPS-TO-PROD.md',
16 | 'changelog_uri' => 'https://github.com/sgerov/railsmaker/blob/main/CHANGELOG.md'
17 | }
18 | s.authors = ['Sava Gerov']
19 | s.email = ['sava@gerov.es']
20 | s.files = Dir['lib/**/*', 'exe/*', 'README.md', 'CHANGELOG.md']
21 |
22 | # Runtime dependencies
23 | s.add_runtime_dependency 'clearance', '2.9.3'
24 | s.add_runtime_dependency 'kamal', '2.5.2'
25 | s.add_runtime_dependency 'lograge', '0.14.0'
26 | s.add_runtime_dependency 'logstash-event', '1.2.02'
27 | s.add_runtime_dependency 'mailjet', '1.8'
28 | s.add_runtime_dependency 'omniauth', '2.1.2'
29 | s.add_runtime_dependency 'omniauth-google-oauth2', '1.2.1'
30 | s.add_runtime_dependency 'omniauth-rails_csrf_protection', '1.0.2'
31 | s.add_runtime_dependency 'opentelemetry-exporter-otlp', '0.29.1'
32 | s.add_runtime_dependency 'opentelemetry-instrumentation-all', '0.73.1'
33 | s.add_runtime_dependency 'opentelemetry-sdk', '1.7.0'
34 | s.add_runtime_dependency 'rails', '8.0.1'
35 | s.add_runtime_dependency 'sentry-rails', '5.22.4'
36 | s.add_runtime_dependency 'sentry-ruby', '5.22.4'
37 | s.add_runtime_dependency 'sitemap_generator', '6.3.0'
38 | s.add_runtime_dependency 'tailwindcss-rails', '4.0.0'
39 | s.add_runtime_dependency 'thor', '1.3.2'
40 | s.add_runtime_dependency 'tzinfo-data', '1.2025.1' # for stripped-down ubuntu installations
41 |
42 | s.license = 'MIT'
43 | s.required_ruby_version = '>= 3.2.0'
44 | s.homepage = 'https://railsmaker.com'
45 | end
46 |
--------------------------------------------------------------------------------
/test/fixtures/.kamal/secrets:
--------------------------------------------------------------------------------
1 | # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
2 | # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
3 | # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
4 |
5 | # Example of extracting secrets from 1password (or another compatible pw manager)
6 | # SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
7 | # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS})
8 | # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS})
9 |
10 | # Use a GITHUB_TOKEN if private repositories are needed for the image
11 | # GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
12 |
13 | # Grab the registry password from ENV
14 | KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
15 |
16 | # Improve security by using a password manager. Never check config/master.key into git!
17 | RAILS_MASTER_KEY=$(cat config/master.key)
18 |
--------------------------------------------------------------------------------
/test/fixtures/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 | # check=error=true
3 |
4 | # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
5 | # docker build -t test_app_hehe .
6 | # docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name test_app_hehe test_app_hehe
7 |
8 | # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
9 |
10 | # Make sure RUBY_VERSION matches the Ruby version in .ruby-version
11 | ARG RUBY_VERSION=3.3.5
12 | FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
13 |
14 | # Rails app lives here
15 | WORKDIR /rails
16 |
17 | # Install base packages
18 | RUN apt-get update -qq && \
19 | apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
20 | rm -rf /var/lib/apt/lists /var/cache/apt/archives
21 |
22 | # Set production environment
23 | ENV RAILS_ENV="production" \
24 | BUNDLE_DEPLOYMENT="1" \
25 | BUNDLE_PATH="/usr/local/bundle" \
26 | BUNDLE_WITHOUT="development"
27 |
28 | # Throw-away build stage to reduce size of final image
29 | FROM base AS build
30 |
31 | # Install packages needed to build gems
32 | RUN apt-get update -qq && \
33 | apt-get install --no-install-recommends -y build-essential git pkg-config unzip && \
34 | rm -rf /var/lib/apt/lists /var/cache/apt/archives
35 |
36 | ENV BUN_INSTALL=/usr/local/bun
37 | ENV PATH=/usr/local/bun/bin:$PATH
38 | ARG BUN_VERSION=1.2.0
39 | RUN curl -fsSL https://bun.sh/install | bash -s -- "bun-v${BUN_VERSION}"
40 |
41 | # Install application gems
42 | COPY Gemfile Gemfile.lock ./
43 | RUN bundle install && \
44 | rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
45 | bundle exec bootsnap precompile --gemfile
46 |
47 | # Install node modules
48 | COPY package.json bun.lockb ./
49 | RUN bun install --frozen-lockfile
50 |
51 | # Copy application code
52 | COPY . .
53 |
54 | # Precompile bootsnap code for faster boot times
55 | RUN bundle exec bootsnap precompile app/ lib/
56 |
57 | # Precompiling assets for production without requiring secret RAILS_MASTER_KEY
58 | RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
59 |
60 |
61 |
62 |
63 | # Final stage for app image
64 | FROM base
65 |
66 | # Copy built artifacts: gems, application
67 | COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
68 | COPY --from=build /rails /rails
69 |
70 | # Run and own only the runtime files as a non-root user for security
71 | RUN groupadd --system --gid 1000 rails && \
72 | useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
73 | chown -R rails:rails db log storage tmp
74 | USER 1000:1000
75 |
76 | # Entrypoint prepares the database.
77 | ENTRYPOINT ["/rails/bin/docker-entrypoint"]
78 |
79 | # Start server via Thruster by default, this can be overwritten at runtime
80 | EXPOSE 80
81 | CMD ["./bin/thrust", "./bin/rails", "server"]
82 |
--------------------------------------------------------------------------------
/test/fixtures/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | gem 'lograge', '~> 0.14.0'
6 | gem 'opentelemetry-sdk', '~> 1.6.0'
7 | gem 'rails', '~> 8.0'
8 |
--------------------------------------------------------------------------------
/test/fixtures/app/assets/tailwind/application.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
--------------------------------------------------------------------------------
/test/fixtures/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
3 | allow_browser versions: :modern
4 | end
5 |
--------------------------------------------------------------------------------
/test/fixtures/app/credentials.example.yml:
--------------------------------------------------------------------------------
1 | # Needed credentials.yml. To edit yours:
2 | # VISUAL="vim" bin/rails credentials:edit
3 | google_oauth:
4 | client_id: "your-client-id"
5 | client_secret: "your-client-secret"
6 | sentry_dsn: "your-sentry-dsn"
7 | mailjet:
8 | api_key: "your-api-key"
9 | secret_key: "your-secret-key"
10 | app:
11 | host: "your-host.com"
12 | mailer_sender: "YourApp "
--------------------------------------------------------------------------------
/test/fixtures/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/test/fixtures/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TestApp
5 | <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
6 | <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
7 | <%# Plausible Analytics %>
8 |
9 |
14 | <%= Sentry.get_trace_propagation_meta.html_safe if defined?(Sentry) %>
15 |
16 |
17 | <%= render "layouts/navbar" unless current_page?(root_path) %>
18 |
19 | <%= render "shared/flash" %>
20 | <%= yield %>
21 | <%= render "shared/footer" %>
22 |
23 | <%= render 'shared/structured_data' %>
24 |
25 |
--------------------------------------------------------------------------------
/test/fixtures/app/views/main/index.html.erb:
--------------------------------------------------------------------------------
1 | Welcome to My App
2 | This is the main landing page for MyNewApp.
--------------------------------------------------------------------------------
/test/fixtures/auth/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class User < ApplicationRecord
4 | include Clearance::User
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/config/deploy.yml:
--------------------------------------------------------------------------------
1 | # Name of your application. Used to uniquely configure containers.
2 | service: test_app_hehe
3 |
4 | # Name of the container image.
5 | image: your-user/test_app_hehe
6 |
7 | # Deploy to these servers.
8 | servers:
9 | web:
10 | - 192.168.0.1
11 | # job:
12 | # hosts:
13 | # - 192.168.0.1
14 | # cmd: bin/jobs
15 |
16 | # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
17 | # Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
18 | #
19 | # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
20 | proxy:
21 | ssl: true
22 | host: app.example.com
23 |
24 | # Credentials for your image host.
25 | registry:
26 | # Specify the registry server, if you're not using Docker Hub
27 | # server: registry.digitalocean.com / ghcr.io / ...
28 | username: your-user
29 |
30 | # Always use an access token rather than real password when possible.
31 | password:
32 | - KAMAL_REGISTRY_PASSWORD
33 |
34 | # Inject ENV variables into containers (secrets come from .kamal/secrets).
35 | env:
36 | secret:
37 | - RAILS_MASTER_KEY
38 | clear:
39 | # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
40 | # When you start using multiple servers, you should split out job processing to a dedicated machine.
41 | SOLID_QUEUE_IN_PUMA: true
42 |
43 | # Set number of processes dedicated to Solid Queue (default: 1)
44 | # JOB_CONCURRENCY: 3
45 |
46 | # Set number of cores available to the application on each server (default: 1).
47 | # WEB_CONCURRENCY: 2
48 |
49 | # Match this to any external database server to configure Active Record correctly
50 | # Use test_app_hehe-db for a db accessory server on same machine via local kamal docker network.
51 | # DB_HOST: 192.168.0.2
52 |
53 | # Log everything from Rails
54 | # RAILS_LOG_LEVEL: debug
55 |
56 | # Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation:
57 | # "bin/kamal logs -r job" will tail logs from the first server in the job section.
58 | aliases:
59 | console: app exec --interactive --reuse "bin/rails console"
60 | shell: app exec --interactive --reuse "bash"
61 | logs: app logs -f
62 | dbc: app exec --interactive --reuse "bin/rails dbconsole"
63 |
64 |
65 | # Use a persistent storage volume for sqlite database files and local Active Storage files.
66 | # Recommended to change this to a mounted volume path that is backed up off server.
67 | volumes:
68 | - "test_app_hehe_storage:/rails/storage"
69 |
70 |
71 | # Bridge fingerprinted assets, like JS and CSS, between versions to avoid
72 | # hitting 404 on in-flight requests. Combines all files from new and old
73 | # version inside the asset_path.
74 | asset_path: /rails/public/assets
75 |
76 | # Configure the image builder.
77 | builder:
78 | arch: amd64
79 |
80 | # # Build image via remote server (useful for faster amd64 builds on arm64 computers)
81 | # remote: ssh://docker@docker-builder-server
82 | #
83 | # # Pass arguments and secrets to the Docker build process
84 | # args:
85 | # RUBY_VERSION: 3.3.5
86 | # secrets:
87 | # - GITHUB_TOKEN
88 | # - RAILS_MASTER_KEY
89 |
90 | # Use a different ssh user than root
91 | # ssh:
92 | # user: app
93 |
94 | # Use accessory services (secrets come from .kamal/secrets).
95 | # accessories:
96 | # db:
97 | # image: mysql:8.0
98 | # host: 192.168.0.2
99 | # # Change to 3306 to expose port to the world instead of just local network.
100 | # port: "127.0.0.1:3306:3306"
101 | # env:
102 | # clear:
103 | # MYSQL_ROOT_HOST: '%'
104 | # secret:
105 | # - MYSQL_ROOT_PASSWORD
106 | # files:
107 | # - config/mysql/production.cnf:/etc/mysql/my.cnf
108 | # - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
109 | # directories:
110 | # - data:/var/lib/mysql
111 | # redis:
112 | # image: redis:7.0
113 | # host: 192.168.0.2
114 | # port: 6379
115 | # directories:
116 | # - data:/data
117 |
--------------------------------------------------------------------------------
/test/fixtures/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'opentelemetry/sdk'
4 |
5 | OpenTelemetry::SDK.configure(&:use_all)
6 |
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/test/fixtures/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/integer/time'
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.enable_reloading = false
8 |
9 | # Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
10 | config.eager_load = true
11 |
12 | # Full error reports are disabled.
13 | config.consider_all_requests_local = false
14 |
15 | # Turn on fragment caching in view templates.
16 | config.action_controller.perform_caching = true
17 |
18 | # Cache assets for far-future expiry since they are all digest stamped.
19 | config.public_file_server.headers = { 'cache-control' => "public, max-age=#{1.year.to_i}" }
20 |
21 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
22 | # config.asset_host = "http://assets.example.com"
23 |
24 | # Store uploaded files on the local file system (see config/storage.yml for options).
25 | config.active_storage.service = :local
26 |
27 | # Assume all access to the app is happening through a SSL-terminating reverse proxy.
28 | config.assume_ssl = true
29 |
30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31 | config.force_ssl = true
32 |
33 | # Skip http-to-https redirect for the default health check endpoint.
34 | # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
35 |
36 | # Log to STDOUT with the current request id as a default log tag.
37 | config.log_tags = [:request_id]
38 | config.logger = ActiveSupport::TaggedLogging.logger(STDOUT)
39 |
40 | # Change to "debug" to log everything (including potentially personally-identifiable information!)
41 | config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info')
42 |
43 | # Prevent health checks from clogging up the logs.
44 | config.silence_healthcheck_path = '/up'
45 |
46 | # Don't log any deprecations.
47 | config.active_support.report_deprecations = false
48 |
49 | # Replace the default in-process memory cache store with a durable alternative.
50 | config.cache_store = :solid_cache_store
51 |
52 | # Replace the default in-process and non-durable queuing backend for Active Job.
53 | config.active_job.queue_adapter = :solid_queue
54 | config.solid_queue.connects_to = { database: { writing: :queue } }
55 |
56 | # Ignore bad email addresses and do not raise email delivery errors.
57 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
58 | # config.action_mailer.raise_delivery_errors = false
59 |
60 | # Set host to be used by links generated in mailer templates.
61 | config.action_mailer.default_url_options = { host: 'example.com' }
62 |
63 | # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit.
64 | # config.action_mailer.smtp_settings = {
65 | # user_name: Rails.application.credentials.dig(:smtp, :user_name),
66 | # password: Rails.application.credentials.dig(:smtp, :password),
67 | # address: "smtp.example.com",
68 | # port: 587,
69 | # authentication: :plain
70 | # }
71 |
72 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
73 | # the I18n.default_locale when a translation cannot be found).
74 | config.i18n.fallbacks = true
75 |
76 | # Do not dump schema after migrations.
77 | config.active_record.dump_schema_after_migration = false
78 |
79 | # Only use :id for inspections in production.
80 | config.active_record.attributes_for_inspect = [:id]
81 |
82 | # Enable DNS rebinding protection and other `Host` header attacks.
83 | # config.hosts = [
84 | # "example.com", # Allow requests from example.com
85 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
86 | # ]
87 | #
88 | # Skip DNS rebinding protection for the default health check endpoint.
89 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
90 | end
91 |
--------------------------------------------------------------------------------
/test/fixtures/config/initializers/clearance.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Clearance.configure do |config|
4 | config.mailer_sender = Rails.application.credentials.dig(:app, :mailer_sender)
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/config/initializers/devise.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Devise.setup do |config|
4 | config.mailer_sender = 'please-change-me@example.com'
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/config/initializers/lograge.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.configure do
4 | config.lograge.enabled = true
5 | config.lograge.formatter = Lograge::Formatters::Logstash.new
6 | config.lograge.custom_options = lambda do |event|
7 | {
8 | params: event.payload[:params].except('controller', 'action')
9 | }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/fixtures/config/initializers/sentry.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Sentry.init do |config|
4 | config.dsn = 'your-api'
5 | config.breadcrumbs_logger = %i[active_support_logger http_logger]
6 | end
7 |
--------------------------------------------------------------------------------
/test/fixtures/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root to: 'home#index'
3 | end
4 |
--------------------------------------------------------------------------------
/test/fixtures/db/migrate/add_omniauth_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddOmniauthToUsers < ActiveRecord::Migration[8.0]
2 | def change
3 | add_column :users, :provider, :string
4 | add_column :users, :uid, :string
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/lib/railsmaker/generators/app_generator_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class AppGeneratorTest < Rails::Generators::TestCase
6 | include GeneratorHelper
7 | tests RailsMaker::Generators::AppGenerator
8 |
9 | destination File.expand_path('../../tmp', __dir__)
10 |
11 | def setup
12 | super
13 | # stub out the real `rails new`
14 | Rails::Generators::AppGenerator.stubs(:start).returns(true)
15 |
16 | RailsMaker::Generators::AppGenerator.any_instance
17 | .stubs(:current_dir)
18 | .returns(destination_root)
19 | end
20 |
21 | def teardown
22 | FileUtils.rm_rf(destination_root)
23 | super
24 | end
25 |
26 | def test_generates_app_successfully
27 | app_name = 'my_test_app'
28 | copy_test_fixtures(app_name)
29 |
30 | run_generator [
31 | '--name', app_name,
32 | '--docker', 'my_docker_user',
33 | '--ip', '192.168.0.99',
34 | '--domain', 'test.example.com'
35 | ]
36 |
37 | assert_file "#{app_name}/Gemfile"
38 | assert_file "#{app_name}/config/deploy.yml" do |content|
39 | assert_match(/my_docker_user/, content)
40 | assert_match(/192\.168\.0\.99/, content)
41 | assert_match(/test\.example\.com/, content)
42 | assert_match(/ssl: true\n\s+forward_headers: true/, content)
43 | end
44 | end
45 |
46 | def test_generates_app_successfully_with_skip_daisyui
47 | app_name = 'my_test_app_daisyui'
48 | copy_test_fixtures(app_name)
49 |
50 | run_generator [
51 | '--name', app_name,
52 | '--docker', 'my_docker_user',
53 | '--ip', '192.168.0.99',
54 | '--domain', 'test.example.com',
55 | '--skip-daisyui'
56 | ]
57 |
58 | assert_file "#{app_name}/Gemfile" do |content|
59 | assert_no_match(/daisyui/, content)
60 | end
61 |
62 | assert_file "#{app_name}/config/deploy.yml" do |content|
63 | assert_no_match(/daisyui/, content)
64 | end
65 | end
66 |
67 | def test_generates_app_with_custom_registry
68 | app_name = 'my_test_app'
69 | copy_test_fixtures(app_name)
70 |
71 | run_generator [
72 | '--name', app_name,
73 | '--docker', 'my_docker_user',
74 | '--ip', '192.168.0.99',
75 | '--domain', 'test.example.com',
76 | '--registry_url', 'registry.myapp.com'
77 | ]
78 |
79 | assert_file "#{app_name}/config/deploy.yml" do |content|
80 | assert_match(/server: registry\.myapp\.com/, content)
81 | assert_match(/username: my_docker_user/, content)
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/test/lib/railsmaker/generators/auth_generator_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class AuthGeneratorTest < Rails::Generators::TestCase
6 | include GeneratorHelper
7 |
8 | tests RailsMaker::Generators::AuthGenerator
9 | destination File.expand_path('../../tmp', __dir__)
10 |
11 | def setup
12 | super
13 |
14 | # Stub the migration file lookup
15 | migration_path = 'db/migrate/20240101000000_add_omniauth_to_users.rb'
16 | Dir.stubs(:[]).with('db/migrate/*add_omniauth_to_users.rb').returns([migration_path])
17 | end
18 |
19 | def teardown
20 | FileUtils.rm_rf(destination_root)
21 | end
22 |
23 | def test_generator_configures_authentication
24 | run_generator %w[--mailer_sender=test@example.com]
25 |
26 | assert_file 'Gemfile' do |content|
27 | assert_match(/gem "clearance", "~> 2.9.3"/, content)
28 | assert_match(/gem "omniauth", "~> 2.1.2"/, content)
29 | assert_match(/gem "omniauth-google-oauth2", "~> 1.2.1"/, content)
30 | assert_match(/gem "omniauth-rails_csrf_protection", "~> 1.0.2"/, content)
31 | end
32 |
33 | assert_file 'config/initializers/clearance.rb' do |content|
34 | assert_match(/config.mailer_sender = Rails.application.credentials.dig\(:app, :mailer_sender\)/, content)
35 | assert_match(%r{config.redirect_url = "/demo"}, content)
36 | end
37 |
38 | assert_file 'config/initializers/omniauth.rb' do |content|
39 | assert_match(/provider :google_oauth2/, content)
40 | assert_match(/Rails.application.credentials.dig\(:google_oauth, :client_id\)/, content)
41 | assert_match(/Rails.application.credentials.dig\(:google_oauth, :client_secret\)/, content)
42 | end
43 |
44 | assert_file 'app/models/user.rb' do |content|
45 | assert_match(/include Clearance::User/, content)
46 | assert_match(/from_omniauth\(auth\)/, content)
47 | end
48 |
49 | assert_file 'app/controllers/omniauth_callbacks_controller.rb' do |content|
50 | assert_match(/class OmniauthCallbacksController < ApplicationController/, content)
51 | assert_match(/def google_oauth2/, content)
52 | assert_match(/redirect_to demo_url/, content)
53 | end
54 |
55 | assert_file 'config/routes.rb' do |content|
56 | assert_match(
57 | %r{get "auth/:provider/callback", to: "omniauth_callbacks#google_oauth2", constraints: { provider: "google_oauth2" }}, content
58 | )
59 | assert_match(%r{get 'auth/failure', to: 'omniauth_callbacks#failure'}, content)
60 | end
61 | end
62 |
63 | def test_generator_with_custom_email
64 | run_generator %w[--mailer_sender=custom@example.com]
65 | assert_file 'config/initializers/clearance.rb' do |content|
66 | assert_match(/config.mailer_sender = Rails.application.credentials.dig\(:app, :mailer_sender\)/, content)
67 | assert_match(%r{config.redirect_url = "/demo"}, content)
68 | end
69 |
70 | assert_file 'config/initializers/omniauth.rb' do |content|
71 | assert_match(/provider :google_oauth2/, content)
72 | end
73 |
74 | assert_file 'app/controllers/omniauth_callbacks_controller.rb' do |content|
75 | assert_match(/redirect_to demo_url/, content)
76 | end
77 | end
78 |
79 | def test_generator_creates_git_commit
80 | assert_generator_git_commit('Add authentication with Clearance and OmniAuth')
81 | run_generator %w[--mailer_sender=test@example.com]
82 | end
83 |
84 | def test_generator_creates_user_model_with_omniauth
85 | run_generator %w[--mailer_sender=test@example.com]
86 | assert_file 'app/models/user.rb' do |content|
87 | assert_match(/def self.from_omniauth\(auth\)/, content)
88 | assert_match(/user.email = auth.info.email/, content)
89 | assert_match(/user.password = SecureRandom.hex\(10\)/, content)
90 | end
91 | end
92 |
93 | def test_generator_creates_migration_with_indices
94 | run_generator %w[--mailer_sender=test@example.com]
95 | migration_file = Dir['db/migrate/*add_omniauth_to_users.rb'].first
96 | assert_file migration_file do |content|
97 | assert_match(/add_column :users, :provider, :string/, content)
98 | assert_match(/add_column :users, :uid, :string/, content)
99 | assert_match(/add_index :users, \[:provider, :uid\], unique: true/, content)
100 | end
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/test/lib/railsmaker/generators/litestream_generator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LitestreamGeneratorTest < Rails::Generators::TestCase
4 | include GeneratorHelper
5 |
6 | tests RailsMaker::Generators::LitestreamGenerator
7 | destination File.expand_path('../../tmp', __dir__)
8 |
9 | def teardown
10 | FileUtils.rm_rf(destination_root)
11 | end
12 |
13 | def test_generator_creates_litestream_config
14 | run_generator ['--bucketname=MyApp', '--name=MyApp', '--ip=192.168.1.1']
15 |
16 | assert_file 'config/litestream.yml' do |content|
17 | assert_match(/access-key-id: \${LITESTREAM_ACCESS_KEY_ID}/, content)
18 | assert_match(/secret-access-key: \${LITESTREAM_SECRET_ACCESS_KEY}/, content)
19 | assert_match(/endpoint: \${LITESTREAM_ENDPOINT}/, content)
20 | assert_match(/region: \${LITESTREAM_REGION}/, content)
21 | end
22 | end
23 |
24 | def test_generator_adds_kamal_secrets
25 | run_generator ['--bucketname=MyApp', '--name=MyApp', '--ip=192.168.1.1']
26 |
27 | assert_file '.kamal/secrets' do |content|
28 | assert_match(/LITESTREAM_ACCESS_KEY_ID=/, content)
29 | assert_match(/LITESTREAM_SECRET_ACCESS_KEY=/, content)
30 | assert_match(/LITESTREAM_ENDPOINT=/, content)
31 | assert_match(/LITESTREAM_REGION=/, content)
32 | assert_match(/LITESTREAM_BUCKET_NAME=MyApp/, content)
33 | end
34 | end
35 |
36 | def test_generator_adds_deployment_config
37 | run_generator ['--bucketname=MyApp', '--name=MyApp', '--ip=192.168.1.1']
38 |
39 | assert_file 'config/deploy.yml' do |content|
40 | assert_match(/accessories:/, content)
41 | assert_match(/litestream:/, content)
42 | assert_match(%r{image: litestream/litestream:0\.3}, content)
43 | assert_match(/host: 192\.168\.1\.1/, content)
44 | assert_match(/volumes:/, content)
45 | assert_match(/production\.sqlite3/, content)
46 | end
47 | end
48 |
49 | def test_generator_creates_git_commit
50 | assert_generator_git_commit('Add Litestream configuration')
51 | run_generator ['--bucketname=MyApp', '--name=MyApp', '--ip=192.168.1.1']
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/lib/railsmaker/generators/mailjet_generator_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # test/lib/railsmaker/generators/mailjet_generator_test.rb
4 |
5 | require 'test_helper'
6 |
7 | class MailjetGeneratorTest < Rails::Generators::TestCase
8 | include GeneratorHelper
9 |
10 | tests RailsMaker::Generators::MailjetGenerator
11 | destination File.expand_path('../../tmp', __dir__)
12 |
13 | def teardown
14 | FileUtils.rm_rf(destination_root)
15 | end
16 |
17 | def test_generator_creates_gemfile_with_required_gems
18 | run_generator ['--name=MyApp', '--domain=myapp.com']
19 | assert_file 'Gemfile' do |content|
20 | assert_match(/gem "mailjet", "~> 1\.8"/, content)
21 | end
22 | end
23 |
24 | def test_generator_creates_initializer
25 | run_generator ['--name=MyApp', '--domain=myapp.com']
26 | assert_file 'config/initializers/mailjet.rb' do |content|
27 | assert_match(/Mailjet\.configure do \|config\|/, content)
28 | assert_match(/config\.api_key = Rails\.application\.credentials\.dig\(:mailjet, :api_key\)/, content)
29 | assert_match(/config\.secret_key = Rails\.application\.credentials\.dig\(:mailjet, :secret_key\)/, content)
30 | assert_match(/config\.default_from = Rails\.application\.credentials\.dig\(:app, :mailer_sender\)/, content)
31 | assert_match(/config\.api_version = "v3\.1"/, content)
32 | end
33 | end
34 |
35 | def test_generator_configures_mailer
36 | run_generator ['--name=MyApp', '--domain=myapp.com']
37 | assert_file 'config/environments/production.rb' do |content|
38 | assert_match(/config\.action_mailer\.delivery_method = :mailjet_api/, content)
39 | assert_match(
40 | /config\.action_mailer\.default_url_options = \{ host: Rails\.application\.credentials\.dig\(:app, :host\) \}/, content
41 | )
42 | end
43 |
44 | assert_file 'app/mailers/application_mailer.rb' do |content|
45 | assert_match(/default from: Rails\.application\.credentials\.dig\(:app, :mailer_sender\)/, content)
46 | end
47 | end
48 |
49 | def test_generator_creates_git_commit
50 | assert_generator_git_commit('Add Mailjet configuration')
51 | run_generator ['--name=MyApp', '--domain=myapp.com']
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/lib/railsmaker/generators/opentelemetry_generator_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class OpentelemetryGeneratorTest < Rails::Generators::TestCase
6 | include GeneratorHelper
7 |
8 | tests RailsMaker::Generators::OpentelemetryGenerator
9 | destination File.expand_path('../../tmp', __dir__)
10 |
11 | def teardown
12 | FileUtils.rm_rf(destination_root)
13 | end
14 |
15 | def test_generator_creates_gemfile_with_required_gems
16 | run_generator ['--name=my-service']
17 | assert_file 'Gemfile' do |content|
18 | assert_match(/gem "opentelemetry-sdk", "~> 1.6.0"/, content)
19 | assert_match(/gem "opentelemetry-exporter-otlp", "~> 0.29.1"/, content)
20 | assert_match(/gem "opentelemetry-instrumentation-all", "~> 0.72.0"/, content)
21 | assert_match(/gem "lograge", "~> 0.14.0"/, content)
22 | assert_match(/gem "logstash-event", "~> 1.2.02"/, content)
23 | end
24 | end
25 |
26 | def test_generator_configures_opentelemetry_in_environment
27 | run_generator ['--name=my-service']
28 | assert_file 'config/environment.rb' do |content|
29 | assert_match(%r{require 'opentelemetry/sdk'}, content)
30 | assert_match(/OpenTelemetry::SDK.configure do \|c\|/, content)
31 | assert_match(/c.use_all/, content)
32 | end
33 | end
34 |
35 | def test_generator_configures_opentelemetry_with_custom_service_name
36 | service_name = 'custom-service'
37 | run_generator ["--name=#{service_name}"]
38 | assert_file 'config/deploy.yml' do |content|
39 | assert_match(/OTEL_SERVICE_NAME: #{service_name}/, content)
40 | end
41 | end
42 |
43 | def test_generator_creates_lograge_config
44 | run_generator ['--name=my-service']
45 | assert_file 'config/initializers/lograge.rb'
46 | end
47 |
48 | def test_generator_adds_plausible_script_correctly
49 | service_name = 'my-service'
50 | run_generator ["--name=#{service_name}"]
51 | assert_file 'config/deploy.yml' do |content|
52 | assert_match(%r{OTEL_EXPORTER_OTLP_ENDPOINT: http://host\.docker\.internal:4318}, content)
53 | end
54 | end
55 |
56 | def test_generator_creates_git_commit
57 | assert_generator_git_commit('Add OpenTelemetry')
58 | run_generator ['--name=my-service']
59 | end
60 |
61 | def test_generator_handles_existing_configuration
62 | run_generator ['--name=my-service']
63 | run_generator ['--name=my-service']
64 | # Ensure no duplicate configurations
65 | assert_file 'config/deploy.yml' do |content|
66 | assert_equal content.scan(/OTEL_EXPORTER_OTLP_ENDPOINT:/).size, 1
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/test/lib/railsmaker/generators/plausible_instrumentation_generator_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class PlausibleInstrumentationGeneratorTest < Rails::Generators::TestCase
6 | include GeneratorHelper
7 |
8 | tests RailsMaker::Generators::PlausibleInstrumentationGenerator
9 | destination File.expand_path('../../tmp', __dir__)
10 |
11 | def setup
12 | super
13 | @app_domain = 'myapp.com'
14 | @analytics_domain = 'analytics.myapp.com'
15 | end
16 |
17 | def teardown
18 | FileUtils.rm_rf(destination_root)
19 | end
20 |
21 | def test_generator_adds_plausible_script_to_layout
22 | run_generator ['--domain', @app_domain, '--analytics', @analytics_domain]
23 |
24 | expected_script = <<~HTML.indent(4)
25 | <%# Plausible Analytics %>
26 |
27 |
28 | HTML
29 |
30 | assert_file 'app/views/layouts/application.html.erb' do |content|
31 | assert_match expected_script.strip, content
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/test/lib/railsmaker/generators/sentry_generator_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class SentryGeneratorTest < Rails::Generators::TestCase
6 | include GeneratorHelper
7 |
8 | tests RailsMaker::Generators::SentryGenerator
9 | destination File.expand_path('../../tmp', __dir__)
10 |
11 | def teardown
12 | FileUtils.rm_rf(destination_root)
13 | end
14 |
15 | def test_generator_configures_sentry
16 | run_generator
17 | assert_file 'Gemfile' do |content|
18 | assert_match(/gem "sentry-ruby", "~> 5\.22\.3"/, content)
19 | assert_match(/gem "sentry-rails", "~> 5\.22\.3"/, content)
20 | end
21 | end
22 |
23 | def test_generator_creates_git_commit
24 | assert_generator_git_commit('Add Sentry')
25 | run_generator
26 | end
27 |
28 | def test_generator_creates_sentry_initializer_correctly
29 | run_generator
30 | assert_file 'config/initializers/sentry.rb' do |content|
31 | assert_match(/Sentry\.init do \|config\|/, content)
32 | assert_match(/config\.dsn = Rails\.application\.credentials\.dig\(:sentry_dsn\)/, content)
33 | assert_match(/config\.breadcrumbs_logger = \[ :active_support_logger, :http_logger \]/, content)
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/test/lib/railsmaker/generators/ui_generator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class UiGeneratorTest < Rails::Generators::TestCase
4 | include GeneratorHelper
5 |
6 | tests RailsMaker::Generators::UiGenerator
7 | destination File.expand_path('../../tmp', __dir__)
8 |
9 | def teardown
10 | FileUtils.rm_rf(destination_root)
11 | super
12 | end
13 |
14 | def test_ui_generator_adds_seo_capabilities
15 | assert_generator_git_commit 'Add sample UI layer'
16 | RailsMaker::Generators::BaseGenerator.any_instance.expects(:rails_command).with('sitemap:install')
17 |
18 | run_generator ['--domain=example.com', '--name=MyApp']
19 |
20 | assert_file 'Gemfile' do |content|
21 | assert_match(/gem "sitemap_generator", "6\.3\.0"/, content)
22 | end
23 |
24 | assert_file 'Dockerfile' do |content|
25 | assert_match(/# Generate sitemap/, content)
26 | assert_match(/RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails sitemap:refresh/, content)
27 | end
28 | end
29 |
30 | def test_ui_generator_copies_views_and_assets
31 | run_generator ['--domain=example.com', '--name=MyApp']
32 |
33 | assert_directory 'app/views'
34 | assert_directory 'app/assets/images'
35 | assert_directory 'app/controllers'
36 | assert_directory 'app/helpers'
37 | assert_directory 'app/javascript'
38 | assert_directory 'public'
39 |
40 | assert_file 'app/views/shared/_structured_data.html.erb'
41 | assert_file 'public/robots.txt'
42 | assert_file 'config/sitemap.rb'
43 | end
44 |
45 | def test_ui_generator_adds_routes
46 | run_generator ['--domain=example.com', '--name=MyApp']
47 |
48 | assert_file 'config/routes.rb' do |content|
49 | assert_match(/get "demo", to: "demo#index", as: :demo/, content)
50 | assert_match(/get "analytics", to: "demo#analytics", as: :analytics/, content)
51 | assert_match(/get "support", to: "demo#support", as: :support/, content)
52 | assert_match(/get "privacy", to: "pages#privacy", as: :privacy/, content)
53 | assert_match(/get "terms", to: "pages#terms", as: :terms/, content)
54 | end
55 | end
56 |
57 | def test_ui_generator_with_duplicate_files
58 | run_generator ['--domain=example.com', '--name=MyApp']
59 | run_generator ['--domain=example.com', '--name=MyApp']
60 | # Ensure no duplicate routes
61 | assert_file 'config/routes.rb' do |content|
62 | assert_equal content.scan(/get "demo", to: "demo#index", as: :demo/).size, 1
63 | end
64 | # Ensure no duplicate asset files
65 | assert_file 'public/robots.txt'
66 | end
67 |
68 | def test_ui_generator_creates_git_commit
69 | assert_generator_git_commit('Add sample UI layer')
70 | run_generator ['--domain=example.com', '--name=MyApp']
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/test/support/generator_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails/generators/test_case'
4 |
5 | module GeneratorHelper
6 | def setup
7 | prepare_destination
8 | copy_test_fixtures
9 |
10 | RailsMaker::Generators::BaseGenerator.any_instance.stubs(:git).returns(true)
11 | RailsMaker::Generators::BaseGenerator.any_instance.stubs(:rails_command).returns(true)
12 | RailsMaker::Generators::BaseGenerator.any_instance.stubs(:rake).returns(true)
13 | end
14 |
15 | def copy_test_fixtures(subfolder = nil)
16 | base_dest = subfolder ? File.join(destination_root, subfolder) : destination_root
17 |
18 | # Create all required directories under the base destination.
19 | %w[
20 | .kamal
21 | config
22 | config/environments
23 | config/initializers
24 | lib/templates/opentelemetry
25 | lib/templates/config
26 | app/assets/tailwind
27 | app/controllers
28 | app/mailers
29 | app/models
30 | app/views/layouts
31 | lib/templates/app/views/main
32 | db/migrate
33 | ].each do |dir|
34 | mkdir_p File.join(base_dest, dir)
35 | end
36 |
37 | # Fixtures are for files created by other generators
38 | copy_file fixture_path('db/migrate/add_omniauth_to_users.rb'),
39 | File.join(base_dest, 'db/migrate/20240101000000_add_omniauth_to_users.rb')
40 |
41 | copy_file fixture_path('Gemfile'), File.join(base_dest, 'Gemfile')
42 | copy_file fixture_path('Dockerfile'), File.join(base_dest, 'Dockerfile')
43 | copy_file fixture_path('.kamal/secrets'), File.join(base_dest, '.kamal/secrets')
44 | copy_file fixture_path('config/environment.rb'), File.join(base_dest, 'config/environment.rb')
45 | copy_file fixture_path('config/environments/production.rb'),
46 | File.join(base_dest, 'config/environments/production.rb')
47 | copy_file fixture_path('config/routes.rb'), File.join(base_dest, 'config/routes.rb')
48 | copy_file fixture_path('app/views/layouts/application.html.erb'),
49 | File.join(base_dest, 'app/views/layouts/application.html.erb')
50 | copy_file fixture_path('app/controllers/application_controller.rb'),
51 | File.join(base_dest, 'app/controllers/application_controller.rb')
52 | copy_file fixture_path('config/initializers/sentry.rb'), # sentry generator creates this file
53 | File.join(base_dest, 'config/initializers/sentry.rb')
54 | copy_file fixture_path('config/initializers/clearance.rb'), # clearance generator creates this file
55 | File.join(base_dest, 'config/initializers/clearance.rb')
56 | copy_file fixture_path('config/initializers/devise.rb'),
57 | File.join(base_dest, 'config/initializers/devise.rb')
58 | copy_file fixture_path('config/initializers/lograge.rb'),
59 | File.join(base_dest, 'lib/templates/opentelemetry/lograge.rb')
60 | copy_file fixture_path('auth/user.rb'), File.join(base_dest, 'app/models/user.rb')
61 | copy_file fixture_path('config/deploy.yml'), File.join(base_dest, 'config/deploy.yml')
62 | copy_file fixture_path('app/credentials.example.yml'),
63 | File.join(base_dest, 'lib/templates/config/credentials.example.yml')
64 | copy_file fixture_path('app/views/main/index.html.erb'),
65 | File.join(base_dest, 'lib/templates/app/views/main/index.html.erb')
66 | copy_file fixture_path('app/assets/tailwind/application.css'),
67 | File.join(base_dest, 'app/assets/tailwind/application.css')
68 | copy_file fixture_path('app/mailers/application_mailer.rb'),
69 | File.join(base_dest, 'app/mailers/application_mailer.rb')
70 | end
71 |
72 | private
73 |
74 | def fixture_path(rel_path)
75 | File.expand_path(File.join('test', 'fixtures', rel_path))
76 | end
77 |
78 | def mkdir_p(path)
79 | FileUtils.mkdir_p(path)
80 | end
81 |
82 | def copy_file(src, dest)
83 | FileUtils.cp(src, dest)
84 | end
85 |
86 | def assert_generator_git_commit(message)
87 | RailsMaker::Generators::BaseGenerator.any_instance
88 | .expects(:git)
89 | .with(add: '.', commit: %(-m '#{message}'))
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'simplecov'
4 |
5 | SimpleCov.start do
6 | add_filter '/test/'
7 |
8 | add_group 'Generators', 'lib/railsmaker/generators'
9 | end
10 |
11 | require 'minitest/autorun'
12 | require 'minitest/reporters'
13 | require 'mocha/minitest'
14 | require 'railsmaker'
15 |
16 | Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
17 |
18 | # Load all support files
19 | Dir[File.join(__dir__, 'support', '**', '*.rb')].sort.each { |f| require f }
20 |
--------------------------------------------------------------------------------