Renuo Applications Setup Guide

This repo is the Renuo collection of best-practices to set-up apps. We are a Rails company, so the most value probably will be found in the parts concerning Rails. But anyways you'll also find a lot about the inner workings of Renuo.

Some Notes on the Side

If you are reading this document, it means that you have to setup a new application. A new project started and it's now time to set everything up so that everyone, in your team, can start working on it.

This document will try to be as minimalist as possible and provide you with all the steps to set up the application as fast as possible. There are things, in Renuo projects, which are mandatory, other that are suggested. 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.

You are always welcome to challenge the guide and improve it with a Pull Request.

The basic things that need to be ready before the team can start working on a project are:

  • An existing git repository containing the project
  • Two branches: main and develop
  • A README with essential information about the application
  • Convenience-scripts: bin/setup, bin/check, bin/fastcheck, bin/run
  • One running, green test
  • Continuous integration (CI) ready, running and green for both branches
  • Continuous deployment (CD) ready and running for both branches
  • The application deployed for both branches

As an appendix, you'll find a checklist you can use to follow the guide.

❗ Do not blindly follow this guide, always think about what you are doing and why. If you think something is wrong or simply outdated, improve this guide with a Pull Request.

We want you to know exactly the reason behind each single step of this guide.

Thank you for your work and have fun! 🎉

License

Attribution 4.0 International (CC BY 4.0)

Create a Git Repository

At Renuo we currently use GitHub as our git repository. You should already be part of the Renuo Organisation and have permissions to do so. If that's not the case, double check the Laptop Setup Guide or ask wg-operations.

To create a new GitHub project you can use the tool you prefer, but it should have the following characteristics:

  • Should be under renuo organisation
  • Should have [project-name] as a name
  • Should be private (unless you are creating an OpenSource project)

Use the command hub create -p renuo/[project-name] to create the repo and add it to the origin of the current folder.

Public repos need a license

If your repository is public, ensure that it contains a license. We usually use the MIT license if possible or a CreativeCommons license for documentation-only repositories (such as the application setup guide 🙂). You can add a license directly on GitHub while initializing a repository by selecting a license template in the "Add a license" dropdown. However, if the repository is already initialized, you're still able to add a license using a template:

  • Click Create new file
  • Use LICENSE for the filename
  • Then click on Choose a license template and select the MIT license
  • Fill in Renuo AG in the full name placeholder
  • Click submit and commit the file

Gitflow

At Renuo we follow gitflow convention and we use it in every project. Please check it out and read how it works if you don’t know it yet. It’s very important that you know how gitflow works to work at Renuo.

Since we follow gitflow, we have two main branches connected, via CD, to two servers, we call "main" and "develop".

graph LR
A[master] --> CI1(CI) --> CD1(CD)
CD1(CD) --> S11(server)
CD1(CD) --> S12(server)

    B[develop] --> CI2(CI) --> CD2(CD)
    CD2(CD) --> S21(server)
    CD2(CD) --> S22(server)

    B[develop] --> C[feature/1337-ff] --> CI3(CI)
sequenceDiagram
    actor Developer
    participant G as GitHub
    participant CI
    participant S as Server

    rect rgb(191, 223, 255)

    Developer->>G: git push origin develop or master
    G->>CI: notify about code change
    CI->>G: checkout code
    CI->>CI: run tests
    CI->>S: deploy
    end

Go Live

DNS, SSL & SMTP

If the final domain isn't already in use, you can configure it also already: Add a CNAME DNS record pointing to the app ([project-name]-main.renuoapp.ch).

URL rewriting on Cloudflare

For user comfort we redirect HTTP calls to https://example.com to https://www.example.com. This is done via page rules in Cloudflare.

  1. Add a new page rule
  2. Enter example.com/*
  3. Choose "Forwarding URL"
  4. Choose "301 - Permanent Redirect"
  5. Enter https://www.example.com/$1
  6. Click "Save and Deploy"

Heroku

  • Check the size and amount of dynos on Heroku
  • Check the database size plan on Heroku and upgrade if it is foreseeable that 10'000 rows are exceeded in a short time
  • Check additional addons and according plans on Heroku

Other

  • Reset admin credentials, seeds, ... if necessary
  • Test the whole application by hand if everything is working as it should

Naming Conventions

Naming the project properly is very important and even more important is doing it from the beginning. We carefully choose the names for our projects and we always stick to the following conventions so you are asked to do the same.

Project Name

  • The project name [project-name] only uses [a-z0-9] and dash -. No underscore _.
  • A project name is easy to remember and easy to pronounce. In the best case it consists of one word.
  • The project name should be unique and not too long.
  • It should not contain any version information.

Extended Use

  • Use [project-name] for project names and services which are branch-independent.
  • Use [project-name]-[branch] for deployed projects ([branch] means the gitflow branch and not RAILS_ENV).
  • Use [project-name]-[purpose]-[branch] for deployed projects (e.g. one11-web-main).
  • Use [project-name]-local-[user]-[rails_env] for local names which interact with online services (e.g. S3).

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.

Examples

  • food-calendar, food-calendar-develop, food-calendar-develop-assets
  • food-calendar-api, food-calendar-api-develop, food-calendar-api-develop-assets
  • bauer-shoes, bauer-cars, bauer-cars-static
  • vdrb-kas, vdrb-mv
  • red-shoes, blue-hats (two projects which are independent and have the same customer)

Scope of Application

The naming conventions should be applied everywhere. Some examples:

  • Amazon S3 (usually [project-name]-[branch])
  • Github ([project-name])
  • Heroku ([project-name]-[branch])
  • Redmine ([project-name])
  • Semaphore CI (servers are named [project-name]-[branch])
  • Drive ([project-name])
  • New Relic ([project-name]-[branch])
  • Get Sentry
  • App name in Rails
  • Sparkpost Account
  • External services (e.g. datatrans)
  • Database names
  • Nginx / Apache
  • Config files
  • Directory names
  • Analytics, Webmaster tools, Adwords
  • Etc…

Security

Email contact

Make it easy for others to let you know about security issues. Please add a security email contact to the HTML footer or the about page of every app. Either use security@<project-domain> or [email protected]. Incoming issues will be treated with high priority by wg-operations.

There is also a .well-known/security.txt file you can provide:

Contact: mailto:[email protected]
Expires: 2050-11-11T13:37:42.000Z
Preferred-Languages: de, en

Cipher suite review

Review your SSL/TLS configuration periodically: https://www.ssllabs.com/ssltest/

Checklist

  • Application name chosen
  • Rails Application created
  • Git Repository created and configured
  • CI configured
  • Server created
  • CD configured
  • App running
  • Linting tools installed
  • RSpec installed
  • Suggested gems included
  • Sentry, Appsignal and/or NewRelic configured depending on your choice.
  • Logs on Appsignal configured
  • Cloudflare configured
  • README written and complete
  • Uptime Monitor configured
  • robots.txt configured

Ruby On Rails - Application Setup Guide

This setup will cover a pure, monolithic Rails Applications. This is the most frequent type of application we have at Renuo and is probably also the easiest to setup. The application (and relative GitHub repo) will be named after the [project-name] you chose before.

Have you chosen a [project-name] yet? If not, please do so now. Check our Naming Conventions

  1. Initialise the Rails Application
  2. Push to Git Repository
  3. Initialise Gitflow
  4. Configure Git Repository
  5. Create an Application Server
  6. Configure the CI / CD

Once here, your app should be up and running on all three environments.

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.

  1. RSpec
  2. Linting and automatic checks
  3. Gems and libraries 💎
  4. Cloudflare
  5. README

🎉 Finally you are ready to start working on you new project! 🎉

While everyone starts working there are some more things which you should setup. Most are not optional, but the rest of the team can start working even if those are not in place yet.

  1. AppSignal
  2. Sentry (optional)
  3. NewRelic (optional)
  4. robots.txt
  5. Percy (optional)
  6. Protect develop environment

Some services should be configured accordingly to the packages bought by the customer. Once the new application is created, please add the project to the monitoring list and discuss with the PO how the service should be configured.

  1. Uptimerobot
  2. Depending on the monitoring list, also Sentry notifications need to be configured.
  3. Depfu security monitoring
  4. Depending on the monitoring list, also Papertrail alerts need to be configured.

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 useful services:

  1. Run Javascript tests with Jest
  2. Pull Requests Template
  3. Slack and Project Notifications
  4. Send emails
  5. Sparkpost & Mailtrap
  6. Devise
  7. Sidekiq
  8. Cucumber
  9. Amazon S3 and Cloudfront
  10. Carrierwave Upload
  11. awesome_print gem 'awesome_print'
  12. bootstrap
  13. font-awesome
  14. bullet gem 'bullet'
  15. lograge gem 'lograge'
  16. Rack Tracker (Google Analytics) gem 'rack-tracker' --> see Google Analytics
  17. Typescript
  18. Favicons
  19. Rack CORS
  20. Rack Attack
  21. 🔥 Hotjar
  22. SEO
    • redirect non-www to www
    • Header tags
  23. wicked pdf gem wicked_pdf
  24. Recaptcha v3
  25. Wallee Payment Integration

Initialise the Rails Application

Default Rails setup

  • Ensure that your asdf plugins are up to date with asdf plugin update --all.

  • Install the latest Ruby version with asdf install ruby latest (Check if it's supported by Heroku).

  • Switch your global Ruby to the fresh one: asdf global ruby latest.

  • Run gem update --system to update Ruby's default gems (e.g. bundler).

  • Check if you are using the latest stable version of Rails with rails -v and update it if you are not. You can do this with gem update rails. Beware of beta versions.

  • Start a new Rails project using

rails new [project-name] --database=postgresql --skip-ci --no-skip-test --skip-action-mailbox --template https://raw.githubusercontent.com/renuo/applications-setup-guide/main/ruby_on_rails/template.rb

where the project-name is exactly the one you chose before.

⚠️ You may want to choose a different database than Postgres, but most of the time this will be your choice.
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 or Angular
You might also need actionmailbox of course, so always double-check the parameters that you are using.

⭐️ This setup does not include either js-bundling nor css-bundling by default.
It will start with the simplest possible Rails setup and will use sprockets and importmaps.
If you need to do fancy stuff, discuss with your team the opportunity of including a js-bundling and css-bundling tool.
We want to go "no build" whenever possible.

  • Run bin/setup

  • Run bundle exec rails db:migrate to generate an empty schema.rb file.

  • Then check your default Rails setup by running rails s and visiting http://localhost:3000. You should be on Rails now, yay!

  • Finally check if http://localhost:3000/up is green.

Adjustments

Some adjustments are made automatically by the template, but you should check them. Some other adjustments must be performed manually.

Automatic adjustments

⭐The Gemfile reads the required ruby version from the .ruby-version file. This is used by Heroku to determine what version to use. 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

⭐️renuocop replaces the default rubocop-rails-omakase. We have our own set of rules at Renuo. You can discuss them at https://github.com/renuo/renuocop and you can also contribute to them.

⭐️a bin/check script is added to the project. This script will run all the tests of the project. It is used in our CI and can be used locally to check if everything is fine. You can customize it to your needs.

⭐️a bin/fastcheck script is added to the project. This script will run all the linters of the project. It is used in our CI and can be customized to your needs. It will be used as a hook before pushing to quickly check for linting issues.

⭐️a bin/run script is added to the project. This script will start the application.

⭐️bin/check, bin/fastcheck and bin/run are standardized tools for more convenience at Renuo.

Manual adjustments

Please perform these adjustments manually:

ENV variables with Figaro

  • Add figaro to Gemfile. Check the gem homepage to see how to install the gem (usually bundle exec figaro install is enough). Delete the newly created file config/application.yml...

  • and create config/application.example.yml where you will specify the only environment variable you need for now: SECRET_KEY_BASE.

  • Going forward we will only push the config/application.example.yml file to the repository in order to protect our env variables.

  • Add application.yml to .gitignore

  • Add the following section to your bin/setup script so that the application.yml is created from the application.example.yml when the project is setup locally:

    puts "\n== Copying sample files =="
    unless File.exist?('config/application.yml')
      system! 'cp config/application.example.yml config/application.yml'
    end
    
  • add one more key to application.example.yml APP_PORT: 3000

    Make sure it comes before any rails comands.

  • To ensure you have all the required keys from the application.example.yml in your application.yml, create the initializer for figaro in config/initializers/figaro.rb:

    Figaro.require_keys(YAML.load_file('config/application.example.yml').keys - %w[test production development])
    
  • Run bin/setup again.

Configuration customisation

  • Update config/application.rb and set the default language and timezone

    config.time_zone = 'Zurich' # may vary
    config.i18n.default_locale = :de # may vary
    
  • Update your config/environments/production.rb settings:

    config.force_ssl = true # uncomment
    config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "warn") # change
    
  • Update config/environments/development.rb settings:

    config.action_controller.action_on_unpermitted_parameters = :raise
    config.i18n.raise_on_missing_translations = true # uncomment
    
  • Update config/environments/test.rb settings:

    config.action_controller.action_on_unpermitted_parameters = :raise
    config.i18n.raise_on_missing_translations = true # uncomment
    config.i18n.exception_handler = Proc.new { |exception| raise exception.to_exception } # add
    config.active_record.verbose_query_logs = true # add
    
    # add the following lines to the end of the file
    config.to_prepare do
      ActiveSupport.on_load(:active_record) do
        ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
      end
    end
    
  • The default Content Security Policies 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.

  • If you're using a js-bundling tool, let's clean up after asset precompilation to reduce Heroku slug size. Add this to the Rakefile:

    Rake::Task['assets:clean'].enhance do
      FileUtils.remove_dir('node_modules', true)
      FileUtils.remove_dir('vendor/javascript', true)
    end
    

Finalising

  • Check if the following scripts run successfully: bin/setup, bin/check, bin/run
  • If they do, commit all your changes to the main branch with Git.

Push to Git Repository

It's now time to push to the git repository and configure our CI and CD to deploy our application on Heroku. To do that you first need to Create a Git Repository.

After creating the repo you can connect your project to the remote git repo (if you didn't use hub create command)

git remote add origin [email protected]:renuo/[project-name].git

and push using:

git add .
git commit -m "Initial commit"
git push -u origin main

Initialise Gitflow

You can initialise gitflow in you project with git flow init -d

Then push also your new develop branch git push --set-upstream origin develop

Once you have pushed all the two branches you can finish the configuration of Git Repository

Configure the Git Repository

These are the suggested configurations for our GitHub repositories. Please stick to it unless you have special needs.

  • Options

    • Features: Remove Wikis, Issues and Projects
    • Merge button: Automatically delete head branches
    • Merge button: Remove Allow merge commits and Allow rebase merging
    • Merge button: Allow auto-merge
  • Manage access

    • Add staff team as a collaborator with Admin access
    • Add security team as collaborator with Write access
  • Branches

    • Default branch: develop. Click update
    • Add these rules for the two branches develop and main:
      • Require pull request reviews before merging
      • Require status checks to pass before merging (after you configured the CI add it to the required checks)
      • Always suggest updating pull request branches
  • Autolink references

    • Add a new Autolink reference with:
      • Reference prefix: TICKET-
      • Target URL: https://redmine.renuo.ch/issues/<num>

Team

Each project has a team owning it. The team is named after the project: [team-name] = [project-name]. Thanks to this we can:

  • see who is responsible for a project;

  • assign issues to the right team;

  • assign pull requests to the right team.

  • Create a team with the name of the project and add all the developers working on it;

  • Give to each team member the role "maintainer";

  • Add the team to the repository with the "administrator" role;

  • Add a CODEOWNERS file with the team name in it:

# .github/CODEOWNERS

* @renuo/[team-name]

Setup Heroku Application

Prerequisites:

Setup the remote configuration

Run the command renuo create-heroku-app [project-name] to generate a script which will create and configure all Heroku apps. [project-name] string length is limited to 22 characters.

Please review the script before running it and execute only the commands you need and understand.

If you don't know what a command does: read the documentation and then execute it.

If you think that the script is outdated please open a Pull Request on the renuo-cli project.

Setup Rails for Heroku

  • Add a file called Procfile to your code root:

    web: bundle exec puma -C config/puma.rb
    

    It's read by Heroku to start the web app and worker jobs.

  • Add a file called .slugignore to your code root:

    /spec
    /.semaphore
    

    Like this you can mark files and folders to be excluded from the Heroku slug.

Configure the CI

At Renuo we always use a CI (Continuous Integration) system to test our applications. It's essential to guarantee that all the tests pass before building and releasing a new version through our CD system. Our projects use SemaphoreCI 2.0.

Before configuring the CI, you should already have a Git Repository with the code, a bin/check command to execute, and the main branches already pushed and ready to be tested.

  1. Proceed to https://renuo.semaphoreci.com/ and login through GitHub with [email protected] (1Password)
  2. Follow these instructions to install semaphore CLI https://docs.semaphoreci.com/reference/sem-command-line-tool/
  3. Create a project here: https://renuo.semaphoreci.com/new_project
  4. Go to the project's artifact settings: Settings > Artifacts
  5. Set the retention policy for project, workflow and job artifacts to /**/* and 2 weeks

