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
- Check your DNS records, for example
CNAME
to Heroku (see their docs)TXT
records for SparkPost sending domainsCAA
records (see Cloudflare)
- If SparkPost has been set up with the renuoapp.ch domain and the project has its own domain now, set up SparkPost again with its own domain
- Verify that SparkPost mails are working and the sending domain is validated.
- Verify that SSL is working correctly
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.
- Add a new page rule
- Enter
example.com/*
- Choose "Forwarding URL"
- Choose "301 - Permanent Redirect"
- Enter
https://www.example.com/$1
- 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
- Initialise the Rails Application
- Push to Git Repository
- Initialise Gitflow
- Configure Git Repository
- Create an Application Server
- 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.
🎉 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.
- AppSignal
- Sentry (optional)
- NewRelic (optional)
- robots.txt
- Percy (optional)
- 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.
- Uptimerobot
- Depending on the monitoring list, also Sentry notifications need to be configured.
- Depfu security monitoring
- 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:
- Run Javascript tests with Jest
- Pull Requests Template
- Slack and Project Notifications
- Send emails
- Sparkpost & Mailtrap
- Devise
- Sidekiq
- Cucumber
- Amazon S3 and Cloudfront
- Carrierwave Upload
- awesome_print
gem 'awesome_print'
- bootstrap
- font-awesome
- bullet
gem 'bullet'
- lograge
gem 'lograge'
- Rack Tracker (Google Analytics)
gem 'rack-tracker'
--> see Google Analytics - Typescript
- Favicons
- Rack CORS
- Rack Attack
- 🔥 Hotjar
- SEO
- redirect non-www to www
- Header tags
- wicked pdf
gem wicked_pdf
- Recaptcha v3
- 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 withgem 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 emptyschema.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 (usuallybundle exec figaro install
is enough). Delete the newly created fileconfig/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 theapplication.yml
is created from theapplication.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 yourapplication.yml
, create the initializer for figaro inconfig/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 timezoneconfig.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
andmain
:- 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
- Default branch:
-
Autolink references
- Add a new Autolink reference with:
- Reference prefix:
TICKET-
- Target URL:
https://redmine.renuo.ch/issues/<num>
- Reference prefix:
- Add a new Autolink reference with:
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:
- You've read about what Heroku is
- You have a Heroku account.
- You have installed the
renuo-cli
gem
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.
- Proceed to https://renuo.semaphoreci.com/ and login through GitHub with [email protected] (1Password)
- Follow these instructions to install semaphore CLI https://docs.semaphoreci.com/reference/sem-command-line-tool/
- Create a project here: https://renuo.semaphoreci.com/new_project
- Go to the project's artifact settings:
Settings
>Artifacts
- Set the retention policy for project, workflow and job artifacts to
/**/*
and2 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.
- Add a file called
.nvmrc
to the project root, where you specify the latest node version - 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.
You should now see a third block where your deployment runs to Heroku. Make sure it is green and deploys correctly:
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 calledspec
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
tobin/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:
✅ 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 theRails::HealthController
class.
You can find an example that also checks the database connection in this file.
Verify
- Open the two apps
Check that you see a green page in each app.
Javascript error reporter
-
Create the module
spec/support/javascript_error_reporter.rb
-
Verify that
config.include JavaScriptErrorReporter, type: :system, js: true
is in yourrails_helper.rb
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 runrails 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
- Now open
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.
-
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'
toapplication.example.yml
-
Add
# SENTRY_ENVIRONMENT: 'local'
toapplication.example.yml
-
Add
# CSP_REPORT_URI
toapplication.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 theCSP_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 thanHEROKU_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.
- Ask wg-operations to add the project to Percy.
- Visit this link.
- Fill in the name with
[project-name]
and select the github repository of the project.
Setup the CI
- Add the PERCY_TOKEN and PERCY_PROJECT env variables to the CI.
They can be found under
https://percy.io/renuo/[project-name]/settings
. - 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
- Add the gem
percy-capybara
to the test group in the Gemfile and runbundle install
. - Follow the setup instructions here
- 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
-
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
orhttps://customdomain/home/check
- Where
-
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.
-
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'
toapplication.example.yml
-
Add
# SENTRY_ENVIRONMENT: 'local'
toapplication.example.yml
-
Add
# CSP_REPORT_URI
toapplication.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 theCSP_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.
- 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.
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:
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:
- Open the project Notifications settings (
https://renuo.semaphoreci.com/notifications
) - Create New Notification
- Name of the Notification ->
[project-name]
- Name of the Rule ->
Slack notifications
- Branches ->
main
- Slack Endpoint: Use the Webhook URL from other projects
- Send to Slack channel:
#project-[project-name]
- 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!
- Go to https://app.sparkpost.com/auth and log in with the credentials for sparkpost+enviroment@renuo.ch found in the credential store
- Create one new subaccount and name it like your project
- 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
- Write down the API-key in the credential store (in a list under sparkpost+enviroment@renuo.ch), because it's only showed once!
- Credentials for SMTP setup on your app can be found here, password is your generated API-key
- (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]
) - Verify your Email DNS configuration with https://mxtoolbox.com/SuperTool.aspx
- 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!
- 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 theproduction
block and install itbundle install
-
Update the
Procfile
and set theworker
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
toconfig.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
- Temporarily switch the adapter in
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 itbundle 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
andSIDEKIQ_PASSWORD
as the credentials to the dashboard to yourapplication.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:
app/controllers/home_controller.rb
(If you haven't done so already) in the RSpec section.features/home_check.feature
features/step_definitions/home_check_steps.rb
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 updatabase_cleaner
for Cucumber tests.factory_bot.rb
makes FactoryBot's methods (likebuild
andcreate
) 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.
-
Run
renuo create-aws-project
-
Follow the steps and answer the questions
-
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 [...]
-
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.
- Visit https://us-east-1.console.aws.amazon.com/cloudfront/v3/home?region=eu-central-1#/distributions and edit your distribution.
- Enter the CNAMEs as aliases
- Click "Request certificate" (this opens a new tab with Amazon ACM, make sure it's region is us-east-1)
- Enter all the CNAMEs you want to have as aliases (normally only one)
- Enter the domain ownership verification records into Cloudflare (CNAME with cryptic values)
- Submit the ACM form and wait for the certificate to being issued.
- Go back to the Cloudfront distribution, refresh the certifactes drop down and choose your new certificate.
- 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.
- Add
gem 'carrierwave'
andgem 'fog-aws'
- In
environment.rb
addrequire 'carrierwave/orm/activerecord'
- Add to your
application.example.yml
:
#CARRIERWAVE_SALT: 'rake secret'
#S3_ACCESS_KEY_ID: ''
#S3_ASSET_HOST: ''
#S3_BUCKET_NAME: ''
#S3_SECRET_ACCESS_KEY: ''
- 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
- The
UploaderBasepath
is used, so we don't polute thepublic/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
- 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
- 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
- We recommend to use image processing only when needed, since it's slow. Therefore the initializer sets it to
false
. But forsystem
-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 Picture
s
- Add a model with the image instance:
class UserPicture < ApplicationRecord
mount_uploader :picture, PictureUploader
belongs_to :user, inverse_of: :user_pictures
end
- 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
- Simplify access to the pictures in
User
class User < ApplicationRecord
def pictures
user_pictures.map(&:picture)
end
end
- 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
- 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)
- Add
gem 'mini_magick'
if you plan to resize images (quite always needed) - 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
- Look up the auth token which can be found here. The credentials can be found on passwork
- 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
- Navigate to the font-awesome gallery list
- 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:
Setting | Value |
---|---|
Source name | Rails |
Platform | Heroku Log Drain |
Message format | logfmt |
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 ofGemfile
- Add
gem 'wkhtmltopdf-binary'
togroup :development, :test
- Add
gem 'wkhtmltopdf-heroku'
togroup :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:
- Use
[project-name]
as a label for the site. - Choose reCAPTCHA v3
- Add all the domains on which reCAPTCHA should work (
renuoapp.ch
should also be included for thedevelop
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:
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:
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.
- Initialise the React Native app.
react-native init [project-name] --template typescript
- Push to Git Repository
- Initialise Gitflow
- Configure CI
- Release
Now the app should be downloadable via Testflight / Play store. Other recommended tools:
Optional steps:
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
- log in here and create three bundle identifiers -
ch.renuo.[project-name]
,ch.renuo.[project-name].staging
,ch.renuo.[project-name].dev
: https://developer.apple.com/account/resources/identifiers/list - make sure to select push notifications in the capabilities if it's needed
- log in there with [email protected] account
- then as a second step, you need to create the app listing for production & staging apps in https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app
- (wait a couple of minutes until the new app identifier is synchronized to all lists)
- click the plus button and select new app
- when you have this, you should be already able to publish the app.
- configure Fastlane (copy & paste the config from another app (e.g.: https://github.com/renuo/citymessenger/tree/develop/app/fastlane),
- change the identifier and run
bundle exec fastlane match development && bundle exec fastlane match appstore
). - run it also for the staging app:
APP_BUNDLE_ID="ch.renuo.[project-name]" bundle exec fastlane match development && APP_BUNDLE_ID="ch.renuo.[project-name]" bundle exec fastlane match appstore
- This will create the certificates an push them to the repo here https://github.com/renuo/fastlane-ios-certificates
- change the identifier and run
Build configuration
Open the Xcode project under ios/[project-name].xcworkspace
.
- Create a
Release Staging
build configuration.
Click on Duplicate using Release configuration
.
-
Go to
Product -> Scheme -> Manage schemes
. -
Duplicate the scheme
[project-name]
. Edit it. SelectShared
and make sure you have selected onlyRun
,Profile
, andArchive
for[project-name]
target. Go through each build step and selectRelease Staging
as build configuration. -
Go to the
Build Settings
and click onAll
andCombined
filters.- Look for
Product Bundle Identifiers
and add.dev
forDebug
and.staging
forRelease Staging
. - Change the
Provisioning Profile
to match the same identifiers. - Add
BUNDLE_DISPLAY_NAME
underUser-Defined variables
. Set app names for each variant.
- Look for
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
- React-navigation
- react-native-config to support .env files
- apisauce for API services
- i18n-js for internalization
- Native Base - collection of useful UI components.
- Moment for date handling utilities
- Lodash
- Redux (If the app needs more complex state management)
- React-native-offline for offline mode
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.
-
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].
- Go to the tab 'Verwalten' under settings and you will see a dropdown on the left with all the project
- Chose the option above that says 'Create Account'
- Fill out the Data based on your project.
- In Property Management, under Data Steam, choose th platform you'd like to create the tracking for. Make sure 'Enhanced Measurement' is enabled.
- Once you saved, you will see the tracking snippet. Note: You can always find it again later under Porperty/Data Streams.
- 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:
a) Javascript only: gtag script (recommended)
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 projectswg-operations
for persistent internal things (e.g. Renuo Dashboard)tmp
for trials (e.g. learning week, presentations)system-gsuite
is for Google app script
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:
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:
- Open the project Notifications settings (
https://renuo.semaphoreci.com/notifications
) - Create New Notification
- Name of the Notification ->
[project-name]
- Name of the Rule ->
Slack notifications
- Branches ->
main
- Slack Endpoint: Use the Webhook URL from other projects
- Send to Slack channel:
#project-[project-name]
- 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
Branch | Domain | Deployment |
---|---|---|
develop | https://[project-name] -develop.renuoapp.ch | auto |
main | https://[project-name] -main.renuoapp.ch | release |
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
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