├── .github └── workflows │ ├── gh-pages.yml │ └── linting.yml ├── .gitignore ├── .mdlrc ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── SUMMARY.md ├── bin ├── check └── setup ├── book.toml ├── checklist.md ├── configure_git_repository.md ├── create_git_repository.md ├── fastlane.md ├── gitflow.md ├── go_live.md ├── google_analytics.md ├── google_apis.md ├── i18n.md ├── images ├── app_environments.png ├── depfu_automatic_merging.png ├── depfu_engine_updates.png ├── google_app_1.png ├── google_app_2.png ├── papertrail_addon.png ├── papertrail_config.png ├── papertrail_events.png ├── papertrail_finished_config.png ├── papertrail_prompt.png ├── papertrail_query_config.png ├── semaphore_cd.png ├── semaphore_ci.png ├── semaphore_icon.png ├── sentry.png └── xcode_configurations.png ├── js └── README.md ├── naming_conventions.md ├── ruby_on_rails ├── README.md ├── app_initialisation.md ├── appsignal.md ├── aws.md ├── bootstrap.md ├── bullet.md ├── cloudflare.md ├── compile_readme.md ├── configure_ci.md ├── configure_percy.md ├── content_security_policy.md ├── create_application_server_deploio.md ├── create_application_server_heroku.md ├── cucumber.md ├── depfu.md ├── devise.md ├── environment_protection.md ├── first_git_push.md ├── font_awesome.md ├── hotjar.md ├── initialise_gitflow.md ├── jest.md ├── linting_and_automatic_check.md ├── newrelic.md ├── recaptcha.md ├── robots_txt.md ├── rspec.md ├── send_emails.md ├── sentry.md ├── sidekiq.md ├── suggested_libraries.md ├── template.rb ├── uptimerobot.md ├── vcr.md ├── wallee.md └── wicked_pdf.md ├── security.md ├── slack_and_notifications.md ├── sparkpost_and_mailtrap.md └── templates ├── .coffeelint.json ├── .csscomb.json ├── .editorconfig ├── .erb-lint.yml ├── .eslintrc.json ├── .mdlrc ├── .reek.yml ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── .sass-lint.yml ├── .scss-lint.yml ├── .stylelintrc.yml ├── PULL_REQUEST_TEMPLATE.md ├── Procfile ├── README.md ├── app ├── controllers │ └── rails │ │ └── health_controller.rb ├── javascript │ ├── appsignal.js │ └── sentry.js ├── mailers │ └── application_mailer.rb └── views │ └── shared │ ├── _appsignal.html.erb │ └── _sentry.html.erb ├── bin ├── check ├── commit-msg ├── fastcheck └── run ├── bitrise └── bitrise.yml ├── config ├── application.example.yml ├── database.mysql.example.yml ├── database.sqlite.example.yml ├── database.yml ├── initializers │ ├── appsignal.rb │ ├── lograge.rb │ └── sentry.rb └── newrelic.yml ├── features ├── env │ ├── capybara.rb │ ├── database_cleaner.rb │ ├── factory_bot.rb │ └── warden.rb ├── home_check.feature └── step_definitions │ └── home_check_steps.rb ├── pull_requests_template.md ├── spec ├── rails_helper.rb ├── spec_helper.rb ├── support │ └── javascript_error_reporter.rb └── system │ └── health_spec.rb └── tslint.json /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-24.04 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup mdBook 15 | uses: peaceiris/actions-mdbook@v2 16 | with: 17 | mdbook-version: 'latest' 18 | - name: Emojify Book 19 | run: | 20 | set -x 21 | wget --no-verbose https://github.com/shonfeder/emojitsu/releases/download/0.1.1/gh-actions-emojitsu 22 | chmod +x gh-actions-emojitsu 23 | find . -type f -name "*.md" -exec ./gh-actions-emojitsu emojify -i {} \; 24 | - run: mdbook build 25 | - name: Deploy 26 | uses: peaceiris/actions-gh-pages@v4 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./book 30 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-24.04 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: ruby/setup-ruby@v1 18 | with: 19 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 20 | - run: bin/check 21 | build-mdbook: 22 | runs-on: ubuntu-24.04 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Setup mdBook 26 | uses: peaceiris/actions-mdbook@v2 27 | with: 28 | mdbook-version: 'latest' 29 | - name: Emojify Book 30 | run: | 31 | set -x 32 | wget --no-verbose https://github.com/shonfeder/emojitsu/releases/download/0.1.1/gh-actions-emojitsu 33 | chmod +x gh-actions-emojitsu 34 | find . -type f -name "*.md" -exec ./gh-actions-emojitsu emojify -i {} \; 35 | - run: mdbook build 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | book 3 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD013", "~MD026", "~MD029", "~MD032", "~MD036", "~MD002", "~MD034", "~MD031", "~MD028", "~MD027" 2 | git_recurse true 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | group :test do 6 | gem 'mdl' 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | chef-utils (18.0.161) 5 | concurrent-ruby 6 | concurrent-ruby (1.1.10) 7 | kramdown (2.4.0) 8 | rexml 9 | kramdown-parser-gfm (1.1.0) 10 | kramdown (~> 2.0) 11 | mdl (0.11.0) 12 | kramdown (~> 2.3) 13 | kramdown-parser-gfm (~> 1.1) 14 | mixlib-cli (~> 2.1, >= 2.1.1) 15 | mixlib-config (>= 2.2.1, < 4) 16 | mixlib-shellout 17 | mixlib-cli (2.1.8) 18 | mixlib-config (3.0.27) 19 | tomlrb 20 | mixlib-shellout (3.2.7) 21 | chef-utils 22 | rexml (3.2.5) 23 | tomlrb (2.0.3) 24 | 25 | PLATFORMS 26 | arm64-darwin-21 27 | arm64-darwin-22 28 | arm64-darwin-24 29 | x86_64-linux 30 | 31 | DEPENDENCIES 32 | mdl 33 | 34 | BUNDLED WITH 35 | 2.3.19 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Renuo Applications Setup Guide 2 | 3 | This repo is the [Renuo](https://www.renuo.ch) collection of best-practices to set-up apps. 4 | We are a [Rails company](https://rubyonrails.org/foundation), so the most value probably 5 | will be found in the parts concerning Rails. But anyways you'll also find a lot about the 6 | inner workings of Renuo. 7 | 8 | * [Ruby on Rails – Application Setup Guide](./ruby_on_rails/README.md) 9 | 10 | ## Some Notes on the Side 11 | 12 | If you are reading this document, it means that you have to setup a new application. 13 | A new project started and it's now time to set everything up so that **everyone**, 14 | in your team, **can start working on it**. 15 | 16 | This document will try to be as minimalist as possible and provide you with all the steps to set up the application as 17 | fast as possible. There are things, in Renuo projects, which are mandatory, other that are suggested. 18 | This guide is the result of more than ten years of experience, so this means three things: it's very robust, very opinionated, and possibly very outdated. 19 | 20 | **You are always welcome to challenge the guide and improve it with a Pull Request.** 21 | 22 | The basic things that need to be ready before the team can start working on a project are: 23 | 24 | * An existing *git* repository containing the project 25 | * Two branches: *main* and *develop* 26 | * A README with essential information about the application 27 | * Convenience-scripts: `bin/setup`, `bin/check`, `bin/fastcheck`, `bin/run` 28 | * One running, green test 29 | * Continuous integration (*CI*) ready, running and green for both branches 30 | * Continuous deployment (*CD*) ready and running for both branches 31 | * The application deployed for both branches 32 | 33 | As an appendix, you'll find a [checklist](checklist.md) you can use to follow the guide. 34 | 35 | **:exclamation: Do not blindly follow this guide, always think about what you are doing and why. 36 | If you think something is wrong or simply outdated, improve this guide with a Pull Request.** 37 | 38 | We want you to know exactly the reason behind each single step of this guide. 39 | 40 | Thank you for your work and have fun! :tada: 41 | 42 | ## License 43 | 44 | [Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/legalcode) 45 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Overview](README.md) 4 | 5 | # General 6 | 7 | - [Create a Git Repository](create_git_repository.md) 8 | - [GitFlow](gitflow.md) 9 | - [Go Live!](go_live.md) 10 | - [Naming Conventions](naming_conventions.md) 11 | - [Security](security.md) 12 | - [Project Checklist](checklist.md) 13 | 14 | # Rails Application Setup Guide 15 | 16 | - [Getting started](ruby_on_rails/README.md) 17 | 18 | - [Setup]() 19 | - [Initialise the Rails App](ruby_on_rails/app_initialisation.md) 20 | - [Push to Git Repository](ruby_on_rails/first_git_push.md) 21 | - [Initialise Gitflow](ruby_on_rails/initialise_gitflow.md) 22 | - [Configure Git Repository](configure_git_repository.md) 23 | - [Create an Application Server (Heroku)](ruby_on_rails/create_application_server_heroku.md) 24 | - [Create an Application Server (Deploio)](ruby_on_rails/create_application_server_deploio.md) 25 | - [Configure the CI/CD](ruby_on_rails/configure_ci.md) 26 | - [Tools]() 27 | - [RSpec](ruby_on_rails/rspec.md) 28 | - [Linting and automatic checks](ruby_on_rails/linting_and_automatic_check.md) 29 | - [Gems and libraries](ruby_on_rails/suggested_libraries.md) 30 | - [Cloudflare](ruby_on_rails/cloudflare.md) 31 | - [Additional Services]() 32 | - [Sentry](ruby_on_rails/sentry.md) 33 | - [NewRelic](ruby_on_rails/newrelic.md) 34 | - [Robots.txt](ruby_on_rails/robots_txt.md) 35 | - [Percy](ruby_on_rails/configure_percy.md) 36 | - [Protect develop environment](ruby_on_rails/environment_protection.md) 37 | - [Customer Plan Services]() 38 | - [Uptimerobot](ruby_on_rails/uptimerobot.md) 39 | - [Depfu Security monitoring](ruby_on_rails/depfu.md) 40 | 41 | - [Gems]() 42 | - [Jest](ruby_on_rails/jest.md) 43 | - [Send Emails](ruby_on_rails/send_emails.md) 44 | - [Sparkpost & Mailtrap](sparkpost_and_mailtrap.md) 45 | - [Devise](ruby_on_rails/devise.md) 46 | - [Sidekiq](ruby_on_rails/sidekiq.md) 47 | - [Cucumber](ruby_on_rails/cucumber.md) 48 | - [Amazon S3 and Cloudfront](ruby_on_rails/aws.md) 49 | - [Bootstrap](ruby_on_rails/bootstrap.md) 50 | - [FontAwesome](ruby_on_rails/font_awesome.md) 51 | - [Bullet](ruby_on_rails/bullet.md) 52 | - [Lograge](ruby_on_rails/appsignal.md) 53 | - [Hotjar](ruby_on_rails/hotjar.md) 54 | - [Wicked PDF](ruby_on_rails/wicked_pdf.md) 55 | - [Recaptcha v3](ruby_on_rails/recaptcha.md) 56 | 57 | # Services 58 | 59 | - [Google Analytics](google_analytics.md) 60 | - [Google Apis](google_apis.md) 61 | - [Slack and Notifications](slack_and_notifications.md) 62 | 63 | 64 | # Templates 65 | 66 | - [README](templates/README.md) 67 | - [Pull Request Template](templates/pull_requests_template.md) 68 | -------------------------------------------------------------------------------- /bin/check: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | set -euo pipefail 4 | 5 | bundle exec mdl $(find . -name '*.md'|grep -E -v '(SUMMARY.md|vendor/)') 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | bundle check || bundle install 6 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["devs@Renuo"] 3 | language = "en" 4 | multilingual = false 5 | src = "" 6 | title = "Renuo - Application Setup Guide" 7 | 8 | [build] 9 | create-missing = false 10 | use-default-preprocessors = false 11 | 12 | [output.html.search] 13 | limit-results = 10 14 | -------------------------------------------------------------------------------- /checklist.md: -------------------------------------------------------------------------------- 1 | # Checklist 2 | 3 | - [ ] Application name chosen 4 | - [ ] Rails Application created 5 | - [ ] Git Repository created and configured 6 | - [ ] CI configured 7 | - [ ] Server created 8 | - [ ] CD configured 9 | - [ ] App running 10 | - [ ] Linting tools installed 11 | - [ ] RSpec installed 12 | - [ ] Suggested gems included 13 | - [ ] Sentry, Appsignal and/or NewRelic configured depending on your choice. 14 | - [ ] Logs on Appsignal configured 15 | - [ ] Cloudflare configured 16 | - [ ] README written and complete 17 | - [ ] Uptime Monitor configured 18 | - [ ] robots.txt configured 19 | -------------------------------------------------------------------------------- /configure_git_repository.md: -------------------------------------------------------------------------------- 1 | # Configure the GitHub Repository 2 | 3 | These are the suggested configurations for our GitHub repositories. 4 | Please stick to it unless you have special needs. 5 | 6 | * General Settings 7 | * Features: Remove *Wikis*, *Issues* and *Projects* 8 | * Pull Requests 9 | * Disable *Allow merge commits* and *Allow rebase merging* 10 | * Allow auto-merge 11 | * Automatically delete head branches 12 | * Always suggest updating pull request branches 13 | * Manage access 14 | * Add *staff* team as a collaborator with Admin access 15 | * Add *security* team as collaborator with Write access 16 | * Branches 17 | * Default branch: either `main` or `develop` depending on whether you want one or two environments. 18 | * Rules/Rulesets 19 | * `develop` 20 | * Enforcement status: `Active` 21 | * Branch targeting criteria: `develop` 22 | * Bypass list: add `Repository Admin` Role with *allow for pull requests only* option 23 | * Restrict deletions 24 | * Require linear history 25 | * Require a pull request before merging 26 | * Require status checks to pass 27 | * Select `ci/semaphore/push` 28 | * Block force pushes 29 | * `main` (same as develop but...) 30 | * Branch targeting criteria: `main` 31 | * ❌ Require a pull request before merging 32 | * ❌ Require status checks to pass 33 | 34 | * Autolink references 35 | * Add a new Autolink reference with: 36 | * Reference prefix: `TICKET-` 37 | * Target URL: `https://redmine.renuo.ch/issues/` 38 | 39 | ## Team 40 | 41 | Each project has a team owning it. The team is named after the project: `[team-name] = [project-name]`. 42 | Thanks to this we can: 43 | 44 | * see who is responsible for a project; 45 | * assign issues to the right team; 46 | * assign pull requests to the right team. 47 | 48 | * Create a team with the name of the project and add all the developers working on it; 49 | * Give to each team member the role "maintainer"; 50 | * Add the team to the repository with the "administrator" role; 51 | * Add a CODEOWNERS file with the team name in it: 52 | 53 | ```markdown 54 | # .github/CODEOWNERS 55 | 56 | * @renuo/[team-name] 57 | ``` 58 | -------------------------------------------------------------------------------- /create_git_repository.md: -------------------------------------------------------------------------------- 1 | # Create a Git Repository 2 | 3 | At Renuo we currently use [GitHub](https://github.com/) as our git repository. You should already be part of the Renuo Organisation and have permissions to do so. 4 | If that's not the case, double check the Laptop Setup Guide or ask wg-operations. 5 | 6 | To create a new GitHub project you can use the tool you prefer, but it should have the following characteristics: 7 | * Should be under `renuo` organisation 8 | * Should have `[project-name]` as a name 9 | * Should be private (unless you are creating an OpenSource project) 10 | 11 | Use the command `hub create -p renuo/[project-name]` to create the repo and add it to the origin of the current folder. 12 | 13 | ## Public repos need a license 14 | 15 | If your repository is public, ensure that it contains a license. 16 | We usually use the [MIT](https://choosealicense.com/licenses/mit/) license if possible or a [CreativeCommons](https://creativecommons.org/licenses/) license for documentation-only repositories (such as the application setup guide 🙂). 17 | You can add a license directly on GitHub while initializing a repository by selecting a license template in the "Add a license" dropdown. 18 | However, if the repository is already initialized, you're still able to add a license using a template: 19 | * Click `Create new file` 20 | * Use `LICENSE` for the filename 21 | * Then click on `Choose a license template` and select the MIT license 22 | * Fill in Renuo AG in the full name placeholder 23 | * Click submit and commit the file 24 | -------------------------------------------------------------------------------- /fastlane.md: -------------------------------------------------------------------------------- 1 | # fastlane 2 | 3 | We manage our iOS app certificates with [fastlane](https://fastlane.tools/). 4 | They are managed in this private repository: [fastlane-ios-certificates](https://github.com/renuo/fastlane-ios-certificates). 5 | 6 | ## Renewing Apple iOS certificates with fastlane 7 | 8 | Once a year the certificates will expire, so we have to create new ones. 9 | We do it like that in each of our projects: 10 | 11 | ```sh 12 | # Replace development certs 13 | bundle exec fastlane match nuke development 14 | bundle exec fastlane match development 15 | 16 | # Replace distribution certs 17 | bundle exec fastlane match nuke distribution --safe_remove_certs 18 | bundle exec fastlane match appstore 19 | ``` 20 | -------------------------------------------------------------------------------- /gitflow.md: -------------------------------------------------------------------------------- 1 | # Gitflow 2 | 3 | At Renuo we follow [gitflow convention](http://nvie.com/posts/a-successful-git-branching-model/) and we use it in every project. 4 | Please check it out and read how it works if you don’t know it yet. 5 | It’s very important that you know how gitflow works to work at Renuo. 6 | 7 | Since we follow gitflow, we have two main branches connected, via CD, to two servers, we call "main" and "develop". 8 | 9 | ```mermaid 10 | graph LR 11 | A[master] --> CI1(CI) --> CD1(CD) 12 | CD1(CD) --> S11(server) 13 | CD1(CD) --> S12(server) 14 | 15 | B[develop] --> CI2(CI) --> CD2(CD) 16 | CD2(CD) --> S21(server) 17 | CD2(CD) --> S22(server) 18 | 19 | B[develop] --> C[feature/1337-ff] --> CI3(CI) 20 | ``` 21 | 22 | ```mermaid 23 | sequenceDiagram 24 | actor Developer 25 | participant G as GitHub 26 | participant CI 27 | participant S as Server 28 | 29 | rect rgb(191, 223, 255) 30 | 31 | Developer->>G: git push origin develop or master 32 | G->>CI: notify about code change 33 | CI->>G: checkout code 34 | CI->>CI: run tests 35 | CI->>S: deploy 36 | end 37 | ``` 38 | -------------------------------------------------------------------------------- /go_live.md: -------------------------------------------------------------------------------- 1 | # Go Live 2 | 3 | ## DNS, SSL & SMTP 4 | 5 | * Check your DNS records, for example 6 | * `CNAME` to Heroku ([see their docs](https://devcenter.heroku.com/articles/custom-domains)) 7 | * `TXT` records for [SparkPost sending domains](https://support.sparkpost.com/docs/getting-started/setting-up-domains) 8 | * `CAA` records ([see Cloudflare](https://developers.cloudflare.com/ssl/edge-certificates/caa-records/#create-caa-records)) 9 | * If SparkPost has been set up with the renuoapp.ch domain and the project has its own domain now, set up SparkPost again with its own domain 10 | * Verify that SparkPost mails are working and the [sending domain is validated](https://app.sparkpost.com/domains/list/sending). 11 | * Verify that SSL is working correctly 12 | 13 | If the final domain isn't already in use, you can configure it also already: 14 | Add a CNAME DNS record pointing to the app (`[project-name]-main.renuoapp.ch`). 15 | 16 | ### URL rewriting on Cloudflare 17 | 18 | For user comfort we redirect HTTP calls to to . 19 | This is done via page rules in Cloudflare. 20 | 21 | 1. Add a new page rule 22 | 1. Enter `example.com/*` 23 | 1. Choose "Forwarding URL" 24 | 1. Choose "301 - Permanent Redirect" 25 | 1. Enter `https://www.example.com/$1` 26 | 1. Click "Save and Deploy" 27 | 28 | ## Heroku 29 | 30 | * Check the size and amount of dynos on Heroku 31 | * Check the database size plan on Heroku and upgrade if it is foreseeable that 10'000 rows are exceeded in a short time 32 | * Check additional addons and according plans on Heroku 33 | 34 | ## Deploio 35 | 36 | * Verify the machine type of the PostgreSQL database in the PostgreSQL resource view 37 | * Confirm the configured application size in the application configuration tab 38 | * Ensure the application replica count is correctly set in the application configuration tab 39 | 40 | ## Other 41 | 42 | * Reset admin credentials, seeds, ... if necessary 43 | * Test the whole application by hand if everything is working as it should 44 | -------------------------------------------------------------------------------- /google_analytics.md: -------------------------------------------------------------------------------- 1 | # Google Analytics 2 | 3 | Google Analytics is only set up for the main branch (since \*.renuoapp.ch domains are tracked via Cloudflare injection). 4 | 5 | To add a new project to the GA account go to and login as google@renuo.ch. 6 | 7 | 1. Go to the tab 'Verwalten' under settings and you will see a dropdown on the left with all the project 8 | 1. Chose the option above that says 'Create Account' 9 | 1. Fill out the Data based on your project. 10 | 1. In Property Management, under Data Steam, choose th platform you'd like to create the tracking for. Make sure 'Enhanced Measurement' is enabled. 11 | 1. Once you saved, you will see the tracking snippet. Note: You can always find it again later under Porperty/Data Streams. 12 | 1. Write down the Measurement ID (the string in the snippet with all caps and starting with an G-XXX....) and note it - you will need it for the Heroku config. 13 | 14 | Choose one of the given options to set up Google Analytics: 15 | 16 | ## a) Javascript only: gtag script (recommended) 17 | 18 | This way is recommended in the normal case, because it doesn't involve another gem dependency. Since Google proposes the 19 | Tag Manager as a default, the Analytics Script is a bit hidden. Tag Manager makes sites slower, therefor one has to decide on a case-by-case basis whether the advantages of a tag manager outweigh the disadvantages. In each case use only the gtag 20 | script which you can find [here](https://developers.google.com/analytics/devguides/collection/gtagjs#install_the_global_site_tag) 21 | 22 | ```html 23 | 24 | 25 | 32 | ``` 33 | 34 | Make sure you insert this script at the end of the `` tag of the page (not in the ``). 35 | 36 | NOTE: There is a default IP anonymization feature in GA4. We no longer need to perform this step manually. 37 | 38 | ## b) Ruby rack-tracker 39 | 40 | There's a gem which can be used for a lot of trackers: 41 | 42 | ```rb 43 | group :production do 44 | ... 45 | gem 'rack-tracker' 46 | end 47 | ``` 48 | 49 | and write to `config/environments/production.rb` 50 | 51 | ```ruby 52 | config.middleware.use(Rack::Tracker) do 53 | if ENV['GOOGLE_ANALYTICS_ID'].present? 54 | handler :google_analytics, tracker: ENV['GOOGLE_ANALYTICS_ID'], anonymize_ip: true, advertising: true 55 | end 56 | end 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /google_apis.md: -------------------------------------------------------------------------------- 1 | # Google APIs 2 | 3 | * If you need Google APIs in your project (e. g. Google Maps) proceed to the [Google Cloud Console](https://console.cloud.google.com) 4 | using the google@renuo.ch account. 5 | * Create a project named `[project-name]` under the renuo.ch organisation and use that project for all your environments. 6 | * Attach the _wg-operations_ billing account 7 | * Choose the correct resource folder 8 | * `clients` for client projects 9 | * `wg-operations` for persistent internal things (e.g. Renuo Dashboard) 10 | * `tmp` for trials (e.g. learning week, presentations) 11 | * `system-gsuite` is for Google app script 12 | 13 | ![google_app_1](images/google_app_1.png) 14 | 15 | Before continuing, make sure that your new project is selected in the top navigational header of the Google Console. 16 | 17 | ## Maps JavaScript API 18 | 19 | **API key generation** 20 | 21 | In order to user the Google APIs you will need to generate new credentials. Because we are using only one project per application, but would like to separate the usage in development from the one in production, name the keys like so: `maps-main` and `maps-develop`. Because we might need a key to also test the map locally, add a separate one named `maps-local`. 22 | 23 | **API key restrictions** 24 | 25 | To prevent quota and key theft, we need to add some restrictions to our keys. There are two types of restrictions: Key restrictions and API restrictions. For the develop API key add the following ones: 26 | 27 | ![google_app_2](images/google_app_2.png) 28 | 29 | For main, enable only the specific domain **and** the `renuoapp.ch` domain. 30 | 31 | For the key we use locally, enable the specific localhost domain (`project-name.localhost`). 32 | 33 | ## Geocoding API 34 | 35 | **API key generation** 36 | 37 | As for the JS API, create two different keys for the two environments. For this API, name the keys like this: `geocoding-main` and `geocoding-develop`. 38 | 39 | **API key restrictions** 40 | 41 | Because the Geocoding API key doesn't support HTTP referrer restrictions (which isn't a problem anyway since the key won't be exposed), you only need to add an API restriction, restricting the key to the Geocoding API. 42 | -------------------------------------------------------------------------------- /i18n.md: -------------------------------------------------------------------------------- 1 | # I18n 2 | 3 | ## Switching language 4 | 5 | The following determines the used locale by the following priorities: 6 | 7 | 1. `locale` parameter 8 | 1. `Http-Accept` header 9 | 1. application default locale 10 | 11 | ```ruby 12 | # app/application_controller.rb 13 | class ApplicationController < ActionController::Base 14 | around_action :switch_locale 15 | 16 | def default_url_options(options = {}) 17 | { locale: I18n.locale }.merge(options) 18 | end 19 | 20 | private 21 | 22 | def switch_locale(&action) 23 | locale = extract_params_locale || extract_accept_header_locale || I18n.default_locale 24 | I18n.with_locale(locale, &action) 25 | end 26 | 27 | def extract_params_locale 28 | I18n.locale_available?(params[:locale]) ? params[:locale] : nil 29 | end 30 | 31 | def extract_accept_header_locale 32 | accepting_locales = (request.env['HTTP_ACCEPT_LANGUAGE'] || '').split(',').map do |part| 33 | part.strip.scan(/^[a-z]{2}/).first 34 | end 35 | 36 | accepting_locales.find { |locale| I18n.locale_available?(locale) } 37 | end 38 | end 39 | 40 | # spec/controllers/application_controller_spec.rb 41 | RSpec.describe ApplicationController do 42 | controller do 43 | def fake_action 44 | render plain: 'fake' 45 | end 46 | end 47 | 48 | before do 49 | custom_routes = proc do 50 | scope '(:locale)' do 51 | get 'fake_action' => 'anonymous#fake_action' 52 | end 53 | end 54 | 55 | routes.draw(&custom_routes) 56 | end 57 | 58 | describe '#set_locale' do 59 | before do 60 | allow(I18n).to receive(:with_locale) 61 | end 62 | 63 | context 'when locale param AND header are specified' do 64 | let(:params) { { locale: 'fr' } } 65 | 66 | before do 67 | request.headers['HTTP_ACCEPT_LANGUAGE'] = 'it-CH, it;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5' 68 | end 69 | 70 | it 'locale param takes precedence' do 71 | get :fake_action, params: params 72 | expect(I18n).to have_received(:with_locale).with('fr') 73 | end 74 | end 75 | 76 | context 'when only the locale param is specified' do 77 | let(:params) { { locale: 'fr' } } 78 | 79 | it 'uses it' do 80 | get :fake_action, params: params 81 | expect(I18n).to have_received(:with_locale).with('fr') 82 | end 83 | end 84 | 85 | context 'when only the header is specified' do 86 | before do 87 | request.headers['HTTP_ACCEPT_LANGUAGE'] = 'en, fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5' 88 | end 89 | 90 | it 'the first supported locale from the header is taken' do 91 | get :fake_action 92 | expect(I18n).to have_received(:with_locale).with('fr') 93 | end 94 | end 95 | 96 | context 'when neither locale param nor header are specified' do 97 | it 'uses the default one' do 98 | get :fake_action 99 | expect(I18n).to have_received(:with_locale).with(I18n.default_locale) 100 | end 101 | end 102 | end 103 | end 104 | ``` 105 | 106 | Don't forget to add a system (or request) spec to glue together the abstraction you just added with 107 | the real world: 108 | 109 | ```ruby 110 | require 'rails_helper' 111 | 112 | RSpec.describe 'Multilanguage support' do 113 | it 'can switch the locale per navigation link' do 114 | visit root_path(locale: 'fr') 115 | expect(page).to have_content(I18n.t('lobby.start_button', locale: 'fr')) 116 | click_link('IT', class: 'nav-link') 117 | expect(page).to have_content(I18n.t('lobby.start_button', locale: 'it')) 118 | end 119 | end 120 | ``` 121 | 122 | ## Simple Canonical headers 123 | 124 | Add our [`rack-canonical-header` gem](https://github.com/renuo/rack-canonical-header) 125 | to your `Gemfile` in production to automatically patch the HTTP header tag `Link`. 126 | 127 | ```rb 128 | group :production do 129 | gem 'rack-canonical-header' 130 | end 131 | ``` 132 | 133 | This requires you to provide an env variable `CANONICAL_HOST` in production. 134 | 135 | ## Advanced headers for multi-language support. 136 | 137 | The following sets `Content-Language` and `Link` headers (`canonical` and `hreflang`): 138 | 139 | ```ruby 140 | # app/controllers/concerns/set_response_headers_concern.rb 141 | module SetResponseHeadersConcern 142 | extend ActiveSupport::Concern 143 | 144 | def set_response_headers 145 | set_content_language_header 146 | set_link_header 147 | end 148 | 149 | private 150 | 151 | def set_content_language_header 152 | response.set_header('Content-Language', I18n.available_locales.join(', ')) 153 | end 154 | 155 | def set_link_header 156 | hreflangs = I18n.available_locales.map(&method(:hreflang_link_value)) 157 | canonical = params[:locale].present? ? nil : canonical_link_value(I18n.locale) 158 | response.set_header('Link', [*hreflangs, canonical].compact.join(', ')) 159 | end 160 | 161 | def hreflang_link_value(locale) 162 | "<#{url_for(locale: locale, only_path: false)}>; rel=\"alternate\"; hreflang=\"#{locale}\"" 163 | end 164 | 165 | def canonical_link_value(locale) 166 | "<#{url_for(locale: locale, only_path: false)}>; rel=\"canonical\"" 167 | end 168 | end 169 | 170 | # app/application_controller.rb 171 | class ApplicationController < ActionController::Base 172 | include SetResponseHeadersConcern 173 | after_action :set_response_headers 174 | end 175 | 176 | # spec/controllers/application_controller_spec.rb 177 | RSpec.describe ApplicationController do 178 | controller do 179 | def fake_action 180 | render plain: 'fake' 181 | end 182 | end 183 | 184 | before do 185 | custom_routes = proc do 186 | scope '(:locale)' do 187 | get 'fake_action' => 'anonymous#fake_action' 188 | end 189 | end 190 | 191 | Rails.application.routes.draw(&custom_routes) # needed for url_for in SetResponseHeadersConcern 192 | routes.draw(&custom_routes) # needed for RSpec 193 | end 194 | 195 | after do 196 | Rails.application.reload_routes! 197 | end 198 | 199 | describe '#set_response_headers' do 200 | it { expect(described_class.ancestors).to include(SetResponseHeadersConcern) } 201 | 202 | it 'sets the canonical header for unlocalized requests' do 203 | get :fake_action, params: {} 204 | expect(response.headers['Link']).to match(/canonical/) 205 | end 206 | 207 | it 'sets the canonical header for explicitly unlocalized requests' do 208 | get :fake_action, params: { locale: nil } 209 | expect(response.headers['Link']).to match(/canonical/) 210 | end 211 | 212 | it 'does NOT set the canonical header for localized requests' do 213 | get :fake_action, params: { locale: 'de' } 214 | expect(response.headers['Link']).not_to match(/canonical/) 215 | end 216 | 217 | it 'sets the hreflang headers', :aggregate_failures do 218 | get :fake_action 219 | expect(response.headers['Link']).to match(/hreflang="de"/) 220 | expect(response.headers['Link']).to match(/hreflang="fr"/) 221 | end 222 | 223 | it 'sets content language header', :aggregate_failures do 224 | get :fake_action 225 | expect(response.headers['Content-Language']).to match(/de/) 226 | expect(response.headers['Content-Language']).to match(/fr/) 227 | end 228 | end 229 | end 230 | ``` 231 | -------------------------------------------------------------------------------- /images/app_environments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/app_environments.png -------------------------------------------------------------------------------- /images/depfu_automatic_merging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/depfu_automatic_merging.png -------------------------------------------------------------------------------- /images/depfu_engine_updates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/depfu_engine_updates.png -------------------------------------------------------------------------------- /images/google_app_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/google_app_1.png -------------------------------------------------------------------------------- /images/google_app_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/google_app_2.png -------------------------------------------------------------------------------- /images/papertrail_addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/papertrail_addon.png -------------------------------------------------------------------------------- /images/papertrail_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/papertrail_config.png -------------------------------------------------------------------------------- /images/papertrail_events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/papertrail_events.png -------------------------------------------------------------------------------- /images/papertrail_finished_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/papertrail_finished_config.png -------------------------------------------------------------------------------- /images/papertrail_prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/papertrail_prompt.png -------------------------------------------------------------------------------- /images/papertrail_query_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/papertrail_query_config.png -------------------------------------------------------------------------------- /images/semaphore_cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/semaphore_cd.png -------------------------------------------------------------------------------- /images/semaphore_ci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/semaphore_ci.png -------------------------------------------------------------------------------- /images/semaphore_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/semaphore_icon.png -------------------------------------------------------------------------------- /images/sentry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/sentry.png -------------------------------------------------------------------------------- /images/xcode_configurations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/images/xcode_configurations.png -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | # Frontend apps 2 | 3 | ## Trivial app 4 | 5 | Consider using only static files (example: [Green Card Predictor](https://github.com/renuo/green-card-probability-frontend)) 6 | and CDNs. 7 | 8 | ## Simple JS app 9 | 10 | If you need a really simple JavaScript application and you need more than just an *index.html*, you can checkout this template: 11 | 12 | 13 | It contains a very simple *webpack* configuration (production ready) featuring: 14 | 15 | * Cucumber tests 16 | * Fonts 17 | * Images 18 | * Favicon 19 | 20 | The rest is done in these three files: 21 | 22 | * index.html 23 | * index.css 24 | * index.js 25 | 26 | ## Vue.js 27 | 28 | An alternative to the JS app mentioned above is a setup with Vue.js. 29 | It provides a scalable foundation for your app and it's easy to understand. 30 | 31 | To get started, install the [vue-cli](https://github.com/vuejs/vue-cli) by running `npm install -g @vue/cli`. 32 | Then run `vue create [app-name]` to generate a vue app. 33 | This creates a minimal setup with some default configurations and 34 | if you wish, with some testing frameworks included like [Cypress](https://www.cypress.io) or [Jest](https://facebook.github.io/jest/). 35 | 36 | An example Vue.js application can be found [here](https://github.com/renuo/tamedia-altersheime). 37 | 38 | ## Complex JS app 39 | 40 | Use either React or Angular. 41 | -------------------------------------------------------------------------------- /naming_conventions.md: -------------------------------------------------------------------------------- 1 | # Naming Conventions 2 | 3 | Naming the project properly is very important and even more important is doing it from the beginning. 4 | We carefully choose the names for our projects and we always stick to the following conventions so you are 5 | asked to do the same. 6 | 7 | ## Project Name 8 | 9 | * The project name `[project-name]` only uses `[a-z0-9]` and dash `-`. No underscore `_`. 10 | * A project name is easy to remember and easy to pronounce. In the best case it consists of one word. 11 | * The project name should be unique and not too long. 12 | * It should not contain any version information. 13 | 14 | ## Extended Use 15 | 16 | * Use `[project-name]` for project names and services which are branch-independent. 17 | * Use `[project-name]-[branch]` for deployed projects (`[branch]` means the gitflow branch and not RAILS_ENV). 18 | * Use `[project-name]-[purpose]-[branch]` for deployed projects (e.g. one11-web-main). 19 | * Use `[project-name]-local-[user]-[rails_env]` for local names which interact with online services (e.g. S3). 20 | 21 | **Note:** Previously on Heroku, the convention was to use `[project-name]-[branch]-[purpose]` for deployed projects (e.g. kingschair-main-assets). This has been updated due to deplo.io. 22 | 23 | ## Examples 24 | 25 | * food-calendar, food-calendar-develop, food-calendar-develop-assets 26 | * food-calendar-api, food-calendar-api-develop, food-calendar-api-develop-assets 27 | * bauer-shoes, bauer-cars, bauer-cars-static 28 | * vdrb-kas, vdrb-mv 29 | * red-shoes, blue-hats (two projects which are independent and have the same customer) 30 | 31 | ## Scope of Application 32 | 33 | The naming conventions should be applied everywhere. Some examples: 34 | 35 | * Amazon S3 (usually [project-name]-[branch]) 36 | * Github ([project-name]) 37 | * Heroku ([project-name]-[branch]) 38 | * Redmine ([project-name]) 39 | * Semaphore CI (servers are named [project-name]-[branch]) 40 | * Drive ([project-name]) 41 | * New Relic ([project-name]-[branch]) 42 | * Get Sentry 43 | * App name in Rails 44 | * Sparkpost Account 45 | * External services (e.g. datatrans) 46 | * Database names 47 | * Nginx / Apache 48 | * Config files 49 | * Directory names 50 | * Analytics, Webmaster tools, Adwords 51 | * Etc… 52 | -------------------------------------------------------------------------------- /ruby_on_rails/README.md: -------------------------------------------------------------------------------- 1 | # Ruby On Rails - Application Setup Guide 2 | 3 | This setup will cover a pure, monolithic Rails Applications. 4 | This is the most frequent type of application we have at [Renuo](https://renuo.ch) and is probably also the easiest to setup. 5 | The application (and relative GitHub repo) will be named after the `[project-name]` you chose before. 6 | 7 | > [!NOTE] 8 | > Have you chosen a `[project-name]` yet? If not, please do so now. Check our [Naming Conventions](../naming_conventions.md) 9 | 10 | > [!NOTE] 11 | > Have you decided if you need two environments (develop and main) or just one? 12 | > As a rule of thumb: for customers we always use two environments, for internal projects we usually only use one. 13 | > Why the difference? Because we can bare the risk of having a bug in an internal project, but we cannot do that for a customer. 14 | 15 | 1. [Initialise the Rails Application](app_initialisation.md) 16 | 1. [Push to Git Repository](first_git_push.md) 17 | 1. [Initialise Gitflow](initialise_gitflow.md) 18 | 1. [Configure Git Repository](../configure_git_repository.md) 19 | 1. [Create an Application Server for Deploio](create_application_server_deploio.md) or [Create an Application Server for Heroku](create_application_server_heroku.md) 20 | 1. [Configure the CI / CD](configure_ci.md) 21 | 22 | Once here, your app should be up and running on all environments. 23 | 24 | It's now time to introduce some more tools which will help you and the team to keep a high quality during the project development. 25 | 26 | 1. [RSpec](rspec.md) 27 | 1. [Linting and automatic checks](linting_and_automatic_check.md) 28 | 1. [Gems and libraries :gem:](suggested_libraries.md) 29 | 1. [Cloudflare](cloudflare.md) 30 | 1. [README](compile_readme.md) 31 | 32 | :tada: Finally you are ready to start working on you new project! :tada: 33 | 34 | While everyone starts working there are some more things which you should setup. 35 | Most are not optional, but the rest of the team can start working even if those are not in place yet. 36 | 37 | 1. [AppSignal](appsignal.md) 38 | 1. [Sentry](sentry.md) (optional) 39 | 1. [NewRelic](newrelic.md) (optional) 40 | 1. [robots.txt](robots_txt.md) 41 | 1. [Percy](configure_percy.md) (optional) 42 | 1. [Protect develop environment](environment_protection.md) 43 | 44 | Some services should be configured accordingly to the packages bought by the customer. 45 | Once the new application is created, please add the project to the 46 | [monitoring list](https://docs.google.com/spreadsheets/d/1FY4jqByO-aI5sDan0hD7ULu6a2-eLsmO6kgdCFlPmuY/edit#gid=0) 47 | and discuss with the PO how the service should be configured. 48 | 49 | 1. [Uptimerobot](uptimerobot.md) 50 | 1. Depending on the monitoring list, also [Sentry notifications](sentry.md) need to be configured. 51 | 1. [Depfu security monitoring](depfu.md) 52 | 1. Depending on the monitoring list, also [Papertrail alerts](papertrail.md) need to be configured. 53 | 54 | Here you will find a series of chapters and guides on how to setup some of the gems we use most often and some other 55 | useful services: 56 | 57 | 1. [Run Javascript tests with Jest](jest.md) 58 | 1. [Pull Requests Template](../pull_requests_template.md) 59 | 1. [Slack and Project Notifications](../slack_and_notifications.md) 60 | 1. [Send emails](send_emails.md) 61 | 1. [Sparkpost & Mailtrap](../sparkpost_and_mailtrap.md) 62 | 1. [Devise](devise.md) 63 | 1. [Sidekiq](sidekiq.md) 64 | 1. [Cucumber](cucumber.md) 65 | 1. [Amazon S3 and Cloudfront](aws.md) 66 | 1. awesome_print `gem 'awesome_print'` 67 | 1. [bootstrap](bootstrap.md) 68 | 1. [font-awesome](font_awesome.md) 69 | 1. [bullet](bullet.md) `gem 'bullet'` 70 | 1. [lograge](appsignal.md) `gem 'lograge'` 71 | 1. Rack Tracker (Google Analytics) `gem 'rack-tracker'` --> see [Google Analytics](../google_analytics.md) 72 | 1. Favicons 73 | 1. [Rack CORS](https://github.com/cyu/rack-cors) 74 | 1. [Rack Attack](https://github.com/rack/rack-attack#installing) 75 | 1. [:fire: Hotjar](hotjar.md) 76 | 1. SEO 77 | * redirect non-www to www 78 | * Header tags 79 | 1. [wicked pdf](wicked_pdf.md) `gem wicked_pdf` 80 | 1. [Recaptcha v3](recaptcha.md) 81 | 1. [Wallee Payment Integration](wallee.md) 82 | -------------------------------------------------------------------------------- /ruby_on_rails/app_initialisation.md: -------------------------------------------------------------------------------- 1 | # Initialise the Rails Application 2 | 3 | ## Default Rails setup 4 | 5 | * Ensure that your asdf plugins are up to date with `asdf plugin update --all`. 6 | 7 | * Install the latest Ruby version with `asdf install ruby latest` (Check if it's [supported by Heroku](https://devcenter.heroku.com/articles/ruby-support#ruby-versions)). 8 | 9 | * Switch your global Ruby to the fresh one: `asdf global ruby latest`. 10 | 11 | * Run `gem update --system` to update Ruby's default gems (e.g. `bundler`). 12 | 13 | * [Check if you are using the latest stable version of Rails](http://rubyonrails.org/) with `rails -v` and update it if you are not. 14 | You can do this with `gem update rails`. Beware of beta versions. 15 | 16 | * Start a new Rails project using 17 | ``` 18 | rails new [project-name] --database=postgresql --skip-kamal --skip-ci --skip-action-mailbox --template https://raw.githubusercontent.com/renuo/applications-setup-guide/main/ruby_on_rails/template.rb 19 | ``` 20 | where the `project-name` is exactly the one you chose before. 21 | 22 | > ⚠️ You may want to choose a different database than Postgres, but most of the time this will be your choice.\ 23 | > If you do not need a DB you may rethink the fact that you may not need Rails at all: Take a look at [Sinatra](http://www.sinatrarb.com/) or [Angular](https://angular.io/)\ 24 | > You might also need actionmailbox of course, so always double-check the parameters that you are using. 25 | 26 | > ⭐️ This setup does not include either js-bundling nor css-bundling by default.\ 27 | > It will start with the simplest possible Rails setup and will use sprockets and importmaps.\ 28 | > If you need to do fancy stuff, discuss with your team the opportunity of including a js-bundling and css-bundling tool.\ 29 | > We want to go ["no build"](https://www.youtube.com/watch?v=iqXjGiQ_D-A) whenever possible. 30 | 31 | * Run `bin/setup` 32 | 33 | * Run `bundle exec rails db:migrate` to generate an empty `schema.rb` file. 34 | 35 | * Then check your default Rails setup by running `rails s` and visiting http://localhost:3000. 36 | You should be on Rails now, yay! 37 | * Finally check if http://localhost:3000/up is green. 38 | 39 | ## Adjustments 40 | 41 | Some adjustments are made automatically by the template, but you should check them. 42 | Some other adjustments must be performed manually. 43 | 44 | ### Automatic adjustments 45 | 46 | > ⭐The Gemfile reads the required ruby version from the `.ruby-version` file. 47 | > [This is used by Heroku to determine what version to use.](https://devcenter.heroku.com/articles/ruby-versions) 48 | > Deploio reads the ruby version from the Gemfile, with the .ruby-version file inlined into it. https://paketo.io/docs/howto/ruby/#override-the-detected-ruby-version 49 | 50 | > ⭐️renuocop replaces the default rubocop-rails-omakase. We have our own set of rules at Renuo. 51 | > You can discuss them at https://github.com/renuo/renuocop and you can also contribute to them. 52 | 53 | > ⭐️a bin/check script is added to the project. This script will run all the tests of the project. 54 | > It is used in our CI and can be used locally to check if everything is fine. You can customize it to your needs. 55 | 56 | > ⭐️a bin/fastcheck script is added to the project. 57 | > This script will run all the linters of the project. It is used in our CI and can be customized to your needs. 58 | > It will be used as a hook before pushing to quickly check for linting issues. 59 | 60 | > ⭐️a bin/run script is added to the project. This script will start the application. 61 | 62 | > ⭐️bin/check, bin/fastcheck and bin/run are standardized tools for more convenience at Renuo. 63 | 64 | ### Manual adjustments 65 | 66 | Please perform these adjustments manually: 67 | 68 | #### ENV variables 69 | 70 | * Add `dotenv-rails` to Gemfile. Check the [gem homepage](https://github.com/bkeepers/dotenv) to see how to install the gem. 71 | * and create `.env.example` in the root folder of the project where you will specify the only environment variable you need for now: 72 | `SECRET_KEY_BASE`. 73 | * Going forward we will only push the `.env.example` file to the repository in order to protect our env variables. 74 | * Add .env to .gitignore 75 | * Add the following section to your `bin/setup` script so that the `.env` is created from the `.env` when the project is setup locally: 76 | 77 | ```ruby 78 | puts "\n== Copying sample files ==" 79 | unless File.exist?('.env') 80 | system! 'cp .env.example .env' 81 | end 82 | ``` 83 | 84 | * add one more key to .env.example `APP_PORT=3000` 85 | * To ensure you have all the required keys from the `.env.example` in your `.env`, 86 | create the initializer for dotenv-rails in `config/initializers/dotenv_rails.rb`: 87 | 88 | ```ruby 89 | Dotenv.require_keys(Dotenv.parse(".env.example").keys) 90 | ``` 91 | 92 | * Run `bin/setup` again. 93 | 94 | ### Secrets 95 | 96 | We store the secrets necessary to configure the project locally in a 1password Item. 97 | Create a new Item for the project called `[project-name] project secrets` of type note. 98 | Right click on the vault and select `Copy Private link`. 99 | 100 | * Run the command `renuo fetch-secrets --init [the vault private link]` to create an empty secrets file. 101 | 102 | The Item contains the fields that represent the ENV variables. You can use Text or Password fields. 103 | Check existing projects for an example of the usage. 104 | 105 | #### Configuration customisation 106 | 107 | * Update `config/application.rb` and set the default language and timezone 108 | 109 | ```ruby 110 | config.time_zone = 'Zurich' # may vary 111 | config.i18n.default_locale = :de # may vary 112 | ``` 113 | 114 | * Update your `config/environments/production.rb` settings: 115 | 116 | ```ruby 117 | config.force_ssl = true # uncomment 118 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "warn") # change 119 | ``` 120 | 121 | * Update `config/environments/development.rb` settings: 122 | 123 | ```ruby 124 | config.action_controller.action_on_unpermitted_parameters = :raise 125 | config.i18n.raise_on_missing_translations = true # uncomment 126 | 127 | config.generators do |g| 128 | g.apply_rubocop_autocorrect_after_generate! 129 | end 130 | ``` 131 | 132 | * Update `config/environments/test.rb` settings: 133 | 134 | ```ruby 135 | config.action_controller.action_on_unpermitted_parameters = :raise 136 | config.i18n.raise_on_missing_translations = true # uncomment 137 | config.i18n.exception_handler = Proc.new { |exception| raise exception.to_exception } # add 138 | config.active_record.verbose_query_logs = true # add 139 | 140 | # add the following lines to the end of the file 141 | config.to_prepare do 142 | ActiveSupport.on_load(:active_record) do 143 | ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true 144 | end 145 | end 146 | ``` 147 | 148 | * The default [Content Security Policies](https://github.com/renuo/applications-setup-guide/blob/master/ruby_on_rails/content_security_policy.md) should not always be activated, but rather only where there are platform secrets that need to be secured. This rule can be overwritten by a customer, if he opted into CSP when selecting his maintenance plans. 149 | 150 | * If you're using a js-bundling tool, let's clean up after asset precompilation 151 | to reduce the size of your deployment. Although deplo.io doesn't have Heroku slugs, it is still good to set up. Add this to the `Rakefile`: 152 | 153 | ```ruby 154 | Rake::Task['assets:clean'].enhance do 155 | FileUtils.remove_dir('node_modules', true) 156 | FileUtils.remove_dir('vendor/javascript', true) 157 | end 158 | ``` 159 | 160 | ## Finalising 161 | 162 | * Check if the following scripts run successfully: `bin/setup`, `bin/check`, `bin/run` 163 | * If they do, commit all your changes to the main branch with Git. 164 | -------------------------------------------------------------------------------- /ruby_on_rails/appsignal.md: -------------------------------------------------------------------------------- 1 | # Logs & error monitoring with AppSignal 2 | 3 | AppSignal is a service to record logs, monitor errors and performance. 4 | Recording logs works independently from the tech stack. So you should use AppSignal 5 | to record logs even if you don't use Rails. In Heroku we'll add a log drain to 6 | redirect the multiplexed Logplex logs to AppSignal in any case. 7 | 8 | 1. [Backend](#backend) 9 | 2. [Frontend](#frontend) 10 | 3. [Verify the installation](#verify-the-installation) 11 | 12 | ## Backend 13 | 14 | ### Logs & errors 15 | 16 | If you want to log errors and metrics, you need to install the AppSignal agent 17 | into your app. See integration instructions for [Ruby/Rails](https://docs.appsignal.com/logging/platforms/integrations/ruby.html). 18 | 19 | * Add the following gem to your Gemfile: 20 | ```ruby 21 | gem 'appsignal', github: 'renuo/appsignal-ruby' 22 | ``` 23 | * Add a AppSignal configuration file [`config/initializers/appsignal.rb`](../templates/config/initializers/appsignal.rb) 24 | * Add the new variables to your Heroku environments: 25 | 26 | ```yml 27 | APPSIGNAL_APP_ENV: "main | develop" 28 | APPSIGNAL_APP_NAME: "project name without env" 29 | APPSIGNAL_IGNORE_ERRORS: "ActiveRecord::RecordNotFound,ActionController::UnknownFormat" 30 | APPSIGNAL_PUSH_API_KEY: "from appsignal" 31 | # APPSIGNAL_SAMPLING_RATE: "1.0' 32 | ``` 33 | 34 | We use the same push key for all apps. You can either copy it from another project or "create" an app on appsignal. 35 | This will just show you the key and tell you to deploy the app with it for the app to be created. 36 | 37 | Once you deploy the app and collect data the app will show up in the appsignal dashboard. 38 | Navigate to **Logging** -> **Manage Resources** and **Add log resource** with these settings: 39 | 40 | | Setting | Value | 41 | | -------------- | ------------------ | 42 | | Source name | `Rails` | 43 | | Platform | `Heroku Log Drain` | 44 | | Message format | `logfmt` | 45 | 46 | Then add this ingestion endpoint as a log drain using the Heroku commands displayed. 47 | 48 | ### Only Logs 49 | 50 | Choose the "JavaScript" option on the AppSignal project page to 51 | setup your project without an active agent. 52 | 53 | ### Configuration adjustments 54 | 55 | #### Correct Severity 56 | 57 | According to [the docs](https://docs.appsignal.com/logging/platforms/heroku.html), getting the severity to be anything but "INFO" is not possible using the heroku drain. 58 | 59 | However, there is now a way to send the `"severity=XYZ"` logfmt information and have that be applied correctly in appsignal. Unfortunately, just setting this seems to break the recognition of `request_id` in the format `[1234-5678]`. So we have to override the `ActiveSupport::TaggedLogging::Formatter` to add both the `severity` and the `request_id` in logfmt syntax. 60 | 61 | ```ruby 62 | # frozen_string_literal: true 63 | 64 | if Rails.env.production? 65 | module ActiveSupport 66 | module TaggedLogging 67 | module Formatter 68 | def call(severity, timestamp, progname, msg) 69 | logfmt_msg = ["severity=#{severity}", tags_text, msg].compact.join(' ') 70 | super(severity, timestamp, progname, logfmt_msg) 71 | end 72 | 73 | def tags_text 74 | current_tags.map do |tag| 75 | if tag.is_a? Hash 76 | tag.map { |k, v| "#{k}=#{v}" } 77 | else 78 | "[#{tag}]" 79 | end 80 | end.flatten.join(' ') 81 | end 82 | end 83 | end 84 | end 85 | end 86 | ``` 87 | 88 | and 89 | 90 | ```ruby 91 | # config/environments/production.rb 92 | Rails.application.configure do 93 | # We use our custom key value tagging 94 | config.log_tags = [->(request) { { request_id: request.request_id } }] 95 | logger = ActiveSupport::Logger.new(STDOUT) 96 | config.logger = ActiveSupport::TaggedLogging.new(logger) 97 | end 98 | ``` 99 | 100 | ### Lograge 101 | 102 | We use [lograge](https://github.com/roidrage/lograge) in many projects. Here is how to configure 103 | it with AppSignal to get properly tagged logs. 104 | 105 | Using this configuration we get the fully tagged lograge lines and also 106 | the full stack trace with each line tagged with the request id. This allows 107 | us to filter by request id with one click and get all relevant log data at once. 108 | 109 | _With AppSignal gem_ 110 | 111 | ```ruby 112 | # config/initializers/lograge.rb 113 | if ENV['LOGRAGE_ENABLED'] == 'true' 114 | Rails.application.configure do 115 | config.lograge.enabled = true 116 | config.lograge.keep_original_rails_log = true 117 | config.lograge.logger = Appsignal::Logger.new( 118 | "rails", 119 | format: Appsignal::Logger::LOGFMT 120 | ) 121 | config.lograge.custom_payload do |controller| 122 | { 123 | request_id: controller.request.request_id 124 | } 125 | end 126 | end 127 | end 128 | ``` 129 | 130 | _Without AppSignal gem_ 131 | 132 | ```ruby 133 | # config/environments/production.rb 134 | Rails.application.configure do 135 | … 136 | 137 | if ENV['RAILS_LOG_TO_STDOUT'].present? 138 | config.log_tags = [->(request) { { request_id: request.request_id } }] 139 | logger = ActiveSupport::Logger.new($stdout) 140 | config.logger = ActiveSupport::TaggedLogging.new(logger) 141 | end 142 | end 143 | ``` 144 | 145 | ```ruby 146 | # config/initializers/lograge.rb 147 | Rails.application.configure do 148 | … 149 | 150 | if ENV['LOGRAGE_ENABLED'] == 'true' 151 | config.lograge.enabled = true 152 | config.lograge.keep_original_rails_log = true 153 | end 154 | end 155 | ``` 156 | 157 | ### Automation 158 | 159 | Unfortunately Appsignal doesn't provide an API for project configuration. 160 | So if you need to do something on a lot of projects, you have to do it manually. 161 | 162 | Project creation can be automated though with the following script. 163 | Run it in a tmp folder. **It writes into a file on disk.** 164 | 165 | ```rb 166 | #!/usr/bin/env ruby 167 | require 'optparse' 168 | require 'appsignal' 169 | require 'appsignal/demo' 170 | 171 | PUSH_API_KEY = "XXX" 172 | 173 | options = {} 174 | OptionParser.new do |opt| 175 | opt.on('--env APPSIGNAL_ENV') { |o| options[:env] = o } 176 | opt.on('--name APPSIGNAL_NAME') { |o| options[:name] = o } 177 | end.parse! 178 | 179 | raise OptionParser::MissingArgument if options[:env].nil? || options[:name].nil? 180 | 181 | File.write 'config/initializers/appsignal.rb', <<~RUBY 182 | if defined?(Appsignal) 183 | Appsignal.configure do |config| 184 | %w[HTTP_REFERER HTTP_USER_AGENT HTTP_AUTHORIZATION REQUEST_URI].each do |header| 185 | config.request_headers << header 186 | end 187 | end 188 | end 189 | RUBY 190 | 191 | Appsignal.config = Appsignal::Config.new(Dir.pwd, options[:env]) 192 | Appsignal::Demo.transmit 193 | ``` 194 | 195 | ## Frontend 196 | 197 | While the backend uses a secret `PUSH_API_KEY` to authenticate with AppSignal, the frontend uses a public `FRONTEND_API_KEY` 198 | to authenticate with AppSignal. This key can only be read once the project is created on AppSignal. 199 | So once the project is created, the frontend API key can be found in the "Push and deploy" section of your project settings. 200 | 201 | Checkout the [AppSignal documentation](https://docs.appsignal.com/front-end/installation.html) if you need more information. 202 | 203 | _Installation steps:_ 204 | * Add the new frontend API key to your Heroku environments: 205 | ```yml 206 | APPSIGNAL_FRONTEND_API_KEY: "from appsignal" 207 | ``` 208 | * Install the package: `yarn add @appsignal/javascript` 209 | * Include [_appsignal.html](../templates/app/views/shared/_appsignal.html.erb) in your header. 210 | ```erb 211 | <%= render 'shared/appsignal' %> 212 | ``` 213 | * Include [appsignal.js](../templates/app/javascript/appsignal.js) in your JS assets. 214 | 215 | ## Verify the installation 216 | 217 | ### Error monitoring 218 | 219 | #### Ruby 220 | 221 | For each environment of your app, connect to the `heroku run rails console --app [project-name]-[branch-name]` and raise an exception using Appsignal: 222 | 223 | ``` 224 | begin 225 | 1 / 0 226 | rescue ZeroDivisionError => exception 227 | Appsignal.send_error(exception) 228 | end 229 | ``` 230 | 231 | You should find the exception of the ZeroDivisionError on Appsignal after a minute or two. 232 | 233 | #### Javascript 234 | 235 | Open the dev console in chrome, and run 236 | 237 | ```js 238 | try { 239 | throw new Error('test appsignal js'); 240 | } catch(e) { 241 | Appsignal.sendError(e) 242 | } 243 | ``` 244 | 245 | On Appsignal you should find "Uncaught Error: test appsignal js". 246 | -------------------------------------------------------------------------------- /ruby_on_rails/aws.md: -------------------------------------------------------------------------------- 1 | # Amazon Services 2 | 3 | The following Amazon services are involved in our app setups 4 | 5 | * Amazon **S3** is Amazon's Simple Cloud Storage Service, 6 | and used in most of your projects to store images and files. 7 | * Amazon **CloudFront** is a large scale, global, and feature rich CDN. 8 | We mostly use it together with S3 to provide a proper HTTP endpoint (caching, header forwarding, etc.). 9 | You could also host a Single Page Application (SPA). 10 | * Amazon **ACM** issues certificates which can be used for custom Cloudfront distribution domains 11 | * Amazon **IAM** issues resource policies. 12 | * We use a special "renuo-app-setup" user to setup our projects. 13 | * Each app has an own user to separate tenants properly. The user belongs to "renuo-apps-v2" 14 | * You can find a graphical overview in [this lightning talk](https://docs.google.com/presentation/d/1E-6hQB7ZezsVlkESEQVJmdZtdTUWRalT8XjkriurIQc/edit#slide=id.g3ba9e981d1_0_7). 15 | 16 | ## Setup 17 | 18 | ### Preconditions 19 | 20 | #### renuo-cli 21 | 22 | You will need Renuo-CLI to be set up and at the newest version: 23 | 24 | `gem install renuo-cli` --> see [renuo-cli](https://github.com/renuo/renuo-cli) 25 | 26 | Make sure `renuo -v` shows the [newest version](https://github.com/renuo/renuo-cli/tags) 27 | 28 | #### aws-cli 29 | 30 | Retrieve the credentials "AWS Profile 'renuo-app-setup' for s3 setup" from the password manager at first (or ask _wg-operations_ for help). 31 | 32 | You'll need to use `aws-cli`. You can either just continue with "Start the Setup". The command will ensure that everything is set up properly. 33 | Or you can install it manually: 34 | 35 | ``` 36 | brew install awscli 37 | aws configure --profile renuo-app-setup 38 | ``` 39 | 40 | If you want to check your config, run `aws configure --profile renuo-app-setup list`. 41 | 42 | We would recommend setting default region name to `eu-central-1`. The default output format is json and should not be changed. 43 | 44 | ### Command generation 45 | 46 | The following command will generate command-line-commands to set up S3 and CloudFront. 47 | You'll need to run them by yourself after reviewing the output. 48 | 49 | 1. Run `renuo create-aws-project` 50 | 1. Follow the steps and answer the questions 51 | 1. Now it will print you out a series of commands e.g.: 52 | 53 | ```sh 54 | # AWS main 55 | 56 | aws --profile renuo-app-setup iam create-user --user-name <> 57 | [...] 58 | 59 | # AWS develop 60 | [...] 61 | ``` 62 | 1. Review the commands carefully (e.g. make sure that you enable bucket versioning) 63 | 64 | ### Executing the commands 65 | 66 | Running the commands will print some JSON pages to your screen. 67 | **Copy each `SecretAccessKey` and `AccessKeyId` to your credentials store!** 68 | 69 | Once you have worked through the commands, you are ready to use S3 for Active Storage within your Rails app by configuring the storage.yml file (as below) and setting `config.active_storage.service = :amazon` in your production.rb file. 70 | 71 | ### Custom Cloudfront Distribution CNAME Aliases 72 | 73 | If you want to serve your S3 bucket via a custom domain, you need to add the CNAMEs to 74 | your Cloudfront Distibution manually. 75 | 76 | 1. Visit and edit 77 | your distribution. 78 | 1. Enter the CNAMEs as aliases 79 | 1. Click "Request certificate" (this opens a new tab with Amazon ACM, make sure it's region is us-east-1) 80 | 1. Enter all the CNAMEs you want to have as aliases (normally only one) 81 | 1. Enter the domain ownership verification records into Cloudflare (CNAME with cryptic values) 82 | 1. Submit the ACM form and wait for the certificate to being issued. 83 | 1. Go back to the Cloudfront distribution, refresh the certifactes drop down and choose your new certificate. 84 | 1. Save the changes to the Cloudfront distribution. 85 | 86 | ### Rails Configuration 87 | 88 | You then can use an _ActiveStorage_ configuration like this: 89 | 90 | ```yaml 91 | amazon: 92 | service: S3 93 | access_key_id: <%= ENV['AWS_S3_ACCESS_KEY_ID'] %> 94 | secret_access_key: <%= ENV['AWS_S3_SECRET_ACCESS_KEY'] %> 95 | bucket: <%= ENV['AWS_S3_BUCKET'] %> 96 | region: "eu-central-1" 97 | ``` 98 | -------------------------------------------------------------------------------- /ruby_on_rails/bootstrap.md: -------------------------------------------------------------------------------- 1 | # Bootstrap 2 | 3 | You can use the npm package of the latest version of Bootstrap. 4 | 5 | `yarn add bootstrap` 6 | 7 | and include it in your stylesheet pack with: 8 | 9 | ```scss 10 | @import '~bootstrap/scss/bootstrap'; 11 | ``` 12 | 13 | If you want to use also the javascript part of Bootstrap you need both popper.js and jquery. 14 | Add them with: 15 | 16 | `yarn add jquery popper.js` 17 | 18 | and configure them in `environment.js`: 19 | 20 | ```js 21 | const webpack = require('webpack') 22 | environment.plugins.append('Provide', new webpack.ProvidePlugin({ 23 | $: 'jquery', 24 | jQuery: 'jquery', 25 | Popper: ['popper.js', 'default'] 26 | })); 27 | ``` 28 | 29 | Change `extract_css: false` to `extract_css: true` in `config/webpacker.yml` 30 | 31 | and finally, import bootstrap library in `application.js` with 32 | 33 | ```js 34 | import 'bootstrap/dist/js/bootstrap'; 35 | ``` 36 | 37 | ## Simple Form 38 | 39 | If you use the gem [Simple Form](https://github.com/plataformatec/simple_form), 40 | you need to adjust the configuration in the `config/initializers/simple_form.rb` file. 41 | 42 | Here are some recommended options: 43 | 44 | ```ruby 45 | config.wrappers :default, class: 'form-group', 46 | hint_class: :field_with_hint, error_class: 'has-danger' do |b| 47 | ``` 48 | 49 | ```ruby 50 | config.error_notification_class = 'alert alert-danger' 51 | ``` 52 | 53 | ```ruby 54 | b.use :error, wrap_with: { tag: :span, class: 'invalid-feedback' } 55 | ``` 56 | 57 | ```ruby 58 | config.label_class = 'form-control-label' 59 | ``` 60 | 61 | ```ruby 62 | config.input_class = 'form-control' 63 | ``` 64 | 65 | To make the error highlighting work you need to add some css to your application 66 | 67 | ```scss 68 | .invalid-feedback { 69 | display: block; 70 | } 71 | 72 | .has-danger { 73 | .form-control { 74 | border-color: $form-feedback-invalid-color; 75 | } 76 | } 77 | ``` 78 | 79 | Please note that this is a workaround, as there is yet no way to add an error class directly onto an input. 80 | However, there is an open issue on Simple Form: 81 | Once this feature is added, please remove the css. 82 | 83 | For the styling of the pull down date selectors or checkboxes, you need to write some wrappers, that you can add 84 | to the input element. 85 | It is best to create a separate config file for this. 86 | 87 | Once the issue is done, you can also configure simple form 88 | with the command `rails generate simple_form:install --bootstrap4`. 89 | 90 | ```ruby 91 | # config/initializers/simple_form_bootstrap.rb 92 | 93 | SimpleForm.setup do |config| 94 | config.wrappers :inline_date, tag: 'div', error_class: 'has-danger' do |b| 95 | b.use :html5 96 | b.use :label, class: 'control-label' 97 | b.wrapper tag: 'div', class: 'form-inline row' do |ba| 98 | ba.use :input, class: 'form-control form-inline', wrap_with: { class: 'col-md-6' } 99 | ba.use :error, wrap_with: { tag: 'span', class: 'invalid-feedback' } 100 | ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } 101 | end 102 | end 103 | 104 | config.wrappers :inline_checkbox, tag: 'div', class: 'control-group', error_class: 'has-error' do |b| 105 | b.use :html5 106 | b.wrapper tag: 'div', class: 'controls' do |ba| 107 | ba.use :label_input, wrap_with: { class: 'checkbox inline' } 108 | ba.use :error, wrap_with: { tag: 'span', class: 'help-inline' } 109 | ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } 110 | end 111 | end 112 | 113 | config.wrapper_mappings = { 114 | check_boxes: :inline_checkbox, 115 | date: :inline_date, 116 | datatime: :inline_date 117 | } 118 | end 119 | 120 | ``` 121 | -------------------------------------------------------------------------------- /ruby_on_rails/bullet.md: -------------------------------------------------------------------------------- 1 | # Bullet 2 | 3 | We don't like N+1 queries. Nobody does. 4 | If you don't know what we are talking about, 5 | please read [this article](https://www.sitepoint.com/silver-bullet-n1-problem/) that explains it pretty well. 6 | 7 | In order to identify possible N+1 queries, we use the gem [`bullet`](https://github.com/flyerhzm/bullet). 8 | 9 | Please add the gem to both `development` and `test` group of the Gemfile since we'll use it also in our tests. 10 | 11 | Those are our favourite configurations: 12 | 13 | * in development we enable it and we see the issues both a footer in the page and also in the logs 14 | 15 | ```ruby 16 | # config/environments/development.rb 17 | 18 | config.after_initialize do 19 | Bullet.enable = true 20 | Bullet.bullet_logger = true 21 | Bullet.add_footer = true 22 | end 23 | ``` 24 | 25 | * in test we enable it and raise an exception in case a N+1 is identified (or an unused eager loading) 26 | 27 | ```ruby 28 | # config/environments/test.rb 29 | 30 | config.after_initialize do 31 | Bullet.enable = true 32 | Bullet.bullet_logger = true 33 | Bullet.raise = true 34 | end 35 | ``` 36 | -------------------------------------------------------------------------------- /ruby_on_rails/cloudflare.md: -------------------------------------------------------------------------------- 1 | # Cloudflare 2 | 3 | Setup Cloudflare: 4 | 5 | * Now open 6 | * 7 | * 8 | * 9 | 10 | Check that you: 11 | * see "1+2=3" in each app. 12 | * have been redirected to https. 13 | 14 | ## Crypto settings 15 | 16 | When setting up a new site on Cloudflare, make sure you set SSL to "Full" under Crypto settings. You may end up in endless loop of redirects if it stays on the default setting ("Flexible") 17 | -------------------------------------------------------------------------------- /ruby_on_rails/compile_readme.md: -------------------------------------------------------------------------------- 1 | # README File 2 | 3 | It's time to compile our README.md file for the project. 4 | Please follow the [README](../templates/README.md). -------------------------------------------------------------------------------- /ruby_on_rails/configure_ci.md: -------------------------------------------------------------------------------- 1 | # Configure the CI 2 | 3 | At Renuo we **always** use a CI (Continuous Integration) system to test our applications. It's essential to guarantee 4 | that all the tests pass before building and releasing a new version through our CD system. Our projects use 5 | [SemaphoreCI](). 6 | 7 | Before configuring the CI, you should already have a Git Repository with the code, a `bin/check` command to execute, 8 | and the main branches already pushed and ready to be tested. 9 | 10 | 1. Proceed to and login through GitHub with renuobot@renuo.ch ([1Password](https://start.1password.com/open/i?a=QZNJJCCDWVCGBGI73Z2L55KSGE&v=crlutt26yprmp6thr573qxsxkq&i=u7rirvnrf5fjxd25caiq7ib6vq&h=renuo.1password.com)) 11 | 1. Follow these instructions to install semaphore CLI https://docs.semaphoreci.com/reference/sem-command-line-tool/ 12 | 1. Create a project here: 13 | 1. Go to the project's artifact settings: `Settings` > `Artifacts` 14 | 1. Set the retention policy for project, workflow and job artifacts to `/**/*` and `2 weeks` 15 | 16 | ## Rails specific configuration 17 | 18 | ```sh 19 | renuo configure-semaphore 20 | ``` 21 | 22 | The command will copy the necessary templates to `.semaphore` folder using the renuo-cli. These files need to be maintained on the [renuo-cli repository](https://github.com/renuo/renuo-cli/tree/main). 23 | Adapt the files and remove the `develop` related ones if you don't use the `develop` branch. 24 | 25 | 1. Add a file called `.nvmrc` to the project root, where you specify the latest node version 26 | 1. Commit the files to both branches, push and watch the CI run. 27 | 28 | When all builds are green, then you have properly configured your CI and CD. 29 | 30 | ![semaphoreci_2](../images/semaphore_ci.png) 31 | 32 | You should now see a third block where your deployment runs to Heroku. 33 | Make sure it is green and deploys correctly: 34 | 35 | ![semaphoreci_2](../images/semaphore_cd.png) 36 | 37 | ## Conclusion 38 | 39 | You have now your application running on all the environments. 40 | From now on, all the changes you will push on *develop* or *main* 41 | branches in GitHub will be automatically deployed to the related server. 42 | 43 | It's time to create some first Pull Requests with some improvements. 44 | 45 | **Don't forget to go back to the GitHub settings and add the CI to the required checks!** 46 | -------------------------------------------------------------------------------- /ruby_on_rails/configure_percy.md: -------------------------------------------------------------------------------- 1 | # Configure Percy 2 | 3 | Percy is a service that recognizes UI changes between pull requests. Read more about it [here](https://percy.io) 4 | 5 | ## Create a new project on the Percy website. 6 | 7 | 1. Ask wg-operations to add the project to Percy. 8 | 1. Visit [this](https://percy.io/organizations/renuo/projects/new) link. 9 | 1. Fill in the name with `[project-name]` and select the github repository of the project. 10 | 11 | ## Setup the CI 12 | 13 | 1. Add the PERCY_TOKEN and PERCY_PROJECT env variables to the CI. 14 | They can be found under `https://percy.io/renuo/[project-name]/settings`. 15 | 1. Also add PERCY_TARGET_BRANCH and set it to `develop`. Like that Percy always compares the screenshots 16 | to the develop branch. 17 | 18 | ## Setup the Rails application 19 | 20 | 1. Add the gem `percy-capybara` to the test group in the Gemfile and run `bundle install`. 21 | 1. Follow the setup instructions [here](https://percy.io/docs/clients/ruby/capybara-rails#setup) 22 | 1. If the application uses the gem `vcr`, 23 | follow the instructions [here](https://percy.io/docs/clients/ruby/capybara-rails#_web-mock/vcr-users). 24 | 25 | ## Start using Percy 26 | 27 | Create a snapshot in any capybara spec by adding the following line: 28 | 29 | `Percy::Capybara.snapshot(page, name: 'name of your snapshot')` 30 | 31 | ## When to add screenshots 32 | 33 | Usually it's enough to add one screenshot for each view. 34 | In special cases you may want to add more screenshots. 35 | 36 | ## Encoding 37 | 38 | For Percy to render all characters correctly, every page that has a screenshot needs to have 39 | the header ``. 40 | -------------------------------------------------------------------------------- /ruby_on_rails/content_security_policy.md: -------------------------------------------------------------------------------- 1 | # Content Security Policy 2 | 3 | By default, the Content Security Policy (CSP) should be enabled in every new project, 4 | since it is an added layer of security that helps in detecting and mitigating attacks, 5 | such as Cross Site Scripting (XSS) or Data Injection Attacks. 6 | 7 | It works by telling the browser which sources are trustworthy. Everything that does not 8 | come from a trusted source is blocked and a violation is reported to the report-uri (You 9 | can also opt-in for reporting only. However, this is not recommended since it is not 10 | effective when it comes to actually preventing an attack. One possible scenario, where 11 | reporting only would be useful would be, when CSP should be introduced to a new project, 12 | but blocking would possibly cause the site to malfunction). 13 | 14 | ## Setup 15 | 16 | Content Security Policy can be enabled by uncommenting the code given in 17 | `config/initializers/content_security_policy.rb`. This file has the following structure 18 | after configuration: 19 | 20 | ```ruby 21 | Rails.application.config.content_security_policy do |policy| 22 | policy.default_src :self, :https 23 | policy.font_src :self, :https, ... 24 | policy.img_src ... 25 | policy.object_src ... 26 | policy.script_src ... 27 | policy.style_src ... 28 | policy.connect_src :self, :https, 'http://localhost:3035', 'ws://localhost:3035' if Rails.env.development? 29 | policy.connect_src :self, :https if Rails.env.production? 30 | 31 | policy.report_uri ENV['CSP_REPORT_URI'] if ENV['CSP_REPORT_URI'] 32 | end 33 | ``` 34 | 35 | Details on how to setup Sentry reporting can be found in the [Sentry guide](https://github.com/renuo/applications-setup-guide/blob/master/ruby_on_rails/sentry.md#backend-rails). 36 | 37 | ## Common Rules 38 | 39 | **Warning:** This list may be incomplete. Please feel free to modify or add if you see something missing! 40 | 41 | ### Sentry 42 | 43 | | Source | Rule | 44 | | ----------- | ---------------------------------------------- | 45 | | connect-src | `https://sentry.io` | 46 | | script-src | `https://cdn.ravenjs.com` (if loaded from CDN) | 47 | 48 | ### Google Tag Manager 49 | 50 | | Source | Rule | 51 | | ---------- | --------------------------- | 52 | | script-src | `'nonce-{generated_nonce}'` | 53 | | img-src | `www.googletagmanager.com` | 54 | 55 | Where the tracking script looks like: 56 | 57 | ```html 58 | 59 | ``` 60 | 61 | Alternatively, if `'unsafe-inline'` is already set, the nonce can be left out. 62 | 63 | Starting from Rails 6 on, the `javascript_tag` view helper also accepts a nonce option: 64 | 65 | ```erbruby 66 | <%= javascript_tag nonce: true do %> 67 | // code 68 | <% end %> 69 | ``` 70 | 71 | #### With Preview Mode Enabled 72 | 73 | | Source | Rule | 74 | | ---------- | ------------------------------------------------------------ | 75 | | script-src | `https://tagmanager.google.com` | 76 | | style-src | `https://tagmanager.google.com https://fonts.googleapis.com` | 77 | | img-src | `https://ssl.gstatic.com https://www.gstatic.com` | 78 | | font-src | `https://fonts.gstatic.com data:` | 79 | 80 | #### Universal Analytics (Google Analytics) 81 | 82 | | Source | Rule | 83 | | ----------- | ------------------------------------------------------------------- | 84 | | script-src | `https://www.google-analytics.com https://ssl.google-analytics.com` | 85 | | img-src | `https://www.google-analytics.com` | 86 | | connect-src | `https://www.google-analytics.com` | 87 | 88 | For a more details list, see https://developers.google.com/tag-manager/web/csp 89 | 90 | ### Facebook Tracking Pixel 91 | 92 | | Source | Rule | 93 | | ---------- | -------------------------------------- | 94 | | script-src | `'unsafe-inline' connect.facebook.net` | 95 | 96 | ### Google Fonts 97 | 98 | | Source | Rule | 99 | | --------- | ------------------------------ | 100 | | style-src | `https://fonts.googleapis.com` | 101 | 102 | ### Stripe 103 | 104 | #### Checkout 105 | 106 | | Source | Rule | 107 | | ----------- | ------------------------------------------ | 108 | | connect-src | `https://checkout.stripe.com g.stripe.com` | 109 | | frame-src | `https://checkout.stripe.com` | 110 | | script-src | `https://checkout.stripe.com` | 111 | | img-src | `https://*.stripe.com` | 112 | 113 | #### Stripe.js 114 | 115 | | Source | Rule | 116 | | ----------- | ------------------------------------------------ | 117 | | connect-src | `https://api.stripe.com` | 118 | | frame-src | `https://js.stripe.com https://hooks.stripe.com` | 119 | | script-src | `https://js.stripe.com` | 120 | 121 | ### Aiaibot 122 | 123 | | Source | Rule | 124 | | ----------- | ---------------------------------------------------- | 125 | | connect-src | `https://api.aiaibot.com https://sentry.aiaibot.com` | 126 | -------------------------------------------------------------------------------- /ruby_on_rails/create_application_server_deploio.md: -------------------------------------------------------------------------------- 1 | # Setup Application Server for Deploio 2 | 3 | ## Prerequisites 4 | 5 | Before setting up your application, ensure you have completed the following for Deploio. 6 | 7 | ### Prerequisites for Deploio 8 | 9 | - You've [read about what Deploio is](https://docs.nine.ch/docs/deplo-io/getting-started-with-deploio). 10 | - You have a Deploio account. 11 | - You have installed the `renuo-cli` gem. 12 | - You have installed the `nctl` command. 13 | - You have logged in using `nctl`. 14 | 15 | ## Setup Your Application 16 | 17 | ### Setup Deploio Application 18 | 19 | #### Remote Configuration 20 | 21 | Run the command to generate a script which will create and configure all Deploio apps. `[project-name]` string length is limited to 63 characters: 22 | 23 | ```sh 24 | renuo create-deploio-app [project-name] 25 | ``` 26 | 27 | Please review the script before running it and execute only the commands you need and understand. 28 | In particular, you might need only one of the two environments if you decided to not use `develop`. 29 | 30 | **If you don't know what a command does: read the documentation and then execute it.** 31 | 32 | If you think that the script is outdated, please open a Pull Request on the [renuo-cli](https://github.com/renuo/renuo-cli) project. 33 | 34 | ## Next Steps 35 | 36 | That's it! You have now configured your application with Deploio. No more configuration is needed. 37 | -------------------------------------------------------------------------------- /ruby_on_rails/create_application_server_heroku.md: -------------------------------------------------------------------------------- 1 | # Setup Application Server for Heroku 2 | 3 | ## Prerequisites 4 | 5 | Before setting up your application, ensure you have completed the following for Heroku. 6 | 7 | ### Prerequisites for Heroku 8 | 9 | - You've [read about what Heroku is](https://www.heroku.com/platform). 10 | - You have a Heroku account. 11 | - You have installed the `renuo-cli` gem. 12 | 13 | ## Setup Your Application 14 | 15 | ### Setup Heroku Application 16 | 17 | #### Remote Configuration 18 | 19 | Run the command to generate a script which will create and configure all Heroku apps. `[project-name]` string length is limited to 22 characters: 20 | 21 | ```sh 22 | renuo create-heroku-app [project-name] 23 | ``` 24 | 25 | Please review the script before running it and execute only the commands you need and understand. 26 | In particular, you might need only one of the two environments if you decided to not use `develop`. 27 | 28 | **If you don't know what a command does: read the documentation and then execute it.** 29 | 30 | If you think that the script is outdated, please open a Pull Request on the [renuo-cli](https://github.com/renuo/renuo-cli) project. 31 | 32 | #### Setup Rails for Heroku 33 | 34 | 1. Add a file called `Procfile` to your code root: 35 | 36 | ```sh 37 | web: bundle exec puma -C config/puma.rb 38 | ``` 39 | 40 | This file is read by Heroku to start the web app and worker jobs. 41 | 42 | 2. Add a file called `.slugignore` to your code root: 43 | 44 | ```sh 45 | /spec 46 | /.semaphore 47 | ``` 48 | 49 | This file allows you to mark files and folders to be excluded from the Heroku 50 | -------------------------------------------------------------------------------- /ruby_on_rails/cucumber.md: -------------------------------------------------------------------------------- 1 | # Cucumber 2 | 3 | [Cucumber](https://cucumber.io) is a testing framework which allows us to specify feature tests using plain language. 4 | We use Cucumber for e2e tests in some of our projects. We integrate it using the `cucumber-rails` gem. 5 | 6 | ## Installing `cucumber-rails` 7 | 8 | The installation process of `cucumber-rails` is also documented on its 9 | [Github page](https://github.com/cucumber/cucumber-rails). 10 | 11 | To install `cucumber-rails`, add the following gems to your Gemfile: 12 | 13 | ```rb 14 | group :test do 15 | gem 'capybara', require: false 16 | gem 'cucumber-rails', require: false 17 | gem 'database_cleaner' 18 | end 19 | ``` 20 | 21 | Run `bundle install` to install the gems. 22 | 23 | Finally, set up Cucumber with 24 | 25 | ```sh 26 | rails generate cucumber:install 27 | ``` 28 | 29 | At this time, it is recommended to inspect the generated files and commit. Before committing, make sure, Rubocop does 30 | not raise any issues. Therefore, you will have to add the following exception to `.rubocop.yml`: 31 | 32 | ```yaml 33 | AllCops: 34 | Exclude: 35 | - 'lib/tasks/cucumber.rake' 36 | ``` 37 | 38 | ## Running your first Cucumber test 39 | 40 | If you add Cucumber to an existing project, test a real page instead of using the given example test. 41 | 42 | Add the following files: 43 | 44 | * [`app/controllers/home_controller.rb`](../templates/app/controllers/home_controller.rb) (If you haven't done so 45 | already) in the [RSpec](rspec.md) section. 46 | * [`features/home_check.feature`](../templates/features/home_check.feature) 47 | * [`features/step_definitions/home_check_steps.rb`](../templates/features/step_definitions/home_check_steps.rb) 48 | 49 | Now, you can run Cucumber in your terminal: 50 | 51 | ```sh 52 | cucumber 53 | ``` 54 | 55 | You may get a couple of deprecation warnings. There is currently an 56 | [issue](https://github.com/cucumber/cucumber-rails/issues/346) about them on the Github page of `rspec-rails` 57 | 58 | ## Adding Cucumber tests to `bin/check` 59 | 60 | Add the following code to `bin/check`, so the Cucumber tests are run on CI: 61 | 62 | ```sh 63 | NO_COVERAGE=true bundle exec cucumber --format progress 64 | if [ $? -ne 0 ]; then 65 | echo 'Cucumber did not run successfully, commit aborted' 66 | exit 1 67 | fi 68 | ``` 69 | 70 | Assure that `bin/check` fails, if you have a broken Cucumber test. 71 | 72 | ## Custom environment 73 | 74 | Probably, you are going to need the following configuration files in `/features/env`: 75 | 76 | * [`capybara.rb`](../templates/features/env/capybara.rb) enables headless Chrome. This is needed in Cucumber tests 77 | which require Javascript (i.e. scenarios tagged with `@javascript`). 78 | * [`database_cleaner.rb`](../templates/features/env/database_cleaner.rb) sets up `database_cleaner` for Cucumber tests. 79 | * [`factory_bot.rb`](../templates/features/env/factory_bot.rb) makes FactoryBot's methods (like `build` and `create`) 80 | accessible in the step definition file. 81 | * [`warden.rb`](../templates/features/env/warden.rb) sets up the test environment, if you are using Warden (i.e. if you 82 | are using Devise). 83 | -------------------------------------------------------------------------------- /ruby_on_rails/depfu.md: -------------------------------------------------------------------------------- 1 | # Depfu 2 | 3 | Depfu is a service which checks out the Gemfile / yarn.lock of a project for problematic 4 | dependencies. It will automatically create a pull request to the project 5 | if a security vulnerability has been disclosed. 6 | 7 | 1. Ask wg-operations to add repository access for Depfu to you new Github 8 | repository. 9 | 10 | That's all :-) 11 | 12 | Update strategy should be set to `Grouped Updates`, frequency: `monthly`. Assignee should be set. 13 | 14 | ## Engine Updates 15 | 16 | Enable minor engine updates. 17 | 18 | ![Depfu Engine Updates](../images/depfu_engine_updates.png) 19 | 20 | **Note:** If you are using Heroku, the latest Ruby / node version may not yet 21 | be available on their platform, so you may need to delay the upgrade. Check the 22 | following GitHub repositories to see if Heroku added support already: 23 | 24 | * 25 | * 26 | 27 | ## Automatic merging 28 | 29 | To speed up the merging of smaller upgrades (like security fixes) we enable 30 | Automatic merging like this: 31 | 32 | ![Depfu Automatic Merging](../images/depfu_automatic_merging.png) 33 | 34 | Since PRs need to be approved before depfu can merge them we add a GitHub 35 | Actions workflow to automatically approve PRs from depfu: 36 | 37 | ```yaml 38 | # .github/workflows/depfu_autoapprove.yml 39 | name: Depfu auto-approve 40 | on: 41 | pull_request_target: 42 | types: [opened] 43 | 44 | permissions: 45 | pull-requests: write 46 | 47 | jobs: 48 | depfu: 49 | runs-on: ubuntu-latest 50 | if: ${{ github.actor == 'depfu[bot]' }} 51 | steps: 52 | - name: Approve all depfu PRs 53 | run: gh pr review --approve "$PR_URL" 54 | env: 55 | PR_URL: ${{github.event.pull_request.html_url}} 56 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 57 | ``` 58 | -------------------------------------------------------------------------------- /ruby_on_rails/devise.md: -------------------------------------------------------------------------------- 1 | # Devise 2 | 3 | :warning: If you are going to use devise we suggest you to [send_emails](send_emails.md) first. :warning: 4 | 5 | * Add the following gem `gem 'devise'` and install it 6 | 7 | ```sh 8 | bundle install 9 | rails generate devise:install 10 | ``` 11 | 12 | * update `config/initializers/devise.rb` and set 13 | 14 | ```rb 15 | config.secret_key = ENV['DEVISE_SECRET_KEY'] 16 | config.mailer_sender = ENV['MAIL_SENDER'] 17 | config.pepper = ENV['DEVISE_PEPPER'] 18 | ``` 19 | 20 | * add the two variables to the `.env.example` 21 | 22 | ```bash 23 | DEVISE_SECRET_KEY="rake secret" 24 | DEVISE_PEPPER="rake secret" 25 | ``` 26 | 27 | Open a pull request! :tada: 28 | -------------------------------------------------------------------------------- /ruby_on_rails/environment_protection.md: -------------------------------------------------------------------------------- 1 | # Staging Environment Protection 2 | 3 | ## Configuration for Heroku 4 | 5 | Add 6 | 7 | ```bash 8 | # BASIC_AUTH="admin:some-memorable-password" 9 | ``` to `.env.example`, then run the following command: 10 | 11 | ```sh 12 | heroku config:set BASIC_AUTH='admin:[first-memorable-password]' --app [your-app]-develop 13 | ``` 14 | Finally, save the passwords in 1Password. 15 | 16 | ## Configuration for Deploio 17 | 18 | HTTP Basic Authentication should be configured to prevent public traffic on our development applications. 19 | 20 | With Deploio, configure Basic Auth in the Rails app: 21 | 22 | ### Managing Basic Auth via Rails 23 | 24 | To manage Basic Auth via Rails, use the following commands: 25 | 26 | ```sh 27 | nctl config set --project {PROJECT_NAME} --application {APPLICATION_NAME} --env=BASIC_AUTH={USERNAME}:{PASSWORD} 28 | nctl config set --project {PROJECT_NAME} --application {APPLICATION_NAME} --basic-auth false 29 | ``` 30 | 31 | ## ApplicationController Configuration 32 | 33 | Configure the `ApplicationController` like this when managing Basic Auth via Rails: 34 | 35 | ```ruby 36 | class ApplicationController < ActionController::Base 37 | # ... 38 | 39 | ENV['BASIC_AUTH'].to_s.split(':').presence&.then do |username, password| 40 | http_basic_authenticate_with name: username, password: password 41 | end 42 | 43 | # ... 44 | end 45 | ``` 46 | -------------------------------------------------------------------------------- /ruby_on_rails/first_git_push.md: -------------------------------------------------------------------------------- 1 | # Push to Git Repository 2 | 3 | It's now time to push to the git repository and configure our CI and CD to deploy our application on Heroku. 4 | To do that you first need to [Create a Git Repository](../create_git_repository.md). 5 | 6 | After creating the repo you can connect your project to the remote git repo (if you didn't use `hub create` command) 7 | 8 | ```sh 9 | git remote add origin git@github.com:renuo/[project-name].git 10 | ``` 11 | 12 | and push using: 13 | 14 | ```sh 15 | git add . 16 | git commit -m "Initial commit" 17 | git push -u origin main 18 | ``` 19 | -------------------------------------------------------------------------------- /ruby_on_rails/font_awesome.md: -------------------------------------------------------------------------------- 1 | # Font-Awesome 2 | 3 | You can either include font-awesome through their CDN or install it via npm/yarn. 4 | 5 | ## Installation with yarn 6 | 7 | ### Set up 8 | 9 | #### PRO version 10 | 11 | 1. Look up the auth token which can be found [here](https://fontawesome.com/account). The credentials can be found on passwork 12 | 1. Follow [these steps](https://fontawesome.com/how-to-use/on-the-web/setup/using-package-managers#installing-pro) to set up _font-awesome_ pro either per project or globally. 13 | 14 | #### Free 15 | 16 | For the free version of _font-awesome 5_ just run `yarn add @fortawesome/fontawesome-free` 17 | 18 | ### Inclusion for Webpacker 19 | 20 | Include this code in your `stylesheets.scss` 21 | ``` 22 | $fa-font-path: '~@fortawesome/fontawesome-free/webfonts'; 23 | @import '~@fortawesome/fontawesome-free/scss/fontawesome'; 24 | @import '~@fortawesome/fontawesome-free/scss/solid'; 25 | ``` 26 | 27 | ## Usage 28 | 29 | 1. Navigate to the [font-awesome gallery list](https://fontawesome.com/icons?d=gallery) 30 | 1. Search for the icon that you wish to include and copy paste the `` tag into your application: 31 | For example for the Angular symbol I can just use this tag: `` 32 | -------------------------------------------------------------------------------- /ruby_on_rails/hotjar.md: -------------------------------------------------------------------------------- 1 | # :fire: Hotjar 2 | 3 | * Add a new site on the Hotjar dashboard using the Renuo Hotjar account 4 | (credentials are in Passwork). Note the site ID of the generated site. 5 | 6 | * Add the following gem to your Gemfile: 7 | 8 | ```ruby 9 | group :production do 10 | gem 'rack-tracker' 11 | end 12 | ``` 13 | 14 | * Configure the tracker in `production.rb`: 15 | 16 | ```ruby 17 | if ENV['HOTJAR_SITE_ID'].present? 18 | config.middleware.use(Rack::Tracker) do 19 | handler :hotjar, site_id: ENV['HOTJAR_SITE_ID'] 20 | end 21 | end 22 | ``` 23 | -------------------------------------------------------------------------------- /ruby_on_rails/initialise_gitflow.md: -------------------------------------------------------------------------------- 1 | # Initialise Gitflow 2 | 3 | You can initialise gitflow in you project with `git flow init -d` 4 | 5 | Then push also your new develop branch `git push --set-upstream origin develop` if you have one. 6 | 7 | Once you have pushed all the branches, you can finish the [configuration of Git Repository](../configure_git_repository.md) 8 | -------------------------------------------------------------------------------- /ruby_on_rails/jest.md: -------------------------------------------------------------------------------- 1 | # Run Javascript tests with Jest 2 | 3 | When you start writing Javascript code, you have to test it. Webpacker doesn't come (yet) with a default test tool. 4 | Here is a configuration suggestion to start testing using Jest. 5 | 6 | * Install Jest 7 | ```bash 8 | ./bin/yarn add --dev jest 9 | ``` 10 | 11 | * Add the following to the `package.json` 12 | 13 | ```json 14 | "scripts": { 15 | "test": "jest --coverage" 16 | }, 17 | "jest": { 18 | "roots": [ 19 | "spec/javascripts" 20 | ], 21 | "setupFiles": [ 22 | "./spec/javascripts/setup-jest.js" 23 | ], 24 | "coverageThreshold": { 25 | "global": { 26 | "branches": 100.0, 27 | "functions": 100.0, 28 | "lines": 100.0, 29 | "statements": 100.0 30 | } 31 | }, 32 | "coverageDirectory": "./coverage/jest" 33 | } 34 | ``` 35 | This creates a `yarn test` command which runs your tests, including coverage. 36 | It also configures the root of your tests into spec/javascripts folder and the coverage thresholds. 37 | 38 | * Create the file `spec/javascripts/setup-jest.js` and, if you are using JQuery, add: 39 | 40 | ```js 41 | import $ from 'jquery'; 42 | global.$ = global.jQuery = $; 43 | ``` 44 | In this file you create the configuration that is necessary before running the tests. 45 | 46 | * Add the following to your `.babelrc` configuration file: 47 | 48 | ``` 49 | "env": { 50 | "test": { 51 | "plugins": ["transform-es2015-modules-commonjs"] 52 | } 53 | } 54 | ``` 55 | 56 | Now you can run your tests with `yarn test` and they should fail because you don't have any test. 57 | 58 | Add your Javascript tests check run to `bin/check`: 59 | 60 | ``` 61 | bin/yarn test 62 | if [ $? -ne 0 ]; then 63 | echo 'Javascript tests did not run successfully, commit aborted' 64 | exit 1 65 | fi 66 | ``` 67 | 68 | Add your tests to the `spec/javascripts` folder, 69 | naming them `yourtest.spec.js` to be automatically recognised by Jest as tests. 70 | 71 | A template for a test could be the following: 72 | 73 | ```js 74 | // spec/javascripts/my_class.spec.js 75 | 76 | import MyClass from '../../app/webpacker/src/javascripts/my_class'; 77 | 78 | describe('MyClass', () => { 79 | 80 | beforeEach(() => { 81 | ... 82 | }); 83 | 84 | describe('#amethod', () => { 85 | it('runs a test', () => { 86 | new MyClass(); 87 | expect(1).toEqual(2); 88 | }); 89 | }); 90 | }); 91 | 92 | ``` 93 | -------------------------------------------------------------------------------- /ruby_on_rails/linting_and_automatic_check.md: -------------------------------------------------------------------------------- 1 | # Linting and automatic checks :white_check_mark: 2 | 3 | All Renuo projects contain (and your project must contain as well) the following linters. 4 | Every linter consists of a gem (usually) and a command to add to our `bin/fastcheck` script. 5 | 6 | Check out the `bin/fastcheck` [fastcheck](../templates/bin/fastcheck) for the final version of it. 7 | 8 | ## Renuocop :cop: 9 | 10 | Renuocop is based on Standard Ruby and is a set of rules that we use to lint our Ruby code. 11 | It's already included in your Gemfile by default. 12 | 13 | You can execute it and correct the issues you'll find. 14 | 15 | `bundle exec rubocop -A` will fix ~~most~~ all of them automatically. 16 | 17 | ## Brakeman 18 | 19 | Brakeman comes by default with Rails. Add it to the `bin/fastcheck` script. 20 | 21 | ``` 22 | bundle exec brakeman -q -z --no-summary --no-pager 23 | ``` 24 | 25 | ## Mdl 26 | 27 | An optional check for markdown files. You can include it or not. Discuss within your team. 28 | 29 | ```ruby 30 | group :development, :test do 31 | gem 'mdl', require: false 32 | end 33 | ``` 34 | 35 | ## SCSS lint 36 | 37 | > _Note_: Your Semaphore configuration might have to be adjusted if you decide to use `npm`. 38 | 39 | To lint the SASS/SCSS files in our project you can use the `stylelint` npm package. 40 | 41 | `npm install stylelint stylelint-config-standard-scss` 42 | 43 | Add to the project the [linter configuration file](../templates/.stylelintrc.yml) and check the [`bin/fastcheck` 44 | template](../templates/bin/fastcheck) to see the command to execute the SCSS linting. 45 | 46 | ## Erb lint 47 | 48 | ```ruby 49 | group :development, :test do 50 | gem 'erb_lint', require: false 51 | end 52 | ``` 53 | 54 | ## ESLint 55 | 56 | ``` 57 | npm install eslint 58 | npx eslint --init (Use a popular style guide -> Airbnb) 59 | ``` 60 | 61 | then extend the `bin/fastcheck` script with: 62 | 63 | ``` 64 | yarn eslint app/javascript 65 | ``` 66 | 67 | The templates folder contains a template for the eslint configuration. 68 | 69 | ## All Good! 70 | 71 | Now your `bin/fastcheck` is not that fast anymore :smile: 72 | -------------------------------------------------------------------------------- /ruby_on_rails/newrelic.md: -------------------------------------------------------------------------------- 1 | # NewRelic 2 | 3 | NewRelic is a service to monitor app performance. 4 | 5 | * Add the following gem to your Gemfile: 6 | 7 | ```ruby 8 | group :production do 9 | gem 'newrelic_rpm' 10 | end 11 | ``` 12 | 13 | * Add a NewRelic configuration file [`config/newrelic.yml`](../templates/config/newrelic.yml) folder. 14 | (**Note:** If you are not using Heroku, adjust the app name to something else than `HEROKU_APP_NAME`) 15 | 16 | * Add the new variables to your Heroku environments and `.env`: 17 | 18 | ```yml 19 | NEW_RELIC_LICENSE_KEY: "from newrelic" 20 | ``` 21 | -------------------------------------------------------------------------------- /ruby_on_rails/recaptcha.md: -------------------------------------------------------------------------------- 1 | # reCAPTCHA v3 2 | 3 | Here you will learn how to implement reCAPTCHA v3 into a project. It is pretty simple to use and doesn't 4 | hinder the users at all, as it all runs in the background. 5 | 6 | ## Implementation 7 | 8 | Before you start, consider checking which page you would like to protect against suspicious activity (like spambots). 9 | 10 | ### Site registration 11 | 12 | After you've decided, you can go to the [reCAPTCHA admin panel](https://www.google.com/recaptcha/admin) and register 13 | a new site. Make sure you're using the Renuo admin account. 14 | 15 | To register the site, you can follow these steps: 16 | 1. Use `[project-name]` as a label for the site. 17 | 1. Choose reCAPTCHA v3 18 | 1. Add all the domains on which reCAPTCHA should work (`renuoapp.ch` should also be included for the `develop` environment) 19 | 20 | After you've provided all the necessary data, you will be forwarded to a page where you will find the `site` and 21 | `secret` keys. These will be needed later on (you can always find them in the site settings). 22 | 23 | ### Frontend implementation 24 | 25 | For the frontend implementation, you can follow [these steps](https://developers.google.com/recaptcha/docs/v3) 26 | and integrate reCAPTCHA into the chosen view. 27 | 28 | ### Backend implementation 29 | 30 | For the validation to work, you will also need some backend code which will call the reCAPTCHA verification API, 31 | which then returns a score based on which it will be decided if the request (to your site) was normal or suspicious. You can follow [these steps](https://developers.google.com/recaptcha/docs/verify) to do it. 32 | 33 | If you have trouble integrating reCAPTCHA, you can find some inspiration by checking out one of these pull requests: 34 | 1. [Kingschair](https://github.com/renuo/kingschair2/pull/182) 35 | 1. [Germann](https://github.com/renuo/germann/pull/314) 36 | 37 | ## Testing 38 | 39 | In order to test it, you can add `localhost` as a domain in the 40 | [reCAPTCHA admin panel](https://www.google.com/recaptcha/admin) and provide the required keys in 41 | the `.env`. If your integration worked, then you will see a small container in the bottom right 42 | corner of your chosen page, something like this: 43 | ![reCAPTCHA demonstration](https://user-images.githubusercontent.com/31915276/55479133-ae4f8c80-561d-11e9-9df2-d94cbc5f92d3.png) 44 | -------------------------------------------------------------------------------- /ruby_on_rails/robots_txt.md: -------------------------------------------------------------------------------- 1 | # robots.txt 2 | 3 | It is time to configure the `robots.txt` file properly, to avoid crawlers to find our develop environments. 4 | The main environment should be the only one searchable in the end. 5 | 6 | Make sure that there is a `robots.txt` file in the public folder or your project (Rails should have created it). 7 | This file will only be used in environments where the `BLOCK_ROBOTS` environment variable is not set. 8 | If it is set then a custom middleware catches calls to `/robots.txt` 9 | 10 | Add the following gem: 11 | 12 | ```ruby 13 | group :production do 14 | gem "norobots" 15 | end 16 | ``` 17 | 18 | ## Setting Environment Variables 19 | 20 | ### Heroku 21 | 22 | Add the environment variable `BLOCK_ROBOTS=true` in your develop environment: 23 | 24 | ```sh 25 | heroku config:add BLOCK_ROBOTS=true --app [project-name]-develop 26 | ``` 27 | 28 | ### Deploio 29 | 30 | Set the environment variable using: 31 | 32 | ```sh 33 | nctl update app [app-name] --env=BLOCK_ROBOTS=true --project=[project-name] 34 | ``` 35 | -------------------------------------------------------------------------------- /ruby_on_rails/rspec.md: -------------------------------------------------------------------------------- 1 | # Setup RSpec 2 | 3 | Even though Rails uses Minitest per default, RSpec is the *de-facto* standard at Renuo. 4 | 5 | Add the following gems to your Gemfile: 6 | 7 | ```ruby 8 | group :development, :test do 9 | gem "factory_bot_rails" 10 | gem "rspec-rails" 11 | gem "parallel_tests" 12 | end 13 | 14 | group :test do 15 | gem "shoulda-matchers" 16 | gem "simplecov", require: false 17 | gem "super_diff" 18 | end 19 | ``` 20 | 21 | You should know exactly why you are adding each one of them and why is necessary. 22 | 23 | * Also add `/coverage/` to your `.gitignore` file. 24 | * Remove the `test` folder from your project (there will be one called `spec` later). 25 | 26 | ## Configuration 27 | 28 | * Install rspec via `rails generate rspec:install` 29 | * Create a bin stub with `bundle binstubs rspec-core` 30 | * At the top of the `spec/spec_helper.rb` 31 | 32 | ```ruby 33 | require 'simplecov' 34 | SimpleCov.start "rails" do 35 | add_filter "app/channels/application_cable/channel.rb" 36 | add_filter "app/channels/application_cable/connection.rb" 37 | add_filter "app/jobs/application_job.rb" 38 | add_filter "app/mailers/application_mailer.rb" 39 | add_filter "app/models/application_record.rb" 40 | add_filter ".semaphore-cache" 41 | enable_coverage :branch 42 | minimum_coverage line: 100, branch: 100 43 | end 44 | ``` 45 | 46 | to run code coverage and exclude files with less then 5 lines of code. 47 | 48 | * Inside `spec/spec_helper.rb` we suggest you to uncomment/enable the following: 49 | 50 | ```ruby 51 | config.disable_monkey_patching! 52 | config.default_formatter = 'doc' if config.files_to_run.one? 53 | config.profile_examples = 5 54 | config.order = :random 55 | Kernel.srand config.seed 56 | 57 | config.define_derived_metadata do |meta| 58 | meta[:aggregate_failures] = true 59 | end 60 | ``` 61 | 62 | Please check the [spec_helper template](../templates/spec/spec_helper.rb) 63 | 64 | * Add the configurations: 65 | 66 | ```rb 67 | # spec/rails_helper.rb: 68 | 69 | # after `require 'rspec/rails'` 70 | require 'capybara/rspec' 71 | require 'capybara/rails' 72 | require 'selenium/webdriver' 73 | require 'super_diff/rspec-rails' 74 | 75 | Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 76 | 77 | # ... (omitted configs here) 78 | 79 | RSpec.configure do |config| 80 | # ... (omitted configs here) 81 | 82 | config.before do |example| 83 | ActionMailer::Base.deliveries.clear 84 | I18n.locale = I18n.default_locale 85 | Rails.logger.debug { "--- #{example.location} ---" } 86 | end 87 | 88 | config.after do |example| 89 | Rails.logger.debug { "--- #{example.location} FINISHED ---" } 90 | end 91 | 92 | config.before(:each, type: :system) do 93 | driven_by :rack_test 94 | end 95 | 96 | config.before(:each, type: :system, js: true) do 97 | driven_by ENV['SELENIUM_DRIVER']&.to_sym || :selenium_chrome_headless 98 | Capybara.page.current_window.resize_to(1280, 800) 99 | end 100 | end 101 | ``` 102 | 103 | ```yml 104 | # .env.example 105 | # SELENIUM_DRIVER="selenium_chrome" 106 | SELENIUM_DRIVER="selenium_chrome_headless" 107 | ``` 108 | 109 | ```rb 110 | # config/environments/development.rb 111 | 112 | config.generators do |g| 113 | g.test_framework :rspec 114 | end 115 | 116 | ``` 117 | 118 | Please check the full [rails_helper template](../templates/spec/rails_helper.rb) to compare. 119 | 120 | * Add the line `bundle exec parallel_rspec` to `bin/check` 121 | 122 | > **Note**: If you want to debug a spec, you can simply uncomment the line `SELENIUM_DRIVER` in the .env to not run it headless: 123 | 124 | ![CleanShot 2021-06-25 at 16 54 22](https://user-images.githubusercontent.com/1319150/123443347-1bbcae80-d5d6-11eb-8ba5-0d2c9ae4a37c.gif) 125 | 126 | ## :white_check_mark: Our first (green) test 127 | 128 | We are now going to write a first test to ensure that the whole configuration is working: 129 | 130 | * `bin/check` should be green :white_check_mark: 131 | * Write the test [`spec/system/health_spec.rb`](../templates/spec/system/health_spec.rb) 132 | * Run `bin/check` and the test should pass and coverage is 100%. 133 | 134 | Commit and push your changes! :tada: 135 | 136 | > ⭐️ The default health check path for Rails is `/up`. [Learn more in the Rails guides](https://edgeapi.rubyonrails.org/classes/Rails/HealthController.html).\ 137 | > To customize the health check and add additional checks, you can override the `Rails::HealthController` class.\ 138 | > You can find an example that also checks the database connection [in this file](../templates/app/controllers/rails/health_controller.rb). 139 | 140 | ## Verify 141 | 142 | Check that you see a green page in each app. 143 | 144 | ### Heroku 145 | 146 | * Open the two apps 147 | * 148 | * 149 | 150 | ### Deploio 151 | 152 | The host name contains a generated hash. The name can be accessed via: 153 | 154 | ```sh 155 | nctl get applications --project={PROJECT_NAME} 156 | ``` 157 | 158 | ## Javascript error reporter 159 | 160 | * Create the module [`spec/support/javascript_error_reporter.rb`](../templates/spec/support/javascript_error_reporter.rb) 161 | 162 | * Verify that `config.include JavaScriptErrorReporter, type: :system, js: true` is in your [`rails_helper.rb`](../templates/spec/rails_helper.rb) 163 | 164 | Please check the [rails_helper template](../templates/spec/rails_helper.rb) to compare. 165 | -------------------------------------------------------------------------------- /ruby_on_rails/send_emails.md: -------------------------------------------------------------------------------- 1 | # :mailbox: Send Emails 2 | 3 | As soon as you have to send emails please follow those suggestions. 4 | They will help you having a proper system to deliver emails and development environment. 5 | 6 | ## Configuration 7 | 8 | * Add the following to your Gemfile and `bundle install` 9 | 10 | ```ruby 11 | group :development do 12 | gem 'letter_opener' 13 | end 14 | ``` 15 | 16 | * add the following to `.env.example` 17 | 18 | ```yml 19 | APP_HOST="[project-name].localhost" 20 | APP_PORT="3000" 21 | MAIL_SENDER="yourname+@example.com" 22 | MAIL_HOST="" 23 | MAIL_USERNAME="" 24 | MAIL_PASSWORD="" 25 | ``` 26 | 27 | * update `app/mailers/application_mailer.rb` 28 | 29 | ```ruby 30 | class ApplicationMailer < ActionMailer::Base 31 | default from: ENV.fetch('MAIL_SENDER') # <-- change this 32 | layout 'mailer' 33 | end 34 | ``` 35 | 36 | * add the following to `config/application.rb` 37 | 38 | ```ruby 39 | config.action_mailer.default_url_options = { host: ENV.fetch('APP_HOST'), port: ENV.fetch('APP_PORT') } 40 | ``` 41 | 42 | * add the following to `config/environments/development.rb`: 43 | 44 | ```ruby 45 | config.action_mailer.delivery_method = :letter_opener 46 | ``` 47 | 48 | * add the following `config/environments/production.rb`: 49 | 50 | ```ruby 51 | config.action_mailer.smtp_settings = { 52 | address: ENV.fetch('MAIL_HOST'), 53 | port: 587, 54 | enable_starttls_auto: true, 55 | user_name: ENV.fetch('MAIL_USERNAME'), 56 | password: ENV.fetch('MAIL_PASSWORD'), 57 | authentication: 'login', 58 | domain: ENV.fetch('APP_HOST') 59 | } 60 | ``` 61 | 62 | ## Sparkpost & Mailtrap 63 | 64 | Follow the [sparkpost](../sparkpost_and_mailtrap.md) section to configure Sparkpost and Mailtrap on your production environment 65 | -------------------------------------------------------------------------------- /ruby_on_rails/sentry.md: -------------------------------------------------------------------------------- 1 | # Sentry 2 | 3 | ## General configuration 4 | 5 | * Go to https://www.sentry.io and login as the renuo monitor user. 6 | 7 | * Create a project named `[project-name]`. 8 | 9 | * Add the project to the *#renuo* team if the client pays for monitoring, to the `#no-notifications` otherwise. 10 | 11 | * Note the DSN key. 12 | 13 | ![sentry_dsn](../images/sentry.png) 14 | 15 | Add the ENV variables to the `.env` files for each environment. 16 | 17 | Use the **same** `SENTRY_DSN` across all environments, but set a different `SENTRY_ENVIRONMENT` in each environment (e.g. `main`, `develop`). 18 | 19 | This allows all errors to be tracked in a single Sentry project while still being grouped by environment. 20 | 21 | The project's Sentry issues can be monitored on the [Renuo Dashboard](https://dashboard.renuo.ch/redmine_projects). To configure this, ensure that the `dash` attribute for the project on Redmine is equal to the Sentry project name. 22 | 23 | The different environments will be automatically detected, and you can monitor and view the Sentry issues from one place. 24 | 25 | ## Backend (Rails) 26 | 27 | * Add sentry gems to the project: 28 | 29 | ```ruby 30 | group :production do 31 | gem "sentry-rails" 32 | gem "sentry-ruby" 33 | gem "sentry-sidekiq" # If the project uses Sidekiq for background jobs 34 | end 35 | ``` 36 | 37 | * Add a Sentry initializer to your project [`config/initializers/sentry.rb`](../templates/config/initializers/sentry.rb). 38 | * Add the following to your `.env.example` file: 39 | ```bash 40 | # .env.example 41 | # SENTRY_DSN="find_me_on_password_manager" 42 | # SENTRY_ENVIRONMENT="local" 43 | # CSP_REPORT_URI="" 44 | ``` 45 | 46 | * Enable CSP Reporting to Sentry in `config/initializers/content_security_policy.rb` and allow unsafe inline JS: 47 | 48 | ```ruby 49 | Rails.application.config.content_security_policy do |policy| 50 | ... 51 | policy.report_uri ENV['CSP_REPORT_URI'] if ENV['CSP_REPORT_URI'] 52 | end 53 | ``` 54 | 55 | You can find the correct value in `Sentry -> Project Settings -> Security Headers -> REPORT URI`. Add the environment to the `CSP_REPORT_URI` using `&sentry_environment=main`. 56 | 57 | ## Frontend (Javascript) 58 | 59 | * Install the npm package: `yarn add @sentry/browser` 60 | * Include [_sentry.html](../templates/app/views/shared/_sentry.html.erb) in your header. 61 | * Include [sentry.js](../templates/app/javascript/sentry.js) in your JS assets. 62 | 63 | ## Verify the installation 64 | 65 | ### Ruby 66 | 67 | For each Heroku app, connect to the `heroku run rails console --app [project-name]-[branch-name]` and raise an exception using Sentry: 68 | 69 | ``` 70 | begin 71 | 1 / 0 72 | rescue ZeroDivisionError => exception 73 | Sentry.capture_exception(exception) 74 | end 75 | ``` 76 | 77 | On `https://sentry.io/renuo/[project-name]` you should find the exception of the ZeroDivisionError. 78 | 79 | ### Javascript 80 | 81 | Open the dev console in chrome, and run 82 | 83 | ```js 84 | try { 85 | throw new Error('test sentry js'); 86 | } catch(e) { 87 | Sentry.captureException(e) 88 | } 89 | ``` 90 | 91 | On `https://sentry.io/renuo/[project-name]` you should find "Uncaught Error: test sentry js". 92 | -------------------------------------------------------------------------------- /ruby_on_rails/sidekiq.md: -------------------------------------------------------------------------------- 1 | # Sidekiq 2 | 3 | As soon as you have to do work with background jobs please follow those suggestions. 4 | They will help you having a proper system to do background work. 5 | We use [sidekiq](https://github.com/mperham/sidekiq) because it works well on Heroku and is easy to setup (suckerpunch for example causes memory issues on Heroku). 6 | 7 | * Add the following gem `gem 'sidekiq'` to the `production` block and install it 8 | 9 | ```sh 10 | bundle install 11 | ``` 12 | 13 | * Update the `Procfile` and set the `worker` line, so it looks like this: 14 | 15 | ```rb 16 | web: bundle exec puma -C config/puma.rb 17 | worker: bundle exec sidekiq -C config/sidekiq.yml 18 | ``` 19 | 20 | * Configure the active job adapter in `config/environments/production.rb` 21 | 22 | ```rb 23 | config.active_job.queue_adapter = :sidekiq 24 | ``` 25 | 26 | * Configure the active job adapter in `config/environments/development.rb` 27 | 28 | ```rb 29 | config.active_job.queue_adapter = :async 30 | ``` 31 | 32 | * Configure the active job adapter in `config/environments/test.rb` 33 | 34 | ```rb 35 | config.active_job.queue_adapter = :test 36 | ``` 37 | 38 | * Create a file `config/sidekiq.yml`, there you can setup your options for sidekiq. 39 | It could look something like this. 40 | You may need to inform yourself about what 41 | [queue configuration](https://github.com/mperham/sidekiq/wiki/Advanced-Options#queues) 42 | is right for your project. 43 | 44 | ```yml 45 | :concurrency: <%= ENV["SIDEKIQ_CONCURRENCY"] || 5 %> 46 | :queues: 47 | - [mailers, 1] 48 | - [default, 1] 49 | ``` 50 | 51 | Note: if you are on the free Redis plan 52 | you may need to limit your concurrency to not exceed the connection limit. 53 | 54 | * On Heroku you need to: 55 | * Add the Heroku [Redis addon](https://elements.heroku.com/addons/heroku-redis) 56 | * And to start it turn on the worker 57 | 58 | * To run Sidekiq locally you need to: 59 | * Temporarily switch the adapter in `config/environments/development.rb` to 60 | ```rb 61 | config.active_job.queue_adapter = :sidekiq 62 | ``` 63 | * Install Redis if not yet installed `brew install redis` 64 | * Start Redis `redis-server` 65 | * Start Sidekiq `bundle exec sidekiq -C config/sidekiq.yml` 66 | * Start your server 67 | 68 | ## Sidekiq-Cron 69 | 70 | If you need finer graded control about your scheduled jobs than the Heroku scheduler 71 | provides you, you can use [Sidekiq-Cron](https://github.com/ondrejbartas/sidekiq-cron). 72 | This way you can for example schedule a job every minute. 73 | 74 | * Add the following gem `gem 'sidekiq-cron'` and install it 75 | 76 | ```sh 77 | bundle install 78 | ``` 79 | 80 | * update/create `config/initializers/sidekiq.rb` and add the following lines: 81 | 82 | ```rb 83 | if defined? Sidekiq 84 | schedule_file = 'config/schedule.yml' 85 | 86 | if File.exist?(schedule_file) && Sidekiq.server? 87 | errors = Sidekiq::Cron::Job.load_from_hash!(YAML.load_file(schedule_file)) 88 | Rails.logger.error "Errors loading scheduled jobs: #{errors}" if errors.any? 89 | end 90 | end 91 | ``` 92 | 93 | * create file `config/schedule.yml`. There you can specify your jobs. 94 | A simple job which runs every 5 minutes could look something like this. 95 | (For more options, see the [Readme](https://github.com/ondrejbartas/sidekiq-cron/blob/master/README.md) of the gem) 96 | 97 | ```yml 98 | MyExampleJob: 99 | cron: "*/5 * * * *" 100 | class: "MyJob" 101 | queue: default 102 | ``` 103 | 104 | ## Sidekiq monitoring 105 | 106 | If you want to provide a sidekiq dashboard and see which tasks failed or run through, you can use the [Sidekiq monitoring](https://github.com/mperham/sidekiq/wiki/Monitoring): 107 | 108 | * Update/Create `config/initializers/sidekiq.rb` and add the following lines: 109 | 110 | ```rb 111 | if defined? Sidekiq 112 | require 'sidekiq/web' 113 | 114 | Sidekiq::Web.use(Rack::Auth::Basic) do |user, password| 115 | [user, password] == [ENV['SIDEKIQ_USER'], ENV['SIDEKIQ_PASSWORD']] 116 | end 117 | end 118 | ``` 119 | 120 | * Add `SIDEKIQ_USER` and `SIDEKIQ_PASSWORD` as the credentials to the dashboard to your `application.example.yml`. 121 | * Add the following line inside `routes.rb`: 122 | 123 | ```rb 124 | Rails.application.routes.draw do 125 | ... 126 | mount Sidekiq::Web => '/sidekiq' if defined? Sidekiq::Web 127 | ... 128 | ``` 129 | 130 | ### Error monitoring 131 | 132 | In order to report messages, exceptions or to trace events, it is recommended to install the `sentry-sidekiq` gem. 133 | -------------------------------------------------------------------------------- /ruby_on_rails/suggested_libraries.md: -------------------------------------------------------------------------------- 1 | # Suggested gems 2 | 3 | Here is an hopefully up-to-date version of gems which we strongly suggest to include in your project. 4 | Please include them or find a good reason not to. 5 | 6 | > :exclamation: Please follow the guide of each of these libraries to know how to properly install them. 7 | 8 | > **:bulb:** Do you know all of them? Do you know why we'd like them to be included? 9 | 10 | ```rb 11 | gem 'simple_form' 12 | 13 | group :development do 14 | gem 'better_errors' 15 | gem 'binding_of_caller' 16 | end 17 | 18 | group :development, :test do 19 | gem 'awesome_print' 20 | gem 'bullet' 21 | end 22 | 23 | group :production do 24 | gem 'lograge' 25 | end 26 | ``` 27 | 28 | > **:bulb:** Note that to install `simple_form` you need to run `rails generate simple_form:install --bootstrap` (without option if not using Bootstrap) after adding it to your Gemfile. 29 | -------------------------------------------------------------------------------- /ruby_on_rails/template.rb: -------------------------------------------------------------------------------- 1 | # replace rubocop-rails-omakase with renuocop 2 | 3 | gsub_file "Gemfile", /gem "rubocop-rails-omakase"/, "gem \"renuocop\"" 4 | 5 | insert_into_file "Gemfile", after: /^group :development do\n/ do 6 | <<~RUBY 7 | gem "renuo-cli", require: false 8 | RUBY 9 | end 10 | 11 | # replace bin/rails db:prepare with bin/rails db:setup in bin/setup 12 | gsub_file "bin/setup", "bin/rails db:prepare", "bin/rails db:setup" 13 | 14 | # add the renuo fetch-secrets command in bin/setup, before bin/rails db:setup 15 | insert_into_file "bin/setup", before: "\n puts \"\\n== Preparing database ==\"" do 16 | <<-RUBY 17 | puts "\\n== Fetching 1password dependencies ==" 18 | system! 'renuo fetch-secrets' 19 | RUBY 20 | end 21 | 22 | # remove the block unless ARGV.include?("--skip-server"). We don't want to start the server directly during setup. 23 | gsub_file "bin/setup", /\n{0,2}[ \t]*unless ARGV.include\?\("--skip-server"\).*?end/m, "" 24 | 25 | # add "ruby file: ".ruby-version" to the Gemfile under the line starting with "source" 26 | insert_into_file "Gemfile", after: /^source.*\n/ do 27 | <<~RUBY 28 | ruby file: ".ruby-version" 29 | RUBY 30 | end 31 | 32 | # override the content of .rubocop.yml 33 | create_file ".rubocop.yml", force: true do 34 | <<~RUBOCOP 35 | inherit_gem: 36 | renuocop: config/base.yml 37 | RUBOCOP 38 | end 39 | 40 | create_file "bin/run", force: true do 41 | <<~RUN 42 | #!/usr/bin/env bash 43 | set -euo pipefail 44 | 45 | rails s 46 | RUN 47 | end 48 | run "chmod +x bin/run" 49 | 50 | create_file "bin/check", force: true do 51 | <<~CHECK 52 | #!/usr/bin/env bash 53 | set -euo pipefail 54 | 55 | bin/rails zeitwerk:check 56 | CHECK 57 | end 58 | run "chmod +x bin/check" 59 | 60 | create_file "bin/fastcheck", force: true do 61 | <<~FASTCHECK 62 | #!/usr/bin/env bash 63 | set -euo pipefail 64 | 65 | if ! bundle exec rubocop -D -c .rubocop.yml --fail-fast 66 | then 67 | echo 'rubocop detected issues!' 68 | bundle exec rubocop -A -D -c .rubocop.yml 69 | echo 'Tried to auto correct the issues, but must be reviewed manually, commit aborted' 70 | exit 1 71 | fi 72 | FASTCHECK 73 | end 74 | run "chmod +x bin/fastcheck" 75 | 76 | after_bundle do 77 | run "bundle exec rubocop -A" 78 | 79 | puts "We want to be sure that you are aware of what has been done by the Renuo template." 80 | puts "Please read the following list of changes and confirm that you understand them." 81 | ask "We have included renuo fetch-secrets in bin/setup. Do you know why?" 82 | ask "The Gemfile ruby version has been set to the version in the .ruby-version file. You know what this means. Ok?" 83 | ask "Your project is now using renuocop instead of rubocop-rails-omakase. You know both gems and why it has been replaced. Ok?" 84 | ask "A .rubocop.yml file has been created with the default configuration. You know what this means. Ok?" 85 | ask "A bin/run script has been created. You know why. Ok?" 86 | ask "A bin/check script has been created. You know why and what the commands in it do. Ok?" 87 | ask "A bin/fastcheck script has been created to run rubocop with the default configuration." 88 | ask "You can run the script with `bin/fastcheck`. You know why we have a bin/fastcheck file. Ok?" 89 | end 90 | -------------------------------------------------------------------------------- /ruby_on_rails/uptimerobot.md: -------------------------------------------------------------------------------- 1 | # Uptimerobot Monitoring 2 | 3 | To ensure that our application is always up and running, we offer a monitoring 4 | service to the customers. 5 | 6 | When we are still developing a new application, the uptimerobot check should not be 7 | setup to avoid premature costs. Once the go-live date is very close, we enable 8 | the monitoring only for the `main` environment, which *must* have a paid 9 | dyno. 10 | 11 | ## Setup 12 | 13 | You will need Renuo-CLI to be set up and at the newest version: 14 | `gem install renuo-cli` --> see [renuo-cli](https://github.com/renuo/renuo-cli) 15 | 16 | 1. Run the command `renuo setup-uptimerobot [url]` 17 | * Where `url` is the address you want to monitor. e.g. `https://[project-name]-main.renuoapp.ch/up` or `https://customdomain.ch/up` 18 | 19 | 1. The app will ask for the `api-key` for uptimerobot. It can be found at the companies' password manager. 20 | Paste it and press enter to continue. 21 | 22 | The command will setup the project in a paused state. You can start it once your app has a paid dyno. 23 | 24 | **Until then do not start the monitoring.** 25 | 26 | ## Examples 27 | 28 | * `renuo setup-uptimerobot https://germann.ch/up` 29 | 30 | ## Replacing monitors 31 | 32 | It's cumbersome to exchange a monitor for all projects. 33 | You can utilize this script for that: 34 | 35 | ```rb 36 | require "json" 37 | require "open3" 38 | 39 | API_KEY = "XXX" 40 | OFFSET = 0 # default page size is 50 41 | 42 | # Use Open3 to capture stdout and stderr to avoid shell-specific parsing issues 43 | monitors_response, stderr, status = Open3.capture3("curl -X POST -H \"Content-Type: application/x-www-form-urlencoded\" -H \"Cache-Control: no-cache\" -d 'api_key=#{API_KEY}&format=json&alert_contacts=1&offset=#{OFFSET}' \"https://api.uptimerobot.com/v2/getMonitors\" | jq -c '[.monitors[] | {id: .id, alert_contacts: .alert_contacts}]'") 44 | 45 | unless status.success? 46 | puts "Error executing command: #{stderr}" 47 | exit 48 | end 49 | 50 | monitors = JSON.parse(monitors_response) 51 | monitors.each do |monitor| 52 | id = monitor["id"] 53 | 54 | # Add your new monitor here so that it is added to all projects 55 | monitor["alert_contacts"] << { "id" => raise("TODO: replace this"), "threshold" => 0, "recurrence" => 0 } 56 | 57 | alert_contacts = monitor["alert_contacts"].map { |contact| 58 | contact.values_at("id", "threshold", "recurrence").compact.join("_") 59 | }.join("-") 60 | 61 | cmd = %(curl -X POST -H "Cache-Control: no-cache" -H "Content-Type: application/x-www-form-urlencoded" -d 'api_key=#{API_KEY}&format=json&id=#{id}&alert_contacts=#{alert_contacts}' "https://api.uptimerobot.com/v2/editMonitor") 62 | puts cmd 63 | end 64 | ``` 65 | -------------------------------------------------------------------------------- /ruby_on_rails/vcr.md: -------------------------------------------------------------------------------- 1 | # VCR 2 | 3 | VCR is a gem for recording HTTP requests to external services and replaying them. 4 | A VCR setup should be very specifically tied to the tests which need VCR because it's quite expensive. 5 | 6 | Here's an example configuration we use at Renuo: 7 | 8 | ```rb 9 | # spec/rails_helper.rb 10 | 11 | RSpec.configure do |config| 12 | # … 13 | 14 | # Disable VCR completely for tests that are not tagged with :vcr 15 | config.around do |example| 16 | if example.metadata[:vcr] 17 | example.run 18 | else 19 | VCR.turned_off { example.run } 20 | end 21 | end 22 | end 23 | 24 | # WebMock catches everything per default, we allow localhost for Capybara/Selenium 25 | WebMock.disable_net_connect!(allow_localhost: true) 26 | 27 | VCR.configure do |c| 28 | c.configure_rspec_metadata! 29 | c.debug_logger = $stderr if ENV['DEBUG'] == 'true' 30 | 31 | c.hook_into :webmock 32 | c.ignore_localhost = true 33 | 34 | c.cassette_library_dir = 'spec/vcr' 35 | c.default_cassette_options = { 36 | decode_compressed_response: true, 37 | allow_unused_http_interactions: false, 38 | record: ENV['VCR'] ? ENV['VCR'].to_sym : :once, # re-record with VCR=all 39 | drop_unused_requests: true # only when re-recording 40 | } 41 | 42 | # Filter out sensitive data from the cassettes 43 | env_keys = Dotenv.parse(".env.example").keys 44 | env_keys.each { |key| c.filter_sensitive_data("<#{key}>") { ENV.fetch(key, nil) } } 45 | end 46 | ``` 47 | 48 | Some considerations: 49 | * Do you really want/need VCR? A fake may be better: https://thoughtbot.com/blog/how-to-stub-external-services-in-tests#create-a-fake-hello-sinatra 50 | * Do you test so specifically that WebMock would be the better tool? 51 | * Does your project have special needs for tweaks: https://blog.arkency.com/3-tips-to-tune-your-vcr-in-tests/ 52 | * How often do you want to re-record your cassettes? Out-of-date replays may give you a false sense of safety. 53 | * Where do you want to re-record your cassettes? Maybe nightly on the CI? 54 | 55 | -------------------------------------------------------------------------------- /ruby_on_rails/wallee.md: -------------------------------------------------------------------------------- 1 | # Wallee 2 | 3 | ## Payment Page 4 | 5 | ### Setup Wallee 6 | 7 | 1. Visit [Wallee](https://app-wallee.com/). 8 | 2. Go to `Account` -> `Application User`, create a new user and store the credentials. 9 | 3. Go to `Space` -> `Settings` -> `Payment` and setup a new processor. Choose `Bogus Processor` for testing. 10 | 11 | ### Configure Rails 12 | 13 | 1. Use the [`wallee-ruby-sdk`](https://github.com/wallee-payment/ruby-sdk) gem to interact with the Wallee API. 14 | 15 | ```ruby 16 | # Gemfile 17 | 18 | gem 'wallee-ruby-sdk' 19 | ``` 20 | 21 | 2. Configure the credentials 22 | 23 | ```ruby 24 | # config/initializers/wallee.rb 25 | 26 | Wallee.configure do |config| 27 | config.user_id = ENV.fetch("WALLEE_APP_USER_ID") 28 | config.authentication_key = ENV.fetch("WALLEE_APP_AUTHENTICATION_KEY") 29 | end 30 | ``` 31 | 32 | 3. Add a tool class to create a payment page URL. 33 | Be aware that this is very simple tooling. 34 | 35 | ```ruby 36 | # app/models/wallee/payment_page.rb 37 | 38 | module Wallee 39 | class PaymentPage 40 | SPACE_ID = ENV.fetch("WALLEE_SPACE_ID") 41 | 42 | # Simplest Payment integration I could think of. 43 | # Attention: Check VAT, shipping address and shipping method 44 | # 45 | # @example 46 | # Wallee::PaymentPage.url_for( 47 | # cart: [ 48 | # {name: "Eintritt Erwachsene", sku: "adult_entry", price: 30, quantity: 2} 49 | # ], 50 | # address: { 51 | # city: "Zürich", 52 | # country: "CH", 53 | # email_address: "customer@example.com", 54 | # family_name: "Muster", 55 | # given_name: "Hans", 56 | # postcode: "8000", 57 | # street: "Musterstrasse 1" 58 | # } 59 | # ) do |transaction| 60 | # # Save transaction.id to the order 61 | # end 62 | def self.url_for(cart:, address:, reference:, success_url: nil, failed_url: nil, &) 63 | raise "Cart is empty" if cart.empty? 64 | raise "Bad cart format" unless cart.all? { |item| item.keys == [:name, :sku, :price, :quantity] } 65 | 66 | line_items = cart.map.with_index do |item, index| 67 | Wallee::LineItemCreate.new({ 68 | amountIncludingTax: item[:price] * item[:quantity], 69 | name: item[:name], 70 | quantity: item[:quantity], 71 | shippingRequired: true, 72 | sku: item[:sku], 73 | taxes: [Wallee::TaxCreate.new({rate: 0, title: "VAT"})], 74 | type: Wallee::LineItemType::PRODUCT, 75 | uniqueId: "#{item[:sku]}-#{index}" 76 | }) 77 | end 78 | 79 | billing_address = shipping_address = Wallee::AddressCreate.new( 80 | city: address[:city], 81 | country: address[:country], 82 | emailAddress: address[:email_address], 83 | familyName: address[:family_name], 84 | givenName: address[:given_name], 85 | postcode: address[:postcode], 86 | street: address[:street] 87 | ) 88 | 89 | transaction_create = Wallee::TransactionCreate.new({ 90 | billingAddress: billing_address, 91 | currency: "CHF", 92 | customerPresence: Wallee::CustomersPresence::VIRTUAL_PRESENT, 93 | failedUrl: failed_url, 94 | language: "de_CH", 95 | lineItems: line_items, 96 | merchantReference: reference, 97 | shippingAddress: shipping_address, 98 | shippingMethod: "Brief A-Post", 99 | successUrl: success_url 100 | }) 101 | 102 | transaction_service = Wallee::TransactionService.new 103 | transaction = transaction_service.create(SPACE_ID, transaction_create) 104 | Rails.logger.info "Wallee transaction created for #{reference}, see #{admin_url(transaction.id)}" 105 | 106 | yield transaction.freeze if block_given? 107 | 108 | transaction_payment_page_service = Wallee::TransactionPaymentPageService.new 109 | transaction_payment_page_service.payment_page_url(SPACE_ID, transaction.id) 110 | end 111 | 112 | def self.admin_url(transaction_id) 113 | "https://app-wallee.com/s/#{SPACE_ID}/payment/transaction/view/#{URI.encode_uri_component transaction_id}" 114 | end 115 | end 116 | end 117 | ``` 118 | 119 | 4. Add the tests 120 | 121 | ```ruby 122 | # spec/models/wallee/payment_page_spec.rb 123 | 124 | require "rails_helper" 125 | 126 | RSpec.describe Wallee::PaymentPage do 127 | describe ".url_for" do 128 | subject(:url_for) { described_class.url_for(cart: cart, address: address, reference: "my-transaction") } 129 | 130 | context "when cart is empty" do 131 | let(:cart) { [] } 132 | let(:address) { double } 133 | 134 | it "raises an error" do 135 | expect { url_for }.to raise_error("Cart is empty") 136 | end 137 | end 138 | 139 | context "when cart is in bad format" do 140 | let(:cart) { [{blub: 30, quantity: 2}] } 141 | let(:address) { double } 142 | 143 | it "raises an error" do 144 | expect { url_for }.to raise_error("Bad cart format") 145 | end 146 | end 147 | 148 | context "when cart is valid", vcr: "wallee/payment_url_success" do 149 | let(:cart) do 150 | [ 151 | {name: "Eintritt Erwachsene", sku: "adult_entry", price: 30, quantity: 2}, 152 | {name: "Eintritt 16-24 J.", sku: "child_6_10_entry", price: 24, quantity: 1}, 153 | {name: "Eintritt 11-15 J.", sku: "child_11_15_entry", price: 18, quantity: 0}, 154 | {name: "Eintritt 3-10 J.", sku: "child_16_24_entry", price: 10, quantity: 1}, 155 | {name: "Gutscheinkarte", sku: "free_coupon", price: 50, quantity: 1} 156 | ] 157 | end 158 | 159 | let(:address) do 160 | { 161 | city: "Zürich", 162 | country: "CH", 163 | email_address: "customer@example.com", 164 | family_name: "Muster", 165 | given_name: "Hans", 166 | postcode: "8000", 167 | street: "Musterstrasse 1" 168 | } 169 | end 170 | 171 | it { is_expected.to match(URI::DEFAULT_PARSER.make_regexp(["https"])) } 172 | 173 | context "when given a block" do 174 | it "yields a transaction" do 175 | expect { |b| described_class.url_for(cart: cart, address: address, reference: "1", &b) }.to yield_with_args(Wallee::Transaction) 176 | end 177 | end 178 | end 179 | end 180 | 181 | describe ".admin_url" do 182 | it "generates an admin url for a transaction" do 183 | expect(described_class.admin_url("1337")).to match(URI::DEFAULT_PARSER.make_regexp(["https"])) 184 | end 185 | 186 | it "escapes transaction id" do 187 | expect(described_class.admin_url("#")).to include("%23") 188 | end 189 | end 190 | end 191 | ``` 192 | 193 | 5. Redirect to the payment page in your controller 194 | 195 | ```ruby 196 | redirect_to Wallee::PaymentPage.url_for(…) do |transaction| 197 | # Save transaction.id to the order or whatever you need 198 | end 199 | ``` 200 | 201 | 6. **You're done.** You can receive money now if you setup a real payment processor. 202 | Customers can receive emails directly from Wallee and you can print shipping labels from the Wallee backend. 203 | 204 | ## Fulfillment via Webhook in Rails 205 | 206 | Often a payment page is not enough. You need to know the payment state in your Rails app. 207 | You can either poll the Wallee API or use a webhook. Here's an example of a webhook controller. 208 | Be aware that this is quite integrated with the business logic of your app, e.g. see the `ShopOrder` model. 209 | 210 | 1. Add the route 211 | 212 | ```ruby 213 | # config/routes.rb 214 | 215 | post "shop/wallee_webhook", to: "shop#wallee_webhook" 216 | ``` 217 | 218 | 2. Add the controller 219 | 220 | ```ruby 221 | # app/controllers/shop_controller.rb 222 | 223 | class ShopController < ApplicationController 224 | def wallee_webhook 225 | request_payload = request.body.read 226 | signature = request.env["HTTP_X_SIGNATURE"] 227 | 228 | if signature.blank? 229 | Rails.logger.info "Signature missing" 230 | head :bad_request and return 231 | end 232 | 233 | webhook_encryption_service = Wallee::WebhookEncryptionService.new 234 | unless webhook_encryption_service.is_content_valid(signature, request_payload) 235 | Rails.logger.info "Webhook signature invalid" 236 | head :bad_request and return 237 | end 238 | 239 | entity_type, entity_id = JSON.parse(request_payload).slice("listenerEntityTechnicalName", "entityId").values 240 | if entity_type == "Transaction" 241 | transaction_service = Wallee::TransactionService.new 242 | transaction = transaction_service.read(ENV.fetch("WALLEE_SPACE_ID"), entity_id) 243 | 244 | order = ShopOrder.find_by!(wallee_transaction_id: transaction.id) 245 | order.update!(wallee_transaction_state: transaction.state) 246 | else 247 | Rails.logger.info "Unknown entity type: #{entity_type}" 248 | head :bad_request 249 | end 250 | end 251 | end 252 | ``` 253 | 254 | 3. Add the tests 255 | 256 | ```ruby 257 | require "rails_helper" 258 | 259 | RSpec.describe ShopController do 260 | describe "#wallee_webhook" do 261 | context "when signature is not valid" do 262 | it "returns http bad request" do 263 | post wallee_webhook_path, as: :json 264 | expect(response).to have_http_status(:bad_request) 265 | end 266 | end 267 | 268 | # Set wallee_transaction_id to something which exists on Wallee before rerecording this cassette 269 | context "when signature is valid", vcr: "wallee/webhook_success" do 270 | before do 271 | allow_any_instance_of(Wallee::WebhookEncryptionService).to receive(:is_content_valid).and_return(true) 272 | end 273 | 274 | let(:wallee_transaction_id) { "194681414" } 275 | 276 | it "goes the 😊 path" do 277 | create(:shop_order, wallee_transaction_id: wallee_transaction_id) 278 | post wallee_webhook_path, 279 | env: {"HTTP_X_SIGNATURE" => "fake"}, 280 | params: {listenerEntityTechnicalName: "Transaction", entityId: wallee_transaction_id}, 281 | as: :json 282 | 283 | expect(response).to have_http_status(:success) 284 | end 285 | end 286 | 287 | describe "order update" do 288 | before do 289 | allow_any_instance_of(Wallee::WebhookEncryptionService).to receive(:is_content_valid).and_return(true) 290 | allow_any_instance_of(Wallee::TransactionService).to receive(:read).and_return(instance_double(Wallee::Transaction, id: "194681414", state: wallee_transaction_state)) 291 | create(:shop_order, wallee_transaction_id: wallee_transaction_id, wallee_transaction_state: "PENDING") 292 | end 293 | 294 | let(:order) { ShopOrder.find_by(wallee_transaction_id: "194681414") } 295 | let(:webhook_request) do 296 | post wallee_webhook_path, 297 | env: {"HTTP_X_SIGNATURE" => "fake"}, 298 | params: {listenerEntityTechnicalName: "Transaction", entityId: "194681414"}, 299 | as: :json 300 | end 301 | 302 | context "when transaction has failed" do 303 | let(:wallee_transaction_state) { Wallee::TransactionState::FAILED } 304 | 305 | it "updates the order" do 306 | expect { webhook_request }.to change { order.reload.wallee_transaction_state }.from("PENDING").to("FAILED") 307 | end 308 | 309 | it "does not fulfill" do 310 | expect(ApplicationMailer).not_to receive(:order_confirmation_mail).and_call_original 311 | webhook_request 312 | end 313 | end 314 | 315 | context "when transaction is complete" do 316 | let(:wallee_transaction_state) { Wallee::TransactionState::FULFILL } 317 | 318 | it "updates the order" do 319 | expect { webhook_request }.to change { order.reload.wallee_transaction_state }.from("PENDING").to("FULFILL") 320 | end 321 | 322 | it "does fulfill" do 323 | expect(ApplicationMailer).to receive(:order_confirmation_mail).and_call_original 324 | webhook_request 325 | end 326 | end 327 | end 328 | end 329 | end 330 | ``` 331 | 332 | 4. … and you're far from done. You need to handle errors, other state changes, and so on. But this is a good start. 333 | 5. Oh, and you need to setup the webhook in the Wallee backend, so that it actually sends requests to your Rails app. 334 | -------------------------------------------------------------------------------- /ruby_on_rails/wicked_pdf.md: -------------------------------------------------------------------------------- 1 | # Wicked PDF 2 | 3 | Can be used to generate PDFs and supports HTML to PDF. 4 | 5 | :warning: Up to now, Wicked PDF does not support Bootstrap 4, so if you want to use Bootstrap 4 Templates, use another library :warning: 6 | 7 | * Add `gem 'wicked_pdf'` to the main section of `Gemfile` 8 | * Add `gem 'wkhtmltopdf-binary'` to `group :development, :test` 9 | * Add `gem 'wkhtmltopdf-heroku'` to `group :production` 10 | 11 | By default, it adds no layout, you you may want to add a layout: 12 | 13 | * Create a `pdf.pdf.erb` 14 | * Use the method to add stylesheets : `wicked_pdf_stylesheet_link_tag 'pdf', media: 'all'` 15 | * Use the method `wicked_pdf_image_tag` to insert images to the layout. 16 | 17 | *Usage:* 18 | 19 | ```rb 20 | render pdf: <>, print_media_type: true, layout: 'pdf', disposition: 'attachment' 21 | ``` 22 | 23 | :warning: Consider using a job-runner to not block the server during creation of PDFs. 24 | 25 | ## Heroku 26 | 27 | Ensure that fonts are installed on Heroku, otherwise the PDF will look different compared to the one generated locally. The fonts need to be installed with a buildpack and put in the `~/.fonts` folder. 28 | If you need Arial, you can add the following buildpack on Heroku: 29 | 30 | `https://github.com/propertybase/heroku-buildpack-fonts` 31 | 32 | :warning: Make sure you add this as a first buildpack before `heroku/ruby`! 33 | -------------------------------------------------------------------------------- /security.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Email contact 4 | 5 | Make it easy for others to let you know about security issues. 6 | Please add a security email contact to the HTML footer or the about page of every app. 7 | Either use `security@` or `security@renuo.ch`. 8 | Incoming issues will be treated with high priority by _wg-operations_. 9 | 10 | There is also a `.well-known/security.txt` file you can provide: 11 | 12 | ```txt 13 | Contact: mailto:security@renuo.ch 14 | Expires: 2050-11-11T13:37:42.000Z 15 | Preferred-Languages: de, en 16 | ``` 17 | 18 | In case your repo is public, it is recommended to add a `SECURITY.md` to your repository's root, docs, or .github folder: 19 | 20 | ``` 21 | # Security Policy 22 | 23 | ## Reporting a Vulnerability 24 | 25 | To report a security vulnerability, please email security@renuo.ch. 26 | ``` 27 | 28 | ## Cipher suite review 29 | 30 | Review your SSL/TLS configuration periodically: 31 | -------------------------------------------------------------------------------- /slack_and_notifications.md: -------------------------------------------------------------------------------- 1 | # Slack and Notifications 2 | 3 | If you want to keep your team up-to-date with some notifications about the project and to discuss project-related topics 4 | we suggest you to use [Slack](https://renuo.slack.com/). 5 | 6 | You are, for sure, already a member of slack. 7 | 8 | ## Project Channel 9 | 10 | You can create a project-dedicated channel on slack naming it `#project-[project-name]` (e.g. `#project-bookshelf`, 11 | `#project-gifcoins`, etc..). 12 | 13 | Invite all team members involved in the project to the channel. 14 | 15 | ## Deploy Notifications 16 | 17 | One notification you may want to receive on this channel is about when a new deployment on main has been performed. In 18 | order to do that you must be an admin of the Renuo Slack Organisation. If you are not an admin, ask wg-operations to do 19 | it for you communicating the `[project-name]`. 20 | 21 | :warning: **You must have already setup [Automatic Deployment through SemaphoreCI](ruby_on_rails/configure_ci.md)** :warning: 22 | If you used Renuo CLI to configure SemaphoreCI, the notifications should be already created. For manual setup, follow these steps: 23 | 24 | 1. Open the project Notifications settings (`https://renuo.semaphoreci.com/notifications`) 25 | 1. Create New Notification 26 | 1. Name of the Notification -> `[project-name]` 27 | 1. Name of the Rule -> `Slack notifications` 28 | 1. Branches -> `main` 29 | 1. Slack Endpoint: Use the Webhook URL from other projects 30 | 1. Send to Slack channel: `#project-[project-name]` 31 | 1. Save changes 32 | 33 | > We do not want to pollute the channel with many notifications, therefore we suggest to only send a notification about 34 | > deployments to production. 35 | -------------------------------------------------------------------------------- /sparkpost_and_mailtrap.md: -------------------------------------------------------------------------------- 1 | # SparkPost & Mailtrap 2 | 3 | ⚠️ Always use subaccounts in Sparkpost! 4 | 5 | > Otherwise there may be compliance issues which can lead to the closing down of the whole Renuo account. 6 | 7 | ## Introduction 8 | 9 | **Main (Sparkpost, sample-app@yourdomain.tld)** 10 | 11 | - Each app is using a separate subaccount under the main account 12 | - The Domain should be set up and verified under the subaccount's sending domains 13 | - Login: sparkpost+main@renuo.ch 14 | 15 | **Develop (Sparkpost, renuoapp.ch)** 16 | 17 | - Each app is using a separate subaccount under the main account 18 | - The emails will be sent with **\*@renuoapp.ch** as your mail sender 19 | - Login: sparkpost+develop@renuo.ch 20 | 21 | - If you want, you can also use Mailtrap for develop. Create a new Inbox and use this credentials 22 | 23 | **Testing (Mailtrap)** 24 | 25 | - Login: operations@renuo.ch 26 | - The Email will be caught by Mailtrap and not forwarded to the intended receiver 27 | - You can login to Mailtrap to see the sent email 28 | 29 | ## Sparkpost 30 | 31 | :warning: 32 | Always use subaccounts for the project, so that the whole account doesn't get suspended / blocked in case of compliance issues! 33 | 34 | 1. Go to and log in with the credentials for 35 | sparkpost+_enviroment_@renuo.ch found in the credential store 36 | 1. Create [one new subaccount](https://app.sparkpost.com/account/subaccounts) and name it like your project 37 | 1. Create [a new API-Key for your subaccount](https://app.sparkpost.com/account/api-keys/create) and assign it to the new subaccount, with the following permissions: *Send via SMTP, Sending Domains: Read/Write* 38 | 1. Write down the API-key in the credential store (in a list under sparkpost+_enviroment_@renuo.ch), because it's only showed once! 39 | 1. Credentials for SMTP setup on your app can be found [here](https://app.sparkpost.com/account/smtp), password is your generated API-key 40 | 1. (if domain is known) Add your sending domain 41 | [here](https://app.sparkpost.com/domains/create?type=sending). Assign it to 42 | the subaccount. Set up SPF, DKIM and DMARC with TXT DNS records (only use 43 | `renuoapp.ch` within the `sparkpost+develop@renuo.ch`) 44 | 1. Verify your Email DNS configuration with 45 | 1. Set up your ENV-variables and test if the mails are working. Manual test emails can be send via the following command in the rails console (production environment): `ActionMailer::Base.mail(to: 'yourname@renuo.ch', from: ENV['MAIL_SENDER'], subject: 'Testmail', body: 'Mail content').deliver_now!` 46 | 1. Send a test email to or and check the result 47 | 48 | For DNS setup also see [Go Live](go_live.md) 49 | 50 | ENV-variables example: 51 | 52 | ``` 53 | MAIL_USERNAME: 'SMTP_Injection' 54 | MAIL_PASSWORD: 'YOUR API KEY' 55 | MAIL_HOST: 'smtp.sparkpostmail.com' 56 | MAIL_SENDER: 'Sample App ' 57 | ``` 58 | 59 | Or with a custom domain: 60 | 61 | ``` 62 | MAIL_SENDER: 'Sample App ' 63 | ``` 64 | 65 | ## Mailtrap 66 | 67 | ENV-variables example: 68 | 69 | ``` 70 | MAIL_USERNAME: 'found in credential store' 71 | MAIL_PASSWORD: 'found in credential store' 72 | MAIL_HOST: 'smtp.mailtrap.io' 73 | MAIL_SENDER: 'Sample App ' 74 | ``` 75 | 76 | Set up your ENV-variables and test if the mails are working. Manual test emails can be send via the following command in the rails console (production environment): `ActionMailer::Base.mail(to: 'yourname@renuo.ch', from: ENV['MAIL_SENDER'], subject: 'Testmail', body: 'Mail content').deliver_now!` 77 | -------------------------------------------------------------------------------- /templates/.coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "level": "error" 4 | }, 5 | "camel_case_classes": { 6 | "level": "error" 7 | }, 8 | "coffeescript_error": { 9 | "level": "error" 10 | }, 11 | "colon_assignment_spacing": { 12 | "level": "error", 13 | "spacing": { 14 | "left": 0, 15 | "right": 1 16 | } 17 | }, 18 | "cyclomatic_complexity": { 19 | "value": 10, 20 | "level": "error" 21 | }, 22 | "duplicate_key": { 23 | "level": "error" 24 | }, 25 | "empty_constructor_needs_parens": { 26 | "level": "error" 27 | }, 28 | "ensure_comprehensions": { 29 | "level": "error" 30 | }, 31 | "indentation": { 32 | "value": 2, 33 | "level": "error" 34 | }, 35 | "line_endings": { 36 | "level": "error", 37 | "value": "unix" 38 | }, 39 | "max_line_length": { 40 | "value": 300, 41 | "level": "error", 42 | "limitComments": true 43 | }, 44 | "missing_fat_arrows": { 45 | "level": "error" 46 | }, 47 | "newlines_after_classes": { 48 | "value": 3, 49 | "level": "error" 50 | }, 51 | "no_backticks": { 52 | "level": "error" 53 | }, 54 | "no_debugger": { 55 | "level": "error" 56 | }, 57 | "no_empty_functions": { 58 | "level": "error" 59 | }, 60 | "no_empty_param_list": { 61 | "level": "error" 62 | }, 63 | "no_implicit_braces": { 64 | "level": "ignore", 65 | "strict": true 66 | }, 67 | "no_implicit_parens": { 68 | "strict": true, 69 | "level": "ignore" 70 | }, 71 | "no_interpolation_in_single_quotes": { 72 | "level": "error" 73 | }, 74 | "no_plusplus": { 75 | "level": "ignore" 76 | }, 77 | "no_stand_alone_at": { 78 | "level": "error" 79 | }, 80 | "no_tabs": { 81 | "level": "error" 82 | }, 83 | "no_throwing_strings": { 84 | "level": "error" 85 | }, 86 | "no_trailing_semicolons": { 87 | "level": "error" 88 | }, 89 | "no_trailing_whitespace": { 90 | "level": "error", 91 | "allowed_in_comments": false, 92 | "allowed_in_empty_lines": true 93 | }, 94 | "no_unnecessary_double_quotes": { 95 | "level": "error" 96 | }, 97 | "no_unnecessary_fat_arrows": { 98 | "level": "error" 99 | }, 100 | "non_empty_constructor_needs_parens": { 101 | "level": "error" 102 | }, 103 | "prefer_english_operator": { 104 | "level": "error", 105 | "doubleNotLevel": "error" 106 | }, 107 | "space_operators": { 108 | "level": "error" 109 | }, 110 | "spacing_after_comma": { 111 | "level": "error" 112 | }, 113 | "transform_messes_up_line_numbers": { 114 | "level": "error" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /templates/.csscomb.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | ".git/**", 4 | "node_modules/**", 5 | "bower_components/**" 6 | ], 7 | 8 | "always-semicolon": true, 9 | "block-indent": " ", 10 | "color-case": "lower", 11 | "color-shorthand": true, 12 | "element-case": "lower", 13 | "eof-newline": true, 14 | "leading-zero": false, 15 | "quotes": "single", 16 | "remove-empty-rulesets": true, 17 | "space-after-colon": " ", 18 | "space-after-combinator": " ", 19 | "space-after-opening-brace": "\n", 20 | "space-after-selector-delimiter": "\n", 21 | "space-before-closing-brace": "\n", 22 | "space-before-colon": "", 23 | "space-before-combinator": " ", 24 | "space-before-opening-brace": " ", 25 | "space-before-selector-delimiter": "", 26 | "space-between-declarations": "\n", 27 | "strip-spaces": true, 28 | "unitless-zero": true, 29 | "vendor-prefix-align": true, 30 | "sort-order-fallback": "abc", 31 | "tab-size": 2, 32 | "sort-order": [ 33 | [ 34 | "$include" 35 | ], 36 | [ 37 | "font", 38 | "font-family", 39 | "font-size", 40 | "font-weight", 41 | "font-style", 42 | "font-variant", 43 | "font-size-adjust", 44 | "font-stretch", 45 | "font-effect", 46 | "font-emphasize", 47 | "font-emphasize-position", 48 | "font-emphasize-style", 49 | "font-smooth", 50 | "line-height" 51 | ], 52 | [ 53 | "position", 54 | "z-index", 55 | "top", 56 | "right", 57 | "bottom", 58 | "left" 59 | ], 60 | [ 61 | "display", 62 | "visibility", 63 | "float", 64 | "clear", 65 | "overflow", 66 | "overflow-x", 67 | "overflow-y", 68 | "-ms-overflow-x", 69 | "-ms-overflow-y", 70 | "clip", 71 | "zoom", 72 | "flex-direction", 73 | "flex-order", 74 | "flex-pack", 75 | "flex-align" 76 | ], 77 | [ 78 | "-webkit-box-sizing", 79 | "-moz-box-sizing", 80 | "box-sizing", 81 | "width", 82 | "min-width", 83 | "max-width", 84 | "height", 85 | "min-height", 86 | "max-height", 87 | "margin", 88 | "margin-top", 89 | "margin-right", 90 | "margin-bottom", 91 | "margin-left", 92 | "padding", 93 | "padding-top", 94 | "padding-right", 95 | "padding-bottom", 96 | "padding-left" 97 | ], 98 | [ 99 | "table-layout", 100 | "empty-cells", 101 | "caption-side", 102 | "border-spacing", 103 | "border-collapse", 104 | "list-style", 105 | "list-style-position", 106 | "list-style-type", 107 | "list-style-image" 108 | ], 109 | [ 110 | "content", 111 | "quotes", 112 | "counter-reset", 113 | "counter-increment", 114 | "resize", 115 | "cursor", 116 | "-webkit-user-select", 117 | "-moz-user-select", 118 | "-ms-user-select", 119 | "user-select", 120 | "nav-index", 121 | "nav-up", 122 | "nav-right", 123 | "nav-down", 124 | "nav-left", 125 | "-webkit-transition", 126 | "-moz-transition", 127 | "-ms-transition", 128 | "-o-transition", 129 | "transition", 130 | "-webkit-transition-delay", 131 | "-moz-transition-delay", 132 | "-ms-transition-delay", 133 | "-o-transition-delay", 134 | "transition-delay", 135 | "-webkit-transition-timing-function", 136 | "-moz-transition-timing-function", 137 | "-ms-transition-timing-function", 138 | "-o-transition-timing-function", 139 | "transition-timing-function", 140 | "-webkit-transition-duration", 141 | "-moz-transition-duration", 142 | "-ms-transition-duration", 143 | "-o-transition-duration", 144 | "transition-duration", 145 | "-webkit-transition-property", 146 | "-moz-transition-property", 147 | "-ms-transition-property", 148 | "-o-transition-property", 149 | "transition-property", 150 | "-webkit-transform", 151 | "-moz-transform", 152 | "-ms-transform", 153 | "-o-transform", 154 | "transform", 155 | "-webkit-transform-origin", 156 | "-moz-transform-origin", 157 | "-ms-transform-origin", 158 | "-o-transform-origin", 159 | "transform-origin", 160 | "-webkit-animation", 161 | "-moz-animation", 162 | "-ms-animation", 163 | "-o-animation", 164 | "animation", 165 | "-webkit-animation-name", 166 | "-moz-animation-name", 167 | "-ms-animation-name", 168 | "-o-animation-name", 169 | "animation-name", 170 | "-webkit-animation-duration", 171 | "-moz-animation-duration", 172 | "-ms-animation-duration", 173 | "-o-animation-duration", 174 | "animation-duration", 175 | "-webkit-animation-play-state", 176 | "-moz-animation-play-state", 177 | "-ms-animation-play-state", 178 | "-o-animation-play-state", 179 | "animation-play-state", 180 | "-webkit-animation-timing-function", 181 | "-moz-animation-timing-function", 182 | "-ms-animation-timing-function", 183 | "-o-animation-timing-function", 184 | "animation-timing-function", 185 | "-webkit-animation-delay", 186 | "-moz-animation-delay", 187 | "-ms-animation-delay", 188 | "-o-animation-delay", 189 | "animation-delay", 190 | "-webkit-animation-iteration-count", 191 | "-moz-animation-iteration-count", 192 | "-ms-animation-iteration-count", 193 | "-o-animation-iteration-count", 194 | "animation-iteration-count", 195 | "-webkit-animation-direction", 196 | "-moz-animation-direction", 197 | "-ms-animation-direction", 198 | "-o-animation-direction", 199 | "animation-direction", 200 | "text-align", 201 | "-webkit-text-align-last", 202 | "-moz-text-align-last", 203 | "-ms-text-align-last", 204 | "text-align-last", 205 | "vertical-align", 206 | "white-space", 207 | "text-decoration", 208 | "text-emphasis", 209 | "text-emphasis-color", 210 | "text-emphasis-style", 211 | "text-emphasis-position", 212 | "text-indent", 213 | "-ms-text-justify", 214 | "text-justify", 215 | "letter-spacing", 216 | "word-spacing", 217 | "-ms-writing-mode", 218 | "text-outline", 219 | "text-transform", 220 | "text-wrap", 221 | "text-overflow", 222 | "-ms-text-overflow", 223 | "text-overflow-ellipsis", 224 | "text-overflow-mode", 225 | "-ms-word-wrap", 226 | "word-wrap", 227 | "word-break", 228 | "-ms-word-break", 229 | "-moz-tab-size", 230 | "-o-tab-size", 231 | "tab-size", 232 | "-webkit-hyphens", 233 | "-moz-hyphens", 234 | "hyphens", 235 | "pointer-events" 236 | ], 237 | [ 238 | "opacity", 239 | "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", 240 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", 241 | "-ms-interpolation-mode", 242 | "color", 243 | "border", 244 | "border-width", 245 | "border-style", 246 | "border-color", 247 | "border-top", 248 | "border-top-width", 249 | "border-top-style", 250 | "border-top-color", 251 | "border-right", 252 | "border-right-width", 253 | "border-right-style", 254 | "border-right-color", 255 | "border-bottom", 256 | "border-bottom-width", 257 | "border-bottom-style", 258 | "border-bottom-color", 259 | "border-left", 260 | "border-left-width", 261 | "border-left-style", 262 | "border-left-color", 263 | "-webkit-border-radius", 264 | "-moz-border-radius", 265 | "border-radius", 266 | "-webkit-border-top-left-radius", 267 | "-moz-border-radius-topleft", 268 | "border-top-left-radius", 269 | "-webkit-border-top-right-radius", 270 | "-moz-border-radius-topright", 271 | "border-top-right-radius", 272 | "-webkit-border-bottom-right-radius", 273 | "-moz-border-radius-bottomright", 274 | "border-bottom-right-radius", 275 | "-webkit-border-bottom-left-radius", 276 | "-moz-border-radius-bottomleft", 277 | "border-bottom-left-radius", 278 | "-webkit-border-image", 279 | "-moz-border-image", 280 | "-o-border-image", 281 | "border-image", 282 | "-webkit-border-image-source", 283 | "-moz-border-image-source", 284 | "-o-border-image-source", 285 | "border-image-source", 286 | "-webkit-border-image-slice", 287 | "-moz-border-image-slice", 288 | "-o-border-image-slice", 289 | "border-image-slice", 290 | "-webkit-border-image-width", 291 | "-moz-border-image-width", 292 | "-o-border-image-width", 293 | "border-image-width", 294 | "-webkit-border-image-outset", 295 | "-moz-border-image-outset", 296 | "-o-border-image-outset", 297 | "border-image-outset", 298 | "-webkit-border-image-repeat", 299 | "-moz-border-image-repeat", 300 | "-o-border-image-repeat", 301 | "border-image-repeat", 302 | "outline", 303 | "outline-width", 304 | "outline-style", 305 | "outline-color", 306 | "outline-offset", 307 | "background", 308 | "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", 309 | "background-color", 310 | "background-image", 311 | "background-repeat", 312 | "background-attachment", 313 | "background-position", 314 | "background-position-x", 315 | "-ms-background-position-x", 316 | "background-position-y", 317 | "-ms-background-position-y", 318 | "-webkit-background-clip", 319 | "-moz-background-clip", 320 | "background-clip", 321 | "background-origin", 322 | "-webkit-background-size", 323 | "-moz-background-size", 324 | "-o-background-size", 325 | "background-size", 326 | "box-decoration-break", 327 | "-webkit-box-shadow", 328 | "-moz-box-shadow", 329 | "box-shadow", 330 | "filter:progid:DXImageTransform.Microsoft.gradient", 331 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", 332 | "text-shadow" 333 | ] 334 | ] 335 | } 336 | -------------------------------------------------------------------------------- /templates/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /templates/.erb-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | EnableDefaultLinters: true 3 | exclude: 4 | - '**/rails_admin/*' 5 | - "vendor/**/*" 6 | linters: 7 | SpaceInHtmlTag: 8 | enabled: false 9 | NoJavascriptTagHelper: 10 | enabled: true 11 | correction_style: 'plain' 12 | Rubocop: 13 | enabled: true 14 | rubocop_config: 15 | inherit_from: 16 | - .rubocop.yml 17 | Layout/InitialIndentation: 18 | Enabled: false 19 | Layout/LineLength: 20 | Enabled: true 21 | Layout/TrailingEmptyLines: 22 | Enabled: false 23 | Layout/TrailingWhitespace: 24 | Enabled: false 25 | Layout/BlockAlignment: 26 | Enabled: false 27 | Naming/FileName: 28 | Enabled: false 29 | Style/FrozenStringLiteralComment: 30 | Enabled: false 31 | Lint/UselessAssignment: 32 | Enabled: false 33 | Rails/OutputSafety: 34 | Enabled: false 35 | -------------------------------------------------------------------------------- /templates/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jquery": true 6 | }, 7 | "extends": "airbnb-base", 8 | "rules": { 9 | "max-len": ["error", 120], 10 | "no-unused-vars": [ 11 | "off" 12 | ], 13 | "class-methods-use-this": "off", 14 | "no-new": "off", 15 | "no-undef": "off", 16 | "func-names": "off", 17 | "no-param-reassign": "off", 18 | "no-underscore-dangle": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /templates/.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD013", "~MD002" 2 | git_recurse true 3 | -------------------------------------------------------------------------------- /templates/.reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | detectors: 3 | Attribute: 4 | enabled: false 5 | NilCheck: 6 | enabled: false 7 | DuplicateMethodCall: 8 | max_calls: 2 9 | IrresponsibleModule: 10 | enabled: false 11 | 12 | directories: 13 | "app/helpers": 14 | UtilityFunction: 15 | enabled: false 16 | FeatureEnvy: 17 | enabled: false 18 | "app/jobs": 19 | UtilityFunction: 20 | enabled: false 21 | "app/services": 22 | UtilityFunction: 23 | enabled: false 24 | "app/validators": 25 | UtilityFunction: 26 | enabled: false 27 | "app/controllers": 28 | IrresponsibleModule: 29 | enabled: false 30 | NestedIterators: 31 | max_allowed_nesting: 2 32 | UnusedPrivateMethod: 33 | enabled: false 34 | InstanceVariableAssumption: 35 | enabled: false 36 | "app/mailers": 37 | InstanceVariableAssumption: 38 | enabled: false 39 | FeatureEnvy: 40 | enabled: false 41 | 42 | exclude_paths: 43 | - tmp 44 | - spec 45 | - vendor 46 | - log 47 | - coverage 48 | - config 49 | - bin 50 | - db/migrate 51 | -------------------------------------------------------------------------------- /templates/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /templates/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | renuocop: config/base.yml 3 | 4 | # most likely you would like to copy and adjust the `AllCops` part (e.g. to exclude vendor-files) 5 | # AllCops: 6 | # NewCops: enable 7 | # Exclude: 8 | # - 'vendor/**/*' 9 | # ... and add all the exclusions from the `config/base.yml` 10 | 11 | -------------------------------------------------------------------------------- /templates/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.1 2 | -------------------------------------------------------------------------------- /templates/.sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | formatter: stylish 3 | files: 4 | include: '**/*.s+(a|c)ss' 5 | rules: 6 | # Extends 7 | extends-before-mixins: 2 8 | extends-before-declarations: 2 9 | placeholder-in-extend: 0 10 | 11 | # Mixins 12 | mixins-before-declarations: 0 13 | 14 | # Line Spacing 15 | one-declaration-per-line: 2 16 | empty-line-between-blocks: 2 17 | single-line-per-selector: 2 18 | 19 | # Disallows 20 | no-attribute-selectors: 0 21 | no-color-hex: 0 22 | no-color-keywords: 2 23 | no-color-literals: 0 24 | no-combinators: 0 25 | no-css-comments: 2 26 | no-debug: 2 27 | no-disallowed-properties: 0 28 | no-duplicate-properties: 2 29 | no-empty-rulesets: 2 30 | no-extends: 0 31 | no-ids: 2 32 | no-important: 1 33 | no-invalid-hex: 2 34 | no-mergeable-selectors: 2 35 | no-misspelled-properties: 2 36 | no-qualifying-elements: 0 37 | no-trailing-whitespace: 2 38 | no-trailing-zero: 2 39 | no-transition-all: 2 40 | no-universal-selectors: 0 41 | no-url-domains: 2 42 | no-url-protocols: 2 43 | no-vendor-prefixes: 44 | - 2 45 | - excluded-identifiers: 46 | - '-webkit-font-smoothing' 47 | - ignore-non-standard: true 48 | no-warn: 2 49 | property-units: 0 50 | 51 | # Nesting 52 | declarations-before-nesting: 2 53 | force-attribute-nesting: 0 54 | force-element-nesting: 0 55 | force-pseudo-nesting: 0 56 | 57 | # Name Formats 58 | class-name-format: 2 59 | function-name-format: 2 60 | id-name-format: 0 61 | mixin-name-format: 2 62 | placeholder-name-format: 2 63 | variable-name-format: 2 64 | 65 | # Style Guide 66 | attribute-quotes: 2 67 | bem-depth: 0 68 | border-zero: 0 69 | brace-style: 2 70 | clean-import-paths: 2 71 | empty-args: 2 72 | hex-length: 2 73 | hex-notation: 2 74 | indentation: 2 75 | leading-zero: 0 76 | max-line-length: 0 77 | max-file-line-count: 0 78 | nesting-depth: 2 79 | property-sort-order: 2 80 | pseudo-element: 2 81 | quotes: 2 82 | shorthand-values: 2 83 | url-quotes: 2 84 | variable-for-property: 2 85 | zero-unit: 2 86 | 87 | # Inner Spacing 88 | space-after-comma: 2 89 | space-before-colon: 2 90 | space-after-colon: 2 91 | space-before-brace: 2 92 | space-before-bang: 2 93 | space-after-bang: 2 94 | space-between-parens: 2 95 | space-around-operator: 2 96 | 97 | # Final Items 98 | trailing-semicolon: 2 99 | final-newline: 2 100 | -------------------------------------------------------------------------------- /templates/.scss-lint.yml: -------------------------------------------------------------------------------- 1 | scss_files: 'app/assets/stylesheets/**/*.css.scss' 2 | 3 | linters: 4 | BorderZero: 5 | enabled: false 6 | 7 | Indentation: 8 | severity: warning 9 | width: 2 10 | 11 | PropertySortOrder: 12 | enabled: false 13 | -------------------------------------------------------------------------------- /templates/.stylelintrc.yml: -------------------------------------------------------------------------------- 1 | "extends": "stylelint-config-standard-scss" 2 | -------------------------------------------------------------------------------- /templates/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | What I did and maybe screenshots. 4 | 5 | ### General 6 | 7 | - [ ] I reviewed my code. 8 | - [ ] This PR is relatable to a ticket number (via branch name, title or description). 9 | - [ ] I updated the ticket on Redmine. 10 | 11 | ### New feature 12 | 13 | - [ ] I wrote unit tests and e2e tests. 14 | - [ ] *I updated the environment variables on develop and main* 15 | 16 | #### Feature involving the frontend 17 | 18 | - [ ] I checked the consistency of the layout with the other pages. 19 | - [ ] I checked that it works both on mobile and desktop. 20 | 21 | #### Bug 22 | 23 | - [ ] I wrote a regression test for it. 24 | -------------------------------------------------------------------------------- /templates/Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -C config/puma.rb 2 | -------------------------------------------------------------------------------- /templates/README.md: -------------------------------------------------------------------------------- 1 | # Project Title 2 | 3 | Short project description 4 | 5 | ## Environments 6 | 7 | | Branch | Domain | Deployment | 8 | | ------- | ------------------------------------- | ---------------| 9 | | develop | https://`[project-name]`-develop.renuoapp.ch | auto | 10 | | main | https://`[project-name]`-main.renuoapp.ch | release | 11 | 12 | ## Setup 13 | 14 | ```sh 15 | git clone git@github.com:renuo/[project-name].git 16 | cd [project-name] 17 | bin/setup 18 | ``` 19 | 20 | ### Configuration 21 | 22 | Configure the following: 23 | 24 | * .env 25 | 26 | ### Run 27 | 28 | ```sh 29 | bin/run 30 | ``` 31 | 32 | ### Dependency 33 | 34 | Dependencies list 35 | 36 | ### Tests / Checks 37 | 38 | ```sh 39 | bin/check 40 | ``` 41 | 42 | ## Other 43 | 44 | special stuff 45 | 46 | ## Copyright 47 | 48 | Copyright [Renuo AG](https://www.renuo.ch/). 49 | -------------------------------------------------------------------------------- /templates/app/controllers/rails/health_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rails 4 | class HealthController < ApplicationController 5 | rescue_from(Exception) { render_down } 6 | 7 | def show 8 | val = ActiveRecord::Base.connection.execute('select 1+2 as val').first['val'] 9 | raise unless val.to_i == 3 10 | 11 | render_up 12 | end 13 | 14 | private 15 | 16 | def render_up 17 | render html: html_status(color: 'green') 18 | end 19 | 20 | def render_down 21 | render html: html_status(color: 'red'), status: :internal_server_error 22 | end 23 | 24 | def html_status(color:) # rubocop:disable Rails/OutputSafety 25 | %().html_safe 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /templates/app/javascript/appsignal.js: -------------------------------------------------------------------------------- 1 | import Appsignal from '@appsignal/javascript'; 2 | 3 | export const appsignal = new Appsignal({ 4 | key: window.appsignalConfig.frontendApiKey, 5 | }); 6 | window.Appsignal = appsignal; 7 | -------------------------------------------------------------------------------- /templates/app/javascript/sentry.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/browser'; 2 | 3 | const {sentryConfig, renuoSentryConfig} = window; 4 | Sentry.init({...sentryConfig, ...renuoSentryConfig}); 5 | window.Sentry = Sentry; 6 | -------------------------------------------------------------------------------- /templates/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: ENV['MAIL_SENDER'] 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /templates/app/views/shared/_appsignal.html.erb: -------------------------------------------------------------------------------- 1 | <% if ENV['APPSIGNAL_FRONTEND_API_KEY'].present? %> 2 | 7 | 8 | <%= javascript_include_tag 'appsignal' %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /templates/app/views/shared/_sentry.html.erb: -------------------------------------------------------------------------------- 1 | <% if defined? Sentry %> 2 | <%= javascript_tag nonce: true do -%> 3 | window.sentryConfig = {dsn: '<%= ENV['SENTRY_DSN']%>', environment: '<%= ENV['SENTRY_ENVIRONMENT']%>'}; 4 | <% end -%> 5 | <%= javascript_include_tag 'https://cdn.renuo.ch/sentry-ignore.min.js', crossorigin: 'anonymous' %> 6 | <%= javascript_include_tag 'sentry' %> 7 | <% end %> 8 | -------------------------------------------------------------------------------- /templates/bin/check: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | bin/rails zeitwerk:check 6 | 7 | if ! grep --exclude-dir="app/assets/typings/**" -i -r 'console.log' app spec 8 | then 9 | echo 'console.log found. Please fix them and try again, commit aborted' 10 | exit 1 11 | fi 12 | 13 | DIFF_BEFORE=`git diff | shasum -a 256` 14 | csscomb -v src/app/**/*.scss 15 | DIFF_AFTER=`git diff | shasum -a 256` 16 | if [ $DIFF_BEFORE -ne $DIFF_AFTER ]; then 17 | echo 'csscomb has rearranged some of your css that has to be reviewed manually, commit aborted' 18 | exit 1 19 | fi 20 | 21 | bin/rspec 22 | 23 | # for Cucumber 24 | 25 | NO_COVERAGE=true bundle exec cucumber --format progress 26 | -------------------------------------------------------------------------------- /templates/bin/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | base=$(cat $1) 4 | message=`echo ${base:0:1} | tr '[a-z]' '[A-Z]'`${base:1} 5 | echo "$message" > $1 6 | -------------------------------------------------------------------------------- /templates/bin/fastcheck: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if ! bundle exec rubocop -D -c .rubocop.yml --fail-fast 6 | then 7 | echo 'rubocop detected issues!' 8 | bundle exec rubocop -A -D -c .rubocop.yml 9 | echo 'Tried to auto correct the issues, but must be reviewed manually, commit aborted' 10 | exit 1 11 | fi 12 | 13 | bundle exec brakeman -q -z --no-summary --no-pager 14 | bundle exec mdl ./ -g 15 | 16 | # for scss 17 | yarn run stylelint "./app/assets/stylesheets/**/*.scss" 18 | 19 | # for erb 20 | bundle exec erb_lint --config .erb-lint.yml --lint-all 21 | 22 | # for typescript 23 | tslint -c tslint.json app/javascripts/**/*.ts 24 | 25 | # for coffeescript 26 | coffeelint -f .coffeelint.json app/javascripts/**/*.coffee 27 | 28 | # for javascript 29 | yarn eslint app/javascripts spec/javascripts 30 | -------------------------------------------------------------------------------- /templates/bin/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | bin/rails server 4 | -------------------------------------------------------------------------------- /templates/bitrise/bitrise.yml: -------------------------------------------------------------------------------- 1 | --- 2 | format_version: '7' 3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 4 | project_type: react-native 5 | app: 6 | envs: 7 | - FASTLANE_XCODE_LIST_TIMEOUT: '120' 8 | - opts: 9 | is_expand: false 10 | FASTLANE_WORK_DIR: "." 11 | - VERSION_NUMBER: 1.0.0 12 | trigger_map: 13 | - push_branch: main 14 | workflow: Deploy-main 15 | - push_branch: develop 16 | workflow: Deploy-develop 17 | workflows: 18 | Setup: 19 | steps: 20 | - activate-ssh-key@4.0.5: 21 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 22 | - git-clone@4.0.18: {} 23 | - cache-pull@2.1.3: {} 24 | - script@1.1.6: 25 | title: Install bundler 26 | inputs: 27 | - content: |- 28 | # brew update &> /dev/null 29 | 30 | # brew install rbenv 31 | 32 | # eval "$(rbenv init -)" 33 | cd web && yarn install && cd - 34 | rbenv install 2.6.2 --skip-existing && rbenv global 2.6.2 && gem install bundler cocoapods --no-document 35 | export ORG_GRADLE_PROJECT_VERSION_NUMBER=$VERSION_NUMBER 36 | - cache-push@2.2.3: 37 | inputs: 38 | - cache_paths: "~/.rbenv/versions -> app/.ruby-version" 39 | - yarn@0.1.1: 40 | inputs: 41 | - cache_local_deps: 'yes' 42 | - workdir: "$BITRISE_SOURCE_DIR/app" 43 | - command: install 44 | title: Yarn install 45 | - script@1.1.6: 46 | title: Install pods 47 | inputs: 48 | - content: cd $BITRISE_SOURCE_DIR/app/ios && pod install && cd - 49 | Deploy: 50 | steps: 51 | - certificate-and-profile-installer@1.10.2: {} 52 | - set-xcode-build-number@1.0.9: 53 | inputs: 54 | - build_short_version_string: "$VERSION_NUMBER" 55 | - plist_path: ios/$IOS_PROJECT_NAME/Info.plist 56 | - fastlane: 57 | inputs: 58 | - lane: "$FASTLANE_IOS_LANE" 59 | - update_fastlane: 'false' 60 | - work_dir: "$FASTLANE_WORK_DIR" 61 | - fastlane: 62 | inputs: 63 | - work_dir: "$FASTLANE_WORK_DIR" 64 | - update_fastlane: 'false' 65 | - lane: "$FASTLANE_ANDROID_LANE" 66 | Deploy-develop: 67 | steps: 68 | - change-workdir@1.0.2: 69 | inputs: 70 | - is_create_path: 'false' 71 | - path: "$BITRISE_SOURCE_DIR/app" 72 | title: Workdir app 73 | - script@1.1.6: 74 | title: Prepare .env 75 | inputs: 76 | - content: cp .env.staging .env 77 | - script: 78 | inputs: 79 | - content: |- 80 | openssl aes-256-cbc -md sha256 -in android/key.json.enc -out android/key.json -d -a -pass env:KEY_JSON_PASSWORD 81 | openssl aes-256-cbc -md sha256 -d -a -salt -in android/keystore.properties.enc -out android/keystore.properties -pass env:KEY_JSON_PASSWORD 82 | openssl aes-256-cbc -md sha256 -d -a -salt -in android/keystores/[project-name].jks.enc -out android/keystores/[project-name].jks -pass env:KEY_JSON_PASSWORD 83 | 84 | openssl aes-256-cbc -md sha256 -d -a -salt -in android/app/google-services-develop.json.enc -out android/app/google-services.json -pass env:KEY_JSON_PASSWORD 85 | openssl aes-256-cbc -md sha256 -d -a -salt -in ios/GoogleService-Info-develop.plist.enc -out ios/GoogleService-Info.plist -pass env:KEY_JSON_PASSWORD 86 | title: Decrypt secrets 87 | before_run: 88 | - Setup 89 | after_run: 90 | - Deploy 91 | envs: 92 | - opts: 93 | is_expand: false 94 | APP_BUNDLE_ID: ch.renuo.[project-name].staging 95 | - opts: 96 | is_expand: false 97 | FASTLANE_IOS_LANE: ios deploy_staging 98 | - opts: 99 | is_expand: false 100 | FASTLANE_ANDROID_LANE: android deploy_staging --env staging 101 | - opts: 102 | is_expand: false 103 | IOS_PROJECT_NAME: [project-name] 104 | Deploy-main: 105 | steps: 106 | - change-workdir@1.0.2: 107 | title: Workdir app 108 | inputs: 109 | - is_create_path: 'false' 110 | - path: "$BITRISE_SOURCE_DIR/app" 111 | - script@1.1.6: 112 | title: Prepare .env 113 | inputs: 114 | - content: cp .env.production .env 115 | - script: 116 | inputs: 117 | - content: |- 118 | openssl aes-256-cbc -md sha256 -in android/key.json.enc -out android/key.json -d -a -pass env:KEY_JSON_PASSWORD 119 | openssl aes-256-cbc -md sha256 -d -a -salt -in android/keystore.properties.enc -out android/keystore.properties -pass env:KEY_JSON_PASSWORD 120 | openssl aes-256-cbc -md sha256 -d -a -salt -in android/keystores/[project-name].jks.enc -out android/keystores/[project-name].jks -pass env:KEY_JSON_PASSWORD 121 | 122 | openssl aes-256-cbc -md sha256 -d -a -salt -in android/app/google-services-main.json.enc -out android/app/google-services.json -pass env:KEY_JSON_PASSWORD 123 | openssl aes-256-cbc -md sha256 -d -a -salt -in ios/GoogleService-Info-main.plist.enc -out ios/GoogleService-Info.plist -pass env:KEY_JSON_PASSWORD 124 | title: Decrypt secrets 125 | before_run: 126 | - Setup 127 | after_run: 128 | - Deploy 129 | envs: 130 | - opts: 131 | is_expand: false 132 | APP_BUNDLE_ID: ch.renuo.[project-name] 133 | - opts: 134 | is_expand: false 135 | FASTLANE_IOS_LANE: ios deploy 136 | - opts: 137 | is_expand: false 138 | FASTLANE_ANDROID_LANE: android deploy --env production 139 | - opts: 140 | is_expand: false 141 | IOS_PROJECT_NAME: [project-name] 142 | meta: 143 | bitrise.io: 144 | machine_type: standard 145 | -------------------------------------------------------------------------------- /templates/config/application.example.yml: -------------------------------------------------------------------------------- 1 | # Edit /etc/hosts to let your DNS resolve to 127.0.0.1 or use puma-dev 2 | APP_HOST: '.localhost' 3 | APP_PORT: '3000' 4 | 5 | # Mail config 6 | MAIL_HOST: '' 7 | MAIL_USERNAME: '' 8 | MAIL_PASSWORD: '' 9 | MAIL_SENDER: 'yourname+@example.com' # change your name and domain to @renuo.ch 10 | 11 | # Secrets 12 | SECRET_KEY_BASE: 'rake secret' 13 | DEVISE_SECRET_KEY: 'rake secret' 14 | DEVISE_PEPPER: 'rake secret' 15 | 16 | # Newrelic 17 | NEW_RELIC_LICENSE_KEY: 'TO DO' 18 | NEW_RELIC_APP_NAME: '-' 19 | 20 | # Add 3 new projects for develop/main at https://sentry.io/organizations/renuo/projects/new 21 | SENTRY_DSN: '' 22 | 23 | # Use Renuo Upload instead of Amazon S3 24 | RENUO_UPLOAD_API_KEY: '' 25 | # Where the renuo-upload library gets the credentials for the upload 26 | RENUO_UPLOAD_SIGNING_URL: '' 27 | # Host where the images are available 28 | RENUO_UPLOAD_CDN_HOST: '' 29 | RENUO_UPLOAD_APP_NAME: '-' 30 | 31 | # This is used to seed your database 32 | # Use a random password for develop/main 33 | ADMIN_EMAIL: 'yourname+@example.com' # change your name and domain to @renuo.ch 34 | ADMIN_PASSWORD: 'developer' 35 | 36 | # Have 3 different Google Analytics key so not to mix up data from different applications 37 | GOOGLE_ANALYTICS_ID: '' 38 | 39 | # For the production environment on Heroku we use the DATABASE_URL. The format of it is as follows: 40 | # "postgres://myuser:mypass@localhost/somedatabase" 41 | # Note that this ENV variable is already configured by Heroku when installing the PG extension. 42 | # For the local environment the defaults should be ok. If you need to override the config, use the DATABASE_URL. 43 | 44 | # Try not to use Amazon S3! If you have to, name the bucket: -- 45 | # S3_BUCKET_NAME: '-' 46 | # AWS_ACCESS_KEY_ID: '' 47 | # AWS_SECRET_ACCESS_KEY: '' 48 | -------------------------------------------------------------------------------- /templates/config/database.mysql.example.yml: -------------------------------------------------------------------------------- 1 | mysql: &mysql 2 | adapter: mysql2 3 | encoding: utf8 4 | host: localhost 5 | reconnect: true 6 | pool: 5 7 | username: "" 8 | password: "" 9 | 10 | development: 11 | <<: *mysql 12 | database: _development 13 | 14 | test: 15 | <<: *mysql 16 | database: _test 17 | 18 | production: 19 | <<: *mysql 20 | database: _production 21 | -------------------------------------------------------------------------------- /templates/config/database.sqlite.example.yml: -------------------------------------------------------------------------------- 1 | sqlite: &sqlite 2 | adapter: sqlite3 3 | pool: 5 4 | timeout: 5000 5 | 6 | development: 7 | <<: *sqlite 8 | database: db/development.sqlite3 9 | 10 | test: 11 | <<: *sqlite 12 | database: db/test.sqlite3 13 | 14 | production: 15 | <<: *sqlite 16 | database: db/production.sqlite3 17 | -------------------------------------------------------------------------------- /templates/config/database.yml: -------------------------------------------------------------------------------- 1 | # For details on connection pooling, see rails configuration guide 2 | # http://guides.rubyonrails.org/configuring.html#database-pooling 3 | 4 | default: &default 5 | adapter: postgresql 6 | encoding: unicode 7 | pool: <%= ENV['DB_POOL'] || ENV['MAX_THREADS'] || 5 %> 8 | # Minimum log levels, in increasing order: debug5, debug4, debug3, debug2, debug1, log, notice, warning, error, 9 | # fatal, and panic. Defaults to warning. min_messages: warning 10 | url: <%= ENV['DATABASE_URL'] %> 11 | 12 | development: 13 | <<: *default 14 | database: _development 15 | 16 | test: 17 | <<: *default 18 | database: _test 19 | 20 | production: 21 | <<: *default 22 | -------------------------------------------------------------------------------- /templates/config/initializers/appsignal.rb: -------------------------------------------------------------------------------- 1 | if defined?(Appsignal) 2 | Appsignal.configure do |config| 3 | %w[HTTP_REFERER HTTP_USER_AGENT HTTP_AUTHORIZATION REQUEST_URI].each do |header| 4 | config.request_headers << header 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /templates/config/initializers/lograge.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renuo/applications-setup-guide/481bc87dda0403d773a6d68865f81f5614c56ae7/templates/config/initializers/lograge.rb -------------------------------------------------------------------------------- /templates/config/initializers/sentry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/parameter_filter' 4 | 5 | if defined? Sentry 6 | Sentry.init do |config| 7 | filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters) 8 | config.before_send = lambda do |event, _hint| 9 | filter.filter(event.to_hash) 10 | end 11 | 12 | config.breadcrumbs_logger = %i[sentry_logger active_support_logger http_logger] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /templates/config/newrelic.yml: -------------------------------------------------------------------------------- 1 | production: 2 | license_key: <%= ENV['NEW_RELIC_LICENSE_KEY'] %> 3 | app_name: <%= ENV['HEROKU_APP_NAME'] %> 4 | monitor_mode: true 5 | developer_mode: false 6 | log_level: info 7 | browser_monitoring: 8 | auto_instrument: true 9 | audit_log: 10 | enabled: false 11 | capture_params: true 12 | transaction_tracer: 13 | enabled: true 14 | transaction_threshold: apdex_f 15 | record_sql: raw 16 | stack_trace_threshold: 0.500 17 | explain_enabled: true 18 | explain_threshold: 1.0 19 | error_collector: 20 | enabled: true 21 | ignore_errors: "ActionController::RoutingError,Sinatra::NotFound" 22 | -------------------------------------------------------------------------------- /templates/features/env/capybara.rb: -------------------------------------------------------------------------------- 1 | Capybara.default_driver = :rack_test 2 | Capybara.javascript_driver = :selenium_chrome_headless 3 | -------------------------------------------------------------------------------- /templates/features/env/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | begin 2 | DatabaseCleaner.strategy = :transaction 3 | rescue NameError 4 | raise 'You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it.' 5 | end 6 | 7 | Cucumber::Rails::Database.javascript_strategy = :truncation 8 | -------------------------------------------------------------------------------- /templates/features/env/factory_bot.rb: -------------------------------------------------------------------------------- 1 | World(FactoryBot::Syntax::Methods) 2 | -------------------------------------------------------------------------------- /templates/features/env/warden.rb: -------------------------------------------------------------------------------- 1 | World(Warden::Test::Helpers) 2 | 3 | Warden.test_mode! 4 | After { Warden.test_reset! } 5 | -------------------------------------------------------------------------------- /templates/features/home_check.feature: -------------------------------------------------------------------------------- 1 | Feature: 2 | As a monitoring service 3 | I want to have a simple page 4 | So I can assure the app is alive 5 | 6 | Scenario: 7 | When I visit "/home/check" 8 | Then I see the text "1+2=3" 9 | -------------------------------------------------------------------------------- /templates/features/step_definitions/home_check_steps.rb: -------------------------------------------------------------------------------- 1 | When(/^I visit "([^"]*)"$/) do |path| 2 | visit path 3 | end 4 | 5 | Then(/^I see the text "([^"]*)"$/) do |text| 6 | expect(page).to have_content(text) 7 | end 8 | -------------------------------------------------------------------------------- /templates/pull_requests_template.md: -------------------------------------------------------------------------------- 1 | # Pull Requests Template 2 | 3 | You should probably configure a template for the Pull Requests in your project. 4 | 5 | This template will give a list of reminders and important points that the developer 6 | needs to remember when opening a new Pull Request. 7 | 8 | We think this template is useful as a checklist of things that shouldn't be forgotten 9 | when assigning a Pull Request to one of your colleagues. 10 | 11 | Feel free to personalise the template for your project specific needs. 12 | If you think your changes would be useful also in other projects, 13 | please open a Pull Request to update this template. 14 | 15 | To install the template copy this file into a `.github` folder in your project or 16 | simply run the following command from the root folder of the project: 17 | 18 | ```bash 19 | mkdir -p .github && curl https://raw.githubusercontent.com/renuo/applications-setup-guide/master/templates/PULL_REQUEST_TEMPLATE.md > .github/PULL_REQUEST_TEMPLATE.md 20 | ``` 21 | -------------------------------------------------------------------------------- /templates/spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require 'spec_helper' 5 | require_relative '../config/environment' 6 | # Prevent database truncation if the environment is production 7 | abort('The Rails environment is running in production mode!') if Rails.env.production? 8 | require 'rspec/rails' 9 | require 'capybara/rspec' 10 | require 'capybara/rails' 11 | require 'selenium/webdriver' 12 | require 'super_diff/rspec-rails' 13 | 14 | ActiveRecord::Migration.maintain_test_schema! 15 | 16 | Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 17 | 18 | RSpec.configure do |config| 19 | config.include FactoryBot::Syntax::Methods 20 | config.include ActiveSupport::Testing::TimeHelpers 21 | config.include JavaScriptErrorReporter, type: :system, js: true 22 | 23 | config.use_transactional_fixtures = true 24 | config.infer_spec_type_from_file_location! 25 | 26 | config.before(:each, type: :system) do 27 | driven_by :rack_test 28 | end 29 | 30 | config.before(:all, type: :system) do 31 | Capybara.server = :puma, { Silent: true } 32 | end 33 | 34 | config.before(:each, :js, type: :system) do 35 | driven_by ENV['SELENIUM_DRIVER']&.to_sym || :selenium_chrome_headless 36 | Capybara.page.current_window.resize_to(1280, 800) 37 | end 38 | 39 | config.after do 40 | I18n.locale = I18n.default_locale 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /templates/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | unless ENV['NO_COVERAGE'] 4 | require 'simplecov' 5 | SimpleCov.start 'rails' do 6 | add_filter 'app/channels/application_cable/channel.rb' 7 | add_filter 'app/channels/application_cable/connection.rb' 8 | add_filter 'app/jobs/application_job.rb' 9 | add_filter 'app/mailers/application_mailer.rb' 10 | add_filter 'app/models/application_record.rb' 11 | add_filter '.semaphore-cache' 12 | enable_coverage :branch 13 | minimum_coverage line: 100, branch: 100 14 | end 15 | end 16 | 17 | RSpec.configure do |config| 18 | config.expect_with :rspec do |expectations| 19 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 20 | end 21 | 22 | config.mock_with :rspec do |mocks| 23 | mocks.verify_partial_doubles = true 24 | end 25 | 26 | config.run_all_when_everything_filtered = true 27 | 28 | config.disable_monkey_patching! 29 | 30 | config.default_formatter = 'doc' if config.files_to_run.one? 31 | 32 | config.profile_examples = 5 33 | 34 | config.order = :random 35 | 36 | Kernel.srand config.seed 37 | 38 | config.define_derived_metadata do |meta| 39 | meta[:aggregate_failures] = true 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /templates/spec/support/javascript_error_reporter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JavaScriptErrorReporter 4 | RSpec.configure do |config| 5 | config.after(:each, :js, type: :system) do 6 | errors = page.driver.browser.logs.get(:browser) 7 | 8 | aggregate_failures 'javascript errors' do 9 | errors.each do |error| 10 | expect(error.level).not_to eq('SEVERE'), error.message 11 | 12 | next unless error.level == 'WARNING' 13 | 14 | warn "\e[33m\nJAVASCRIPT WARNING\n#{error.message}\e[0m" 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /templates/spec/system/health_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Health check' do 4 | it 'should check if the app is ok and connected to a database' do 5 | visit '/up' 6 | expect(page).to have_http_status(200) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /templates/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "curly": false, 5 | "eofline": true, 6 | "forin": true, 7 | "indent": [true, "spaces"], 8 | "label-position": true, 9 | "label-undefined": true, 10 | "max-line-length": [true, 140], 11 | "no-arg": true, 12 | "no-bitwise": true, 13 | "no-console": [true, 14 | "debug", 15 | "info", 16 | "time", 17 | "timeEnd", 18 | "trace" 19 | ], 20 | "no-construct": true, 21 | "no-debugger": true, 22 | "no-duplicate-key": true, 23 | "no-duplicate-variable": true, 24 | "no-var-keyword": true, 25 | "no-empty": false, 26 | "no-eval": true, 27 | "no-string-literal": true, 28 | "no-switch-case-fall-through": true, 29 | "trailing-comma": [true, { 30 | "multiline": "never", 31 | "singleline": "never" 32 | }], 33 | "no-trailing-whitespace": true, 34 | "no-unused-expression": true, 35 | "no-unused-variable": false, 36 | "no-unreachable": true, 37 | "no-use-before-declare": true, 38 | "one-line": [true, 39 | "check-open-brace", 40 | "check-catch", 41 | "check-else", 42 | "check-whitespace" 43 | ], 44 | "quotemark": [true, "single"], 45 | "radix": true, 46 | "semicolon": true, 47 | "triple-equals": [true, "allow-null-check"], 48 | "use-strict": true, 49 | "variable-name": true, 50 | "whitespace": [false, 51 | "check-branch", 52 | "check-decl", 53 | "check-operator", 54 | "check-separator", 55 | "check-type" 56 | ] 57 | } 58 | } 59 | --------------------------------------------------------------------------------