Rails specific configuration

renuo configure-semaphore

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.

  1. Add a file called .nvmrc to the project root, where you specify the latest node version
  2. Commit the files to both branches, push and watch the CI run.

When all builds are green, then you have properly configured your CI and CD.

semaphoreci_2

You should now see a third block where your deployment runs to Heroku. Make sure it is green and deploys correctly:

semaphoreci_2

Conclusion

You have now your application running on all the environments. From now on, all the changes you will push on develop or main branches in GitHub will be automatically deployed to the related server.

It's time to create some first Pull Requests with some improvements.

Don't forget to go back to the GitHub settings and add the CI to the required checks!

Setup RSpec

Even though Rails uses Minitest per default, RSpec is the de-facto standard at Renuo. We love RSpec and we strongly suggest to use it.

Add the following gems to your Gemfile:

group :development, :test do
  gem 'factory_bot_rails'
  gem 'rspec-rails'
end

group :test do
  gem 'shoulda-matchers'
  gem 'simplecov', require: false
  gem 'super_diff'
end

You should know exactly why you are adding each one of them and why is necessary.

  • Also add /coverage/ to your .gitignore file.
  • Remove the test folder from your project (there will be one called spec later).

Configuration

  • Install rspec via rails generate rspec:install

  • Create a bin stub with bundle binstubs rspec-core

  • At the top of the spec/spec_helper.rb

    require 'simplecov'
    SimpleCov.start 'rails' do
      add_filter 'app/channels/application_cable/channel.rb'
      add_filter 'app/channels/application_cable/connection.rb'
      add_filter 'app/jobs/application_job.rb'
      add_filter 'app/mailers/application_mailer.rb'
      add_filter 'app/models/application_record.rb'
      add_filter '.semaphore-cache'
      enable_coverage :branch
      minimum_coverage line: 100, branch: 100
    end
    

    to run code coverage and exclude files with less then 5 lines of code.

  • Inside spec/spec_helper.rb we suggest you to uncomment/enable the following:

    config.disable_monkey_patching!
    config.default_formatter = 'doc' if config.files_to_run.one?
    config.profile_examples = 5
    config.order = :random
    Kernel.srand config.seed
    
    config.define_derived_metadata do |meta|
      meta[:aggregate_failures] = true
    end
    

    Please check the spec_helper template

  • Add the configurations:

    # spec/rails_helper.rb:
    
      # after `require 'rspec/rails'`
      require 'capybara/rspec'
      require 'capybara/rails'
      require 'selenium/webdriver'
      require 'super_diff/rspec-rails'
    
      Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
    
      # ... (omitted configs here)
    
      RSpec.configure do |config|
        # ... (omitted configs here)
    
        config.before do |example|
          ActionMailer::Base.deliveries.clear
          I18n.locale = I18n.default_locale
          Rails.logger.debug { "--- #{example.location} ---" }
        end
    
        config.after do |example|
          Rails.logger.debug { "--- #{example.location} FINISHED ---" }
        end
    
        config.before(:each, type: :system) do
          driven_by :rack_test
        end
    
        config.before(:each, type: :system, js: true) do
          driven_by ENV['SELENIUM_DRIVER']&.to_sym || :selenium_chrome_headless
        end
      end
    
    # config/application.example.yml
    test:
      # SELENIUM_DRIVER: 'selenium_chrome'
      SELENIUM_DRIVER: 'selenium_chrome_headless'
    

Please check the full rails_helper template to compare.

  • Add the line bundle exec rspec to bin/check

Note: If you want to debug a spec, you can simply uncomment the line SELENIUM_DRIVER in the application.yml to not run it headless:

CleanShot 2021-06-25 at 16 54 22

✅ Our first (green) test

We are now going to write a first test to ensure that the whole configuration is working:

  • bin/check should be green ✅
  • Write the test spec/system/health_spec.rb
  • Run bin/check and the test should pass and coverage is 100%.

Commit and push your changes! 🎉

⭐️ The default health check path for Rails is /up. Learn more in the Rails guides.
To customize the health check and add additional checks, you can override the Rails::HealthController class.
You can find an example that also checks the database connection in this file.

Verify

Check that you see a green page in each app.

Javascript error reporter

Please check the rails_helper template to compare.

Linting and automatic checks ✅

All Renuo projects contain (and your project must contain as well) the following linters. Every linter consists of a gem (usually) and a command to add to our bin/fastcheck script.

Check out the bin/fastcheck fastcheck for the final version of it.

Renuocop 👮

