mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
Add zero-config self hosting on Render (#612)
* v1 of backend implementation for self hosting * Add docs * Add upgrades controller * Add global helpers for self hosting mode * Add self host settings controller * Conditionally show self hosting settings * Environment and config updates * Complete upgrade prompting flow * Update config for forked repo * Move configuration of github provider within class * Add upgrades cron * Update deploy button * Update guides * Fix render deployer * Typo * Enable auto upgrades * Fix cron * Make upgrade modes more clear and consistent * Trigger new available version * Fix logic for displaying upgrade prompts * Finish implementation * Fix regression * Trigger new version * Add i18n translations * trigger new version * reduce caching time for testing * Decrease cache for testing * trigger upgrade * trigger upgrade * Only trigger deploy once * trigger upgrade * If target is commit, always upgrade if any upgrade is available * trigger upgrade * trigger upgrade * Test release * Change back to maybe repo for defaults * Fix lint errors * Clearer naming * Fix relative link * Add abs path * Relative link * Update docs
This commit is contained in:
parent
2bbf120e2f
commit
5aca2ff9b6
53 changed files with 1356 additions and 111 deletions
33
.env.example
33
.env.example
|
@ -25,5 +25,34 @@ APP_DOMAIN=
|
||||||
# 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.
|
# 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=
|
SENTRY_DSN=
|
||||||
|
|
||||||
# Used to enable specific features unique to the hosted version of Maybe. There's a very high likelihood that you don't need to change this value.
|
# If enabled, an invite code generated by `rake invites:create` is required to sign up as a new user.
|
||||||
HOSTED=false
|
# This is useful for controlling who can sign up for your Maybe instance.
|
||||||
|
REQUIRE_INVITE_CODE=false
|
||||||
|
|
||||||
|
# Enables self hosting features
|
||||||
|
SELF_HOSTING_ENABLED=false
|
||||||
|
|
||||||
|
# The hosting platform used to deploy the app (e.g. "render")
|
||||||
|
# `localhost` (or unset) is used for local development and testing
|
||||||
|
HOSTING_PLATFORM=localhost
|
||||||
|
|
||||||
|
# ======================================================================================================
|
||||||
|
# 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
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -33,6 +33,8 @@ gem "ransack"
|
||||||
gem "stackprof"
|
gem "stackprof"
|
||||||
gem "sentry-ruby"
|
gem "sentry-ruby"
|
||||||
gem "sentry-rails"
|
gem "sentry-rails"
|
||||||
|
gem "rails-settings-cached"
|
||||||
|
gem "octokit"
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
gem "bcrypt", "~> 3.1.7"
|
gem "bcrypt", "~> 3.1.7"
|
||||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -254,6 +254,10 @@ GEM
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.3-x86_64-linux)
|
nokogiri (1.16.3-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
|
octokit (8.1.0)
|
||||||
|
base64
|
||||||
|
faraday (>= 1, < 3)
|
||||||
|
sawyer (~> 0.9)
|
||||||
pagy (8.0.2)
|
pagy (8.0.2)
|
||||||
parallel (1.24.0)
|
parallel (1.24.0)
|
||||||
parser (3.3.0.5)
|
parser (3.3.0.5)
|
||||||
|
@ -291,6 +295,9 @@ GEM
|
||||||
rails-i18n (7.0.8)
|
rails-i18n (7.0.8)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
|
rails-settings-cached (2.9.4)
|
||||||
|
activerecord (>= 5.0.0)
|
||||||
|
railties (>= 5.0.0)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.2.1)
|
rake (13.2.1)
|
||||||
ransack (4.1.1)
|
ransack (4.1.1)
|
||||||
|
@ -349,6 +356,9 @@ GEM
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
|
sawyer (0.9.2)
|
||||||
|
addressable (>= 2.3.5)
|
||||||
|
faraday (>= 0.17.3, < 3)
|
||||||
selenium-webdriver (4.19.0)
|
selenium-webdriver (4.19.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
|
@ -435,11 +445,13 @@ DEPENDENCIES
|
||||||
letter_opener
|
letter_opener
|
||||||
lucide-rails!
|
lucide-rails!
|
||||||
mocha
|
mocha
|
||||||
|
octokit
|
||||||
pagy
|
pagy
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rails!
|
rails!
|
||||||
|
rails-settings-cached
|
||||||
ransack
|
ransack
|
||||||
redis (>= 4.0.1)
|
redis (>= 4.0.1)
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
|
|
21
README.md
21
README.md
|
@ -17,6 +17,19 @@ We spent the better part of $1,000,000 building the app (employees + contractors
|
||||||
|
|
||||||
We're now reviving the product as a fully open-source project. The goal is to let you run the app yourself, for free, and use it to manage your own finances and eventually offer a hosted version of the app for a small monthly fee.
|
We're now reviving the product as a fully open-source project. The goal is to let you run the app yourself, for free, and use it to manage your own finances and eventually offer a hosted version of the app for a small monthly fee.
|
||||||
|
|
||||||
|
## Self Hosting
|
||||||
|
|
||||||
|
You can find [detailed setup guides for self hosting here](docs/self-hosting.md).
|
||||||
|
|
||||||
|
### One-Click Render deploy (recommended)
|
||||||
|
|
||||||
|
<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. Follow the instructions in the [Render self-hosting guide](docs/self-hosting/render.md)
|
||||||
|
|
||||||
## Local Development Setup
|
## Local Development Setup
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
@ -80,14 +93,6 @@ Before contributing, you'll likely find it helpful to [understand context and ge
|
||||||
|
|
||||||
Once you've done that, please visit our [contributing guide](https://github.com/maybe-finance/maybe/blob/main/CONTRIBUTING.md) to get started!
|
Once you've done that, please visit our [contributing guide](https://github.com/maybe-finance/maybe/blob/main/CONTRIBUTING.md) to get started!
|
||||||
|
|
||||||
## Self Hosting
|
|
||||||
|
|
||||||
Our long term goal is to make self-hosting as easy as possible. That said, during these early stages of building the product, we are focusing our efforts on development.
|
|
||||||
|
|
||||||
We will update this section as we get closer to an initial release.
|
|
||||||
|
|
||||||
Please see our [guide on self hosting here](https://github.com/maybe-finance/maybe/wiki/Self-Hosting-Setup-Guide).
|
|
||||||
|
|
||||||
## Repo Activity
|
## Repo Activity
|
||||||
|
|
||||||

|

|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include Authentication
|
include Authentication, Invitable, SelfHostable
|
||||||
include Pagy::Backend
|
include Pagy::Backend
|
||||||
|
|
||||||
before_action :sync_accounts
|
before_action :sync_accounts
|
||||||
|
@ -11,11 +11,6 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def hosted_app?
|
|
||||||
ENV["HOSTED"] == "true"
|
|
||||||
end
|
|
||||||
helper_method :hosted_app?
|
|
||||||
|
|
||||||
def sync_accounts
|
def sync_accounts
|
||||||
return if Current.user.blank?
|
return if Current.user.blank?
|
||||||
|
|
||||||
|
|
12
app/controllers/concerns/invitable.rb
Normal file
12
app/controllers/concerns/invitable.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module Invitable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
helper_method :invite_code_required?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def invite_code_required?
|
||||||
|
ENV["REQUIRE_INVITE_CODE"] == "true"
|
||||||
|
end
|
||||||
|
end
|
12
app/controllers/concerns/self_hostable.rb
Normal file
12
app/controllers/concerns/self_hostable.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module SelfHostable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
helper_method :self_hosted?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def self_hosted?
|
||||||
|
ENV["SELF_HOSTING_ENABLED"] == "true"
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,7 +4,7 @@ class RegistrationsController < ApplicationController
|
||||||
layout "auth"
|
layout "auth"
|
||||||
|
|
||||||
before_action :set_user, only: :create
|
before_action :set_user, only: :create
|
||||||
before_action :claim_invite_code, only: :create, if: :hosted_app?
|
before_action :claim_invite_code, only: :create, if: :invite_code_required?
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@user = User.new
|
@user = User.new
|
||||||
|
|
46
app/controllers/settings/self_hosting_controller.rb
Normal file
46
app/controllers/settings/self_hosting_controller.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
class Settings::SelfHostingController < ApplicationController
|
||||||
|
before_action :verify_self_hosting_enabled
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if all_updates_valid?
|
||||||
|
self_hosting_params.keys.each do |key|
|
||||||
|
Setting.send("#{key}=", self_hosting_params[key].strip)
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to edit_settings_self_hosting_path, notice: t(".success")
|
||||||
|
else
|
||||||
|
flash.now[:error] = @errors.first.message
|
||||||
|
render :edit, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def all_updates_valid?
|
||||||
|
@errors = ActiveModel::Errors.new(Setting)
|
||||||
|
self_hosting_params.keys.each do |key|
|
||||||
|
setting = Setting.new(var: key)
|
||||||
|
setting.value = self_hosting_params[key].strip
|
||||||
|
|
||||||
|
unless setting.valid?
|
||||||
|
@errors.merge!(setting.errors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self_hosting_params[:upgrades_mode] == "auto" && self_hosting_params[:render_deploy_hook].blank?
|
||||||
|
@errors.add(:render_deploy_hook, t("settings.self_hosting.update.render_deploy_hook_error"))
|
||||||
|
end
|
||||||
|
|
||||||
|
@errors.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self_hosting_params
|
||||||
|
params.require(:setting).permit(:render_deploy_hook, :upgrades_mode, :upgrades_target)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_self_hosting_enabled
|
||||||
|
head :not_found unless self_hosted?
|
||||||
|
end
|
||||||
|
end
|
56
app/controllers/upgrades_controller.rb
Normal file
56
app/controllers/upgrades_controller.rb
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
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
|
|
@ -37,7 +37,6 @@ module ApplicationHelper
|
||||||
render partial: "shared/sidebar_modal", locals: { content: content }
|
render partial: "shared/sidebar_modal", locals: { content: content }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def sidebar_link_to(name, path, options = {})
|
def sidebar_link_to(name, path, options = {})
|
||||||
base_class_names = [ "block", "border", "border-transparent", "rounded-xl", "-ml-2", "p-2", "text-sm", "font-medium", "text-gray-500", "flex", "items-center" ]
|
base_class_names = [ "block", "border", "border-transparent", "rounded-xl", "-ml-2", "p-2", "text-sm", "font-medium", "text-gray-500", "flex", "items-center" ]
|
||||||
hover_class_names = [ "hover:bg-white", "hover:border-alpha-black-50", "hover:text-gray-900", "hover:shadow-xs" ]
|
hover_class_names = [ "hover:bg-white", "hover:border-alpha-black-50", "hover:text-gray-900", "hover:shadow-xs" ]
|
||||||
|
|
2
app/helpers/settings/self_hosting_helper.rb
Normal file
2
app/helpers/settings/self_hosting_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module Settings::SelfHostingHelper
|
||||||
|
end
|
13
app/helpers/upgrades_helper.rb
Normal file
13
app/helpers/upgrades_helper.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module UpgradesHelper
|
||||||
|
def upgrade_notification
|
||||||
|
return nil unless ENV["UPGRADES_ENABLED"] == "true"
|
||||||
|
|
||||||
|
completed_upgrade = Upgrader.completed_upgrade
|
||||||
|
return completed_upgrade if completed_upgrade && Current.user.last_alerted_upgrade_commit_sha != completed_upgrade.commit_sha
|
||||||
|
|
||||||
|
available_upgrade = Upgrader.available_upgrade
|
||||||
|
if available_upgrade && Setting.upgrades_mode == "manual" && Current.user.last_prompted_upgrade_commit_sha != available_upgrade.commit_sha
|
||||||
|
available_upgrade
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
app/jobs/auto_upgrade_job.rb
Normal file
31
app/jobs/auto_upgrade_job.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
class AutoUpgradeJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
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
|
|
@ -9,5 +9,9 @@ module Providable
|
||||||
def exchange_rates_provider
|
def exchange_rates_provider
|
||||||
Provider::Synth.new
|
Provider::Synth.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def git_repository_provider
|
||||||
|
Provider::Github.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
47
app/models/provider/github.rb
Normal file
47
app/models/provider/github.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
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: 2.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
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def repo
|
||||||
|
"#{owner}/#{name}"
|
||||||
|
end
|
||||||
|
end
|
19
app/models/setting.rb
Normal file
19
app/models/setting.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Dynamic settings the user can change within the app (helpful for self-hosting)
|
||||||
|
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] } }
|
||||||
|
end
|
57
app/models/upgrader.rb
Normal file
57
app/models/upgrader.rb
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
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
|
17
app/models/upgrader/config.rb
Normal file
17
app/models/upgrader/config.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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
|
12
app/models/upgrader/deployer.rb
Normal file
12
app/models/upgrader/deployer.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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
|
8
app/models/upgrader/deployer/null.rb
Normal file
8
app/models/upgrader/deployer/null.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
class Upgrader::Deployer::Null
|
||||||
|
def deploy(upgrade)
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
message: I18n.t("upgrader.deployer.null_deployer.success_message")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
41
app/models/upgrader/deployer/render.rb
Normal file
41
app/models/upgrader/deployer/render.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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
|
11
app/models/upgrader/provided.rb
Normal file
11
app/models/upgrader/provided.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module Upgrader::Provided
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
include Providable
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
private
|
||||||
|
def fetch_latest_upgrade_candidates_from_provider
|
||||||
|
git_repository_provider.fetch_latest_upgrade_candidates
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
app/models/upgrader/upgrade.rb
Normal file
27
app/models/upgrader/upgrade.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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?
|
||||||
|
version > Maybe.version || (version == Maybe.version && commit_sha != Maybe.commit_sha)
|
||||||
|
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
|
|
@ -10,4 +10,20 @@ class User < ApplicationRecord
|
||||||
generates_token_for :password_reset, expires_in: 15.minutes do
|
generates_token_for :password_reset, expires_in: 15.minutes do
|
||||||
password_salt&.last(10)
|
password_salt&.last(10)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,13 @@
|
||||||
class="hidden absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit">
|
class="hidden absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit">
|
||||||
<%= link_to edit_settings_path, class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
<%= link_to edit_settings_path, class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||||
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||||
<span class="text-gray-900 text-sm">Settings</span>
|
<span class="text-gray-900 text-sm">General Settings</span>
|
||||||
|
<% end %>
|
||||||
|
<% if self_hosted? %>
|
||||||
|
<%= link_to edit_settings_self_hosting_path, class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||||
|
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||||
|
<span class="text-gray-900 text-sm">Self Host Settings</span>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= button_to session_path, method: :delete, class: "w-full text-gray-900 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
<%= button_to session_path, method: :delete, class: "w-full text-gray-900 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||||
<%= lucide_icon("log-out", class: "w-5 h-5 shrink-0") %>
|
<%= lucide_icon("log-out", class: "w-5 h-5 shrink-0") %>
|
||||||
|
@ -86,5 +92,6 @@
|
||||||
</div>
|
</div>
|
||||||
<%= turbo_frame_tag "modal" %>
|
<%= turbo_frame_tag "modal" %>
|
||||||
<%= render "shared/custom_confirm_modal" %>
|
<%= render "shared/custom_confirm_modal" %>
|
||||||
|
<%= render "shared/upgrade_notification" %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,19 +1,13 @@
|
||||||
<%
|
<%
|
||||||
header_title t(".title")
|
header_title t(".title")
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<%= form_with model: @user, url: registration_path do |form| %>
|
<%= form_with model: @user, url: registration_path do |form| %>
|
||||||
<%= auth_messages form %>
|
<%= auth_messages form %>
|
||||||
|
|
||||||
<%= form.email_field :email, autofocus: false, autocomplete: "email", required: "required", placeholder: "you@example.com", label: true %>
|
<%= form.email_field :email, autofocus: false, autocomplete: "email", required: "required", placeholder: "you@example.com", label: true %>
|
||||||
|
|
||||||
<%= form.password_field :password, autocomplete: "new-password", required: "required", label: true %>
|
<%= form.password_field :password, autocomplete: "new-password", required: "required", label: true %>
|
||||||
|
|
||||||
<%= form.password_field :password_confirmation, autocomplete: "new-password", required: "required", label: true %>
|
<%= form.password_field :password_confirmation, autocomplete: "new-password", required: "required", label: true %>
|
||||||
|
<% if invite_code_required? %>
|
||||||
<% if hosted_app? %>
|
|
||||||
<%= form.password_field :invite_code, required: "required", label: true %>
|
<%= form.password_field :invite_code, required: "required", label: true %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= form.submit %>
|
<%= form.submit %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
40
app/views/settings/self_hosting/edit.html.erb
Normal file
40
app/views/settings/self_hosting/edit.html.erb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h1 class="text-3xl font-semibold font-display">Edit Self Hosting Settings</h1>
|
||||||
|
<hr>
|
||||||
|
<%= form_with model: Setting.new, url: settings_self_hosting_path, method: :patch, local: true, html: { class: "space-y-4" } do |form| %>
|
||||||
|
<section class="space-y-3">
|
||||||
|
<h2 class="text-2xl font-semibold">Render Deploy Hook</h2>
|
||||||
|
<p class="text-gray-500">You must fill this in so your app can trigger upgrades when Maybe releases upgrades. Learn more about deploy hooks and how they work in the <%= link_to "Render documentation", "https://docs.render.com/docs/deploy-hooks", target: "_blank", rel: "noopener noreferrer", class: "text-blue-500 hover:underline" %>.</p>
|
||||||
|
<%= form.text_field :render_deploy_hook, label: "Render Deploy Hook", placeholder: "https://api.render.com/deploy/srv-xyz...", value: Setting.render_deploy_hook %>
|
||||||
|
</section>
|
||||||
|
<section class="space-y-3">
|
||||||
|
<h2 class="text-2xl font-semibold">Auto Upgrades Setting</h2>
|
||||||
|
<p class="text-gray-500">This setting controls how often your self hosted app will update and what method it uses to do so.</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<%= form.check_box :upgrades_mode, { checked: Setting.upgrades_mode == "auto", unchecked_value: "manual" }, "auto", "manual" %>
|
||||||
|
<%= form.label :upgrades_mode, "Enable auto upgrades", class: "text-gray-900" %>
|
||||||
|
</div>
|
||||||
|
<% if Setting.upgrades_mode == "auto" %>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<%= form.radio_button :upgrades_target, "release", checked: Setting.upgrades_target == "release" %>
|
||||||
|
<%= form.label :upgrades_target_release, class: Setting.upgrades_target == "release" ? "text-gray-900" : "text-gray-500" do %>
|
||||||
|
<span class="font-semibold">Latest Release (suggested)</span> - you will automatically be upgraded to the latest stable release of Maybe
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<%= form.radio_button :upgrades_target, "commit", checked: Setting.upgrades_target == "commit" %>
|
||||||
|
<%= form.label :upgrades_target_commit, class: Setting.upgrades_target == "commit" ? "text-gray-900" : "text-gray-500" do %>
|
||||||
|
<span class="font-semibold">Latest Commit</span> - you will automatically be upgraded any time the Maybe repo is updated
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="fixed right-5 bottom-5">
|
||||||
|
<button type="submit" class="flex items-center justify-center w-12 h-12 mb-2 bg-black rounded-full shrink-0 grow-0 hover:bg-gray-600">
|
||||||
|
<%= inline_svg_tag("icn-check.svg", class: "text-white fill-current") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
19
app/views/shared/_upgrade_notification.html.erb
Normal file
19
app/views/shared/_upgrade_notification.html.erb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<% if upgrade_notification %>
|
||||||
|
<% upgrade = upgrade_notification %>
|
||||||
|
<div class="bg-white space-y-4 text-right fixed bottom-10 right-10 p-5 border border-alpha-black-200 shadow-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-gray-900"><%= t(".app_upgraded", version: upgrade.to_s) %></p>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-gray-900"><%= 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-gray-900'} 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>
|
||||||
|
<% end %>
|
|
@ -2,6 +2,24 @@
|
||||||
# exit on error
|
# exit on error
|
||||||
set -o errexit
|
set -o errexit
|
||||||
|
|
||||||
|
echo "Installing gems..."
|
||||||
bundle install
|
bundle install
|
||||||
|
|
||||||
|
echo "Precompiling assets..."
|
||||||
./bin/rails assets:precompile
|
./bin/rails assets:precompile
|
||||||
./bin/rails assets:clean
|
./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
|
||||||
|
|
|
@ -1,88 +1,20 @@
|
||||||
# PostgreSQL. Versions 9.3 and up are supported.
|
|
||||||
#
|
|
||||||
# Install the pg driver:
|
|
||||||
# gem install pg
|
|
||||||
# On macOS with Homebrew:
|
|
||||||
# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
|
|
||||||
# On Windows:
|
|
||||||
# gem install pg
|
|
||||||
# Choose the win32 build.
|
|
||||||
# Install PostgreSQL and put its /bin directory on your path.
|
|
||||||
#
|
|
||||||
# Configure Using Gemfile
|
|
||||||
# gem "pg"
|
|
||||||
#
|
|
||||||
default: &default
|
default: &default
|
||||||
adapter: postgresql
|
adapter: postgresql
|
||||||
encoding: unicode
|
encoding: unicode
|
||||||
# For details on connection pooling, see Rails configuration guide
|
|
||||||
# https://guides.rubyonrails.org/configuring.html#database-pooling
|
|
||||||
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||||
host: <%= ENV.fetch("DB_HOST") { "127.0.0.1" } %>
|
host: <%= ENV.fetch("DB_HOST") { "127.0.0.1" } %>
|
||||||
port: <%= ENV.fetch("DB_PORT") { "5432" } %>
|
port: <%= ENV.fetch("DB_PORT") { "5432" } %>
|
||||||
password: <%= ENV.fetch("POSTGRES_PASSWORD") { nil } %>
|
|
||||||
user: <%= ENV.fetch("POSTGRES_USER") { nil } %>
|
user: <%= ENV.fetch("POSTGRES_USER") { nil } %>
|
||||||
|
password: <%= ENV.fetch("POSTGRES_PASSWORD") { nil } %>
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: maybe_development
|
database: <%= ENV.fetch("POSTGRES_DB") { "maybe_development" } %>
|
||||||
|
|
||||||
# The specified database role being used to connect to PostgreSQL.
|
|
||||||
# To create additional roles in PostgreSQL see `$ createuser --help`.
|
|
||||||
# When left blank, PostgreSQL will use the default role. This is
|
|
||||||
# the same name as the operating system user running Rails.
|
|
||||||
#username: maybe
|
|
||||||
|
|
||||||
# The password associated with the PostgreSQL role (username).
|
|
||||||
#password:
|
|
||||||
|
|
||||||
# Connect on a TCP socket. Omitted by default since the client uses a
|
|
||||||
# domain socket that doesn't need configuration. Windows does not have
|
|
||||||
# domain sockets, so uncomment these lines.
|
|
||||||
#host: localhost
|
|
||||||
|
|
||||||
# The TCP port the server listens on. Defaults to 5432.
|
|
||||||
# If your server runs on a different port number, change accordingly.
|
|
||||||
#port: 5432
|
|
||||||
|
|
||||||
# Schema search path. The server defaults to $user,public
|
|
||||||
#schema_search_path: myapp,sharedapp,public
|
|
||||||
|
|
||||||
# Minimum log levels, in increasing order:
|
|
||||||
# debug5, debug4, debug3, debug2, debug1,
|
|
||||||
# log, notice, warning, error, fatal, and panic
|
|
||||||
# Defaults to warning.
|
|
||||||
#min_messages: notice
|
|
||||||
|
|
||||||
# Warning: The database defined as "test" will be erased and
|
|
||||||
# re-generated from your development database when you run "rake".
|
|
||||||
# Do not set this db to the same as development or production.
|
|
||||||
test:
|
test:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: maybe_test
|
database: <%= ENV.fetch("POSTGRES_DB") { "maybe_test" } %>
|
||||||
|
|
||||||
# As with config/credentials.yml, you never want to store sensitive information,
|
|
||||||
# like your database password, in your source code. If your source code is
|
|
||||||
# ever seen by anyone, they now have access to your database.
|
|
||||||
#
|
|
||||||
# Instead, provide the password or a full connection URL as an environment
|
|
||||||
# variable when you boot the app. For example:
|
|
||||||
#
|
|
||||||
# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
|
|
||||||
#
|
|
||||||
# If the connection URL is provided in the special DATABASE_URL environment
|
|
||||||
# variable, Rails will automatically merge its configuration values on top of
|
|
||||||
# the values provided in this file. Alternatively, you can specify a connection
|
|
||||||
# URL environment variable explicitly:
|
|
||||||
#
|
|
||||||
# production:
|
|
||||||
# url: <%= ENV["MY_APP_DATABASE_URL"] %>
|
|
||||||
#
|
|
||||||
# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
|
|
||||||
# for a full overview on how database connection configuration can be specified.
|
|
||||||
#
|
|
||||||
production:
|
production:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: maybe_production
|
database: <%= ENV.fetch("POSTGRES_DB") { "maybe_production" } %>
|
||||||
username: maybe
|
|
||||||
password: <%= ENV["MAYBE_DATABASE_PASSWORD"] %>
|
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
config.good_job.enable_cron = true
|
config.good_job.enable_cron = true
|
||||||
|
|
||||||
|
if ENV["UPGRADES_ENABLED"] == "true"
|
||||||
config.good_job.cron = {
|
config.good_job.cron = {
|
||||||
maintenance: {
|
auto_upgrade: {
|
||||||
cron: "0 22 * * *",
|
cron: "every 30 seconds",
|
||||||
class: "DailyExchangeRateJob"
|
class: "AutoUpgradeJob",
|
||||||
|
description: "Check for new versions of the app and upgrade if necessary"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
16
config/initializers/version.rb
Normal file
16
config/initializers/version.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module Maybe
|
||||||
|
class << self
|
||||||
|
def version
|
||||||
|
Semver.new(semver)
|
||||||
|
end
|
||||||
|
|
||||||
|
def commit_sha
|
||||||
|
`git rev-parse HEAD`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def semver
|
||||||
|
"0.1.0-alpha.1"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
config/locales/models/upgrader/en.yml
Normal file
13
config/locales/models/upgrader/en.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
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
|
8
config/locales/views/settings/en.yml
Normal file
8
config/locales/views/settings/en.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
en:
|
||||||
|
settings:
|
||||||
|
self_hosting:
|
||||||
|
update:
|
||||||
|
render_deploy_hook_error: Render deploy hook must be provided to enable auto
|
||||||
|
upgrades
|
||||||
|
success: Settings updated successfully.
|
|
@ -20,3 +20,9 @@ en:
|
||||||
and account graphs.</p></br><p>The only way you’ll be able to add this entry
|
and account graphs.</p></br><p>The only way you’ll be able to add this entry
|
||||||
back is by re-entering it manually via a new entry</p>"
|
back is by re-entering it manually via a new entry</p>"
|
||||||
title: Delete Entry?
|
title: Delete Entry?
|
||||||
|
shared:
|
||||||
|
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
|
||||||
|
|
10
config/locales/views/upgrades/en.yml
Normal file
10
config/locales/views/upgrades/en.yml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
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
|
|
@ -5,7 +5,10 @@ Rails.application.routes.draw do
|
||||||
resource :session
|
resource :session
|
||||||
resource :password_reset
|
resource :password_reset
|
||||||
resource :password
|
resource :password
|
||||||
resource :settings, only: %i[edit update]
|
|
||||||
|
resource :settings, only: %i[edit update] do
|
||||||
|
resource :self_hosting, only: %i[edit update], controller: "settings/self_hosting"
|
||||||
|
end
|
||||||
|
|
||||||
resources :transactions do
|
resources :transactions do
|
||||||
match "search" => "transactions#search", on: :collection, via: [ :get, :post ], as: :search
|
match "search" => "transactions#search", on: :collection, via: [ :get, :post ], as: :search
|
||||||
|
@ -20,6 +23,14 @@ Rails.application.routes.draw do
|
||||||
resources :valuations
|
resources :valuations
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# For managing self-hosted upgrades and release notifications
|
||||||
|
resources :upgrades, only: [] do
|
||||||
|
member do
|
||||||
|
post :acknowledge
|
||||||
|
post :deploy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||||
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
||||||
get "up" => "rails/health#show", as: :rails_health_check
|
get "up" => "rails/health#show", as: :rails_health_check
|
||||||
|
|
15
db/migrate/20240410183531_create_settings.rb
Normal file
15
db/migrate/20240410183531_create_settings.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
class CreateSettings < ActiveRecord::Migration[7.2]
|
||||||
|
def self.up
|
||||||
|
create_table :settings do |t|
|
||||||
|
t.string :var, null: false
|
||||||
|
t.text :value, null: true
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :settings, %i[var], unique: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.down
|
||||||
|
drop_table :settings
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
class AddLastSeenUpgradeToUser < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
# Self-hosted users will be prompted to upgrade to the latest commit or release.
|
||||||
|
add_column :users, :last_prompted_upgrade_commit_sha, :string
|
||||||
|
|
||||||
|
# All users will be notified when a new commit or release has successfully been deployed.
|
||||||
|
add_column :users, :last_alerted_upgrade_commit_sha, :string
|
||||||
|
end
|
||||||
|
end
|
14
db/schema.rb
generated
14
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.2].define(version: 2024_04_04_112829) do
|
ActiveRecord::Schema[7.2].define(version: 2024_04_11_102931) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -85,7 +85,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_04_04_112829) do
|
||||||
t.uuid "accountable_id"
|
t.uuid "accountable_id"
|
||||||
t.decimal "balance", precision: 19, scale: 4, default: "0.0"
|
t.decimal "balance", precision: 19, scale: 4, default: "0.0"
|
||||||
t.string "currency", default: "USD"
|
t.string "currency", default: "USD"
|
||||||
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Account::Loan'::character varying, 'Account::Credit'::character varying, 'Account::OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Account::Loan'::character varying)::text, ('Account::Credit'::character varying)::text, ('Account::OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
||||||
t.boolean "is_active", default: true, null: false
|
t.boolean "is_active", default: true, null: false
|
||||||
t.enum "status", default: "ok", null: false, enum_type: "account_status"
|
t.enum "status", default: "ok", null: false, enum_type: "account_status"
|
||||||
t.jsonb "sync_warnings", default: "[]", null: false
|
t.jsonb "sync_warnings", default: "[]", null: false
|
||||||
|
@ -200,6 +200,14 @@ ActiveRecord::Schema[7.2].define(version: 2024_04_04_112829) do
|
||||||
t.index ["token"], name: "index_invite_codes_on_token", unique: true
|
t.index ["token"], name: "index_invite_codes_on_token", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "settings", force: :cascade do |t|
|
||||||
|
t.string "var", null: false
|
||||||
|
t.text "value"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["var"], name: "index_settings_on_var", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "transaction_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
create_table "transaction_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "color", default: "#6172F3", null: false
|
t.string "color", default: "#6172F3", null: false
|
||||||
|
@ -234,6 +242,8 @@ ActiveRecord::Schema[7.2].define(version: 2024_04_04_112829) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.datetime "last_login_at"
|
t.datetime "last_login_at"
|
||||||
|
t.string "last_prompted_upgrade_commit_sha"
|
||||||
|
t.string "last_alerted_upgrade_commit_sha"
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["family_id"], name: "index_users_on_family_id"
|
t.index ["family_id"], name: "index_users_on_family_id"
|
||||||
end
|
end
|
||||||
|
|
22
docs/self-hosting.md
Normal file
22
docs/self-hosting.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
The fastest way to get your own version of Maybe running is a "one-click deploy". Below are the currently supported platforms:
|
||||||
|
|
||||||
|
## One-Click Deploys
|
||||||
|
|
||||||
|
### Render (recommended)
|
||||||
|
|
||||||
|
<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. Follow the instructions in the [Render self-hosting guide](self-hosting/render.md)
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
**Estimated cost:** $5-15 per month
|
||||||
|
|
||||||
|
_Docker is not yet supported, but will be soon._
|
||||||
|
|
||||||
|
## Self hosting disclaimer
|
||||||
|
|
||||||
|
While we attempt to provide cost-effective deployment options, please remember, **self-hosting _may_ incur monthly charges on your hosting platform of choice**. While we provide cost estimates for each deployment option, it is your responsibility to manage these costs.
|
70
docs/self-hosting/render.md
Normal file
70
docs/self-hosting/render.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Self Hosting Maybe on Render
|
||||||
|
|
||||||
|
Welcome to the self hosting guide for Maybe on [Render](https://render.com/)!
|
||||||
|
|
||||||
|
Render is our _recommended_ option for hosting 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)
|
33
lib/semver.rb
Normal file
33
lib/semver.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Light wrapper around Gem::Version to support tag parsing
|
||||||
|
class Semver
|
||||||
|
attr_reader :version
|
||||||
|
|
||||||
|
def initialize(version_string)
|
||||||
|
@version_string = version_string
|
||||||
|
@version = Gem::Version.new(version_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def > (other)
|
||||||
|
@version > other.version
|
||||||
|
end
|
||||||
|
|
||||||
|
def < (other)
|
||||||
|
@version < other.version
|
||||||
|
end
|
||||||
|
|
||||||
|
def == (other)
|
||||||
|
@version == other.version
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
@version_string
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_release_tag
|
||||||
|
"v#{@version_string}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_release_tag(tag)
|
||||||
|
new(tag.sub(/^v/, ""))
|
||||||
|
end
|
||||||
|
end
|
57
render.yaml
Normal file
57
render.yaml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
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"
|
||||||
|
preDeployCommand: "bundle exec rails db:migrate"
|
||||||
|
startCommand: "bundle exec rails server"
|
||||||
|
envVars:
|
||||||
|
- key: DATABASE_URL
|
||||||
|
fromDatabase:
|
||||||
|
name: maybe
|
||||||
|
property: connectionString
|
||||||
|
|
||||||
|
- key: SELF_HOSTING_ENABLED
|
||||||
|
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
|
|
@ -25,7 +25,7 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create when hosted requires an invite code" do
|
test "create when hosted requires an invite code" do
|
||||||
in_hosted_app do
|
in_invited_app do
|
||||||
assert_no_difference "User.count" do
|
assert_no_difference "User.count" do
|
||||||
post registration_url, params: { user: {
|
post registration_url, params: { user: {
|
||||||
email: "john@example.com",
|
email: "john@example.com",
|
||||||
|
@ -54,10 +54,10 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def in_hosted_app
|
def in_invited_app
|
||||||
ENV["HOSTED"] = "true"
|
ENV["REQUIRE_INVITE_CODE"] = "true"
|
||||||
yield
|
yield
|
||||||
ensure
|
ensure
|
||||||
ENV["HOSTED"] = nil
|
ENV["REQUIRE_INVITE_CODE"] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
31
test/controllers/settings/self_hosting_controller_test.rb
Normal file
31
test/controllers/settings/self_hosting_controller_test.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class Settings::SelfHostingControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
ENV["SELF_HOSTING_ENABLED"] = "true"
|
||||||
|
sign_in users(:family_admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cannot edit when self hosting is disabled" do
|
||||||
|
ENV["SELF_HOSTING_ENABLED"] = "false"
|
||||||
|
|
||||||
|
get edit_settings_self_hosting_url
|
||||||
|
assert :not_found
|
||||||
|
|
||||||
|
patch settings_self_hosting_url, params: { setting: { render_deploy_hook: "https://example.com" } }
|
||||||
|
assert :not_found
|
||||||
|
end
|
||||||
|
test "should get edit when self hosting is enabled" do
|
||||||
|
get edit_settings_self_hosting_url
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can update settings when self hosting is enabled" do
|
||||||
|
NEW_RENDER_DEPLOY_HOOK = "https://api.render.com/deploy/srv-abc123"
|
||||||
|
assert_nil Setting.render_deploy_hook
|
||||||
|
|
||||||
|
patch settings_self_hosting_url, params: { setting: { render_deploy_hook: NEW_RENDER_DEPLOY_HOOK } }
|
||||||
|
|
||||||
|
assert_equal NEW_RENDER_DEPLOY_HOOK, Setting.render_deploy_hook
|
||||||
|
end
|
||||||
|
end
|
83
test/controllers/upgrades_controller_test.rb
Normal file
83
test/controllers/upgrades_controller_test.rb
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class UpgradesControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
sign_in @user = users(:family_admin)
|
||||||
|
|
||||||
|
ENV["UPGRADES_ENABLED"] = "true"
|
||||||
|
|
||||||
|
@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
|
||||||
|
ENV["UPGRADES_ENABLED"] = "false"
|
||||||
|
|
||||||
|
post "/upgrades/acknowledge/47bb430954292d2fdcc81082af731a16b9587da3"
|
||||||
|
assert_response :not_found
|
||||||
|
|
||||||
|
post "/upgrades/deploy/47bb430954292d2fdcc81082af731a16b9587da3"
|
||||||
|
assert_response :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should acknowledge an upgrade prompt" 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
|
||||||
|
|
||||||
|
test "should acknowledge an upgrade alert" 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
|
||||||
|
|
||||||
|
test "should deploy an upgrade" 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
|
||||||
|
|
||||||
|
test "should rollback user state if upgrade fails" 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
|
25
test/interfaces/git_repository_provider_interface_test.rb
Normal file
25
test/interfaces/git_repository_provider_interface_test.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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
|
7
test/jobs/auto_upgrade_job_test.rb
Normal file
7
test/jobs/auto_upgrade_job_test.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class AutoUpgradeJobTest < ActiveJob::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
9
test/models/provider/github_test.rb
Normal file
9
test/models/provider/github_test.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class Provider::GithubTest < ActiveSupport::TestCase
|
||||||
|
include GitRepositoryProviderInterfaceTest
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@subject = Provider::Github.new(owner: "rails", name: "rails", branch: "main")
|
||||||
|
end
|
||||||
|
end
|
88
test/models/upgrader_test.rb
Normal file
88
test/models/upgrader_test.rb
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
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
|
|
@ -0,0 +1,235 @@
|
||||||
|
---
|
||||||
|
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