├── .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 | [![Support](https://img.shields.io/badge/Support-%F0%9F%8D%B8-yellow?style=for-the-badge)](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 | [![Gem Version](https://img.shields.io/gem/v/railsmaker-core?color=blue&logo=rubygems)](https://rubygems.org/gems/railsmaker-core) 2 | [![Support](https://img.shields.io/badge/Support-%F0%9F%8D%B8-yellow)](https://buymeacoffee.com/sgerov) 3 | [![Live Demo](https://img.shields.io/badge/Live_Demo-Try_Now_-brightgreen?logo=rocket&color=00cc99)](https://railsmaker.com) 4 | [![Live Demo Repo](https://img.shields.io/badge/Live_Demo_Repo-View_Code-blue?logo=github)](https://github.com/sgerov/railsmaker-sample) 5 | [![Guide](https://img.shields.io/badge/Guide-10_Steps_To_Prod-orange?logo=book)](./10-STEPS-TO-PROD.md) 6 | [![DaisyUI Templates](https://img.shields.io/badge/DaisyUI_Templates-70+_Templates-purple?logo=tailwindcss)](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 | [![Support](https://img.shields.io/badge/Support-%F0%9F%8D%B8-yellow?style=for-the-badge)](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 | 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 | 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 |
5 | 6 | 7 | 8 |
9 |
10 |

Analytics

11 |

Track your application metrics

12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 |
29 |
30 |
Total Page Views
31 |
25.6K
32 |
33 | 34 | 35 | 36 | 21% more than last month 37 |
38 |
39 | 40 |
41 |
42 |
43 | 44 | 45 | 46 |
47 |
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 | 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 | 209 |
210 |
211 |
212 |
213 |
214 |
-------------------------------------------------------------------------------- /lib/railsmaker/generators/templates/ui/app/views/demo/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 |
10 |
11 |

Dashboard

12 |

Welcome back, Team

13 |
14 |
15 | 16 |
17 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 |
Active Projects
33 |
12
34 |
35 |
36 | 37 | 38 | 39 |
40 |
41 |
↑ 8% from last month
42 |
43 |
44 | 45 |
46 |
47 |
48 |
49 |
Team Members
50 |
24
51 |
52 |
53 | 54 | 55 | 56 |
57 |
58 |
↑ 2 new this week
59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |
Tasks Completed
67 |
84%
68 |
69 |
70 | 71 | 72 | 73 |
74 |
75 |
↑ 12% improvement
76 |
77 |
78 | 79 |
80 |
81 |
82 |
83 |
Time Tracked
84 |
164h
85 |
86 |
87 | 88 | 89 | 90 |
91 |
92 |
On track with estimates
93 |
94 |
95 |
96 | 97 | 98 |
99 | 100 |
101 |
102 |
103 |
104 |

Current Tasks

105 | 111 |
112 | 113 | 114 | 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 | 281 |
282 |
283 | 284 |
285 |
286 |
287 | 288 |
289 |
290 |
291 |

Sarah commented on API Integration

292 | 293 |
294 |
295 | 296 |
297 |
298 |
299 | 300 |
301 |
302 |
303 |

Mike created New Project

304 | 305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
-------------------------------------------------------------------------------- /lib/railsmaker/generators/templates/ui/app/views/demo/support.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | 7 | 8 |
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 |
  1. Sign up for an account
  2. 30 |
  3. Complete your profile setup
  4. 31 |
  5. Follow our quick setup guide
  6. 32 |
  7. Start building your first project
  8. 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 |
77 | 78 | 79 | 80 |
81 |
82 |

Contact Support

83 |

Our team typically responds within 24 hours

84 |
85 |
86 | 87 |
88 |
89 | 92 | 99 |
100 | 101 |
102 | 105 |
106 | 110 | 114 | 118 |
119 |
120 | 121 |
122 | 126 | 127 |
128 | 129 |
130 | 134 |
135 | 136 | 142 |
143 |
144 |
145 |
146 |
147 |
-------------------------------------------------------------------------------- /lib/railsmaker/generators/templates/ui/app/views/layouts/_navbar.html.erb: -------------------------------------------------------------------------------- 1 | 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 |
5 | 6 | 7 | 8 |
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 |
5 | 6 | 7 | 8 |
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 |
32 | <%= link_to '/auth/google_oauth2', data: { turbo: false }, class: "btn btn-outline w-full" do %> 33 | 34 | 35 | 36 | Sign in with Google 37 | <% end %> 38 |
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 | 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 | 25 | <% end %> 26 | 27 |
28 | <%= form.submit "Create account", class: "btn btn-primary w-full" %> 29 |
30 | 31 |
or continue with
32 | 33 |
34 | <%= link_to '/auth/google_oauth2', data: { turbo: false }, class: "btn btn-outline w-full" do %> 35 | 36 | 37 | 38 | Sign up with Google 39 | <% end %> 40 |
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 | --------------------------------------------------------------------------------