Renuocop is based on Standard Ruby and is a set of rules that we use to lint our Ruby code. It's already included in your Gemfile by default.

You can execute it and correct the issues you'll find.

bundle exec rubocop -A will fix most all of them automatically.

Brakeman

Brakeman comes by default with Rails. Add it to the bin/fastcheck script.

bundle exec brakeman -q -z --no-summary --no-pager

Mdl

An optional check for markdown files. You can include it or not. Discuss within your team.

group :development, :test do
  gem 'mdl', require: false
end

SCSS lint

To lint the SASS/SCSS files in our project you can use the stylelint npm package.

bin/yarn add stylelint stylelint-config-standard-scss

Add to the project the linter configuration file and check the bin/fastcheck template to see the command to execute the SCSS linting.

Erb lint

group :development, :test do
  gem 'erb_lint', require: false
end

ESLint

yarn add eslint
yarn eslint --init (Use a popular style guide -> Airbnb)

then extend the bin/fastcheck script with:

yarn eslint app/javascript

The templates folder contains a template for the eslint configuration.

All Good!

Now your bin/fastcheck is not that fast anymore 😄

Suggested gems

Here is an hopefully up-to-date version of gems which we strongly suggest to include in your project. Please include them or find a good reason not to.

❗ Please follow the guide of each of these libraries to know how to properly install them.

💡 Do you know all of them? Do you know why we'd like them to be included?

gem 'simple_form'

group :development do
  gem 'better_errors'
  gem 'binding_of_caller'
end

group :development, :test do
  gem 'awesome_print'
  gem 'bullet'
end

group :production do
  gem 'lograge'
end

💡 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.

Cloudflare

Setup Cloudflare: https://www.cloudflare.com/a/dns/renuoapp.ch

Check that you:

  • see "1+2=3" in each app.
  • have been redirected to https.

Crypto settings

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")

Sentry

General configuration

  • Go to https://www.sentry.io and login as the renuo monitor user.

  • Create a project named [project-name].

  • Add the project to the #renuo team if the client pays for monitoring, to the #no-notifications otherwise.

  • Note the DSN key.

    sentry_dsn

  • Set the Heroku environment variables. You can use renuo configure-sentry project-name <SENTRY_DSN> to generate the commands for you.

Backend (Rails)

  • Add sentry gems to the project:

    group :production do
      gem 'sentry-rails'
      gem 'sentry-ruby'
      gem 'sentry-sidekiq' # If the project uses Sidekiq for background jobs
    end
    
  • Add a Sentry initializer to your project config/initializers/sentry.rb.

  • Add # SENTRY_DSN: 'find_me_on_password_manager' to application.example.yml

  • Add # SENTRY_ENVIRONMENT: 'local' to application.example.yml

  • Add # CSP_REPORT_URI to application.example.yml

  • Enable CSP Reporting to Sentry in config/initializers/content_security_policy.rb and allow unsafe inline JS:

    Rails.application.config.content_security_policy do |policy|
      ...
      policy.report_uri ENV['CSP_REPORT_URI'] if ENV['CSP_REPORT_URI']
    end
    

    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.

Frontend (Javascript)

  • Install the npm package: yarn add @sentry/browser
  • Include _sentry.html in your header.
  • Include sentry.js in your packs.

Verify the installation

Ruby

For each Heroku app, connect to the heroku run rails console --app [project-name]-[branch-name] and raise an exception using Sentry:

begin
  1 / 0
rescue ZeroDivisionError => exception
  Sentry.capture_exception(exception)
end

On https://sentry.io/renuo/[project-name] you should find the exception of the ZeroDivisionError.

Javascript

Open the dev console in chrome, and run

try {
    throw new Error('test sentry js');
} catch(e) {
    Sentry.captureException(e)
}

On https://sentry.io/renuo/[project-name] you should find "Uncaught Error: test sentry js".

NewRelic

NewRelic is a service to monitor app performance.

  • Add the following gem to your Gemfile:

    group :production do
      gem 'newrelic_rpm'
    end
    
  • Add a NewRelic configuration file config/newrelic.yml folder. (Note: If you are not using Heroku, adjust the app name to something else than HEROKU_APP_NAME)

  • Add the new variables to your Heroku environments and config/application.yml:

    NEW_RELIC_LICENSE_KEY: "from newrelic"
    

robots.txt

It is time to configure the robots.txt file properly, to avoid crawlers to find our develop environments. The main environment should be the only one searchable in the end.

Make sure that there is a robots.txt file in the public folder or your project (Rails should have created it). This file will only be used in environments where the BLOCK_ROBOTS environment variable is not set. If it is set then a custom middleware catches calls to /robots.txt

Add the following gem:

group :production do
  gem 'norobots'
end

Add the variable BLOCK_ROBOTS=true in your develop environment on Heroku:

heroku config:add BLOCK_ROBOTS=true --app [project-name]-develop

Configure Percy

Percy is a service that recognizes UI changes between pull requests. Read more about it here

Create a new project on the Percy website.

  1. Ask wg-operations to add the project to Percy.
  2. Visit this link.
  3. Fill in the name with [project-name] and select the github repository of the project.

Setup the CI

  1. Add the PERCY_TOKEN and PERCY_PROJECT env variables to the CI. They can be found under https://percy.io/renuo/[project-name]/settings.
  2. Also add PERCY_TARGET_BRANCH and set it to develop. Like that Percy always compares the screenshots to the develop branch.

Setup the Rails application

  1. Add the gem percy-capybara to the test group in the Gemfile and run bundle install.
  2. Follow the setup instructions here
  3. If the application uses the gem vcr, follow the instructions here.

Start using Percy

Create a snapshot in any capybara spec by adding the following line:

Percy::Capybara.snapshot(page, name: 'name of your snapshot')

When to add screenshots

Usually it's enough to add one screenshot for each view. In special cases you may want to add more screenshots.

Encoding

For Percy to render all characters correctly, every page that has a screenshot needs to have the header <meta charset="utf-8">.

Staging Environment Protection

HTTP Basic Authentication should be configured to prevent public traffic on our develop applications

To setup authentication, configure the application controller like that:

class ApplicationController < ActionController::Base
  # ...

  ENV['BASIC_AUTH'].to_s.split(':').presence&.then do |username, password|
    http_basic_authenticate_with name: username, password: password
  end

  # ...
end

Add # BASIC_AUTH: 'admin:some-memorable-password' to application.example.yml, run the following commands:

heroku config:set BASIC_AUTH='admin:[first-memorable-password]' --app [your-app]-develop

and save the passwords in 1Password.

Uptimerobot Monitoring

To ensure that our application is always up and running, we offer a monitoring service to the customers.

When we are still developing a new application, the uptimerobot check should not be setup to avoid premature costs. Once the go-live date is very close, we enable the monitoring only for the main environment, which must have a paid dyno.

Setup

You will need Renuo-CLI to be set up and at the newest version: gem install renuo-cli --> see renuo-cli

  1. Run the command renuo setup-uptimerobot [url]

    • Where url is the address you want to monitor. e.g. https://[project-name]-main.renuoapp.ch/home/check or https://customdomain/home/check
  2. The app will ask for the api-key for uptimerobot. It can be found at the companies' password manager. Paste it and press enter to continue.

The command will setup the project in a paused state. You can start it once your app has a paid dyno.

Until then do not start the monitoring.

Examples

  • renuo setup-uptimerobot https://germann.ch/home/check

Replacing monitors

It's cumbersome to exchange a monitor for all projects. You can utilize this script for that:

require "json"
require "open3"

API_KEY = "XXX"
OFFSET = 0 # default page size is 50

# Use Open3 to capture stdout and stderr to avoid shell-specific parsing issues
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}]'")

unless status.success?
  puts "Error executing command: #{stderr}"
  exit
end

monitors = JSON.parse(monitors_response)
monitors.each do |monitor|
  id = monitor["id"]

  # Add your new monitor here so that it is added to all projects
  monitor["alert_contacts"] << { "id" => raise("TODO: replace this"), "threshold" => 0, "recurrence" => 0 }

  alert_contacts = monitor["alert_contacts"].map { |contact|
    contact.values_at("id", "threshold", "recurrence").compact.join("_")
  }.join("-")

  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")
  puts cmd
end

Sentry

General configuration

  • Go to https://www.sentry.io and login as the renuo monitor user.

  • Create a project named [project-name].

  • Add the project to the #renuo team if the client pays for monitoring, to the #no-notifications otherwise.

  • Note the DSN key.

    sentry_dsn

  • Set the Heroku environment variables. You can use renuo configure-sentry project-name <SENTRY_DSN> to generate the commands for you.

Backend (Rails)

  • Add sentry gems to the project:

    group :production do
      gem 'sentry-rails'
      gem 'sentry-ruby'
      gem 'sentry-sidekiq' # If the project uses Sidekiq for background jobs
    end
    
  • Add a Sentry initializer to your project config/initializers/sentry.rb.

  • Add # SENTRY_DSN: 'find_me_on_password_manager' to application.example.yml

  • Add # SENTRY_ENVIRONMENT: 'local' to application.example.yml

  • Add # CSP_REPORT_URI to application.example.yml

  • Enable CSP Reporting to Sentry in config/initializers/content_security_policy.rb and allow unsafe inline JS:

    Rails.application.config.content_security_policy do |policy|
      ...
      policy.report_uri ENV['CSP_REPORT_URI'] if ENV['CSP_REPORT_URI']
    end
    

    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.

Frontend (Javascript)

  • Install the npm package: yarn add @sentry/browser
  • Include _sentry.html in your header.
  • Include sentry.js in your packs.

Verify the installation

Ruby

For each Heroku app, connect to the heroku run rails console --app [project-name]-[branch-name] and raise an exception using Sentry:

begin
  1 / 0
rescue ZeroDivisionError => exception
  Sentry.capture_exception(exception)
end

On https://sentry.io/renuo/[project-name] you should find the exception of the ZeroDivisionError.

Javascript

Open the dev console in chrome, and run

try {
    throw new Error('test sentry js');
} catch(e) {
    Sentry.captureException(e)
}

On https://sentry.io/renuo/[project-name] you should find "Uncaught Error: test sentry js".

Depfu

Depfu is a service which checks out the Gemfile / yarn.lock of a project for problematic dependencies. It will automatically create a pull request to the project if a security vulnerability has been disclosed.

  1. Ask wg-operations to add repository access for Depfu to you new Github repository.

That's all :-)

Update strategy should be set to Grouped Updates, frequency: monthly. Assignee should be set.

Engine Updates

Enable minor engine updates.

Depfu Engine Updates

Note: If you are using Heroku, the latest Ruby / node version may not yet be available on their platform, so you may need to delay the upgrade. Check the following GitHub repositories to see if Heroku added support already:

Automatic merging

To speed up the merging of smaller upgrades (like security fixes) we enable Automatic merging like this:

Depfu Automatic Merging

