mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
Use Redis for ActiveJob and ActionCable (#2004)
* Use Redis for ActiveJob and ActionCable * Fix alwaysApply setting * Update queue names and weights * Tweak weights * Update job queues * Update docker setup guide * Remove deprecated upgrade columns from users table * Refactor Redis configuration for Sidekiq and caching in production environment * Add Sidekiq Sentry monitoring * queue naming fix * Clean up schema
This commit is contained in:
parent
a7db914005
commit
19cc63c8f4
75 changed files with 328 additions and 1684 deletions
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
description: This rule explains the project's tech stack and code conventions
|
||||
globs: *
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
This rule serves as high-level documentation for how the Maybe codebase is structured.
|
||||
|
||||
|
@ -19,7 +20,7 @@ This rule serves as high-level documentation for how the Maybe codebase is struc
|
|||
- TailwindCSS for styles
|
||||
- Lucide Icons for icons
|
||||
- Database: PostgreSQL
|
||||
- Jobs: GoodJob
|
||||
- Jobs: Sidekiq + Redis
|
||||
- External
|
||||
- Payments: Stripe
|
||||
- User bank data syncing: Plaid
|
||||
|
|
96
.env.example
96
.env.example
|
@ -1,20 +1,31 @@
|
|||
# ================================ PLEASE READ ==========================================
|
||||
# This file outlines all the possible environment variables supported by the Maybe app.
|
||||
#
|
||||
# This includes several features that are for our "hosted" version of Maybe, which most
|
||||
# open-source contributors won't need.
|
||||
# ================================ PLEASE READ ===========================================================
|
||||
# This file outlines all the possible environment variables supported by the Maybe app for self hosting.
|
||||
#
|
||||
# If you are developing locally, you should be referencing `.env.local.example` instead.
|
||||
# =======================================================================================
|
||||
# If you're a developer setting up your local environment, please use `.env.local.example` instead.
|
||||
# ========================================================================================================
|
||||
|
||||
# Required self-hosting vars
|
||||
# --------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Enables self hosting features (should be set to true unless you know what you're doing)
|
||||
SELF_HOSTED=true
|
||||
|
||||
# Secret key used to encrypt credentials (https://api.rubyonrails.org/v7.1.3.2/classes/Rails/Application.html#method-i-secret_key_base)
|
||||
# Has to be a random string, generated eg. by running `openssl rand -hex 64`
|
||||
SECRET_KEY_BASE=secret-value
|
||||
|
||||
# Optional self-hosting vars
|
||||
# --------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Optional: Synth API Key for exchange rates + stock prices
|
||||
# (you can also set this in your self-hosted settings page)
|
||||
# Get it here: https://synthfinance.com/
|
||||
SYNTH_API_KEY=
|
||||
|
||||
# Custom port config
|
||||
# For users who have other applications listening at 3000, this allows them to set a value puma will listen to.
|
||||
PORT=3000
|
||||
|
||||
# Exchange Rate & Stock Pricing API
|
||||
# This is used to convert between different currencies in the app. In addition, it fetches global stock prices. We use Synth, which is a Maybe product. You can sign up for a free account at synthfinance.com.
|
||||
SYNTH_API_KEY=
|
||||
|
||||
# SMTP Configuration
|
||||
# This is only needed if you intend on sending emails from your Maybe instance (such as for password resets or email financial reports).
|
||||
# Resend.com is a good option that offers a free tier for sending emails.
|
||||
|
@ -37,60 +48,20 @@ POSTGRES_USER=postgres
|
|||
# This is the domain that your Maybe instance will be hosted at. It is used to generate links in emails and other places.
|
||||
APP_DOMAIN=
|
||||
|
||||
## Error and Performance Monitoring
|
||||
# The app uses Sentry to monitor errors and performance. In reality, you likely don't need this unless you're deploying Maybe to many users.
|
||||
SENTRY_DSN=
|
||||
|
||||
# If enabled, an invite code generated by `rake invites:create` is required to sign up as a new user.
|
||||
# This is useful for controlling who can sign up for your Maybe instance.
|
||||
REQUIRE_INVITE_CODE=false
|
||||
|
||||
# Enables self hosting features (should be set to true for most folks)
|
||||
SELF_HOSTED=true
|
||||
|
||||
# The hosting platform used to deploy the app (e.g. "render")
|
||||
# `localhost` (or unset) is used for local development and testing
|
||||
HOSTING_PLATFORM=localhost
|
||||
|
||||
# Secret key used to encrypt credentials (https://api.rubyonrails.org/v7.1.3.2/classes/Rails/Application.html#method-i-secret_key_base)
|
||||
# Has to be a random string, generated eg. by running `openssl rand -hex 64`
|
||||
SECRET_KEY_BASE=secret-value
|
||||
|
||||
# Disable enforcing SSL connections
|
||||
# DISABLE_SSL=true
|
||||
|
||||
# ======================================================================================================
|
||||
# Upgrades Module - responsible for triggering upgrade alerts, prompts, and auto-upgrade functionality
|
||||
# ======================================================================================================
|
||||
#
|
||||
# UPGRADES_ENABLED: Enables Upgrader class functionality.
|
||||
# UPGRADES_MODE: Controls how the app will upgrade. `manual` means the user must manually upgrade the app. `auto` means the app will upgrade automatically (great for self-hosting)
|
||||
# UPGRADES_TARGET: Controls what the app will upgrade to. `release` means the app will upgrade to the latest release. `commit` means the app will upgrade to the latest commit.
|
||||
#
|
||||
UPGRADES_ENABLED=false # unless editing the flow, you should keep this `false` locally in development
|
||||
UPGRADES_MODE=manual # `manual` or `auto`
|
||||
UPGRADES_TARGET=release # `release` or `commit`
|
||||
|
||||
|
||||
# ======================================================================================================
|
||||
# Git Repository Module - responsible for fetching latest commit data for upgrades
|
||||
# ======================================================================================================
|
||||
#
|
||||
GITHUB_REPO_OWNER=maybe-finance
|
||||
GITHUB_REPO_NAME=maybe
|
||||
GITHUB_REPO_BRANCH=main
|
||||
|
||||
# ======================================================================================================
|
||||
# Active Storage Configuration - responsible for storing file uploads
|
||||
# ======================================================================================================
|
||||
#
|
||||
# * Defaults to disk storage but you can also use Amazon S3, Google Cloud Storage, or Microsoft Azure Storage.
|
||||
# * Defaults to disk storage but you can also use Amazon S3 or Cloudflare R2
|
||||
# * Set the appropriate environment variables to use these services.
|
||||
# * Ensure libvips is installed on your system for image processing - https://github.com/libvips/libvips
|
||||
#
|
||||
# Amazon S3
|
||||
# ==========
|
||||
# ACTIVE_STORAGE_SERVICE=amazon
|
||||
# ACTIVE_STORAGE_SERVICE=amazon <- Enables Amazon S3 storage
|
||||
# S3_ACCESS_KEY_ID=
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_REGION= # defaults to `us-east-1` if not set
|
||||
|
@ -98,26 +69,9 @@ GITHUB_REPO_BRANCH=main
|
|||
#
|
||||
# Cloudflare R2
|
||||
# =============
|
||||
# ACTIVE_STORAGE_SERVICE=cloudflare
|
||||
# ACTIVE_STORAGE_SERVICE=cloudflare <- Enables Cloudflare R2 storage
|
||||
# CLOUDFLARE_ACCOUNT_ID=
|
||||
# CLOUDFLARE_ACCESS_KEY_ID=
|
||||
# CLOUDFLARE_SECRET_ACCESS_KEY=
|
||||
# CLOUDFLARE_BUCKET=
|
||||
|
||||
# ======================================================================================================
|
||||
# Billing Module - responsible for handling billing
|
||||
# ======================================================================================================
|
||||
#
|
||||
STRIPE_PUBLISHABLE_KEY=
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# ======================================================================================================
|
||||
# Plaid Configuration
|
||||
# ======================================================================================================
|
||||
#
|
||||
PLAID_CLIENT_ID=
|
||||
PLAID_SECRET=
|
||||
PLAID_ENV=
|
||||
PLAID_EU_CLIENT_ID=
|
||||
PLAID_EU_SECRET=
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -64,4 +64,6 @@ coverage
|
|||
.cursorrules
|
||||
|
||||
# Ignore node related files
|
||||
node_modules
|
||||
node_modules
|
||||
|
||||
compose.yml
|
4
Gemfile
4
Gemfile
|
@ -7,6 +7,7 @@ gem "rails", "~> 7.2.2"
|
|||
|
||||
# Drivers
|
||||
gem "pg", "~> 1.5"
|
||||
gem "redis", "~> 5.4"
|
||||
|
||||
# Deployment
|
||||
gem "puma", ">= 5.0"
|
||||
|
@ -25,13 +26,14 @@ gem "turbo-rails"
|
|||
gem "hotwire_combobox"
|
||||
|
||||
# Background Jobs
|
||||
gem "good_job"
|
||||
gem "sidekiq"
|
||||
|
||||
# Error logging
|
||||
gem "vernier"
|
||||
gem "rack-mini-profiler"
|
||||
gem "sentry-ruby"
|
||||
gem "sentry-rails"
|
||||
gem "sentry-sidekiq"
|
||||
gem "logtail-rails"
|
||||
|
||||
# Active Storage
|
||||
|
|
30
Gemfile.lock
30
Gemfile.lock
|
@ -155,8 +155,6 @@ GEM
|
|||
rubocop (>= 1)
|
||||
smart_properties
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.12.2)
|
||||
|
@ -177,18 +175,8 @@ GEM
|
|||
ffi (1.17.1-x86_64-darwin)
|
||||
ffi (1.17.1-x86_64-linux-gnu)
|
||||
ffi (1.17.1-x86_64-linux-musl)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
good_job (4.9.3)
|
||||
activejob (>= 6.1.0)
|
||||
activerecord (>= 6.1.0)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (>= 1.11.0)
|
||||
railties (>= 6.1.0)
|
||||
thor (>= 1.0.0)
|
||||
hashdiff (1.1.2)
|
||||
highline (3.1.2)
|
||||
reline
|
||||
|
@ -336,7 +324,6 @@ GEM
|
|||
public_suffix (6.0.1)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.12)
|
||||
rack-mini-profiler (3.3.1)
|
||||
|
@ -393,6 +380,10 @@ GEM
|
|||
rdoc (6.12.0)
|
||||
psych (>= 4.0.0)
|
||||
redcarpet (3.6.1)
|
||||
redis (5.4.0)
|
||||
redis-client (>= 0.22.0)
|
||||
redis-client (0.24.0)
|
||||
connection_pool
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.0)
|
||||
io-console (~> 0.5)
|
||||
|
@ -458,6 +449,15 @@ GEM
|
|||
sentry-ruby (5.23.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.23.0)
|
||||
sentry-ruby (~> 5.23.0)
|
||||
sidekiq (>= 3.0)
|
||||
sidekiq (8.0.1)
|
||||
connection_pool (>= 2.5.0)
|
||||
json (>= 2.9.0)
|
||||
logger (>= 1.6.2)
|
||||
rack (>= 3.1.0)
|
||||
redis-client (>= 0.23.2)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
|
@ -544,7 +544,6 @@ DEPENDENCIES
|
|||
faraday
|
||||
faraday-multipart
|
||||
faraday-retry
|
||||
good_job
|
||||
hotwire-livereload
|
||||
hotwire_combobox
|
||||
i18n-tasks
|
||||
|
@ -567,6 +566,7 @@ DEPENDENCIES
|
|||
rails (~> 7.2.2)
|
||||
rails-settings-cached
|
||||
redcarpet
|
||||
redis (~> 5.4)
|
||||
rotp (~> 6.3)
|
||||
rqrcode (~> 2.2)
|
||||
rubocop-rails-omakase
|
||||
|
@ -574,6 +574,8 @@ DEPENDENCIES
|
|||
selenium-webdriver
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
sidekiq
|
||||
simplecov
|
||||
stimulus-rails
|
||||
stripe
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
web: bundle exec ${DEBUG:+rdbg -O -n -c --} bin/rails server -b 0.0.0.0
|
||||
css: bundle exec bin/rails tailwindcss:watch
|
||||
worker: bundle exec good_job start
|
||||
worker: bundle exec sidekiq
|
||||
|
|
43
README.md
43
README.md
|
@ -6,9 +6,6 @@
|
|||
<b>Get
|
||||
involved: [Discord](https://link.maybe.co/discord) • [Website](https://maybefinance.com) • [Issues](https://github.com/maybe-finance/maybe/issues)</b>
|
||||
|
||||
_If you're looking for the previous React codebase, you can find it
|
||||
at [maybe-finance/maybe-archive](https://github.com/maybe-finance/maybe-archive)._
|
||||
|
||||
## Backstory
|
||||
|
||||
We spent the better part of 2021/2022 building a personal finance + wealth
|
||||
|
@ -29,9 +26,8 @@ and eventually offer a hosted version of the app for a small monthly fee.
|
|||
|
||||
There are 3 primary ways to use the Maybe app:
|
||||
|
||||
1. Managed (easiest) - _coming soon..._
|
||||
2. [One-click deploy](docs/hosting/one-click-deploy.md)
|
||||
3. [Self-host with Docker](docs/hosting/docker.md)
|
||||
1. Managed (easiest) - we're in alpha and release invites in our Discord
|
||||
2. [Self-host with Docker](docs/hosting/docker.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -84,37 +80,10 @@ If you'd like multi-currency support, there are a few extra steps to follow.
|
|||
|
||||
### Setup Guides
|
||||
|
||||
#### Dev Container (optional)
|
||||
|
||||
This is 100% optional and meant for devs who don't want to worry about
|
||||
installing requirements manually for their platform. You can
|
||||
follow [this guide](https://code.visualstudio.com/docs/devcontainers/containers)
|
||||
to learn more about Dev Containers.
|
||||
|
||||
If you run into `could not connect to server` errors, you may need to change
|
||||
your `.env`'s `DB_HOST` environment variable value to `db` to point to the
|
||||
Postgres container.
|
||||
|
||||
#### Mac
|
||||
|
||||
Please visit
|
||||
our [Mac dev setup guide](https://github.com/maybe-finance/maybe/wiki/Mac-Dev-Setup-Guide).
|
||||
|
||||
#### Linux
|
||||
|
||||
Please visit
|
||||
our [Linux dev setup guide](https://github.com/maybe-finance/maybe/wiki/Linux-Dev-Setup-Guide).
|
||||
|
||||
#### Windows
|
||||
|
||||
Please visit
|
||||
our [Windows dev setup guide](https://github.com/maybe-finance/maybe/wiki/Windows-Dev-Setup-Guide).
|
||||
|
||||
### Testing Emails
|
||||
|
||||
In development, we use `letter_opener` to automatically open emails in your
|
||||
browser. When an email sends locally, a new browser tab will open with a
|
||||
preview.
|
||||
- [Mac dev setup guide](https://github.com/maybe-finance/maybe/wiki/Mac-Dev-Setup-Guide)
|
||||
- [Linux dev setup guide](https://github.com/maybe-finance/maybe/wiki/Linux-Dev-Setup-Guide)
|
||||
- [Windows dev setup guide](https://github.com/maybe-finance/maybe/wiki/Windows-Dev-Setup-Guide)
|
||||
- Dev containers - visit [this guide](https://code.visualstudio.com/docs/devcontainers/containers) to learn more
|
||||
|
||||
## Repo Activity
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ class PagesController < ApplicationController
|
|||
end
|
||||
|
||||
def changelog
|
||||
@release_notes = Provider::Github.new.fetch_latest_release_notes
|
||||
@release_notes = Providers.github.fetch_latest_release_notes
|
||||
|
||||
render layout: "settings"
|
||||
end
|
||||
|
|
|
@ -9,18 +9,6 @@ class Settings::HostingsController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if hosting_params[:upgrades_setting].present?
|
||||
mode = hosting_params[:upgrades_setting] == "manual" ? "manual" : "auto"
|
||||
target = hosting_params[:upgrades_setting] == "commit" ? "commit" : "release"
|
||||
|
||||
Setting.upgrades_mode = mode
|
||||
Setting.upgrades_target = target
|
||||
end
|
||||
|
||||
if hosting_params.key?(:render_deploy_hook)
|
||||
Setting.render_deploy_hook = hosting_params[:render_deploy_hook]
|
||||
end
|
||||
|
||||
if hosting_params.key?(:require_invite_for_signup)
|
||||
Setting.require_invite_for_signup = hosting_params[:require_invite_for_signup]
|
||||
end
|
||||
|
@ -46,7 +34,7 @@ class Settings::HostingsController < ApplicationController
|
|||
|
||||
private
|
||||
def hosting_params
|
||||
params.require(:setting).permit(:render_deploy_hook, :upgrades_setting, :require_invite_for_signup, :require_email_confirmation, :synth_api_key)
|
||||
params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :synth_api_key)
|
||||
end
|
||||
|
||||
def raise_if_not_self_hosted
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
class UpgradesController < ApplicationController
|
||||
before_action :verify_upgrades_enabled
|
||||
|
||||
def acknowledge
|
||||
commit_sha = params[:id]
|
||||
upgrade = Upgrader.find_upgrade(commit_sha)
|
||||
|
||||
if upgrade
|
||||
if upgrade.available?
|
||||
Current.user.acknowledge_upgrade_prompt(upgrade.commit_sha)
|
||||
flash[:notice] = t(".upgrade_dismissed")
|
||||
elsif upgrade.complete?
|
||||
Current.user.acknowledge_upgrade_alert(upgrade.commit_sha)
|
||||
flash[:notice] = t(".upgrade_complete_dismiss")
|
||||
else
|
||||
flash[:alert] = t(".upgrade_not_available")
|
||||
end
|
||||
else
|
||||
flash[:alert] = t(".upgrade_not_found")
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
def deploy
|
||||
commit_sha = params[:id]
|
||||
upgrade = Upgrader.find_upgrade(commit_sha)
|
||||
|
||||
unless upgrade
|
||||
flash[:alert] = t(".upgrade_not_found")
|
||||
return redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
prior_acknowledged_upgrade_commit_sha = Current.user.last_prompted_upgrade_commit_sha
|
||||
|
||||
# Optimistically acknowledge the upgrade prompt
|
||||
Current.user.acknowledge_upgrade_prompt(upgrade.commit_sha)
|
||||
|
||||
upgrade_result = Upgrader.upgrade_to(upgrade)
|
||||
|
||||
if upgrade_result[:success]
|
||||
flash[:notice] = upgrade_result[:message]
|
||||
else
|
||||
# If the upgrade fails, revert to the prior acknowledged upgrade
|
||||
Current.user.acknowledge_upgrade_prompt(prior_acknowledged_upgrade_commit_sha)
|
||||
flash[:alert] = upgrade_result[:message]
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
private
|
||||
def verify_upgrades_enabled
|
||||
head :not_found unless ENV["UPGRADES_ENABLED"] == "true"
|
||||
end
|
||||
end
|
|
@ -1,14 +0,0 @@
|
|||
module UpgradesHelper
|
||||
def get_upgrade_for_notification(user, upgrades_mode)
|
||||
return nil unless ENV["UPGRADES_ENABLED"] == "true"
|
||||
return nil unless user.present?
|
||||
|
||||
completed_upgrade = Upgrader.completed_upgrade
|
||||
return completed_upgrade if completed_upgrade && user.last_alerted_upgrade_commit_sha != completed_upgrade.commit_sha
|
||||
|
||||
available_upgrade = Upgrader.available_upgrade
|
||||
if available_upgrade && upgrades_mode == "manual" && user.last_prompted_upgrade_commit_sha != available_upgrade.commit_sha
|
||||
available_upgrade
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,5 @@
|
|||
class ApplicationJob < ActiveJob::Base
|
||||
# Automatically retry jobs that encountered a deadlock
|
||||
# retry_on ActiveRecord::Deadlocked
|
||||
|
||||
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||
# discard_on ActiveJob::DeserializationError
|
||||
retry_on ActiveRecord::Deadlocked
|
||||
discard_on ActiveJob::DeserializationError
|
||||
queue_as :low_priority # default queue
|
||||
end
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
class AutoUpgradeJob < ApplicationJob
|
||||
queue_as :latency_low
|
||||
|
||||
def perform(*args)
|
||||
raise_if_disabled
|
||||
|
||||
return Rails.logger.info "Skipping auto-upgrades because app is set to manual upgrades. Please set UPGRADES_MODE=auto to enable auto-upgrades" if Setting.upgrades_mode == "manual"
|
||||
|
||||
Rails.logger.info "Searching for available auto-upgrades..."
|
||||
|
||||
candidate = Upgrader.available_upgrade_by_type(Setting.upgrades_target)
|
||||
|
||||
if candidate
|
||||
if Rails.cache.read("last_auto_upgrade_commit_sha") == candidate.commit_sha
|
||||
Rails.logger.info "Skipping auto upgrade: #{candidate.type} #{candidate.commit_sha} deploy in progress"
|
||||
return
|
||||
end
|
||||
|
||||
Rails.logger.info "Auto upgrading to #{candidate.type} #{candidate.commit_sha}..."
|
||||
Upgrader.upgrade_to(candidate)
|
||||
Rails.cache.write("last_auto_upgrade_commit_sha", candidate.commit_sha, expires_in: 1.day)
|
||||
else
|
||||
Rails.logger.info "No auto upgrade available at this time"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def raise_if_disabled
|
||||
raise "Upgrades module is disabled. Please set UPGRADES_ENABLED=true to enable upgrade features" unless ENV["UPGRADES_ENABLED"] == "true"
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
class DataCacheClearJob < ApplicationJob
|
||||
queue_as :default
|
||||
queue_as :low_priority
|
||||
|
||||
def perform(family)
|
||||
ActiveRecord::Base.transaction do
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class DestroyJob < ApplicationJob
|
||||
queue_as :latency_low
|
||||
queue_as :low_priority
|
||||
|
||||
def perform(model)
|
||||
model.destroy
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class EnrichTransactionBatchJob < ApplicationJob
|
||||
queue_as :latency_high
|
||||
queue_as :low_priority
|
||||
|
||||
def perform(account, batch_size = 100, offset = 0)
|
||||
account.enrich_transaction_batch(batch_size, offset)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class FamilyResetJob < ApplicationJob
|
||||
queue_as :default
|
||||
queue_as :low_priority
|
||||
|
||||
def perform(family)
|
||||
# Delete all family data except users
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class FetchSecurityInfoJob < ApplicationJob
|
||||
queue_as :latency_low
|
||||
queue_as :low_priority
|
||||
|
||||
def perform(security_id)
|
||||
return unless Security.provider.present?
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class ImportJob < ApplicationJob
|
||||
queue_as :latency_medium
|
||||
queue_as :high_priority
|
||||
|
||||
def perform(import)
|
||||
import.publish
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class RevertImportJob < ApplicationJob
|
||||
queue_as :latency_low
|
||||
queue_as :medium_priority
|
||||
|
||||
def perform(import)
|
||||
import.revert
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
class SyncJob < ApplicationJob
|
||||
queue_as :latency_medium
|
||||
queue_as :high_priority
|
||||
|
||||
def perform(sync)
|
||||
sleep 1 # simulate work for faster jobs
|
||||
sync.perform
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class UserPurgeJob < ApplicationJob
|
||||
queue_as :latency_low
|
||||
queue_as :low_priority
|
||||
|
||||
def perform(user)
|
||||
user.purge
|
||||
|
|
|
@ -1,43 +1,10 @@
|
|||
class Provider::Github
|
||||
attr_reader :name, :owner, :branch
|
||||
|
||||
def initialize(config = {})
|
||||
@name = config[:name] || ENV.fetch("GITHUB_REPO_NAME", "maybe")
|
||||
@owner = config[:owner] || ENV.fetch("GITHUB_REPO_OWNER", "maybe-finance")
|
||||
@branch = config[:branch] || ENV.fetch("GITHUB_REPO_BRANCH", "main")
|
||||
end
|
||||
|
||||
def fetch_latest_upgrade_candidates
|
||||
Rails.cache.fetch("latest_github_upgrade_candidates", expires_in: 30.minutes) do
|
||||
Rails.logger.info "Fetching latest GitHub upgrade candidates from #{repo} on branch #{branch}..."
|
||||
begin
|
||||
latest_release = Octokit.releases(repo).first
|
||||
latest_version = latest_release ? Semver.from_release_tag(latest_release.tag_name) : Semver.new(Maybe.version)
|
||||
latest_commit = Octokit.branch(repo, branch)
|
||||
|
||||
release_info = if latest_release
|
||||
{
|
||||
version: latest_version,
|
||||
url: latest_release.html_url,
|
||||
commit_sha: Octokit.commit(repo, latest_release.tag_name).sha
|
||||
}
|
||||
end
|
||||
|
||||
commit_info = {
|
||||
version: latest_version,
|
||||
commit_sha: latest_commit.commit.sha,
|
||||
url: latest_commit.commit.html_url
|
||||
}
|
||||
|
||||
{
|
||||
release: release_info,
|
||||
commit: commit_info
|
||||
}
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to fetch latest GitHub commits: #{e.message}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
def initialize
|
||||
@name = "maybe"
|
||||
@owner = "maybe-finance"
|
||||
@branch = "main"
|
||||
end
|
||||
|
||||
def fetch_latest_release_notes
|
||||
|
|
|
@ -2,24 +2,7 @@
|
|||
class Setting < RailsSettings::Base
|
||||
cache_prefix { "v1" }
|
||||
|
||||
field :render_deploy_hook,
|
||||
type: :string,
|
||||
default: ENV["RENDER_DEPLOY_HOOK"],
|
||||
validates: { allow_blank: true, format: { with: /\Ahttps:\/\/api\.render\.com\/deploy\/srv-.+\z/ } }
|
||||
|
||||
field :upgrades_mode,
|
||||
type: :string,
|
||||
default: ENV.fetch("UPGRADES_MODE", "manual"),
|
||||
validates: { inclusion: { in: %w[manual auto] } }
|
||||
|
||||
field :upgrades_target,
|
||||
type: :string,
|
||||
default: ENV.fetch("UPGRADES_TARGET", "release"),
|
||||
validates: { inclusion: { in: %w[release commit] } }
|
||||
|
||||
field :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"]
|
||||
|
||||
field :require_invite_for_signup, type: :boolean, default: false
|
||||
|
||||
field :require_email_confirmation, type: :boolean, default: ENV.fetch("REQUIRE_EMAIL_CONFIRMATION", "true") == "true"
|
||||
end
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
class Upgrader
|
||||
include Provided
|
||||
|
||||
class << self
|
||||
attr_writer :config
|
||||
|
||||
def config
|
||||
@config ||= Config.new
|
||||
end
|
||||
|
||||
def upgrade_to(commit_or_upgrade)
|
||||
upgrade = commit_or_upgrade.is_a?(String) ? find_upgrade(commit_or_upgrade) : commit_or_upgrade
|
||||
config.deployer.deploy(upgrade)
|
||||
end
|
||||
|
||||
def find_upgrade(commit)
|
||||
upgrade_candidates.find { |candidate| candidate.commit_sha == commit }
|
||||
end
|
||||
|
||||
def available_upgrade
|
||||
available_upgrades.first
|
||||
end
|
||||
|
||||
# Default to showing releases first, then commits
|
||||
def completed_upgrade
|
||||
completed_upgrades.find { |upgrade| upgrade.type == "release" } || completed_upgrades.first
|
||||
end
|
||||
|
||||
def available_upgrade_by_type(type)
|
||||
if type == "commit"
|
||||
commit_upgrade = available_upgrades.find { |upgrade| upgrade.type == "commit" }
|
||||
commit_upgrade || available_upgrades.first
|
||||
elsif type == "release"
|
||||
available_upgrades.find { |upgrade| upgrade.type == "release" }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def available_upgrades
|
||||
upgrade_candidates.select(&:available?)
|
||||
end
|
||||
|
||||
def completed_upgrades
|
||||
upgrade_candidates.select(&:complete?)
|
||||
end
|
||||
|
||||
def upgrade_candidates
|
||||
latest_candidates = fetch_latest_upgrade_candidates_from_provider
|
||||
return [] unless latest_candidates
|
||||
|
||||
commit_candidate = Upgrade.new("commit", latest_candidates[:commit])
|
||||
release_candidate = latest_candidates[:release] && Upgrade.new("release", latest_candidates[:release])
|
||||
|
||||
[ release_candidate, commit_candidate ].compact.uniq { |candidate| candidate.commit_sha }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,17 +0,0 @@
|
|||
class Upgrader::Config
|
||||
attr_reader :env, :options
|
||||
|
||||
def initialize(options = {}, env: ENV)
|
||||
@env = env
|
||||
@options = options
|
||||
end
|
||||
|
||||
def deployer
|
||||
factory = Upgrader::Deployer
|
||||
factory.for(hosting_platform)
|
||||
end
|
||||
|
||||
def hosting_platform
|
||||
options[:hosting_platform] || env["HOSTING_PLATFORM"]
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
class Upgrader::Deployer
|
||||
def self.for(platform)
|
||||
case platform
|
||||
when nil, "localhost"
|
||||
Upgrader::Deployer::Null.new
|
||||
when "render"
|
||||
Upgrader::Deployer::Render.new
|
||||
else
|
||||
raise "Unknown platform: #{platform}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
class Upgrader::Deployer::Null
|
||||
def deploy(upgrade)
|
||||
{
|
||||
success: true,
|
||||
message: I18n.t("upgrader.deployer.null_deployer.success_message")
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,41 +0,0 @@
|
|||
class Upgrader::Deployer::Render
|
||||
def deploy(upgrade)
|
||||
if Setting.render_deploy_hook.blank?
|
||||
return {
|
||||
success: false,
|
||||
message: I18n.t("upgrader.deployer.render.error_message_not_set"),
|
||||
troubleshooting_url: "/settings/self_hosting/edit"
|
||||
}
|
||||
end
|
||||
|
||||
Rails.logger.info I18n.t("upgrader.deployer.render.deploy_log_info", type: upgrade.type, commit_sha: upgrade.commit_sha)
|
||||
|
||||
begin
|
||||
uri = URI.parse(Setting.render_deploy_hook)
|
||||
uri.query = [ uri.query, "ref=#{upgrade.commit_sha}" ].compact.join("&")
|
||||
response = Faraday.post(uri.to_s)
|
||||
|
||||
unless response.success?
|
||||
Rails.logger.error I18n.t("upgrader.deployer.render.deploy_log_error", type: upgrade.type, commit_sha: upgrade.commit_sha, error_message: response.body)
|
||||
return default_error_response
|
||||
end
|
||||
|
||||
{
|
||||
success: true,
|
||||
message: I18n.t("upgrader.deployer.render.success_message", commit_sha: upgrade.commit_sha.slice(0, 7))
|
||||
}
|
||||
rescue => e
|
||||
Rails.logger.error I18n.t("upgrader.deployer.render.deploy_log_error", type: upgrade.type, commit_sha: upgrade.commit_sha, error_message: e.message)
|
||||
default_error_response
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def default_error_response
|
||||
{
|
||||
success: false,
|
||||
message: I18n.t("upgrader.deployer.render.error_message_failed_deploy"),
|
||||
troubleshooting_url: I18n.t("upgrader.deployer.render.troubleshooting_url")
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
module Upgrader::Provided
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
private
|
||||
def fetch_latest_upgrade_candidates_from_provider
|
||||
Providers.github.fetch_latest_upgrade_candidates
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,29 +0,0 @@
|
|||
class Upgrader::Upgrade
|
||||
attr_reader :type, :commit_sha, :version, :url
|
||||
|
||||
def initialize(type, data)
|
||||
@type = %w[release commit].include?(type) ? type : raise(ArgumentError, "Type must be either 'release' or 'commit'")
|
||||
@commit_sha = data[:commit_sha]
|
||||
@version = normalize_version(data[:version])
|
||||
@url = data[:url]
|
||||
end
|
||||
|
||||
def complete?
|
||||
commit_sha == Maybe.commit_sha
|
||||
end
|
||||
|
||||
def available?
|
||||
return false if commit_sha == Maybe.commit_sha || version < Maybe.version
|
||||
return false if version == Maybe.version && type == "release"
|
||||
true
|
||||
end
|
||||
|
||||
def to_s
|
||||
type == "release" ? version.to_release_tag : "#{commit_sha.first(7)} (pre-release)"
|
||||
end
|
||||
|
||||
private
|
||||
def normalize_version(version)
|
||||
version.is_a?(Semver) ? version : Semver.new(version)
|
||||
end
|
||||
end
|
|
@ -69,22 +69,6 @@ class User < ApplicationRecord
|
|||
(display_name&.first || email.first).upcase
|
||||
end
|
||||
|
||||
def acknowledge_upgrade_prompt(commit_sha)
|
||||
update!(last_prompted_upgrade_commit_sha: commit_sha)
|
||||
end
|
||||
|
||||
def acknowledge_upgrade_alert(commit_sha)
|
||||
update!(last_alerted_upgrade_commit_sha: commit_sha)
|
||||
end
|
||||
|
||||
def has_seen_upgrade_prompt?(upgrade)
|
||||
last_prompted_upgrade_commit_sha == upgrade.commit_sha
|
||||
end
|
||||
|
||||
def has_seen_upgrade_alert?(upgrade)
|
||||
last_alerted_upgrade_commit_sha == upgrade.commit_sha
|
||||
end
|
||||
|
||||
# Deactivation
|
||||
validate :can_deactivate, if: -> { active_changed? && !active }
|
||||
after_update_commit :purge_later, if: -> { saved_change_to_active?(from: true, to: false) }
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<span class="text-white font-semibold uppercase">Super Admin</span>
|
||||
</div>
|
||||
<div>
|
||||
<%= link_to "Jobs", good_job_url, class: "text-white underline hover:text-gray-100" %>
|
||||
<%= link_to "Jobs", sidekiq_web_url, class: "text-white underline hover:text-gray-100" %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 px-2 py-2 text-white">
|
||||
|
|
|
@ -19,10 +19,6 @@
|
|||
<%= family_notifications_stream %>
|
||||
<%= family_stream %>
|
||||
|
||||
<% if self_hosted? && (upgrade = get_upgrade_for_notification(Current.user, Setting.upgrades_mode)) %>
|
||||
<%= render partial: "shared/upgrade_notification", locals: { upgrade: upgrade } %>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_frame_tag "modal" %>
|
||||
<%= turbo_frame_tag "drawer" %>
|
||||
<%= render "shared/confirm_modal" %>
|
||||
|
|
|
@ -10,7 +10,3 @@
|
|||
|
||||
<%= family_notifications_stream %>
|
||||
<%= family_stream %>
|
||||
|
||||
<% if self_hosted? && (upgrade = get_upgrade_for_notification(Current.user, Setting.upgrades_mode)) %>
|
||||
<%= render partial: "shared/upgrade_notification", locals: { upgrade: upgrade } %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<% if ENV["HOSTING_PLATFORM"] == "render" %>
|
||||
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
|
||||
<div class="space-y-4">
|
||||
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
|
||||
<p class="text-secondary text-sm mb-4"><%= t(".description") %></p>
|
||||
<%= form.url_field :render_deploy_hook, label: t(".render_deploy_hook_label"), placeholder: t(".render_deploy_hook_placeholder"), value: Setting.render_deploy_hook, data: { "auto-submit-form-target" => "auto" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -1,41 +0,0 @@
|
|||
<% if ENV["HOSTING_PLATFORM"] == "render" %>
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
|
||||
<p class="text-secondary text-sm mb-4"><%= t(".description") %></p>
|
||||
|
||||
<%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.radio_button :upgrades_setting, "manual", checked: Setting.upgrades_mode == "manual", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||
<%= form.label :upgrades_mode_manual, t(".manual_title"), class: "text-primary text-sm" do %>
|
||||
<span class="font-medium"><%= t(".manual_title") %></span>
|
||||
<br>
|
||||
<span class="text-secondary">
|
||||
<%= t(".manual_description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.radio_button :upgrades_setting, "release", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "release", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||
<%= form.label :upgrades_mode_release, t(".latest_release_title"), class: "text-primary text-sm" do %>
|
||||
<span class="font-medium"><%= t(".latest_release_title") %></span>
|
||||
<br>
|
||||
<span class="text-secondary">
|
||||
<%= t(".latest_release_description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form.radio_button :upgrades_setting, "commit", checked: Setting.upgrades_mode == "auto" && Setting.upgrades_target == "commit", data: { "auto-submit-form-target" => "auto", "autosubmit-trigger-event": "input" } %>
|
||||
<%= form.label :upgrades_mode_commit, t(".latest_commit_title"), class: "text-primary text-sm" do %>
|
||||
<span class="font-medium"><%= t(".latest_commit_title") %></span>
|
||||
<br>
|
||||
<span class="text-secondary">
|
||||
<%= t(".latest_commit_description") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
<%= settings_section title: t(".general") do %>
|
||||
<div class="space-y-6">
|
||||
<%= render "settings/hostings/upgrade_settings" %>
|
||||
<%= render "settings/hostings/provider_settings" %>
|
||||
<%= render "settings/hostings/synth_settings" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<%# locals: (upgrade:) %>
|
||||
<div class="bg-white space-y-4 text-right fixed bottom-10 right-10 p-5 shadow-border-xs rounded-md z-50 max-w-[350px]">
|
||||
<div>
|
||||
<p><%= link_to upgrade.to_s, upgrade.url, class: "text-sm text-blue-500 underline hover:text-blue-700", target: "_blank" %></p>
|
||||
<% if upgrade.complete? %>
|
||||
<p class="text-primary"><%= t(".app_upgraded", version: upgrade.to_s) %></p>
|
||||
<% else %>
|
||||
<p class="text-primary"><%= t(".new_version_available") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
<%= button_to t(".dismiss"), acknowledge_upgrade_path(upgrade.commit_sha), method: :post, class: "#{upgrade.complete? ? 'bg-gray-900 text-white' : 'bg-gray-100 text-primary'} text-sm font-bold p-2 rounded-lg" %>
|
||||
<% if upgrade.available? %>
|
||||
<%= button_to t(".upgrade_now"), deploy_upgrade_path(upgrade.commit_sha), method: :post, class: "bg-gray-900 hover:bg-gray-700 text-white font-medium text-sm p-2 rounded-lg" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -10,16 +10,3 @@ echo "Precompiling assets..."
|
|||
./bin/rails assets:clean
|
||||
|
||||
echo "Build complete"
|
||||
|
||||
# Self Hosters:
|
||||
#
|
||||
# By default, one-click deploys are free-tier instances (to avoid unexpected charges)
|
||||
# Render does NOT allow free-tier instances to use the `preDeployCommand` feature, so
|
||||
# database migrations must be run in the build step.
|
||||
#
|
||||
# If you're on a paid Render plan, you can remove the `RUN_DB_MIGRATIONS_IN_BUILD_STEP` (or set to `false`)
|
||||
if [ "$RUN_DB_MIGRATIONS_IN_BUILD_STEP" = "true" ]; then
|
||||
echo "Initiating database migrations for the free tier..."
|
||||
bundle exec rails db:migrate
|
||||
echo "Database migrations completed. Reminder: If you've moved to a Render paid plan, you can remove the RUN_DB_MIGRATIONS_IN_BUILD_STEP environment variable to utilize the `preDeployCommand` feature for migrations."
|
||||
fi
|
||||
|
|
100
compose.example.yml
Normal file
100
compose.example.yml
Normal file
|
@ -0,0 +1,100 @@
|
|||
# ===========================================================================
|
||||
# Example Docker Compose file
|
||||
# ===========================================================================
|
||||
#
|
||||
# Purpose:
|
||||
# --------
|
||||
#
|
||||
# This file is an example Docker Compose configuration for self hosting
|
||||
# Maybe on your local machine or on a cloud VPS.
|
||||
#
|
||||
# The configuration below is a "standard" setup that works out of the box,
|
||||
# but if you're running this outside of a local network, it is recommended
|
||||
# to set the environment variables for extra security.
|
||||
#
|
||||
# Setup:
|
||||
# ------
|
||||
#
|
||||
# To run this, you should read the setup guide:
|
||||
#
|
||||
# https://github.com/maybe-finance/maybe/blob/main/docs/hosting/docker.md
|
||||
#
|
||||
# Troubleshooting:
|
||||
# ----------------
|
||||
#
|
||||
# If you run into problems, you should open a Discussion here:
|
||||
#
|
||||
# https://github.com/maybe-finance/maybe/discussions/categories/general
|
||||
#
|
||||
|
||||
x-db-env: &db_env
|
||||
POSTGRES_USER: ${POSTGRES_USER:-maybe_user}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-maybe_password}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-maybe_production}
|
||||
|
||||
x-rails-env: &rails_env
|
||||
<<: *db_env
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE:-a7523c3d0ae56415046ad8abae168d71074a79534a7062258f8d1d51ac2f76d3c3bc86d86b6b0b307df30d9a6a90a2066a3fa9e67c5e6f374dbd7dd4e0778e13}
|
||||
SELF_HOSTED: "true"
|
||||
RAILS_FORCE_SSL: "false"
|
||||
RAILS_ASSUME_SSL: "false"
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
REDIS_URL: redis://redis:6379/1
|
||||
|
||||
services:
|
||||
web:
|
||||
image: ghcr.io/maybe-finance/maybe:latest
|
||||
volumes:
|
||||
- app-storage:/rails/storage
|
||||
ports:
|
||||
- 3000:3000
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
<<: *rails_env
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
worker:
|
||||
image: ghcr.io/maybe-finance/maybe:latest
|
||||
command: bundle exec sidekiq
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
<<: *rails_env
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
<<: *db_env
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
ports:
|
||||
- 6379:6379
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
app-storage:
|
||||
postgres-data:
|
||||
redis-data:
|
|
@ -24,8 +24,6 @@ module Maybe
|
|||
# config.time_zone = "Central Time (US & Canada)"
|
||||
# config.eager_load_paths << Rails.root.join("extras")
|
||||
|
||||
config.active_job.queue_adapter = :good_job
|
||||
|
||||
# TODO: This is here for incremental adoption of localization. This can be removed when all translations are implemented.
|
||||
config.i18n.fallbacks = true
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
development:
|
||||
adapter: postgresql
|
||||
adapter: async
|
||||
|
||||
test:
|
||||
adapter: test
|
||||
|
||||
production:
|
||||
adapter: postgresql
|
||||
adapter: redis
|
||||
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
|
||||
channel_prefix: maybe_production
|
||||
|
|
|
@ -1 +1 @@
|
|||
HMC62biPQuF61XA8tnd/kvwdV2xr/zpfJxG+IHNgGtpuvPXi9oS+YemBGMLte+1Q7elzAAbmKg73699hVLkRcBCk/FaMQjGRF2lnJ9MpxSR/br8Uma2bSH40lIEjxAfzjr4JPSfsHxlArF30hfd+B9obPDOptLQbpENPBsmiuEHX7S0Y8SmKuzDUVrvdfeLoVuMiAZqOP5izpBAbXfvMjI3YH70iJAaPlfAxQqR89O2nSt+N27siyyfkypE3NHQKZFz+Rmo8uJDlaD3eo/uvQN4xsgRCMUar4X2iY4UOd+MIGAPqLzIUhhJ56G5MRDJ4XpJA6RDuGFc/LNyxdXt0WinUX8Yz7zKiKah1NkEhTkH+b2ylFbsN6cjlqcX0yw8Gw8B4osyHQGnj7Tuf1c8k1z3gBoaQALm8zxKCaJ9k6CopVM2GmbpCLcJqjN1L71wCe6MiWsv9LDF/pwuZNG6hWn0oykdkWeBEQyK8g4Wo1AHqgEi8XtRwbaX6yugO5WQFhjQG/LzXcG02E5Co5/r/G7ZSFpRC9ngoOx3LY6MihPRkTIOumCg3HHtAsWBeHe4L/rDIe4A=--hlLxVbnyuYXf7Rku--A6Cwdr3CAW6bRkl1rcRmRw==
|
||||
Be5nAlhacgJFHZJBgO8noswyX/VOrmkMem7wS3YQhoogzG0MCSVxCAVMbFyYFYUwqZrSPkAqUTpgH5OJJ1FB1gZfL9IYYWnEdTzMxM7IvhdDwYllYcM6smbvZEbOiqxLs9VdfC/qFS+1iFtsezBaqxfGdANJsJt3TxoRWl/ZbQ4Od1s0BNkMis1CDZt5RMEQlTz813cE5sXBlxhqEr9/2CaktwPIe5S/Oxrwo8vPFBvrNdox8BysiK9WDik8jJFSVwPSCvg43/MaIJUT0cOILdSxqrATXV143/h6ghNYtrJgoUNFT7wuu0FTU/ovTgtTqQEKG+7PDO1WLFn606bVknjPwfNMGBa9hX3LbRErDDIXNq69um9fPZ8Yq5f9jP++dPbAqbWBEg+JYsZmDgzr7LmtXVzQgAcuMkHaBbL8uxod8S1B6qhXhLNc8Dd1oeHVu0kcLFO2zaqdYRFNEY30JSjjXlG3GExXQE6aEluXvdF2gj9Hjhp7tEXZEJbIx+ZFy+6Xbrd1E2BE8AZUbalExAfudkPSYlAZ+z3fWc2RlNIuBzTYDOWH9Ai8mqsdyGNVEyizXQ==--j/6QtlLtP4mYXIFw--c+AKfDPo9stantWni+u+4Q==
|
|
@ -1,22 +1,7 @@
|
|||
default: &default
|
||||
adapter: postgresql
|
||||
encoding: unicode
|
||||
# Note on DB_POOL_SIZE:
|
||||
# -------------------------------------------------------------------------------------------------------------
|
||||
# To optimize for the simplest self-hosting setup, we run ActionCable, GoodJob, and Rails in the same process.
|
||||
#
|
||||
# This requires DB connections for each:
|
||||
#
|
||||
# Puma: Requires 3 connections (Rails default)
|
||||
# ActionCable: 5 connections (Rails defaults to 4 workers + 1 listener for Postgres adapter)
|
||||
# GoodJob: 15 connections to run in "async" mode. See `good_job.rb` for the breakdown.
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# Total: 23 connections
|
||||
#
|
||||
# We default to this value so that self-hosters don't need to configure anything. Hosted mode will require
|
||||
# a different pool size, as we run ActionCable, GoodJob, and Rails in separate processes.
|
||||
#
|
||||
pool: <%= ENV.fetch("DB_POOL_SIZE") { 23 } %>
|
||||
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>
|
||||
host: <%= ENV.fetch("DB_HOST") { "127.0.0.1" } %>
|
||||
port: <%= ENV.fetch("DB_PORT") { "5432" } %>
|
||||
user: <%= ENV.fetch("POSTGRES_USER") { nil } %>
|
||||
|
|
|
@ -69,11 +69,12 @@ Rails.application.configure do
|
|||
# want to log everything, set the level to "debug".
|
||||
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
|
||||
|
||||
# Use a different cache store in production.
|
||||
# config.cache_store = :mem_cache_store
|
||||
if ENV["CACHE_REDIS_URL"].present?
|
||||
config.cache_store = :redis_cache_store, { url: ENV["CACHE_REDIS_URL"] }
|
||||
end
|
||||
|
||||
config.action_mailer.perform_caching = false
|
||||
|
||||
config.action_mailer.deliver_later_queue_name = :high_priority
|
||||
config.action_mailer.default_url_options = { host: ENV["APP_DOMAIN"] }
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = {
|
||||
|
@ -105,4 +106,7 @@ Rails.application.configure do
|
|||
# ]
|
||||
# Skip DNS rebinding protection for the default health check endpoint.
|
||||
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
|
||||
|
||||
# set REDIS_URL for Sidekiq to use Redis
|
||||
config.active_job.queue_adapter = :sidekiq
|
||||
end
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
Rails.application.configure do
|
||||
config.good_job.enable_cron = true
|
||||
|
||||
if ENV["UPGRADES_ENABLED"] == "true"
|
||||
config.good_job.cron = {
|
||||
auto_upgrade: {
|
||||
cron: "every 2 minutes",
|
||||
class: "AutoUpgradeJob",
|
||||
description: "Check for new versions of the app and upgrade if necessary"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
config.good_job.on_thread_error = ->(exception) { Rails.error.report(exception) }
|
||||
|
||||
# 7 dedicated queue threads + 5 catch-all threads + 3 for job listener, cron, executor = 15 threads allocated
|
||||
# `latency_low` queue for jobs ~30s
|
||||
# `latency_medium` queue for jobs ~1-2 min
|
||||
# `latency_high` queue for jobs ~5+ min
|
||||
config.good_job.queues = "latency_low:2;latency_low,latency_medium:3;latency_low,latency_medium,latency_high:2;*"
|
||||
|
||||
# Auth for jobs admin dashboard
|
||||
ActiveSupport.on_load(:good_job_application_controller) do
|
||||
before_action do
|
||||
raise ActionController::RoutingError.new("Not Found") unless current_user&.super_admin? || Rails.env.development?
|
||||
end
|
||||
|
||||
def current_user
|
||||
session = Session.find_by(id: cookies.signed[:session_token])
|
||||
session&.user
|
||||
end
|
||||
end
|
||||
end
|
9
config/initializers/sidekiq.rb
Normal file
9
config/initializers/sidekiq.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
require "sidekiq/web"
|
||||
|
||||
Sidekiq::Web.use(Rack::Auth::Basic) do |username, password|
|
||||
configured_username = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_USERNAME", "maybe"))
|
||||
configured_password = ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_WEB_PASSWORD", "maybe"))
|
||||
|
||||
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), configured_username) &&
|
||||
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), configured_password)
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
en:
|
||||
upgrader:
|
||||
deployer:
|
||||
null_deployer:
|
||||
success_message: 'No-op: null deployer initiated deploy successfully'
|
||||
render:
|
||||
deploy_log_error: 'Failed to deploy %{type} %{commit_sha} to Render: %{error_message}'
|
||||
deploy_log_info: Deploying %{type} %{commit_sha} to Render...
|
||||
error_message_failed_deploy: Failed to deploy to Render
|
||||
error_message_not_set: Render deploy hook URL is not set
|
||||
success_message: 'Triggered deployment to Render for commit: %{commit_sha}'
|
||||
troubleshooting_url: https://render.com/docs/deploy-hooks
|
|
@ -11,11 +11,6 @@ en:
|
|||
generate_tokens: Generate new code
|
||||
generated_tokens: Generated codes
|
||||
title: Require invite code for signup
|
||||
provider_settings:
|
||||
description: Configure settings for your hosting provider
|
||||
render_deploy_hook_label: Render Deploy Hook URL
|
||||
render_deploy_hook_placeholder: https://api.render.com/deploy/srv-xyz...
|
||||
title: Provider Settings
|
||||
show:
|
||||
general: General Settings
|
||||
invites: Invite Codes
|
||||
|
@ -38,14 +33,4 @@ en:
|
|||
success: Settings updated
|
||||
clear_cache:
|
||||
cache_cleared: Data cache has been cleared. This may take a few moments to complete.
|
||||
upgrade_settings:
|
||||
description: Configure how your application receives updates
|
||||
latest_commit_description: Automatically update to the latest commit (unstable)
|
||||
latest_commit_title: Latest Commit
|
||||
latest_release_description: Automatically update to the most recent release
|
||||
(stable)
|
||||
latest_release_title: Latest Release
|
||||
manual_description: You control when to download and install updates
|
||||
manual_title: Manual
|
||||
title: Auto Upgrade
|
||||
not_authorized: You are not authorized to perform this action
|
||||
|
|
|
@ -10,8 +10,3 @@ en:
|
|||
label: Amount
|
||||
syncing_notice:
|
||||
syncing: Syncing accounts data...
|
||||
upgrade_notification:
|
||||
app_upgraded: The app has been upgraded to %{version}.
|
||||
dismiss: Dismiss
|
||||
new_version_available: A new version of Maybe is available for upgrade.
|
||||
upgrade_now: Upgrade Now
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
en:
|
||||
upgrades:
|
||||
acknowledge:
|
||||
upgrade_complete_dismiss: We hope you enjoy the new features!
|
||||
upgrade_dismissed: Upgrade dismissed
|
||||
upgrade_not_available: Upgrade not available
|
||||
upgrade_not_found: Upgrade not found
|
||||
deploy:
|
||||
upgrade_not_found: Upgrade not found
|
|
@ -1,3 +1,5 @@
|
|||
require "sidekiq/web"
|
||||
|
||||
Rails.application.routes.draw do
|
||||
# MFA routes
|
||||
resource :mfa, controller: "mfa", only: [ :new, :create ] do
|
||||
|
@ -6,7 +8,8 @@ Rails.application.routes.draw do
|
|||
delete :disable
|
||||
end
|
||||
|
||||
mount GoodJob::Engine => "good_job"
|
||||
# Uses basic auth - see config/initializers/sidekiq.rb
|
||||
mount Sidekiq::Web => "/sidekiq"
|
||||
|
||||
get "changelog", to: "pages#changelog"
|
||||
get "feedback", to: "pages#feedback"
|
||||
|
@ -158,14 +161,6 @@ Rails.application.routes.draw do
|
|||
get :accept, on: :member
|
||||
end
|
||||
|
||||
# For managing self-hosted upgrades and release notifications
|
||||
resources :upgrades, only: [] do
|
||||
member do
|
||||
post :acknowledge
|
||||
post :deploy
|
||||
end
|
||||
end
|
||||
|
||||
resources :currencies, only: %i[show]
|
||||
|
||||
resources :impersonation_sessions, only: [ :create ] do
|
||||
|
|
5
config/sidekiq.yml
Normal file
5
config/sidekiq.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
concurrency: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>
|
||||
queues:
|
||||
- [high_priority, 7]
|
||||
- [medium_priority, 2]
|
||||
- [low_priority, 1]
|
|
@ -22,16 +22,4 @@ cloudflare:
|
|||
bucket: <%= ENV['CLOUDFLARE_BUCKET'] %>
|
||||
request_checksum_calculation: "when_required"
|
||||
response_checksum_validation: "when_required"
|
||||
|
||||
# Removed in #702. Uncomment, add gems, update .env.example to enable.
|
||||
#google:
|
||||
# service: GCS
|
||||
# project: <%#= ENV["GCS_PROJECT"] %>
|
||||
# credentials: <%#= Rails.root.join("gcp-storage-keyfile.json") %>
|
||||
# bucket: <%#= ENV["GCS_BUCKET"] %>
|
||||
|
||||
#azure:
|
||||
# service: AzureStorage
|
||||
# storage_account_name: <%#= ENV["AZURE_STORAGE_ACCOUNT_NAME"] %>
|
||||
# storage_access_key: <%#= ENV["AZURE_STORAGE_ACCESS_KEY"] %>
|
||||
# container: <%#= ENV["AZURE_STORAGE_CONTAINER"] %>
|
||||
|
||||
|
|
|
@ -20,11 +20,5 @@ class CreateStockExchanges < ActiveRecord::Migration[7.2]
|
|||
add_index :stock_exchanges, :country
|
||||
add_index :stock_exchanges, :country_code
|
||||
add_index :stock_exchanges, :currency_code
|
||||
|
||||
reversible do |dir|
|
||||
dir.up do
|
||||
load Rails.root.join('db/seeds/exchanges.rb')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
14
db/migrate/20250318212559_remove_good_job.rb
Normal file
14
db/migrate/20250318212559_remove_good_job.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class RemoveGoodJob < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
drop_table :good_job_batches
|
||||
drop_table :good_job_executions
|
||||
drop_table :good_job_processes
|
||||
drop_table :good_job_settings
|
||||
drop_table :good_jobs
|
||||
end
|
||||
|
||||
def down
|
||||
# Add the tables back if needed - see schema.rb for the full table definitions
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
6
db/migrate/20250319145426_remove_self_host_upgrades.rb
Normal file
6
db/migrate/20250319145426_remove_self_host_upgrades.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class RemoveSelfHostUpgrades < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
remove_column :users, :last_prompted_upgrade_commit_sha
|
||||
remove_column :users, :last_alerted_upgrade_commit_sha
|
||||
end
|
||||
end
|
92
db/schema.rb
generated
92
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_03_16_122019) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_03_19_145426) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
|
@ -244,94 +244,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_03_16_122019) do
|
|||
t.boolean "data_enrichment_enabled", default: false
|
||||
end
|
||||
|
||||
create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.text "description"
|
||||
t.jsonb "serialized_properties"
|
||||
t.text "on_finish"
|
||||
t.text "on_success"
|
||||
t.text "on_discard"
|
||||
t.text "callback_queue_name"
|
||||
t.integer "callback_priority"
|
||||
t.datetime "enqueued_at"
|
||||
t.datetime "discarded_at"
|
||||
t.datetime "finished_at"
|
||||
end
|
||||
|
||||
create_table "good_job_executions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.uuid "active_job_id", null: false
|
||||
t.text "job_class"
|
||||
t.text "queue_name"
|
||||
t.jsonb "serialized_params"
|
||||
t.datetime "scheduled_at"
|
||||
t.datetime "finished_at"
|
||||
t.text "error"
|
||||
t.integer "error_event", limit: 2
|
||||
t.text "error_backtrace", array: true
|
||||
t.uuid "process_id"
|
||||
t.interval "duration"
|
||||
t.index ["active_job_id", "created_at"], name: "index_good_job_executions_on_active_job_id_and_created_at"
|
||||
t.index ["process_id", "created_at"], name: "index_good_job_executions_on_process_id_and_created_at"
|
||||
end
|
||||
|
||||
create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.jsonb "state"
|
||||
t.integer "lock_type", limit: 2
|
||||
end
|
||||
|
||||
create_table "good_job_settings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.text "key"
|
||||
t.jsonb "value"
|
||||
t.index ["key"], name: "index_good_job_settings_on_key", unique: true
|
||||
end
|
||||
|
||||
create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.text "queue_name"
|
||||
t.integer "priority"
|
||||
t.jsonb "serialized_params"
|
||||
t.datetime "scheduled_at"
|
||||
t.datetime "performed_at"
|
||||
t.datetime "finished_at"
|
||||
t.text "error"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.uuid "active_job_id"
|
||||
t.text "concurrency_key"
|
||||
t.text "cron_key"
|
||||
t.uuid "retried_good_job_id"
|
||||
t.datetime "cron_at"
|
||||
t.uuid "batch_id"
|
||||
t.uuid "batch_callback_id"
|
||||
t.boolean "is_discrete"
|
||||
t.integer "executions_count"
|
||||
t.text "job_class"
|
||||
t.integer "error_event", limit: 2
|
||||
t.text "labels", array: true
|
||||
t.uuid "locked_by_id"
|
||||
t.datetime "locked_at"
|
||||
t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at"
|
||||
t.index ["batch_callback_id"], name: "index_good_jobs_on_batch_callback_id", where: "(batch_callback_id IS NOT NULL)"
|
||||
t.index ["batch_id"], name: "index_good_jobs_on_batch_id", where: "(batch_id IS NOT NULL)"
|
||||
t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)"
|
||||
t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at_cond", where: "(cron_key IS NOT NULL)"
|
||||
t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at_cond", unique: true, where: "(cron_key IS NOT NULL)"
|
||||
t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))"
|
||||
t.index ["labels"], name: "index_good_jobs_on_labels", where: "(labels IS NOT NULL)", using: :gin
|
||||
t.index ["locked_by_id"], name: "index_good_jobs_on_locked_by_id", where: "(locked_by_id IS NOT NULL)"
|
||||
t.index ["priority", "created_at"], name: "index_good_job_jobs_for_candidate_lookup", where: "(finished_at IS NULL)"
|
||||
t.index ["priority", "created_at"], name: "index_good_jobs_jobs_on_priority_created_at_when_unfinished", order: { priority: "DESC NULLS LAST" }, where: "(finished_at IS NULL)"
|
||||
t.index ["priority", "scheduled_at"], name: "index_good_jobs_on_priority_scheduled_at_unfinished_unlocked", where: "((finished_at IS NULL) AND (locked_by_id IS NULL))"
|
||||
t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)"
|
||||
t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)"
|
||||
end
|
||||
|
||||
create_table "impersonation_session_logs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "impersonation_session_id", null: false
|
||||
t.string "controller"
|
||||
|
@ -652,8 +564,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_03_16_122019) do
|
|||
t.string "password_digest"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "last_prompted_upgrade_commit_sha"
|
||||
t.string "last_alerted_upgrade_commit_sha"
|
||||
t.string "role", default: "member", null: false
|
||||
t.boolean "active", default: true, null: false
|
||||
t.datetime "onboarded_at"
|
||||
|
|
0
db/seeds/.keep
Normal file
0
db/seeds/.keep
Normal file
|
@ -1,36 +0,0 @@
|
|||
# Load exchanges from YAML configuration
|
||||
exchanges_config = YAML.safe_load(
|
||||
File.read(Rails.root.join('config', 'exchanges.yml')),
|
||||
permitted_classes: [],
|
||||
permitted_symbols: [],
|
||||
aliases: true
|
||||
)
|
||||
|
||||
exchanges_config.each do |exchange|
|
||||
next unless exchange['mic'].present? # Skip any invalid entries
|
||||
|
||||
StockExchange.find_or_create_by!(mic: exchange['mic']) do |ex|
|
||||
ex.name = exchange['name']
|
||||
ex.acronym = exchange['acronym']
|
||||
ex.country = exchange['country']
|
||||
ex.country_code = exchange['country_code']
|
||||
ex.city = exchange['city']
|
||||
ex.website = exchange['website']
|
||||
|
||||
# Timezone details
|
||||
if exchange['timezone']
|
||||
ex.timezone_name = exchange['timezone']['timezone']
|
||||
ex.timezone_abbr = exchange['timezone']['abbr']
|
||||
ex.timezone_abbr_dst = exchange['timezone']['abbr_dst']
|
||||
end
|
||||
|
||||
# Currency details
|
||||
if exchange['currency']
|
||||
ex.currency_code = exchange['currency']['code']
|
||||
ex.currency_symbol = exchange['currency']['symbol']
|
||||
ex.currency_name = exchange['currency']['name']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts "Created #{StockExchange.count} stock exchanges"
|
|
@ -1,74 +0,0 @@
|
|||
# ===========================================================================
|
||||
# Example Docker Compose file
|
||||
# ===========================================================================
|
||||
#
|
||||
# Purpose:
|
||||
# --------
|
||||
#
|
||||
# This file is an example Docker Compose configuration for self hosting
|
||||
# Maybe on your local machine or on a cloud VPS.
|
||||
#
|
||||
# The configuration below is a "standard" setup, but may require modification
|
||||
# for your specific environment.
|
||||
#
|
||||
# Setup:
|
||||
# ------
|
||||
#
|
||||
# To run this, you should read the setup guide:
|
||||
#
|
||||
# https://github.com/maybe-finance/maybe/blob/main/docs/hosting/docker.md
|
||||
#
|
||||
# Troubleshooting:
|
||||
# ----------------
|
||||
#
|
||||
# If you run into problems, you should open a Discussion here:
|
||||
#
|
||||
# https://github.com/maybe-finance/maybe/discussions/categories/general
|
||||
#
|
||||
|
||||
services:
|
||||
|
||||
app:
|
||||
image: ghcr.io/maybe-finance/maybe:latest
|
||||
|
||||
volumes:
|
||||
- app-storage:/rails/storage
|
||||
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
SELF_HOSTED: "true"
|
||||
RAILS_FORCE_SSL: "false"
|
||||
RAILS_ASSUME_SSL: "false"
|
||||
GOOD_JOB_EXECUTION_MODE: async
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE:?}
|
||||
DB_HOST: postgres
|
||||
POSTGRES_DB: ${POSTGRES_DB:-maybe_production}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-maybe_user}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
|
||||
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-maybe_user}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-maybe_production}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
app-storage:
|
||||
postgres-data:
|
|
@ -2,10 +2,6 @@
|
|||
|
||||
This guide will help you setup, update, and maintain your self-hosted Maybe application with Docker Compose. Docker Compose is the most popular and recommended way to self-host the Maybe app.
|
||||
|
||||
If you want a _less
|
||||
technical_ way to host the Maybe app, you can [host on Render](/docs/hosting/one-click-deploy.md) as an
|
||||
_**alternative** to Docker Compose_.
|
||||
|
||||
## Setup Guide
|
||||
|
||||
Follow the guide below to get your app running.
|
||||
|
@ -30,7 +26,7 @@ docker run hello-world
|
|||
Open your terminal and create a directory where your app will run. Below is an example command with a recommended directory:
|
||||
|
||||
```bash
|
||||
# Create a directory on your computer for Docker files
|
||||
# Create a directory on your computer for Docker files (name whatever you'd like)
|
||||
mkdir -p ~/docker-apps/maybe
|
||||
|
||||
# Once created, navigate your current working directory to the new folder
|
||||
|
@ -42,8 +38,8 @@ cd ~/docker-apps/maybe
|
|||
Make sure you are in the directory you just created and run the following command:
|
||||
|
||||
```bash
|
||||
# Download the sample docker-compose.yml file from the Maybe Github repository
|
||||
curl -o compose.yml https://raw.githubusercontent.com/maybe-finance/maybe/main/docker-compose.example.yml
|
||||
# Download the sample compose.yml file from the Maybe Github repository
|
||||
curl -o compose.yml https://raw.githubusercontent.com/maybe-finance/maybe/main/compose.example.yml
|
||||
```
|
||||
|
||||
This command will do the following:
|
||||
|
@ -53,6 +49,12 @@ This command will do the following:
|
|||
|
||||
At this point, the only file in your current working directory should be `compose.yml`.
|
||||
|
||||
### Step 3 (optional): Configure your environment
|
||||
|
||||
By default, our `compose.example.yml` file runs without any configuration. That said, if you would like extra security (important if you're running outside of a local network), you can follow the steps below to set things up.
|
||||
|
||||
If you're running the app locally and don't care much about security, you can skip this step.
|
||||
|
||||
#### Create your environment file
|
||||
|
||||
In order to configure the app, you will need to create a file called `.env`, which is where Docker will read environment variables from.
|
||||
|
@ -92,7 +94,7 @@ SECRET_KEY_BASE="replacemewiththegeneratedstringfromthepriorstep"
|
|||
POSTGRES_PASSWORD="replacemewithyourdesireddatabasepassword"
|
||||
```
|
||||
|
||||
### Step 3: Test your app
|
||||
### Step 4: Run the app
|
||||
|
||||
You are now ready to run the app. Start with the following command to make sure everything is working:
|
||||
|
||||
|
@ -106,14 +108,14 @@ Open your browser, and navigate to `http://localhost:3000`.
|
|||
|
||||
If everything is working, you will see the Maybe login screen.
|
||||
|
||||
### Step 4: Create your account
|
||||
### Step 5: Create your account
|
||||
|
||||
The first time you run the app, you will need to register a new account by hitting "create your account" on the login page.
|
||||
|
||||
1. Enter your email
|
||||
2. Enter a password
|
||||
|
||||
### Step 5: Run the app in the background
|
||||
### Step 6: Run the app in the background
|
||||
|
||||
Most self-hosting users will want the Maybe app to run in the background on their computer so they can access it at all times. To do this, hit `Ctrl+C` to stop the running process, and then run the following command:
|
||||
|
||||
|
@ -127,7 +129,7 @@ The `-d` flag will run Docker Compose in "detached" mode. To verify it is runnin
|
|||
docker compose ls
|
||||
```
|
||||
|
||||
### Step 6: Enjoy!
|
||||
### Step 7: Enjoy!
|
||||
|
||||
Your app is now set up. You can visit it at `http://localhost:3000` in your browser.
|
||||
|
||||
|
@ -135,7 +137,7 @@ If you find bugs or have a feature request, be sure to read through our [contrib
|
|||
|
||||
## How to update your app
|
||||
|
||||
The mechanism that updates your self-hosted Maybe app is the GHCR (Github Container Registry) Docker image that you see in the `docker-compose.yml` file:
|
||||
The mechanism that updates your self-hosted Maybe app is the GHCR (Github Container Registry) Docker image that you see in the `compose.yml` file:
|
||||
|
||||
```yml
|
||||
image: ghcr.io/maybe-finance/maybe:latest
|
||||
|
@ -152,13 +154,13 @@ NOT_ automatically update. To update your self-hosted app, run the following com
|
|||
```bash
|
||||
cd ~/docker-apps/maybe # Navigate to whatever directory you configured the app in
|
||||
docker compose pull # This pulls the "latest" published image from GHCR
|
||||
docker compose build app # This rebuilds the app with updates
|
||||
docker compose build # This rebuilds the app with updates
|
||||
docker compose up --no-deps -d app # This restarts the app using the newest version
|
||||
```
|
||||
|
||||
## How to change which updates your app receives
|
||||
|
||||
If you'd like to pin the app to a specific version or tag, all you need to do is edit the `docker-compose.yml` file:
|
||||
If you'd like to pin the app to a specific version or tag, all you need to do is edit the `compose.yml` file:
|
||||
|
||||
```yml
|
||||
image: ghcr.io/maybe-finance/maybe:stable
|
||||
|
@ -168,7 +170,7 @@ After doing this, make sure and restart the app:
|
|||
|
||||
```bash
|
||||
docker compose pull # This pulls the "latest" published image from GHCR
|
||||
docker compose build app # This rebuilds the app with updates
|
||||
docker compose build # This rebuilds the app with updates
|
||||
docker compose up --no-deps -d app # This restarts the app using the newest version
|
||||
```
|
||||
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
# Deploy Maybe in One Click
|
||||
|
||||
Below are our "one-click deploy" options for running Maybe in the cloud:
|
||||
|
||||
## Render
|
||||
|
||||
Welcome to the one-click deploy guide for Maybe on [Render](https://render.com/)!
|
||||
|
||||
Render is a hosting platform with a generous free tier and makes it easy to get
|
||||
started with Maybe:
|
||||
|
||||
- Getting started is FREE
|
||||
- Up and running in <5 minutes
|
||||
- Your Maybe app is automatically deployed to a live URL
|
||||
|
||||
### Estimated Costs
|
||||
|
||||
- FREE to _get up and running_
|
||||
- $7 per month for a basic app (Render requires you to upgrade your database to
|
||||
keep using it)
|
||||
- $14+ per month for optimal performance
|
||||
|
||||
_**IMPORTANT:** if you plan to host Maybe on Render long-term, you MUST upgrade
|
||||
your database to a paid Render service._
|
||||
|
||||
### Instructions
|
||||
|
||||
#### Step 1: Create Render Blueprint
|
||||
|
||||
<a href="https://render.com/deploy?repo=https://github.com/maybe-finance/maybe">
|
||||
<img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" />
|
||||
</a>
|
||||
|
||||
1. Click the button above.
|
||||
2. Sign in or create your account with Render (FREE)
|
||||
3. Give your blueprint a name (we suggest `Maybe`)
|
||||
4. Select the `main` branch
|
||||
5. You should see a section at the bottom with a "Key:Value" field
|
||||
for `SECRET_KEY_BASE`. Do NOT click "generate".
|
||||
6. On your computer, open a terminal and make sure you have
|
||||
the [openssl](https://github.com/openssl/openssl) utility installed on your
|
||||
computer. You can run `openssl --version` to verify it is installed.
|
||||
7. Generate your `SECRET_KEY_BASE` by running the following command in your
|
||||
terminal: `openssl rand -hex 64` ([docs](https://www.openssl.org/docs/man1.1.1/man1/rand.html)).
|
||||
8. Do NOT share this value with anyone.
|
||||
9. Go back to your browser and paste this value in the "Value" field
|
||||
for `SECRET_KEY_BASE`
|
||||
10. Click "Apply". This will take a few minutes.
|
||||
11. Once complete, click on the `maybe` "Web Service". You should see a custom
|
||||
URL in the format `https://maybe-abcd.onrender.com`. Click on it, and you'll
|
||||
see your running Maybe app!
|
||||
|
||||
#### Step 2: Add your deploy hook for auto-updates
|
||||
|
||||
To get new releases, you will need to add your deploy hook to the app.
|
||||
|
||||
1. Click on the `maybe` "Web Service"
|
||||
2. Click "Settings"
|
||||
3. Scroll down to the end of the "Build and Deploy" section until you find the "
|
||||
Deploy Hook"
|
||||
4. Copy this value
|
||||
5. Open your new Maybe app, click your profile, click "Self Host Settings"
|
||||
6. Paste your deploy hook in the settings and save
|
||||
7. You're all set!
|
||||
|
||||
#### Step 3 (IMPORTANT!!!): Upgrade your Render services
|
||||
|
||||
By default, we set you up with a FREE Render web service and a FREE postgres
|
||||
database. We do this for a few reasons:
|
||||
|
||||
- It allows you to take self-hosted Maybe for a FREE test-drive
|
||||
- It prevents newcomers from incurring unexpected hosting charges
|
||||
|
||||
##### Upgrade your Database (REQUIRED)
|
||||
|
||||
All FREE Render databases **will be deleted after a few months**. This means
|
||||
that **you will lose all of your Maybe data**.
|
||||
|
||||
**To avoid losing data, you MUST upgrade your Render database** (a "starter"
|
||||
instance is $7/month)
|
||||
|
||||
You can upgrade your instance directly in the Render dashboard.
|
||||
|
||||
##### Upgrade your Web Service (RECOMMENDED)
|
||||
|
||||
All FREE Render web services use a small amount of memory and "sleep" after
|
||||
periods of inactivity.
|
||||
|
||||
For the _fastest_ Maybe experience, you should upgrade your web service (a "
|
||||
starter" instance is $7/month)
|
60
render.yaml
60
render.yaml
|
@ -1,60 +0,0 @@
|
|||
databases:
|
||||
- name: maybe
|
||||
user: maybe
|
||||
plan: free
|
||||
|
||||
services:
|
||||
- type: web
|
||||
plan: free
|
||||
autoDeploy: false
|
||||
runtime: ruby
|
||||
name: maybe
|
||||
repo: https://github.com/maybe-finance/maybe.git
|
||||
branch: main
|
||||
healthCheckPath: /up
|
||||
buildCommand: "./bin/render-build.sh"
|
||||
|
||||
# Uncomment if you are on a paid plan, and remove RUN_DB_MIGRATIONS_IN_BUILD_STEP from below
|
||||
# preDeployCommand: "bundle exec rails db:migrate"
|
||||
|
||||
startCommand: "bundle exec rails server"
|
||||
envVars:
|
||||
- key: DATABASE_URL
|
||||
fromDatabase:
|
||||
name: maybe
|
||||
property: connectionString
|
||||
|
||||
- key: SELF_HOSTED
|
||||
value: true
|
||||
- key: HOSTING_PLATFORM
|
||||
value: render
|
||||
|
||||
# Since the app is self-hosted, we cannot use master.key to encrypt credentials. App depends entirely on ENV variables
|
||||
# https://api.rubyonrails.org/v7.1.3.2/classes/Rails/Application.html#method-i-secret_key_base
|
||||
#
|
||||
# To generate this, run: `openssl rand -hex 64` or `rails secret`
|
||||
- key: SECRET_KEY_BASE
|
||||
sync: false
|
||||
|
||||
- key: WEB_CONCURRENCY
|
||||
value: 2
|
||||
- key: GOOD_JOB_EXECUTION_MODE
|
||||
value: async # Typically, `external` is used in prod, but this avoids another cron service and is generally fine for a self-hoster given low traffic
|
||||
|
||||
# The app uses this info to know which repo to fetch latest commit data from for upgrades
|
||||
# This should MATCH the `repo` and `branch` keys in the config above ALWAYS
|
||||
- key: GITHUB_REPO_OWNER
|
||||
value: maybe-finance
|
||||
- key: GITHUB_REPO_NAME
|
||||
value: maybe
|
||||
- key: GITHUB_REPO_BRANCH
|
||||
value: main
|
||||
|
||||
# Required to allow your self-hosted instance to be able to upgrade itself
|
||||
- key: UPGRADES_ENABLED
|
||||
value: true
|
||||
|
||||
# If you upgrade your instance to a paid plan, you can set this to false (or remove it)
|
||||
# See note in `render-build.sh` script.
|
||||
- key: RUN_DB_MIGRATIONS_IN_BUILD_STEP
|
||||
value: true
|
|
@ -37,17 +37,6 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
|||
assert_nil Session.find_by(id: session_record.id)
|
||||
end
|
||||
|
||||
test "super admins can access the jobs page" do
|
||||
sign_in users(:maybe_support_staff)
|
||||
get good_job_url
|
||||
assert_redirected_to "http://www.example.com/good_job/jobs?locale=en"
|
||||
end
|
||||
|
||||
test "non-super admins cannot access the jobs page" do
|
||||
get good_job_url
|
||||
assert_response :not_found
|
||||
end
|
||||
|
||||
test "redirects to MFA verification when MFA enabled" do
|
||||
@user.setup_mfa!
|
||||
@user.enable_mfa!
|
||||
|
|
|
@ -25,7 +25,7 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
|
|||
end
|
||||
|
||||
assert_raises(RuntimeError, "Settings not available on non-self-hosted instance") do
|
||||
patch settings_hosting_url, params: { setting: { render_deploy_hook: "https://example.com" } }
|
||||
patch settings_hosting_url, params: { setting: { require_invite_for_signup: true } }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -40,25 +40,11 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
|
|||
|
||||
test "can update settings when self hosting is enabled" do
|
||||
with_self_hosting do
|
||||
NEW_RENDER_DEPLOY_HOOK = "https://api.render.com/deploy/srv-abc123"
|
||||
assert_nil Setting.render_deploy_hook
|
||||
assert_nil Setting.synth_api_key
|
||||
|
||||
patch settings_hosting_url, params: { setting: { render_deploy_hook: NEW_RENDER_DEPLOY_HOOK } }
|
||||
patch settings_hosting_url, params: { setting: { synth_api_key: "1234567890" } }
|
||||
|
||||
assert_equal NEW_RENDER_DEPLOY_HOOK, Setting.render_deploy_hook
|
||||
end
|
||||
end
|
||||
|
||||
test "can choose auto upgrades mode with a deploy hook" do
|
||||
with_self_hosting do
|
||||
NEW_RENDER_DEPLOY_HOOK = "https://api.render.com/deploy/srv-abc123"
|
||||
assert_nil Setting.render_deploy_hook
|
||||
|
||||
patch settings_hosting_url, params: { setting: { render_deploy_hook: NEW_RENDER_DEPLOY_HOOK, upgrades_setting: "release" } }
|
||||
|
||||
assert_equal "auto", Setting.upgrades_mode
|
||||
assert_equal "release", Setting.upgrades_target
|
||||
assert_equal NEW_RENDER_DEPLOY_HOOK, Setting.render_deploy_hook
|
||||
assert_equal "1234567890", Setting.synth_api_key
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class UpgradesControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in @user = users(:family_admin)
|
||||
|
||||
@completed_upgrade = Upgrader::Upgrade.new(
|
||||
"commit",
|
||||
commit_sha: "47bb430954292d2fdcc81082af731a16b9587da3",
|
||||
version: Semver.new("0.0.0"),
|
||||
url: ""
|
||||
)
|
||||
|
||||
@completed_upgrade.stubs(:complete?).returns(true)
|
||||
@completed_upgrade.stubs(:available?).returns(false)
|
||||
|
||||
@available_upgrade = Upgrader::Upgrade.new(
|
||||
"commit",
|
||||
commit_sha: "47bb430954292d2fdcc81082af731a16b9587da4",
|
||||
version: Semver.new("0.1.0"),
|
||||
url: ""
|
||||
)
|
||||
|
||||
@available_upgrade.stubs(:available?).returns(true)
|
||||
@available_upgrade.stubs(:complete?).returns(false)
|
||||
end
|
||||
|
||||
test "controller not available when upgrades are disabled" do
|
||||
MOCK_COMMIT = "47bb430954292d2fdcc81082af731a16b9587da3"
|
||||
|
||||
post acknowledge_upgrade_url(MOCK_COMMIT)
|
||||
assert_response :not_found
|
||||
|
||||
post deploy_upgrade_url(MOCK_COMMIT)
|
||||
assert_response :not_found
|
||||
end
|
||||
|
||||
test "should acknowledge an upgrade prompt" do
|
||||
with_env_overrides UPGRADES_ENABLED: "true" do
|
||||
Upgrader.stubs(:find_upgrade).returns(@available_upgrade)
|
||||
|
||||
post acknowledge_upgrade_url(@available_upgrade.commit_sha)
|
||||
|
||||
@user.reload
|
||||
assert_equal @user.last_prompted_upgrade_commit_sha, @available_upgrade.commit_sha
|
||||
assert :redirect
|
||||
end
|
||||
end
|
||||
|
||||
test "should acknowledge an upgrade alert" do
|
||||
with_env_overrides UPGRADES_ENABLED: "true" do
|
||||
Upgrader.stubs(:find_upgrade).returns(@completed_upgrade)
|
||||
|
||||
post acknowledge_upgrade_url(@completed_upgrade.commit_sha)
|
||||
|
||||
@user.reload
|
||||
assert_equal @user.last_alerted_upgrade_commit_sha, @completed_upgrade.commit_sha
|
||||
assert :redirect
|
||||
end
|
||||
end
|
||||
|
||||
test "should deploy an upgrade" do
|
||||
with_env_overrides UPGRADES_ENABLED: "true" do
|
||||
Upgrader.stubs(:find_upgrade).returns(@available_upgrade)
|
||||
|
||||
post deploy_upgrade_path(@available_upgrade.commit_sha)
|
||||
|
||||
@user.reload
|
||||
assert_equal @user.last_prompted_upgrade_commit_sha, @available_upgrade.commit_sha
|
||||
assert :redirect
|
||||
end
|
||||
end
|
||||
|
||||
test "should rollback user state if upgrade fails" do
|
||||
with_env_overrides UPGRADES_ENABLED: "true" do
|
||||
PRIOR_COMMIT = "47bb430954292d2fdcc81082af731a16b9587da2"
|
||||
@user.update!(last_prompted_upgrade_commit_sha: PRIOR_COMMIT)
|
||||
|
||||
Upgrader.stubs(:find_upgrade).returns(@available_upgrade)
|
||||
Upgrader.stubs(:upgrade_to).returns({ success: false })
|
||||
|
||||
post deploy_upgrade_path(@available_upgrade.commit_sha)
|
||||
|
||||
@user.reload
|
||||
assert_equal @user.last_prompted_upgrade_commit_sha, PRIOR_COMMIT
|
||||
assert :redirect
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
module GitRepositoryProviderInterfaceTest
|
||||
extend ActiveSupport::Testing::Declarative
|
||||
|
||||
test "git repository provider interface" do
|
||||
assert_respond_to @subject, :fetch_latest_upgrade_candidates
|
||||
end
|
||||
|
||||
test "git repository provider response contract" do
|
||||
VCR.use_cassette "git_repository_provider/fetch_latest_upgrade_candidates" do
|
||||
response = @subject.fetch_latest_upgrade_candidates
|
||||
|
||||
assert_valid_upgrade_candidate(response[:release])
|
||||
assert_valid_upgrade_candidate(response[:commit])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def assert_valid_upgrade_candidate(candidate)
|
||||
assert_equal Semver, candidate[:version].class
|
||||
assert_match URI::DEFAULT_PARSER.make_regexp, candidate[:url]
|
||||
assert_match(/\A[0-9a-f]{40}\z/, candidate[:commit_sha])
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class AutoUpgradeJobTest < ActiveJob::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class Provider::GithubTest < ActiveSupport::TestCase
|
||||
include GitRepositoryProviderInterfaceTest
|
||||
|
||||
setup do
|
||||
@subject = Provider::Github.new(owner: "rails", name: "rails", branch: "main")
|
||||
end
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class UpgradeTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
data = {
|
||||
commit_sha: "latestcommit",
|
||||
version: Semver.new("0.1.0-alpha.2")
|
||||
}
|
||||
|
||||
@commit_upgrade = Upgrader::Upgrade.new "commit", data
|
||||
@release_upgrade = Upgrader::Upgrade.new "release", data
|
||||
end
|
||||
|
||||
test "available if latest commit and app not upgraded" do
|
||||
Maybe.stubs(:version).returns(@commit_upgrade.version)
|
||||
Maybe.stubs(:commit_sha).returns("outdatedcommitsha")
|
||||
|
||||
assert @commit_upgrade.available?
|
||||
assert_not @release_upgrade.available?
|
||||
end
|
||||
|
||||
test "available if latest release and app not upgraded" do
|
||||
Maybe.stubs(:version).returns(Semver.new("0.1.0-alpha.1"))
|
||||
Maybe.stubs(:commit_sha).returns("outdatedcommitsha")
|
||||
|
||||
assert @commit_upgrade.available?
|
||||
assert @release_upgrade.available?
|
||||
end
|
||||
|
||||
test "not available if app commit greater or equal to" do
|
||||
Maybe.stubs(:version).returns(@commit_upgrade.version)
|
||||
Maybe.stubs(:commit_sha).returns(@commit_upgrade.commit_sha)
|
||||
|
||||
assert_not @commit_upgrade.available?
|
||||
end
|
||||
end
|
|
@ -1,88 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class UpgraderTest < ActiveSupport::TestCase
|
||||
PRIOR_COMMIT = "47bb430954292d2fdcc81082af731a16b9587da2"
|
||||
CURRENT_COMMIT = "47bb430954292d2fdcc81082af731a16b9587da3"
|
||||
NEXT_COMMIT = "47bb430954292d2fdcc81082af731a16b9587da4"
|
||||
|
||||
PRIOR_VERSION = Semver.new("0.1.0-alpha.3")
|
||||
CURRENT_VERSION = Semver.new("0.1.0-alpha.4")
|
||||
NEXT_VERSION = Semver.new("0.1.0-alpha.5")
|
||||
|
||||
# Default setup assumes app is up to date
|
||||
setup do
|
||||
Upgrader.config = Upgrader::Config.new({ mode: :enabled })
|
||||
|
||||
Maybe.stubs(:version).returns(CURRENT_VERSION)
|
||||
Maybe.stubs(:commit_sha).returns(CURRENT_COMMIT)
|
||||
|
||||
stub_github_data(
|
||||
commit: create_upgrade_stub(CURRENT_VERSION, CURRENT_COMMIT),
|
||||
release: create_upgrade_stub(CURRENT_VERSION, CURRENT_COMMIT)
|
||||
)
|
||||
end
|
||||
|
||||
test "finds 1 completed upgrade, 0 available upgrades when app is up to date" do
|
||||
assert_instance_of Upgrader::Upgrade, Upgrader.completed_upgrade
|
||||
assert_nil Upgrader.available_upgrade
|
||||
end
|
||||
|
||||
test "finds 1 available and 1 completed upgrade when app is on latest release but behind latest commit" do
|
||||
stub_github_data(
|
||||
commit: create_upgrade_stub(CURRENT_VERSION, NEXT_COMMIT),
|
||||
release: create_upgrade_stub(CURRENT_VERSION, CURRENT_COMMIT)
|
||||
)
|
||||
|
||||
assert_instance_of Upgrader::Upgrade, Upgrader.available_upgrade # commit is ahead of release
|
||||
assert_instance_of Upgrader::Upgrade, Upgrader.completed_upgrade # release is completed
|
||||
end
|
||||
|
||||
test "when app is behind latest version and latest commit is ahead of release finds release upgrade and no completed upgrades" do
|
||||
Maybe.stubs(:version).returns(PRIOR_VERSION)
|
||||
Maybe.stubs(:commit_sha).returns(PRIOR_COMMIT)
|
||||
|
||||
stub_github_data(
|
||||
commit: create_upgrade_stub(CURRENT_VERSION, NEXT_COMMIT),
|
||||
release: create_upgrade_stub(CURRENT_VERSION, CURRENT_COMMIT)
|
||||
)
|
||||
|
||||
assert_equal "release", Upgrader.available_upgrade.type
|
||||
assert_nil Upgrader.completed_upgrade
|
||||
end
|
||||
|
||||
test "defaults to app version when no release is found" do
|
||||
stub_github_data(
|
||||
commit: create_upgrade_stub(CURRENT_VERSION, NEXT_COMMIT),
|
||||
release: nil
|
||||
)
|
||||
|
||||
# Upstream is 1 commit ahead, and we assume we're on the same release
|
||||
assert_equal "commit", Upgrader.available_upgrade.type
|
||||
end
|
||||
|
||||
test "gracefully handles empty github info" do
|
||||
Provider::Github.any_instance.stubs(:fetch_latest_upgrade_candidates).returns(nil)
|
||||
|
||||
assert_nil Upgrader.available_upgrade
|
||||
assert_nil Upgrader.completed_upgrade
|
||||
end
|
||||
|
||||
test "deployer is null by default" do
|
||||
Upgrader.config = Upgrader::Config.new({ mode: :enabled })
|
||||
Upgrader::Deployer::Null.any_instance.expects(:deploy).with(nil).once
|
||||
Upgrader.upgrade_to(nil)
|
||||
end
|
||||
|
||||
private
|
||||
def create_upgrade_stub(version, commit_sha)
|
||||
{
|
||||
version: version,
|
||||
commit_sha: commit_sha,
|
||||
url: ""
|
||||
}
|
||||
end
|
||||
|
||||
def stub_github_data(commit: create_upgrade_stub(LATEST_VERSION, LATEST_COMMIT), release: create_upgrade_stub(LATEST_VERSION, LATEST_COMMIT))
|
||||
Provider::Github.any_instance.stubs(:fetch_latest_upgrade_candidates).returns({ commit:, release: })
|
||||
end
|
||||
end
|
File diff suppressed because one or more lines are too long
|
@ -1,235 +0,0 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.github.com/repos/rails/rails/releases
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ""
|
||||
headers:
|
||||
Accept:
|
||||
- application/vnd.github.v3+json
|
||||
User-Agent:
|
||||
- Octokit Ruby Gem 8.1.0
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- GitHub.com
|
||||
Date:
|
||||
- Wed, 10 Apr 2024 19:52:56 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Cache-Control:
|
||||
- public, max-age=60, s-maxage=60
|
||||
Vary:
|
||||
- Accept, Accept-Encoding, Accept, X-Requested-With
|
||||
Etag:
|
||||
- W/"a032e5cc14d6dc10a55126bd742c08afc1365c4cf381d6d5ce3b4014cfbf2de5"
|
||||
X-Github-Media-Type:
|
||||
- github.v3; format=json
|
||||
Link:
|
||||
- <https://api.github.com/repositories/8514/releases?page=2>; rel="next", <https://api.github.com/repositories/8514/releases?page=4>;
|
||||
rel="last"
|
||||
X-Github-Api-Version-Selected:
|
||||
- "2022-11-28"
|
||||
Access-Control-Expose-Headers:
|
||||
- ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining,
|
||||
X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes,
|
||||
X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO,
|
||||
X-GitHub-Request-Id, Deprecation, Sunset
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubdomains; preload
|
||||
X-Frame-Options:
|
||||
- deny
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Xss-Protection:
|
||||
- "0"
|
||||
Referrer-Policy:
|
||||
- origin-when-cross-origin, strict-origin-when-cross-origin
|
||||
Content-Security-Policy:
|
||||
- default-src 'none'
|
||||
X-Ratelimit-Limit:
|
||||
- "60"
|
||||
X-Ratelimit-Remaining:
|
||||
- "53"
|
||||
X-Ratelimit-Reset:
|
||||
- "1712781639"
|
||||
X-Ratelimit-Resource:
|
||||
- core
|
||||
X-Ratelimit-Used:
|
||||
- "7"
|
||||
Accept-Ranges:
|
||||
- bytes
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Github-Request-Id:
|
||||
- C8A7:A3F5F:11C7A6D:1BA83CE:6616EE18
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '[{"tag_name": "v7.1.3.2", "html_url": "http://localhost"}]' # manually abbreviated for clarity
|
||||
recorded_at: Wed, 10 Apr 2024 19:52:56 GMT
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.github.com/repos/rails/rails/branches/main
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ""
|
||||
headers:
|
||||
Accept:
|
||||
- application/vnd.github.v3+json
|
||||
User-Agent:
|
||||
- Octokit Ruby Gem 8.1.0
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- GitHub.com
|
||||
Date:
|
||||
- Wed, 10 Apr 2024 19:52:57 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Cache-Control:
|
||||
- public, max-age=60, s-maxage=60
|
||||
Vary:
|
||||
- Accept, Accept-Encoding, Accept, X-Requested-With
|
||||
Etag:
|
||||
- W/"bbcf30919f0ef5fae2b2a28f58d50e3fb2cea8aa75418d5f2b919a7f857b27d0"
|
||||
X-Github-Media-Type:
|
||||
- github.v3; format=json
|
||||
X-Github-Api-Version-Selected:
|
||||
- "2022-11-28"
|
||||
Access-Control-Expose-Headers:
|
||||
- ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining,
|
||||
X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes,
|
||||
X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO,
|
||||
X-GitHub-Request-Id, Deprecation, Sunset
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubdomains; preload
|
||||
X-Frame-Options:
|
||||
- deny
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Xss-Protection:
|
||||
- "0"
|
||||
Referrer-Policy:
|
||||
- origin-when-cross-origin, strict-origin-when-cross-origin
|
||||
Content-Security-Policy:
|
||||
- default-src 'none'
|
||||
X-Ratelimit-Limit:
|
||||
- "60"
|
||||
X-Ratelimit-Remaining:
|
||||
- "52"
|
||||
X-Ratelimit-Reset:
|
||||
- "1712781639"
|
||||
X-Ratelimit-Resource:
|
||||
- core
|
||||
X-Ratelimit-Used:
|
||||
- "8"
|
||||
Accept-Ranges:
|
||||
- bytes
|
||||
Content-Length:
|
||||
- "3964"
|
||||
X-Github-Request-Id:
|
||||
- C8A8:281896:11B1812:1B69B2F:6616EE19
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
# manually abbreviated for clarity
|
||||
string: '{"commit":{"sha":"84997578c59aa88fe114cef176115f1612b6de6b", "html_url":"https://github.com/rails/rails/commit/84997578c59aa88fe114cef176115f1612b6de6b"}}'
|
||||
recorded_at: Wed, 10 Apr 2024 19:52:57 GMT
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.github.com/repos/rails/rails/commits/v7.1.3.2
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ""
|
||||
headers:
|
||||
Accept:
|
||||
- application/vnd.github.v3+json
|
||||
User-Agent:
|
||||
- Octokit Ruby Gem 8.1.0
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- GitHub.com
|
||||
Date:
|
||||
- Wed, 10 Apr 2024 19:52:57 GMT
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Cache-Control:
|
||||
- public, max-age=60, s-maxage=60
|
||||
Vary:
|
||||
- Accept, Accept-Encoding, Accept, X-Requested-With
|
||||
Etag:
|
||||
- W/"0668fc459669113a200777ee9ddd56a6ca2efb647894b006d3966504c7c82f13"
|
||||
Last-Modified:
|
||||
- Wed, 21 Feb 2024 21:43:55 GMT
|
||||
X-Github-Media-Type:
|
||||
- github.v3; format=json
|
||||
X-Github-Api-Version-Selected:
|
||||
- "2022-11-28"
|
||||
Access-Control-Expose-Headers:
|
||||
- ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining,
|
||||
X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes,
|
||||
X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO,
|
||||
X-GitHub-Request-Id, Deprecation, Sunset
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubdomains; preload
|
||||
X-Frame-Options:
|
||||
- deny
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Xss-Protection:
|
||||
- "0"
|
||||
Referrer-Policy:
|
||||
- origin-when-cross-origin, strict-origin-when-cross-origin
|
||||
Content-Security-Policy:
|
||||
- default-src 'none'
|
||||
X-Ratelimit-Limit:
|
||||
- "60"
|
||||
X-Ratelimit-Remaining:
|
||||
- "51"
|
||||
X-Ratelimit-Reset:
|
||||
- "1712781639"
|
||||
X-Ratelimit-Resource:
|
||||
- core
|
||||
X-Ratelimit-Used:
|
||||
- "9"
|
||||
Accept-Ranges:
|
||||
- bytes
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Github-Request-Id:
|
||||
- C8A9:23AA82:11FFCB8:1C057CD:6616EE19
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
# manually abbreviated for clarity
|
||||
string: '{"sha":"6f0d1ad14b92b9f5906e44740fce8b4f1c7075dc"}'
|
||||
recorded_at: Wed, 10 Apr 2024 19:52:57 GMT
|
||||
recorded_with: VCR 6.2.0
|
Loading…
Add table
Add a link
Reference in a new issue