Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploy notifications #606

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ spec/dummy/public/uploads/*

/vendor
docker-compose.override.yml
.env
.env
mysql
node_modules
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ gem "puma", "~> 5.0"

gem "fog-aws"
gem 'ddtrace', require: 'ddtrace/auto_instrument'

# For emailing
# gem 'aws-sdk-rails', '~> 3.1.0'
jasonfine marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ PATH
jquery-ui-rails (~> 6.0.1)
judge (~> 3.1.0)
judge-simple_form (~> 1.1.0)
jwt (~> 2.8.0)
kaminari
mini_magick
rails (>= 5.0)
Expand Down Expand Up @@ -115,6 +116,7 @@ GEM
bundler
rake
thor (>= 0.14.0)
base64 (0.2.0)
bcrypt (3.1.16)
better_errors (2.9.1)
coderay (>= 1.0.0)
Expand Down Expand Up @@ -240,6 +242,8 @@ GEM
judge-simple_form (1.1.0)
judge (>= 2.0)
simple_form (>= 3.0)
jwt (2.8.0)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
Expand Down
4 changes: 4 additions & 0 deletions app/assets/javascripts/fae/form/_validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,10 @@ Fae.form.validator = {
);
_this.addCustomValidation();
}
// See https://github.com/wearefine/fae/issues/409
// This was preventing the functionality of a checkbox field
// on the user form.
_this.$password_field.data('validate', '');
},

/**
Expand Down
1 change: 0 additions & 1 deletion app/controllers/fae/deploy_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,5 @@ def deploy_site
end
render json: {success: false}
end

end
end
28 changes: 28 additions & 0 deletions app/controllers/fae/netlify_hooks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Fae
class NetlifyHooksController < ActionController::Base

def netlify_hook
unless Rails.env.test?
signature = request.headers["X-Webhook-Signature"]
if signature.blank?
Rails.logger.info 'request.headers["X-Webhook-Signature"] header is missing'
return head :forbidden
end
if Fae.netlify[:notification_hook_signature].blank?
Rails.logger.info "Fae.netlify[:notification_hook_signature] is not set"
return head :forbidden
end

options = {iss: "netlify", verify_iss: true, algorithm: "HS256"}
decoded = JWT.decode(signature, Fae.netlify[:notification_hook_signature], true, options)
unless decoded.first['sha256'] == Digest::SHA256.hexdigest(request.body.read)
Rails.logger.info "Netlify hook signature mismatch, check the value of Fae.netlify[:notification_hook_signature] against the value of the JWS secret token in the Netlify webhook settings."
return head :forbidden
end
end

DeployNotifications.notify_admins(request.body.read).deliver_now
return head :ok
end
end
end
29 changes: 29 additions & 0 deletions app/mailers/fae/deploy_notifications.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Fae
class DeployNotifications < ActionMailer::Base
default from: Fae.deploy_notifications_mailer_sender
layout 'layouts/fae/mailer'

def notify_admins(body = nil, additional_emails = [])
if body.blank?
Rails.logger.info "DeployNotifications.notify_admins called without a body"
return
end
body = JSON.parse(body)

# Don't notify if the deploy is a code push
if body['commit_ref'].present?
Rails.logger.info "#{body['context']} - #{body['id']} is a code push, skipping notification"
Rails.logger.info "#{body['branch']} #{body['commit_ref']}: #{body['commit_message']}"
return
end

@deploy = body
recipients = Fae::User.where(receive_deploy_notifications: true).pluck(:email)
recipients += additional_emails
@fae_options = Fae::Option.instance
current_time_in_zone = Time.now.in_time_zone(@fae_options.time_zone).strftime('%Y-%m-%d %l:%M %p')
subject = "#{@fae_options.title} Deploy Notification #{current_time_in_zone}"
mail(to: recipients, subject: subject)
end
end
end
5 changes: 5 additions & 0 deletions app/models/fae/deploy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Fae
class Deploy < ApplicationRecord
belongs_to :user
end
end
2 changes: 1 addition & 1 deletion app/services/fae/netlify_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def get_deploys
def run_deploy(deploy_hook_type, current_user)
hook = Fae::DeployHook.find_by_environment(deploy_hook_type)
if hook.present?
post("#{hook.url}?trigger_title=#{current_user.full_name.gsub(' ', '+')}+triggered+a+#{deploy_hook_type.titleize}+deploy")
post("#{hook.url}?trigger_title=#{current_user.full_name.gsub(' ', '+')}+triggered+a+#{deploy_hook_type.titleize}+deploy.")
return true
end
false
Expand Down
9 changes: 9 additions & 0 deletions app/views/fae/deploy_notifications/notify_admins.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<% if @deploy['title'].present? %>
<%= @deploy['title'].gsub('+', ' ') %>
<% end %>

<% if @deploy['state'] == 'ready' %>
The deploy was successful.
<% else %>
An error occurred. Please contact your FINE team.
jasonfine marked this conversation as resolved.
Show resolved Hide resolved
<% end %>
1 change: 1 addition & 0 deletions app/views/fae/users/_form.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
= fae_input f, :first_name
= fae_input f, :last_name
= fae_input f, :email
= fae_input f, :receive_deploy_notifications, helper_text: t('fae.user.receive_deploy_notifications_hint')
= fae_input f, :password, helper_text: t('fae.user.password_hint')
= fae_input f, :password_confirmation
- if current_user.admin? || current_user.super_admin?
Expand Down
1 change: 1 addition & 0 deletions app/views/layouts/fae/mailer.text.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
= yield
1 change: 1 addition & 0 deletions config/locales/fae.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ en:
edit: 'Edit'
user:
password_hint: 'To update your password, fill out the fields below. Otherwise leave blank. Passwords must contain at least 8 characters.'
receive_deploy_notifications_hint: 'Check to receive deploy notifications via email.'
header: 'Your Settings'
last_login: 'Last Logged In'
active: 'Active'
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@

get 'deploy/deploys_list' => 'deploy#deploys_list', as: 'deploy_deploys_list'
post 'deploy/deploy_site' => 'deploy#deploy_site', as: 'deploy_deploy_site'

post 'netlify_hooks/netlify_hook' => 'netlify_hooks#netlify_hook', as: 'netlify_hook'

## catch all 404
match "*path" => 'pages#error404', via: [:get, :post]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddReceiveDeployNotificationsToFaeUsers < ActiveRecord::Migration[7.0]
def change
add_column :fae_users, :receive_deploy_notifications, :boolean, default: false
add_index :fae_users, :receive_deploy_notifications
end
end
13 changes: 13 additions & 0 deletions db/migrate/20240222165620_create_fae_deploys.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CreateFaeDeploys < ActiveRecord::Migration[7.0]
def change
create_table :fae_deploys do |t|
t.integer :user_id, index: true
t.string :environment
t.string :deploy_id, index: true
t.string :deploy_status
t.boolean :notified

t.timestamps
end
end
end
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
environment:
- RAILS_ENV=development
- DEV_MYSQL_HOST=db
- FINE_NETLIFY_API_TOKEN=test
- FINE_NETLIFY_API_USER=test
- FINE_NETLIFY_API_TOKEN=abc123
- FINE_NETLIFY_API_USER=[email protected]
db:
# image: postgres
# volumes:
Expand All @@ -37,4 +37,4 @@
ports:
- '3307:3306'
volumes:
- /private/app/dbs:/var/lib/mysql
- ./mysql:/var/lib/mysql
23 changes: 20 additions & 3 deletions docs/features/netlify.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc is a good foundations but I'm going to take a final pass at this before merging.

Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Netlify Deploy Monitor

* [Enabling Deploys](#enabling-deploys)
* [Upgrading](#upgrading)
* [Email Notifications](#email-notifications)

Fae provides and easy integration for statically generated frontends hosted on Netlify. If Fae is delivering content to a statically generated frontend via JSON or GraphQL APIs, updating content in the admin won't trigger a build of the frontend with the new content.
Fae provides an easy integration for statically generated frontends hosted on Netlify. If Fae is delivering content to a statically generated frontend via JSON or GraphQL APIs, updating content in the admin won't trigger a build of the frontend with the new content.

This feature allows a super admin to define multiple deploy environments connected to Netlify deploy hooks. It also establishes a global deploy page so admin and super admin users can trigger builds of any defined Netlify environments.

Expand All @@ -13,8 +13,12 @@ The deploy page will also display the status of the current deploy, along with a

## Enabling Deploys

In your app, run `rake fae:install:migrations` and then migrate the database.

To enable this feature, make sure `config.netlify` is defined in your Fae initializer with all options set correctly.

`notification_hook_signature` is only required if you will use incoming Netlify deploy notification webhooks.

`config/initializers/fae.rb`
```ruby
Fae.setup do |config|
Expand All @@ -26,7 +30,8 @@ Fae.setup do |config|
api_token: 'netlify-api-token',
site: 'site-name-in-netlify',
site_id: 'site-id-in-netlify',
api_base: 'https://api.netlify.com/api/v1/'
api_base: 'https://api.netlify.com/api/v1/',
notification_hook_signature: 'netlify-notification-hook-signature'
}

end
Expand All @@ -35,3 +40,15 @@ end
Then go to the root settings at `/admin/root` while logged in as a super admin and add deploy hooks for each environment you wish to enable in the CMS.

You will see a new nav item labeled "Deploy" that links to `/admin/deploy`. Here you'll be able to trigger and view past deploys.

---

## Email Notifications

In your app, run `rake fae:install:migrations` and then migrate the database.

You can opt-in to email-based notifications for when Netlify deploys complete successfully or fail.

The migration adds a new field to the `Fae::User` model and accompanying checkbox in the user form - `receive_deploy_notifications`, pretty self-explanatory. Any users opted in to this setting will receive the email notifications.

In the https://app.netlify.com dashboard for your project, you'll need to add two new webhook notifications - one for `Deploy succeeded` and `Deploy failed`. Both of these webhooks will point to the same URL: https://your-fae-domain.com/admin/netlify_hooks/netlify_hook (replace your-fae-domain with a real value). Your `JWS secret token` can be any string, but it needs to also be set to, and match what you set it to in the `fae.rb` file's `config.netlify['notification_hook_signature']`.
1 change: 1 addition & 0 deletions docs/topics/initializer.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Fae's default config can be overwritten in a `config/initializers/fae.rb` file.
| --- | ---- | ------- | ----------- |
| devise_secret_key | string | | unique Devise hash, generated on `rails g fae:install` |
| devise_mailer_sender | string | "[email protected]" | This email address will get passed to Devise and used as the from address in the password reset emails. |
| deploy_notifications_mailer_sender | string | "[email protected]" | This email address will get passed to Fae and used as the from address in the deploy notification emails. |
| dashboard_exclusions | array | [] | The dashboard will show all objects with recent activity. |
| max_image_upload_size | integer | 2 | This will set a file size limit on image uploads in MBs. |
| max_file_upload_size | integer | 5 | This will set a file size limit on file uploads in MBs. |
Expand Down
1 change: 1 addition & 0 deletions fae.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Gem::Specification.new do |s|
s.add_dependency 'remotipart', '~> 1.4.0'
s.add_dependency 'simple_form', '<= 5.1'
s.add_dependency 'slim'
s.add_dependency 'jwt', '~> 2.8.0'

s.add_development_dependency 'appraisal'
s.add_development_dependency 'better_errors'
Expand Down
3 changes: 2 additions & 1 deletion lib/fae/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ class Engine < ::Rails::Engine
require 'slim'
require 'kaminari'
require 'fae/version'
require "sprockets/railtie"
require 'sprockets/railtie'
require 'jwt'

config.eager_load_paths += %W(#{config.root}/app)

Expand Down
35 changes: 18 additions & 17 deletions lib/fae/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,25 @@

module Fae
# configurable defaults
mattr_accessor :devise_secret_key, :devise_mailer_sender, :dashboard_exclusions, :max_image_upload_size, :max_file_upload_size, :languages, :recreate_versions, :validation_helpers, :track_changes, :tracker_history_length, :slug_separator, :disabled_environments, :per_page, :use_cache, :use_form_manager, :netlify
mattr_accessor :devise_secret_key, :devise_mailer_sender, :deploy_notifications_mailer_sender, :dashboard_exclusions, :max_image_upload_size, :max_file_upload_size, :languages, :recreate_versions, :validation_helpers, :track_changes, :tracker_history_length, :slug_separator, :disabled_environments, :per_page, :use_cache, :use_form_manager, :netlify

self.devise_secret_key = ''
self.devise_mailer_sender = '[email protected]'
self.dashboard_exclusions = []
self.max_image_upload_size = 2
self.max_file_upload_size = 5
self.languages = {}
self.recreate_versions = false
self.validation_helpers = ValidationHelperCollection.new
self.track_changes = true
self.tracker_history_length = 15
self.slug_separator = '-'
self.disabled_environments = []
self.per_page = 25
self.use_cache = false
self.use_form_manager = false
self.netlify = {}
self.devise_secret_key = ''
self.devise_mailer_sender = '[email protected]'
self.deploy_notifications_mailer_sender = '[email protected]'
self.dashboard_exclusions = []
self.max_image_upload_size = 2
self.max_file_upload_size = 5
self.languages = {}
self.recreate_versions = false
self.validation_helpers = ValidationHelperCollection.new
self.track_changes = true
self.tracker_history_length = 15
self.slug_separator = '-'
self.disabled_environments = []
self.per_page = 25
self.use_cache = false
self.use_form_manager = false
self.netlify = {}

# this function maps the vars from your app into your engine
def self.setup(&block)
Expand Down
8 changes: 7 additions & 1 deletion lib/generators/fae/templates/initializers/fae.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
Fae.setup do |config|

## deploy_notifications_mailer_sender
# This email address will get passed to Fae and
# used as the from address in the deploy notification emails.
# config.deploy_notifications_mailer_sender = '[email protected]'

## devise_mailer_sender
# This email address will get passed to Devise and
# used as the from address in the password reset emails.
Expand Down Expand Up @@ -73,6 +78,7 @@
# api_token: 'netlify-api-token',
# site: 'site-name-in-netlify',
# site_id: 'site-id-in-netlify',
# api_base: 'https://api.netlify.com/api/v1/'
# api_base: 'https://api.netlify.com/api/v1/',
# notification_hook_signature: 'netlify-notification-hook-signature'
# }
end
5 changes: 5 additions & 0 deletions lib/generators/fae/templates/initializers/fae_fine.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
Fae.setup do |config|

## deploy_notifications_mailer_sender
# This email address will get passed to Fae and
# used as the from address in the deploy notification emails.
# config.deploy_notifications_mailer_sender = '[email protected]'

## devise_mailer_sender
# This email address will get passed to Devise and
# used as the from address in the password reset emails.
Expand Down
1 change: 1 addition & 0 deletions spec/dummy/app/assets/javascripts/fae.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//= require fae/vendor/trumbowyg

$(document).ready(function(){

$('.login-body').addClass('test-class');

$("body").on("modal:show", function (e) {
Expand Down
Empty file removed spec/dummy/app/mailers/.keep
Empty file.
2 changes: 2 additions & 0 deletions spec/dummy/config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@
# config.action_view.raise_on_missing_translations = true

config.action_mailer.default_url_options = { :host => 'localhost' }

config.hosts << "21b5-47-26-150-197.ngrok-free.app"
end
Loading