Since PRs need to be approved before depfu can merge them we add a GitHub Actions workflow to automatically approve PRs from depfu:

# .github/workflows/depfu_autoapprove.yml
name: Depfu auto-approve
on:
  pull_request_target:
    types: [opened]

permissions:
  pull-requests: write

jobs:
  depfu:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'depfu[bot]' }}
    steps:
      - name: Approve all depfu PRs
        run: gh pr review --approve "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

Run Javascript tests with Jest

When you start writing Javascript code, you have to test it. Webpacker doesn't come (yet) with a default test tool. Here is a configuration suggestion to start testing using Jest.

  • Install Jest
./bin/yarn add --dev jest
  • Add the following to the package.json
  "scripts": {
    "test": "jest --coverage"
  },
  "jest": {
    "roots": [
      "spec/javascripts"
    ],
    "setupFiles": [
      "./spec/javascripts/setup-jest.js"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 100.0,
        "functions": 100.0,
        "lines": 100.0,
        "statements": 100.0
      }
    },
    "coverageDirectory": "./coverage/jest"
  }

This creates a yarn test command which runs your tests, including coverage. It also configures the root of your tests into spec/javascripts folder and the coverage thresholds.

  • Create the file spec/javascripts/setup-jest.js and, if you are using JQuery, add:
import $ from 'jquery';
global.$ = global.jQuery = $;

In this file you create the configuration that is necessary before running the tests.

  • Add the following to your .babelrc configuration file:
"env": {
  "test": {
    "plugins": ["transform-es2015-modules-commonjs"]
  }
}

Now you can run your tests with yarn test and they should fail because you don't have any test.

Add your Javascript tests check run to bin/check:

bin/yarn test
if [ $? -ne 0 ]; then
  echo 'Javascript tests did not run successfully, commit aborted'
  exit 1
fi

Add your tests to the spec/javascripts folder, naming them yourtest.spec.js to be automatically recognised by Jest as tests.

A template for a test could be the following:

// spec/javascripts/my_class.spec.js

import MyClass from '../../app/webpacker/src/javascripts/my_class';

describe('MyClass', () => {

  beforeEach(() => {
    ...
  });

  describe('#amethod', () => {
    it('runs a test', () => {
      new MyClass();
      expect(1).toEqual(2);
    });
  });
});

Slack and Notifications

If you want to keep your team up-to-date with some notifications about the project and to discuss project-related topics we suggest you to use Slack.

You are, for sure, already a member of slack.

Project Channel

You can create a project-dedicated channel on slack naming it #project-[project-name] (e.g. #project-bookshelf, #project-gifcoins, etc..).

Invite all team members involved in the project to the channel.

Deploy Notifications

One notification you may want to receive on this channel is about when a new deployment on main has been performed. In 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 it for you communicating the [project-name].

You must have already setup Automatic Deployment through SemaphoreCI ⚠ If you used Renuo CLI to configure SemaphoreCI, the notifications should be already created. For manual setup, follow these steps:

  1. Open the project Notifications settings (https://renuo.semaphoreci.com/notifications)
  2. Create New Notification
  3. Name of the Notification -> [project-name]
  4. Name of the Rule -> Slack notifications
  5. Branches -> main
  6. Slack Endpoint: Use the Webhook URL from other projects
  7. Send to Slack channel: #project-[project-name]
  8. Save changes

We do not want to pollute the channel with many notifications, therefore we suggest to only send a notification about deployments to production.

📫 Send Emails

As soon as you have to send emails please follow those suggestions. They will help you having a proper system to deliver emails and development environment.

Configuration

  • Add the following to your Gemfile and bundle install
group :development do
  gem 'letter_opener'
end
  • add the following to config/application.example.yml
APP_HOST: '[project-name].localhost'
APP_PORT: '3000'
MAIL_SENDER: 'yourname+<application>@example.com'
MAIL_HOST: ''
MAIL_USERNAME: ''
MAIL_PASSWORD: ''
  • update app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: ENV.fetch('MAIL_SENDER')  # <-- change this
  layout 'mailer'
end
  • add the following to config/application.rb
config.action_mailer.default_url_options = { host: ENV.fetch('APP_HOST'), port: ENV.fetch('APP_PORT') }
  • add the following to config/environments/development.rb:
config.action_mailer.delivery_method = :letter_opener
  • add the following config/environments/production.rb:
config.action_mailer.smtp_settings = {
  address: ENV.fetch('MAIL_HOST'),
  port: 587,
  enable_starttls_auto: true,
  user_name: ENV.fetch('MAIL_USERNAME'),
  password: ENV.fetch('MAIL_PASSWORD'),
  authentication: 'login',
  domain: ENV.fetch('APP_HOST')
}

Sparkpost & Mailtrap

Follow the sparkpost section to configure Sparkpost and Mailtrap on your production environment

SparkPost & Mailtrap

⚠️ Always use subaccounts in Sparkpost!

Otherwise there may be compliance issues which can lead to the closing down of the whole Renuo account.

Introduction

Main (Sparkpost, [email protected])

  • Each app is using a separate subaccount under the main account
  • The Domain should be set up and verified under the subaccount's sending domains
  • Login: [email protected]

Develop (Sparkpost, renuoapp.ch)

  • Each app is using a separate subaccount under the main account

  • The emails will be sent with *@renuoapp.ch as your mail sender

  • Login: [email protected]

  • If you want, you can also use Mailtrap for develop. Create a new Inbox https://mailtrap.io/inboxes and use this credentials

Testing (Mailtrap)

  • Login: [email protected]
  • The Email will be caught by Mailtrap and not forwarded to the intended receiver
  • You can login to Mailtrap to see the sent email

Sparkpost

⚠ Always use subaccounts for the project, so that the whole account doesn't get suspended / blocked in case of compliance issues!

  1. Go to https://app.sparkpost.com/auth and log in with the credentials for sparkpost+enviroment@renuo.ch found in the credential store
  2. Create one new subaccount and name it like your project
  3. Create a new API-Key for your subaccount and assign it to the new subaccount, with the following permissions: Send via SMTP, Sending Domains: Read/Write
  4. Write down the API-key in the credential store (in a list under sparkpost+enviroment@renuo.ch), because it's only showed once!
  5. Credentials for SMTP setup on your app can be found here, password is your generated API-key
  6. (if domain is known) Add your sending domain here. Assign it to the subaccount. Set up SPF, DKIM and DMARC with TXT DNS records (only use renuoapp.ch within the [email protected])
  7. Verify your Email DNS configuration with https://mxtoolbox.com/SuperTool.aspx
  8. 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: '[email protected]', from: ENV['MAIL_SENDER'], subject: 'Testmail', body: 'Mail content').deliver_now!
  9. Send a test email to https://www.mail-tester.com/ or https://www.experte.com/spam-checker and check the result

For DNS setup also see Go Live

ENV-variables example:

MAIL_USERNAME: 'SMTP_Injection'
MAIL_PASSWORD:  'YOUR API KEY'
MAIL_HOST: 'smtp.sparkpostmail.com'
MAIL_SENDER: 'Sample App <[email protected]>'

Or with a custom domain:

MAIL_SENDER: 'Sample App <[email protected]>'

Mailtrap

ENV-variables example:

MAIL_USERNAME: 'found in credential store'
MAIL_PASSWORD:  'found in credential store'
MAIL_HOST: 'smtp.mailtrap.io'
MAIL_SENDER: 'Sample App <[email protected]>'

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: '[email protected]', from: ENV['MAIL_SENDER'], subject: 'Testmail', body: 'Mail content').deliver_now!

Devise

⚠ If you are going to use devise we suggest you to send_emails first. ⚠

  • Add the following gem gem 'devise' and install it
bundle install
rails generate devise:install
  • update config/initializers/devise.rb and set
config.secret_key = ENV['DEVISE_SECRET_KEY']
config.mailer_sender = ENV['MAIL_SENDER']
config.pepper = ENV['DEVISE_PEPPER']
  • add the two variables to the application.example.yml
DEVISE_SECRET_KEY: 'rake secret'
DEVISE_PEPPER: 'rake secret'

Open a pull request! 🎉

Sidekiq

As soon as you have to do work with background jobs please follow those suggestions. They will help you having a proper system to do background work. We use sidekiq because it works well on Heroku and is easy to setup (suckerpunch for example causes memory issues on Heroku).

  • Add the following gem gem 'sidekiq' to the production block and install it

    bundle install
    
  • Update the Procfile and set the worker line, so it looks like this:

    web: bundle exec puma -C config/puma.rb
    worker: bundle exec sidekiq -C config/sidekiq.yml
    
  • Configure the active job adapter in config/environments/production.rb

    config.active_job.queue_adapter = :sidekiq
    
  • Configure the active job adapter in config/environments/development.rb

    config.active_job.queue_adapter = :async
    
  • Configure the active job adapter in config/environments/test.rb

    config.active_job.queue_adapter = :test
    
  • Create a file config/sidekiq.yml, there you can setup your options for sidekiq. It could look something like this. You may need to inform yourself about what queue configuration is right for your project.

    :concurrency: <%= ENV["SIDEKIQ_CONCURRENCY"] || 5 %>
    :queues:
      - [mailers, 1]
      - [default, 1]
    

    Note: if you are on the free Redis plan you may need to limit your concurrency to not exceed the connection limit.

  • On Heroku you need to:

    • Add the Heroku Redis addon
    • And to start it turn on the worker
  • To run Sidekiq locally you need to:

    • Temporarily switch the adapter in config/environments/development.rb to
      config.active_job.queue_adapter = :sidekiq
      
    • Install Redis if not yet installed brew install redis
    • Start Redis redis-server
    • Start Sidekiq bundle exec sidekiq -C config/sidekiq.yml
    • Start your server

Sidekiq-Cron

If you need finer graded control about your scheduled jobs than the Heroku scheduler provides you, you can use Sidekiq-Cron. This way you can for example schedule a job every minute.

  • Add the following gem gem 'sidekiq-cron' and install it

    bundle install
    
  • update/create config/initializers/sidekiq.rb and add the following lines:

    if defined? Sidekiq
      schedule_file = 'config/schedule.yml'
    
      if File.exist?(schedule_file) && Sidekiq.server?
        errors = Sidekiq::Cron::Job.load_from_hash!(YAML.load_file(schedule_file))
        Rails.logger.error "Errors loading scheduled jobs: #{errors}" if errors.any?
      end
    end
    
  • create file config/schedule.yml. There you can specify your jobs. A simple job which runs every 5 minutes could look something like this. (For more options, see the Readme of the gem)

    MyExampleJob:
      cron: "*/5 * * * *"
      class: "MyJob"
      queue: default
    

Sidekiq monitoring

