1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-25 08: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:
Zach Gollwitzer 2024-04-13 09:28:45 -04:00 committed by GitHub
parent 2bbf120e2f
commit 5aca2ff9b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1356 additions and 111 deletions

View file

@ -25,7 +25,7 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
end
test "create when hosted requires an invite code" do
in_hosted_app do
in_invited_app do
assert_no_difference "User.count" do
post registration_url, params: { user: {
email: "john@example.com",
@ -54,10 +54,10 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
private
def in_hosted_app
ENV["HOSTED"] = "true"
def in_invited_app
ENV["REQUIRE_INVITE_CODE"] = "true"
yield
ensure
ENV["HOSTED"] = nil
ENV["REQUIRE_INVITE_CODE"] = nil
end
end

View 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

View 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

View 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

View file

@ -0,0 +1,7 @@
require "test_helper"
class AutoUpgradeJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end

View 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

View 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

View file

@ -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