If you want to provide a sidekiq dashboard and see which tasks failed or run through, you can use the Sidekiq monitoring:

  • Update/Create config/initializers/sidekiq.rb and add the following lines:

    if defined? Sidekiq
      require 'sidekiq/web'
    
      Sidekiq::Web.use(Rack::Auth::Basic) do |user, password|
        [user, password] == [ENV['SIDEKIQ_USER'], ENV['SIDEKIQ_PASSWORD']]
      end
    end
    
  • Add SIDEKIQ_USER and SIDEKIQ_PASSWORD as the credentials to the dashboard to your application.example.yml.

  • Add the following line inside routes.rb:

    Rails.application.routes.draw do
      ...
      mount Sidekiq::Web => '/sidekiq' if defined? Sidekiq::Web
      ...
    

Error monitoring

In order to report messages, exceptions or to trace events, it is recommended to install the sentry-sidekiq gem.

Cucumber

Cucumber is a testing framework which allows us to specify feature tests using plain language. We use Cucumber for e2e tests in some of our projects. We integrate it using the cucumber-rails gem.

Installing cucumber-rails

The installation process of cucumber-rails is also documented on its Github page.

To install cucumber-rails, add the following gems to your Gemfile:

group :test do
    gem 'capybara', require: false
    gem 'cucumber-rails', require: false
    gem 'database_cleaner'
end

Run bundle install to install the gems.

Finally, set up Cucumber with

rails generate cucumber:install

At this time, it is recommended to inspect the generated files and commit. Before committing, make sure, Rubocop does not raise any issues. Therefore, you will have to add the following exception to .rubocop.yml:

AllCops:
  Exclude:
    - 'lib/tasks/cucumber.rake'

Running your first Cucumber test

If you add Cucumber to an existing project, test a real page instead of using the given example test.

Add the following files:

Now, you can run Cucumber in your terminal:

cucumber

You may get a couple of deprecation warnings. There is currently an issue about them on the Github page of rspec-rails

Adding Cucumber tests to bin/check

Add the following code to bin/check, so the Cucumber tests are run on CI:

NO_COVERAGE=true bundle exec cucumber --format progress
if [ $? -ne 0 ]; then
  echo 'Cucumber did not run successfully, commit aborted'
  exit 1
fi

Assure that bin/check fails, if you have a broken Cucumber test.

Custom environment

Probably, you are going to need the following configuration files in /features/env:

  • capybara.rb enables headless Chrome. This is needed in Cucumber tests which require Javascript (i.e. scenarios tagged with @javascript).
  • database_cleaner.rb sets up database_cleaner for Cucumber tests.
  • factory_bot.rb makes FactoryBot's methods (like build and create) accessible in the step definition file.
  • warden.rb sets up the test environment, if you are using Warden (i.e. if you are using Devise).

Amazon Services

The following Amazon services are involved in our app setups

  • Amazon S3 is Amazon's Simple Cloud Storage Service, and used in most of your projects to store images and files.
  • Amazon CloudFront is a large scale, global, and feature rich CDN. We mostly use it together with S3 to provide a proper HTTP endpoint (caching, header forwarding, etc.). You could also host a Single Page Application (SPA).
  • Amazon ACM issues certificates which can be used for custom Cloudfront distribution domains
  • Amazon IAM issues resource policies.
    • We use a special "renuo-app-setup" user to setup our projects.
    • Each app has an own user to separate tenants properly. The user belongs to "renuo-apps-v2"
    • You can find a graphical overview in this lightning talk.

Setup

Preconditions

renuo-cli

You will need Renuo-CLI to be set up and at the newest version:

gem install renuo-cli --> see renuo-cli

Make sure renuo -v shows the newest version

aws-cli

Retrieve the credentials "AWS Profile 'renuo-app-setup' for s3 setup" from the password manager at first (or ask wg-operations for help).

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. Or you can install it manually:

brew install awscli
aws configure --profile renuo-app-setup

If you want to check your config, run aws configure --profile renuo-app-setup list.

We would recommend setting default region name to eu-central-1. The default output format is json and should not be changed.

Command generation

The following command will generate command-line-commands to set up S3 and CloudFront. You'll need to run them by yourself after reviewing the output.

  1. Run renuo create-aws-project

  2. Follow the steps and answer the questions

  3. Now it will print you out a series of commands e.g.:

    # AWS main
    
        aws --profile renuo-app-setup iam create-user --user-name <<your-project>>
        [...]
    
    # AWS develop
    [...]
    
  4. Review the commands carefully (e.g. make sure that you enable bucket versioning)

Executing the commands

Running the commands will print some JSON pages to your screen. Copy each SecretAccessKey and AccessKeyId to your credentials store!

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.

Custom Cloudfront Distribution CNAME Aliases

If you want to serve your S3 bucket via a custom domain, you need to add the CNAMEs to your Cloudfront Distibution manually.

  1. Visit https://us-east-1.console.aws.amazon.com/cloudfront/v3/home?region=eu-central-1#/distributions and edit your distribution.
  2. Enter the CNAMEs as aliases
  3. Click "Request certificate" (this opens a new tab with Amazon ACM, make sure it's region is us-east-1)
  4. Enter all the CNAMEs you want to have as aliases (normally only one)
  5. Enter the domain ownership verification records into Cloudflare (CNAME with cryptic values)
  6. Submit the ACM form and wait for the certificate to being issued.
  7. Go back to the Cloudfront distribution, refresh the certifactes drop down and choose your new certificate.
  8. Save the changes to the Cloudfront distribution.

Rails Configuration

You then can use an ActiveStorage configuration like this:

amazon:
  service: S3
  access_key_id: <%= ENV['AWS_S3_ACCESS_KEY_ID'] %>
  secret_access_key: <%= ENV['AWS_S3_SECRET_ACCESS_KEY'] %>
  bucket: <%= ENV['AWS_S3_BUCKET'] %>
  region: "eu-central-1"

Carrierwave

⚠️ Don't use Carrierwave for new projects anymore. Use ActiveStorage

Carrierwave is used to upload files to S3.

  1. Add gem 'carrierwave' and gem 'fog-aws'
  2. In environment.rb add require 'carrierwave/orm/activerecord'
  3. Add to your application.example.yml:
#CARRIERWAVE_SALT: 'rake secret'
#S3_ACCESS_KEY_ID: ''
#S3_ASSET_HOST: ''
#S3_BUCKET_NAME: ''
#S3_SECRET_ACCESS_KEY: ''
  1. Add initializers/carrierwave.rb
CarrierWave.configure do |config|
  if Rails.env.test?
    config.storage = :file
    config.enable_processing = false
  elsif ENV['S3_BUCKET_NAME'].present?
    config.fog_provider = 'fog/aws'
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: ENV['S3_ACCESS_KEY_ID'],
      aws_secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
      region: 'eu-central-1'
    }
    config.fog_directory = ENV['S3_BUCKET_NAME']
    config.fog_public = true
    config.fog_attributes = { cache_control: "public, max-age=#{365.days.to_i}" }
    config.storage = :fog
    config.asset_host = ENV['S3_ASSET_HOST']
  else
    config.storage = :file
  end
end
  1. The UploaderBasepath is used, so we don't polute the public/uploader folder in tests. Therefore you have to add this:
module UploaderBasepath
  extend ActiveSupport::Concern
  TEST_UPLOAD_PATH = 'uploads/tmp'

  included do
    private

    # :nocov:
    def base_path_helper
      return TEST_UPLOAD_PATH if Rails.env.test?

      'uploads'
    end
    # :nocov:
  end
end

And also we want to clear the folder after the tests run in a spec/support/carrierwave.rb

RSpec.configure do |config|
  config.after(:suite) do
    FileUtils.rm_rf(Dir[Rails.root.join('public', UploaderBasepath::TEST_UPLOAD_PATH)])
  end
end

  1. The SecurelyUploadable makes sure, that your path is not guessable:
module SecurelyUploadable
  extend ActiveSupport::Concern
  include UploaderBasepath

  included do
    def filename
      "#{time_token}#{file.extension.present? ? ".#{file.extension}" : ''}"
    end

    def store_dir
      "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{unguessable_id}"
    end

    private

    def time_token
      var = :"@#{mounted_as}_time_token"
      model.instance_variable_get(var) || model.instance_variable_set(var, (Time.now.to_f * 1000).to_i)
    end

    def unguessable_id
      secret = [ENV['CARRIERWAVE_SALT'], model.id].join('/')
      Digest::SHA256.hexdigest(secret)
    end
  end
end
  1. Implement an uploader e.g.
class PictureUploader < CarrierWave::Uploader::Base
  include SecurelyUploadable

  # add different sizes like that:
  # version :preview do
  #   process resize_to_fill: [100, 100]
  # end
end
  1. We recommend to use image processing only when needed, since it's slow. Therefore the initializer sets it to false. But for system-specs we want to enable it, so we see how it would look like (also if used with snapshot comparison tools):
  • enable it for system specs:
config.around(type: :system) do |example|
  CarrierWave.configure { |c| c.enable_processing = true }
  example.run
  CarrierWave.configure { |c| c.enable_processing = false }
end

Some further use cases

One image per model

This case is well documented in the carrierwave docs

Multiple images per model

This example will have a model User with multiple Pictures

  1. Add a model with the image instance:
class UserPicture < ApplicationRecord
  mount_uploader :picture, PictureUploader
  belongs_to :user, inverse_of: :user_pictures
end
  1. Let the model accept nested attributes:
class User < ApplicationRecord
  has_many :user_pictures, inverse_of: :user, dependent: :destroy
  accepts_nested_attributes_for :user_pictures, allow_destroy: true
end
  1. Simplify access to the pictures in User
class User < ApplicationRecord
  def pictures
    user_pictures.map(&:picture)
  end
end
  1. Your controller wants to accept new files but also mark some as deleted in the same view:
params.require(:user).permit(*permitted_params).tap do |permitted_params|
  permitted_params[:user_pictures_attributes] = merged_picture_attributes(permitted_params)
end

def merged_picture_attributes(permitted_user_params)
  new_pictures = params.dig(:user, :new_user_pictures_attributes)
  existing_pictures = permitted_user_params[:user_pictures_attributes] || {}
  return existing_pictures if new_pictures.nil?

  existing_pictures.values.push(*new_pictures.map(&:permit!))
end
  1. And a simple form looks then like that:
= form.file_field nil, name: 'user[user_pictures_attributes][][picture]', multiple: true
- if user.user_pictures.any?
  ul
    = form.simple_fields_for :user_pictures do |picture|
      - pic = picture.object
      li
        = picture.input :id, as: :hidden, input_html: { value: pic.id }, wrapper: false
        = image_tag pic.picture.url(:thumb)
        = picture.input :_destroy, as: :boolean, label: t('buttons.destroy')

Store height and with (useful for galleries, where you need to know the size)

  1. Add gem 'mini_magick' if you plan to resize images (quite always needed)
  2. Extend your uploader:
class PictureUploader < CarrierWave::Uploader::Base
  ...
  include CarrierWave::MiniMagick
  ...

  process :store_dimensions

  ...

  private

  def store_dimensions
    model.width, model.height = ::MiniMagick::Image.open(file.file)[:dimensions] if file && model
  end
end

Bootstrap

You can use the npm package of the latest version of Bootstrap.

yarn add bootstrap

and include it in your stylesheet pack with:

@import '~bootstrap/scss/bootstrap';

If you want to use also the javascript part of Bootstrap you need both popper.js and jquery. Add them with:

yarn add jquery popper.js

and configure them in environment.js:

const webpack = require('webpack')
environment.plugins.append('Provide', new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery',
  Popper: ['popper.js', 'default']
}));

Change extract_css: false to extract_css: true in config/webpacker.yml

and finally, import bootstrap library in application.js with

import 'bootstrap/dist/js/bootstrap';

Simple Form

If you use the gem Simple Form, you need to adjust the configuration in the config/initializers/simple_form.rb file.

Here are some recommended options:

config.wrappers :default, class: 'form-group',
                hint_class: :field_with_hint, error_class: 'has-danger' do |b|
config.error_notification_class = 'alert alert-danger'
b.use :error, wrap_with: { tag: :span, class: 'invalid-feedback' }
config.label_class = 'form-control-label'
config.input_class = 'form-control'

To make the error highlighting work you need to add some css to your application

.invalid-feedback {
  display: block;
}

.has-danger {
  .form-control {
    border-color: $form-feedback-invalid-color;
  }
}

Please note that this is a workaround, as there is yet no way to add an error class directly onto an input. However, there is an open issue on Simple Form: https://github.com/plataformatec/simple_form/pull/147 Once this feature is added, please remove the css.

For the styling of the pull down date selectors or checkboxes, you need to write some wrappers, that you can add to the input element. It is best to create a separate config file for this.

Once the issue https://github.com/plataformatec/simple_form/pull/1337 is done, you can also configure simple form with the command rails generate simple_form:install --bootstrap4.

# config/initializers/simple_form_bootstrap.rb

SimpleForm.setup do |config|
  config.wrappers :inline_date, tag: 'div', error_class: 'has-danger' do |b|
    b.use :html5
    b.use :label, class: 'control-label'
    b.wrapper tag: 'div', class: 'form-inline row' do |ba|
      ba.use :input, class: 'form-control form-inline', wrap_with: { class: 'col-md-6' }
      ba.use :error, wrap_with: { tag: 'span', class: 'invalid-feedback' }
      ba.use :hint, wrap_with:  { tag: 'p', class: 'help-block' }
    end
  end

  config.wrappers :inline_checkbox, tag: 'div', class: 'control-group', error_class: 'has-error' do |b|
    b.use :html5
    b.wrapper tag: 'div', class: 'controls' do |ba|
      ba.use :label_input, wrap_with: { class: 'checkbox inline' }
      ba.use :error, wrap_with: { tag: 'span', class: 'help-inline' }
      ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
    end
  end

  config.wrapper_mappings = {
    check_boxes: :inline_checkbox,
    date: :inline_date,
    datatime: :inline_date
  }
end

Font-Awesome

You can either include font-awesome through their CDN or install it via npm/yarn.

Installation with yarn

Set up

PRO version

  1. Look up the auth token which can be found here. The credentials can be found on passwork
  2. Follow these steps to set up font-awesome pro either per project or globally.

Free

For the free version of font-awesome 5 just run yarn add @fortawesome/fontawesome-free

Inclusion for Webpacker

Include this code in your stylesheets.scss

$fa-font-path: '~@fortawesome/fontawesome-free/webfonts';
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
@import '~@fortawesome/fontawesome-free/scss/solid';

Usage

  1. Navigate to the font-awesome gallery list
  2. Search for the icon that you wish to include and copy paste the <i> tag into your application: For example for the Angular symbol I can just use this tag: <i class="fab fa-angular"></i>

Bullet

We don't like N+1 queries. Nobody does. If you don't know what we are talking about, please read this article that explains it pretty well.

In order to identify possible N+1 queries, we use the gem bullet.

Please add the gem to both development and test group of the Gemfile since we'll use it also in our tests.

Those are our favourite configurations:

  • in development we enable it and we see the issues both a footer in the page and also in the logs
# config/environments/development.rb

config.after_initialize do
  Bullet.enable = true
  Bullet.bullet_logger = true
  Bullet.add_footer = true
end
  • in test we enable it and raise an exception in case a N+1 is identified (or an unused eager loading)
# config/environments/test.rb

config.after_initialize do
  Bullet.enable = true
  Bullet.bullet_logger = true
  Bullet.raise = true
end

Logging & AppSignal

AppSignal is a service to record logs, monitor errors and performance. Recording logs works independently from the tech stack. So you should use AppSignal to record logs even if you don't use Rails. In Heroku we'll add a log drain to redirect the multiplexed Logplex logs to AppSignal in any case.

With the AppSignal agent

If you want to log errors and metrics, you need to install the AppSignal agent into your app. See integration instructions for Ruby/Rails.

  • Add the following gem to your Gemfile:

    gem 'appsignal'
    
  • Add a AppSignal configuration file config/appsignal.yml folder.

  • Add the new variables to your Heroku environments:

    APPSIGNAL_APP_ENV: "main | develop"
    APPSIGNAL_APP_NAME: "project name without env"
    APPSIGNAL_IGNORE_ERRORS: "ActiveRecord::RecordNotFound,ActionController::UnknownFormat"
    APPSIGNAL_PUSH_API_KEY: "from appsignal"
    

We use the same push key for all apps. You can either copy it from another project or "create" an app on appsignal. This will just show you the key and tell you to deploy the app with it for the app to be created.

Once you deploy the app and collect data the app will show up in the appsignal dashboard. Navigate to Logging -> Manage Resources and Add log resource with these settings:

SettingValue
Source nameRails
PlatformHeroku Log Drain
Message formatlogfmt

Then add this ingestion endpoint as a log drain using the Heroku commands displayed.

Only Logs

Choose the "JavaScript" option on the AppSignal project page to setup your project without an active agent.

Configuration adjustments

Correct Severity

According to the docs, getting the severity to be anything but "INFO" is not possible using the heroku drain.

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.

# frozen_string_literal: true

if Rails.env.production?
  module ActiveSupport
    module TaggedLogging
      module Formatter
        def call(severity, timestamp, progname, msg)
          logfmt_msg = ["severity=#{severity}", tags_text, msg].compact.join(' ')
          super(severity, timestamp, progname, logfmt_msg)
        end

        def tags_text
          current_tags.map do |tag|
            if tag.is_a? Hash
              tag.map { |k, v| "#{k}=#{v}" }
            else
              "[#{tag}]"
            end
          end.flatten.join(' ')
        end
      end
    end
  end
end

and

# config/environments/production.rb
Rails.application.configure do
  # We use our custom key value tagging
  config.log_tags = [->(request) { { request_id: request.request_id } }]
  logger           = ActiveSupport::Logger.new(STDOUT)
  config.logger    = ActiveSupport::TaggedLogging.new(logger)
end

Lograge

We use lograge in many projects. Here is how to configure it with AppSignal to get properly tagged logs.

Using this configuration we get the fully tagged lograge lines and also the full stack trace with each line tagged with the request id. This allows us to filter by request id with one click and get all relevant log data at once.

With AppSignal gem

# config/initializers/lograge.rb
if ENV['LOGRAGE_ENABLED'] == 'true'
  Rails.application.configure do
    config.lograge.enabled = true
    config.lograge.keep_original_rails_log = true
    config.lograge.logger = Appsignal::Logger.new(
      "rails",
      format: Appsignal::Logger::LOGFMT
    )
    config.lograge.custom_payload do |controller|
      {
        request_id: controller.request.request_id
      }
    end
  end
end

Without AppSignal gem

# config/environments/production.rb
Rails.application.configure do
  …

  if ENV['RAILS_LOG_TO_STDOUT'].present?
    config.log_tags = [->(request) { { request_id: request.request_id } }]
    logger          = ActiveSupport::Logger.new($stdout)
    config.logger   = ActiveSupport::TaggedLogging.new(logger)
  end
end
# config/initializers/lograge.rb
Rails.application.configure do
  …

  if ENV['LOGRAGE_ENABLED'] == 'true'
    config.lograge.enabled = true
    config.lograge.keep_original_rails_log = true
  end
end

Automation

Unfortunately Appsignal doesn't provide an API for project configuration. So if you need to do something on a lot of projects, you have to do it manually.

Project creation can be automated though with the following script. Run it in a tmp folder. It writes into a file on disk.

#!/usr/bin/env ruby
require 'optparse'
require 'appsignal'
require 'appsignal/demo'

PUSH_API_KEY = "XXX"

options = {}
OptionParser.new do |opt|
  opt.on('--env APPSIGNAL_ENV') { |o| options[:env] = o }
  opt.on('--name APPSIGNAL_NAME') { |o| options[:name] = o }
end.parse!

raise OptionParser::MissingArgument if options[:env].nil? || options[:name].nil?

File.write 'config/appsignal.yml', <<~YAML
  #{options[:env]}:
    active: true
    push_api_key: #{PUSH_API_KEY}
    name: "#{options[:name]}"
YAML

Appsignal.config = Appsignal::Config.new(Dir.pwd, options[:env])
Appsignal::Demo.transmit

🔥 Hotjar

  • Add a new site on the Hotjar dashboard using the Renuo Hotjar account (credentials are in Passwork). Note the site ID of the generated site.

  • Add the following gem to your Gemfile:

group :production do
  gem 'rack-tracker'
end
  • Configure the tracker in production.rb:
if ENV['HOTJAR_SITE_ID'].present?
    config.middleware.use(Rack::Tracker) do
      handler :hotjar, site_id: ENV['HOTJAR_SITE_ID']
    end
end

Wicked PDF

Can be used to generate PDFs and supports HTML to PDF.

⚠ Up to now, Wicked PDF does not support Bootstrap 4, so if you want to use Bootstrap 4 Templates, use another library ⚠

  • Add gem 'wicked_pdf' to the main section of Gemfile
  • Add gem 'wkhtmltopdf-binary' to group :development, :test
  • Add gem 'wkhtmltopdf-heroku' to group :production

By default, it adds no layout, you you may want to add a layout:

  • Create a pdf.pdf.erb
  • Use the method to add stylesheets : wicked_pdf_stylesheet_link_tag 'pdf', media: 'all'
  • Use the method wicked_pdf_image_tag to insert images to the layout.

Usage:

render pdf: <<filename>>, print_media_type: true, layout: 'pdf', disposition: 'attachment'

⚠ Consider using a job-runner to not block the server during creation of PDFs.

Heroku

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. If you need Arial, you can add the following buildpack on Heroku:

https://github.com/propertybase/heroku-buildpack-fonts

⚠ Make sure you add this as a first buildpack before heroku/ruby!

reCAPTCHA v3

Here you will learn how to implement reCAPTCHA v3 into a project. It is pretty simple to use and doesn't hinder the users at all, as it all runs in the background.

Implementation

Before you start, consider checking which page you would like to protect against suspicious activity (like spambots).

Site registration

After you've decided, you can go to the reCAPTCHA admin panel and register a new site. Make sure you're using the Renuo admin account.

To register the site, you can follow these steps:

  1. Use [project-name] as a label for the site.
  2. Choose reCAPTCHA v3
  3. Add all the domains on which reCAPTCHA should work (renuoapp.ch should also be included for the develop environment)

After you've provided all the necessary data, you will be forwarded to a page where you will find the site and secret keys. These will be needed later on (you can always find them in the site settings).

Frontend implementation

For the frontend implementation, you can follow these steps and integrate reCAPTCHA into the chosen view.

Backend implementation

For the validation to work, you will also need some backend code which will call the reCAPTCHA verification API, 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 to do it.

If you have trouble integrating reCAPTCHA, you can find some inspiration by checking out one of these pull requests:

  1. Kingschair
  2. Germann

Testing

In order to test it, you can add localhost as a domain in the reCAPTCHA admin panel and provide the required keys in the application.yml. If your integration worked, then you will see a small container in the bottom right corner of your chosen page, something like this: reCAPTCHA demonstration

Frontend apps

Trivial app

Consider using only static files (example: Green Card Predictor) and CDNs.

Simple JS app

If you need a really simple JavaScript application and you need more than just an index.html, you can checkout this template: https://github.com/renuo/tamedia-altersheime/tree/dab2bb6f1bb4c7776e965d227de5b63e06240624

It contains a very simple webpack configuration (production ready) featuring:

  • Cucumber tests
  • Fonts
  • Images
  • Favicon

The rest is done in these three files:

  • index.html
  • index.css
  • index.js

Vue.js

An alternative to the JS app mentioned above is a setup with Vue.js. It provides a scalable foundation for your app and it's easy to understand.

To get started, install the vue-cli by running npm install -g @vue/cli. Then run vue create [app-name] to generate a vue app. This creates a minimal setup with some default configurations and if you wish, with some testing frameworks included like Cypress or Jest.

An example Vue.js application can be found here.

Complex JS app

Use either React or Angular.

React Native

This guide discusses the steps from initialisation of the app up until the release. Some steps are the same as for the Rails app.

  1. Initialise the React Native app.
    react-native init [project-name] --template typescript
    
  2. Push to Git Repository
  3. Initialise Gitflow
  4. Configure CI
  5. Release

Now the app should be downloadable via Testflight / Play store. Other recommended tools:

  1. Jest
  2. Recommended libraries
  3. Linting and automatic checks
  4. README

Optional steps:

  1. Sentry

Push to Git Repository

It's now time to push to the git repository and configure our CI and CD to deploy our application on Heroku. To do that you first need to Create a Git Repository.

After creating the repo you can connect your project to the remote git repo (if you didn't use hub create command)

git remote add origin [email protected]:renuo/[project-name].git

and push using:

git add .
git commit -m "Initial commit"
git push -u origin main

Initialise Gitflow

You can initialise gitflow in you project with git flow init -d

Then push also your new develop branch git push --set-upstream origin develop

Once you have pushed all the two branches you can finish the configuration of Git Repository

Continuous Integration

Just as for Rails apps, we're using Semaphore as CI also for React Native apps. Use the same base configuration.

Release

iOS

Build configuration

Open the Xcode project under ios/[project-name].xcworkspace.

  • Create a Release Staging build configuration.

xcode_configurations

Click on Duplicate using Release configuration.

  • Go to Product -> Scheme -> Manage schemes.

  • Duplicate the scheme [project-name]. Edit it. Select Shared and make sure you have selected only Run, Profile, and Archive for [project-name] target. Go through each build step and select Release Staging as build configuration.

  • Go to the Build Settings and click on All and Combined filters.

    • Look for Product Bundle Identifiers and add .dev for Debug and .staging for Release Staging.
    • Change the Provisioning Profile to match the same identifiers.
    • Add BUNDLE_DISPLAY_NAME under User-Defined variables. Set app names for each variant.

Fastlane is using the staging scheme to deploy the staging version of the app.

Android

Prepare your repo like in this commit: https://github.com/renuo/citymessenger/pull/182/commits/5b7bdc7d2137ffd885ed8fa8f56be1aa3ee550e2

Default configuration

defaultConfig {
    applicationId "ch.renuo.[project-name]"
    ...
    versionCode Integer.parseInt(System.getenv("BITRISE_BUILD_NUMBER") ?: VERSION_CODE)
    versionName System.getenv("VERSION_NUMBER") ?: VERSION_NUMBER
    resValue "string", "build_config_package", "ch.renuo.[project-name]"
}

Staging build type

In android/app/build.gradle:

Define staging build type under buildTypes:

buildTypes {
    debug {
       applicationIdSuffix ".dev"
    }
    release {
        minifyEnabled enableProguardInReleaseBuilds
        proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        signingConfig signingConfigs.release
    }
    releasestaging {
        initWith release
        applicationIdSuffix ".staging"
        matchingFallbacks = ['release', 'debug']
    }
}

Set up env files mapping at the top of the file:

project.ext.envConfigFiles = [
    debug: ".env",
    release: ".env",
    releasestaging: ".env.staging"
]

Signing

Add the following files to .gitignore:

android/keystore.properties
android/key.json

In android/app/build.gradle:

Add the release signing config under signingConfigs:

release {
    if (keystorePasswords['[PROJECT-NAME]_RELEASE_STORE_PASSWORD'] &&
            keystorePasswords['[PROJECT-NAME]_RELEASE_KEY_PASSWORD']) {
        storeFile file([PROJECT-NAME]_RELEASE_STORE_FILE)
        storePassword keystorePasswords['[PROJECT-NAME]_RELEASE_STORE_PASSWORD']
        keyAlias [PROJECT-NAME]_RELEASE_KEY_ALIAS
        keyPassword keystorePasswords['[PROJECT-NAME]_RELEASE_KEY_PASSWORD']
    }
}

Add these variables in android/gradle.properties:

[PROJECT-NAME]_RELEASE_STORE_FILE=../keystores/[project-name].jks
[PROJECT-NAME]_RELEASE_KEY_ALIAS=key0
VERSION_CODE=1
VERSION_NUMBER=0.0.1

and load the keystore.properties file:

def keystorePasswordsFile = rootProject.file("keystore.properties")
def keystorePasswords = new Properties()
keystorePasswords.load(new FileInputStream(keystorePasswordsFile))

Prepare three new passwords and add them to the password manager:

  • App encrypted secrets password
  • Android keystore password
  • Android keystore key0

Create a file keystore.properties and save the two passwords "Android keystore password" and "Android keystore key0" there in the following form:

APP_RELEASE_STORE_PASSWORD=<Android keystore password>
APP_RELEASE_KEY_PASSWORD=<Android keystore key0>

Encrypt this file with the "App encrypted secrets password" to be checked into the git repo:

openssl enc -aes-256-cbc -salt -a -in android/keystore.properties -out android/keystore.properties.enc

Create a signing key using "Android keystore password" and "Android keystore key0" with the key tool:

keytool -genkey -v -keystore [project-name].jks -alias key0 -keyalg RSA -keysize 2048 -validity 10000

You need to log in to https://play.google.com/apps/publish/ with the [email protected] account) and create the app there. You will need much more information for the store listing than for iOS, they require also the screenshots, etc., otherwise it’s not possible to even make the internal testing build.

  • Enable App Signing by Google Play.

  • Add a service account to Google and generate a key.json file (no other configurations), download it and encrypt it with the "App encrypted secrets password".

openssl enc -aes-256-cbc -salt -a -in android/key.json -out android/key.json.enc
  • Add the service account to the app users with the role release manager.

When it’s there, it should also just work with bundle exec fastlane android deploy

Jest

Create a jest.config.js file in the project root with the following contents:

Install some test libraries:

yarn add enzyme-adapter-react-16 enzyme enzyme-to-json
yarn add -D @types/enzyme @types/enzyme-adapter-react-16 jest-fetch-mock
const { defaults: tsjPreset } = require('ts-jest/presets');

module.exports = {
  ...tsjPreset,
  preset: 'react-native',
  testEnvironment: 'jsdom',
  transform: {
    ...tsjPreset.transform,
    '\\.js$': '<rootDir>/node_modules/react-native/jest/preprocessor.js',
  },
  globals: {
    'ts-jest': {
      diagnostics: true,
      babelConfig: true,
    }
  },
  testPathIgnorePatterns: [
    "<rootDir>/e2e/",
    "<rootDir>/node_modules/"
  ],
  snapshotSerializers: ["enzyme-to-json/serializer"],
  testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
  moduleFileExtensions: [
    "ts",
    "tsx",
    "js",
    "jsx",
    "json",
    "node"
  ],
  resetMocks: true,
  coverageReporters: [
    "json",
    "lcov",
    "text-summary"
  ],
  transformIgnorePatterns: [
    "!node_modules"
  ],
  modulePaths: [
    "<rootDir>"
  ],
  setupFiles: [
    "./tests/setup.tsx"
  ],
  collectCoverage: true,
  cacheDirectory: ".jest/cache",
  coveragePathIgnorePatterns: [
    "<rootDir>/node_modules/",
    "<rootDir>/locales/",
    "<rootDir>/e2e",
    "<rootDir>/tests",
    "(.*)/index.ts"
  ],
  coverageThreshold: {
    global: {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100
    }
  },
  reporters: ["default", "jest-junit"],
};

Under tests/setup.tsx create the following file:

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import jestFetchMock from 'jest-fetch-mock';
(global as any).fetch = jestFetchMock;

configure({ adapter: new Adapter() });

Recommended libraries

Linting and automatic checks ✅

All Renuo projects contain (and your project must contain as well) the following linters. Every linter consists of a package (usually) and a command to add to our bin/check script.

ESLint

React Native comes with ESLint by default. We configure the rules to add stricter checks by extending .eslintrc.js:

module.exports = {
    "env": {
        "es6": true
    },
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "project": "tsconfig.json",
        "sourceType": "module"
    },
    "plugins": [
        "@typescript-eslint",
        "@typescript-eslint/tslint"
    ],
    "rules": {
        "@typescript-eslint/adjacent-overload-signatures": "error",
        "@typescript-eslint/array-type": "error",
        "@typescript-eslint/ban-types": "error",
        "@typescript-eslint/class-name-casing": "error",
        "@typescript-eslint/consistent-type-assertions": "off",
        "@typescript-eslint/consistent-type-definitions": "error",
        "@typescript-eslint/explicit-member-accessibility": [
            "error",
            {
                "accessibility": "explicit"
            }
        ],
        "@typescript-eslint/indent": [
            "error",
            4,
            {
                "ObjectExpression": "first",
                "FunctionDeclaration": {
                    "parameters": "first"
                },
                "FunctionExpression": {
                    "parameters": "first"
                }
            }
        ],
        "@typescript-eslint/interface-name-prefix": "off",
        "@typescript-eslint/member-delimiter-style": [
            "error",
            {
                "multiline": {
                    "delimiter": "semi",
                    "requireLast": true
                },
                "singleline": {
                    "delimiter": "semi",
                    "requireLast": false
                }
            }
        ],
        "@typescript-eslint/member-ordering": "error",
        "@typescript-eslint/no-empty-interface": "off",
        "@typescript-eslint/no-explicit-any": "off",
        "@typescript-eslint/no-misused-new": "error",
        "@typescript-eslint/no-namespace": "error",
        "@typescript-eslint/no-parameter-properties": "off",
        "@typescript-eslint/no-this-alias": "error",
        "@typescript-eslint/no-use-before-define": "off",
        "@typescript-eslint/no-var-requires": "error",
        "@typescript-eslint/prefer-for-of": "error",
        "@typescript-eslint/prefer-function-type": "error",
        "@typescript-eslint/prefer-namespace-keyword": "error",
        "@typescript-eslint/quotes": [
            "error",
            "single",
            {
                "avoidEscape": true
            }
        ],
        "@typescript-eslint/semi": [
            "error",
            "always"
        ],
        "@typescript-eslint/triple-slash-reference": "error",
        "@typescript-eslint/type-annotation-spacing": "error",
        "@typescript-eslint/unified-signatures": "error",
        "arrow-body-style": "error",
        "arrow-parens": [
            "off",
            "as-needed"
        ],
        "camelcase": "error",
        "comma-dangle": [
            "error",
            "always-multiline"
        ],
        "complexity": "off",
        "constructor-super": "error",
        "curly": "error",
        "dot-notation": "error",
        "eol-last": "error",
        "eqeqeq": [
            "error",
            "smart"
        ],
        "guard-for-in": "error",
        "id-blacklist": "error",
        "id-match": "error",
        "import/no-extraneous-dependencies": "off",
        "import/no-internal-modules": "off",
        "import/order": "error",
        "max-classes-per-file": [
            "error",
            1
        ],
        "max-len": [
            "error",
            {
                "code": 140
            }
        ],
        "new-parens": "error",
        "no-bitwise": "error",
        "no-caller": "error",
        "no-cond-assign": "error",
        "no-console": "off",
        "no-debugger": "error",
        "no-duplicate-case": "error",
        "no-duplicate-imports": "error",
        "no-empty": "error",
        "no-eval": "error",
        "no-extra-bind": "error",
        "no-fallthrough": "off",
        "no-invalid-this": "off",
        "no-multiple-empty-lines": "error",
        "no-new-func": "error",
        "no-new-wrappers": "error",
        "no-redeclare": "error",
        "no-return-await": "error",
        "no-sequences": "error",
        "no-shadow": [
            "error",
            {
                "hoist": "all"
            }
        ],
        "no-sparse-arrays": "error",
        "no-template-curly-in-string": "error",
        "no-throw-literal": "error",
        "no-trailing-spaces": "error",
        "no-undef-init": "error",
        "no-underscore-dangle": "error",
        "no-unsafe-finally": "error",
        "no-unused-expressions": "error",
        "no-unused-labels": "error",
        "no-var": "error",
        "object-shorthand": "error",
        "one-var": [
            "error",
            "never"
        ],
        "prefer-arrow/prefer-arrow-functions": "error",
        "prefer-const": "error",
        "prefer-object-spread": "error",
        "quote-props": [
            "error",
            "consistent-as-needed"
        ],
        "radix": "error",
        "space-before-function-paren": [
            "error",
            {
                "anonymous": "never",
                "asyncArrow": "always",
                "named": "never"
            }
        ],
        "space-in-parens": [
            "error",
            "always"
        ],
        "spaced-comment": "error",
        "use-isnan": "error",
        "valid-typeof": "off",
        "@typescript-eslint/tslint/config": [
            "error",
            {
                "rules": {
                    "import-spacing": true,
                    "jsdoc-format": [
                        true,
                        "check-multiline-start"
                    ],
                    "jsx-alignment": true,
                    "jsx-boolean-value": true,
                    "jsx-curly-spacing": [
                        true,
                        "never"
                    ],
                    "jsx-equals-spacing": [
                        true,
                        "never"
                    ],
                    "jsx-key": true,
                    "jsx-no-bind": true,
                    "jsx-no-string-ref": true,
                    "jsx-self-close": true,
                    "jsx-wrap-multiline": true,
                    "no-reference-import": true,
                    "object-curly-spacing": true,
                    "one-line": [
                        true,
                        "check-catch",
                        "check-else",
                        "check-finally",
                        "check-open-brace",
                        "check-whitespace"
                    ],
                    "prefer-conditional-expression": true,
                    "whitespace": [
                        true,
                        "check-module"
                    ]
                }
            }
        ]
    }
};

Add the following contents to the bin/check script:

yarn lint

if [ ! $? -eq 0 ]; then
  echo -e '\033[31mYou have linting errors, attempting to fix them. Please try again, commit aborted\033[0m'
  yarn fix
  exit 1
fi

yarn test
if [ ! $? -eq 0 ]; then
  echo -e '\033[31mTests failed.\033[0m'
  exit 1
fi

Add a script to fix ESLint issues to package.json:

"fix": "eslint . --fix"

Sentry

General configuration

  • Go to https://www.sentry.io and login as the renuo monitor user.

  • Create a project named [project-name].

  • Add the project to the #renuo team if the client pays for monitoring, to the #no-notifications otherwise.

  • Note the DSN key.

    sentry_dsn

  • Install the npm package: yarn add @sentry/react-native

Add the Sentry initialisation code into app.js file:

Sentry.init({ dsn: env.SENTRY_DSN, environment: env.SENTRY_ENVIRONMENT });

Add the ENV variables to the .env files for each environment.

Verify the installation

Open the dev console in chrome, and run

try {
    throw new Error('test raven js');
} catch(e) {
    Sentry.captureException(e)
}

On https://sentry.io/renuo/[project-name] you should find "Uncaught Error: test raven js".

Google Analytics

Google Analytics is only set up for the main branch (since *.renuoapp.ch domains are tracked via Cloudflare injection).

To add a new project to the GA account go to https://www.google.com/analytics and login as [email protected].

  1. Go to the tab 'Verwalten' under settings and you will see a dropdown on the left with all the project
  2. Chose the option above that says 'Create Account'
  3. Fill out the Data based on your project.
  4. In Property Management, under Data Steam, choose th platform you'd like to create the tracking for. Make sure 'Enhanced Measurement' is enabled.
  5. Once you saved, you will see the tracking snippet. Note: You can always find it again later under Porperty/Data Streams.
  6. 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.

Choose one of the given options to set up Google Analytics:

This way is recommended in the normal case, because it doesn't involve another gem dependency. Since Google proposes the 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 script which you can find here

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){window.dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-XXXXXXXXXX');
</script>

Make sure you insert this script at the end of the <head> tag of the page (not in the <body>).

NOTE: There is a default IP anonymization feature in GA4. We no longer need to perform this step manually.

b) Ruby rack-tracker

There's a gem which can be used for a lot of trackers: https://github.com/railslove/rack-tracker#installation

group :production do
  ...
  gem 'rack-tracker'
end

and write to config/environments/production.rb

config.middleware.use(Rack::Tracker) do
  if ENV['GOOGLE_ANALYTICS_ID'].present?
    handler :google_analytics, tracker: ENV['GOOGLE_ANALYTICS_ID'], anonymize_ip: true, advertising: true
  end
end

Google APIs

  • If you need Google APIs in your project (e. g. Google Maps) proceed to the Google Cloud Console.

  • Create a project named [project-name] under the renuo.ch organisation and use that project for all your environments.

  • Attach the wg-operations billing account

  • Choose the correct resource folder

    • clients for client projects
    • wg-operations for persistent internal things (e.g. Renuo Dashboard)
    • tmp for trials (e.g. learning week, presentations)
    • system-gsuite is for Google app script

    google_app_1

Before continuing, make sure that your new project is selected in the top navigational header of the Google Console.

Maps JavaScript API

API key generation

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.

API key restrictions

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:

google_app_2

For main, enable only the specific domain and the renuoapp.ch domain.

For the key we use locally, enable the specific localhost domain (project-name.localhost).

Geocoding API

API key generation

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.

API key restrictions

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.

Slack and Notifications

If you want to keep your team up-to-date with some notifications about the project and to discuss project-related topics we suggest you to use Slack.

You are, for sure, already a member of slack.

Project Channel

You can create a project-dedicated channel on slack naming it #project-[project-name] (e.g. #project-bookshelf, #project-gifcoins, etc..).

Invite all team members involved in the project to the channel.

Deploy Notifications

One notification you may want to receive on this channel is about when a new deployment on main has been performed. In 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 it for you communicating the [project-name].

You must have already setup Automatic Deployment through SemaphoreCI ⚠ If you used Renuo CLI to configure SemaphoreCI, the notifications should be already created. For manual setup, follow these steps:

  1. Open the project Notifications settings (https://renuo.semaphoreci.com/notifications)
  2. Create New Notification
  3. Name of the Notification -> [project-name]
  4. Name of the Rule -> Slack notifications
  5. Branches -> main
  6. Slack Endpoint: Use the Webhook URL from other projects
  7. Send to Slack channel: #project-[project-name]
  8. Save changes

We do not want to pollute the channel with many notifications, therefore we suggest to only send a notification about deployments to production.

Project Title

Short project description

Environments

BranchDomainDeployment
develophttps://[project-name]-develop.renuoapp.chauto
mainhttps://[project-name]-main.renuoapp.chrelease

Setup

git clone [email protected]:renuo/[project-name].git
cd [project-name]
bin/setup

Configuration

Configure the following:

  • config/application.yml

Run

bin/run

Dependency

Dependencies list

Tests / Checks

bin/check

Other

special stuff

Copyright 2022 Renuo AG.

Pull Requests Template

You should probably configure a template for the Pull Requests in your project.

This template will give a list of reminders and important points that the developer needs to remember when opening a new Pull Request.

We think this template is useful as a checklist of things that shouldn't be forgotten when assigning a Pull Request to one of your colleagues.

Feel free to personalise the template for your project specific needs. If you think your changes would be useful also in other projects, please open a Pull Request to update this template.

To install the template copy this file into a .github folder in your project or simply run the following command from the root folder of the project:

mkdir -p .github && curl https://raw.githubusercontent.com/renuo/applications-setup-guide/master/templates/PULL_REQUEST_TEMPLATE.md > .github/PULL_REQUEST_TEMPLATE.md