Introduce a staging environment

ref: https://3.basecamp.com/2914079/buckets/21350690/card_tables/cards/8708868711
This commit is contained in:
Mike Dalessio
2025-06-03 17:30:57 -04:00
parent 7355b09382
commit e67d59e341
13 changed files with 112 additions and 47 deletions
+5
View File
@@ -0,0 +1,5 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Staging/RAILS_MASTER_KEY)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
+19 -8
View File
@@ -45,29 +45,40 @@ For testing OpenAI API requests, we use [VCR](https://github.com/vcr/vcr). If yo
that you can get from 1Password in "Fizzy - test.key". Then, when running tests that use Open AI API, you must either set the env variable VCR_RECORD=1
or to add `vcr_record!` to the test. See `VcrTestHelper`.
## Deploying
## Environments
Fizzy is deployed with Kamal. You'll need to have the 1Password CLI set up in order to access the secrets that are used when deploying. Provided you have that, it should be as simple as `bin/kamal deploy` to the correct environment.
### Beta
For beta:
bin/kamal deploy -d beta
Beta is primarily intended for testing product features.
Beta tenant is:
- https://fizzy.37signals.works/
This environment uses local disk for Active Storage.
### Staging
Staging is primarily intended for testing infrastructure changes.
Production tenants are:
- https://37s.fizzy.37signals-staging.com/
- https://dev.fizzy.37signals-staging.com/
- https://qa.fizzy.37signals-staging.com/
This environment uses a FlashBlade bucket for blob storage, and shares nothing with Production. We may periodically copy data here from production.
### Production
And for production:
bin/kamal deploy -d production
Production tenants are:
- https://37s.fizzy.37signals.com/
- https://dev.fizzy.37signals.com/
- https://qa.fizzy.37signals.com/
This environment uses a FlashBlade bucket for blob storage.
+4 -9
View File
@@ -6,14 +6,9 @@ cable: &cable
polling_interval: 0.1.seconds
message_retention: 1.day
development:
<<: *cable
development: *cable
test:
adapter: test
production: &production
<<: *cable
beta:
<<: *production
beta: *cable
staging: *cable
production: *cable
+6 -10
View File
@@ -10,16 +10,12 @@ default_connection: &default_connection
database:
writing: :cache
development:
default: &default
<<: *default_connection
<<: *default_options
test:
<<: *default_options
production: &production
<<: *default_connection
<<: *default_options
beta:
<<: *production
development: *default
test: *default_options
beta: *default
staging: *default
production: *default
+1 -1
View File
@@ -1 +1 @@
I2oqW56RTDrRxQ5HUGXGUbSiVoeICiF6tjSo1oxmGuZKE6wu99S+RvQ6Mf0enCQdbHG0osoCpUvu7iuOMEA3X6vO7ku1YFbCDKugu6tY913bzRl166SWZdSlEwtcr6wtmUkXLoWfDzOt98lJ03nU8YbeDC2AzuBqcYWS2qjqpFrrDK8WQI7IukM4iFEDjesW2Noekyy6z+b5+DxB2lkCMFgim4ExW/ndFmrvDnt+QeR+rB9XW9BF6CEg5pPsBQ9EwtICsacCWdvFFOZ8EaNoOuF+hKIHoKP60UCfQj5EUir/r5pGE0Z83z91+UpdpVCuGvh62uSZoZOjdGAplg/DWWKpGcj5JOA7V2yyGEMlz7NueBInG8t/eEyeX/98fAy4TNUxxvncNB+xsmgPJIqlek7vXtYe06AMwrRvac4QSSpUuwWuKUV8JF4gYT5Gp92rzqmmwsTmjvEMiPlnnVYH83GCtd2f1Se7EkDVTvgLA3idnjLvFCelAU4Vdo9/pLqG+0xorSxQ4D/UXqykxtTSjz6pC2P8J1g6T91TQhcH4tDFuZICOYLyFV8F5OD3jPaP+ml01oME42FIKmyhvcNhMLqluUvFqqtOxgWp54jCnde6MBHJ9W7gejwfSmjyD+pr/5NXgaGaTq9h6NMdMkr0Bis6Cc/EgHZKBpJx6ARH1ulcp9uZ9tBjNCePBNxATrJLKwiXh0ZTLDfDTkgdznkj8u+FAKclxjIZ63td3hGe--5kL30BIpNtC7wcsf--YaV9PXL74Wl2ePaAdT5Crg==
/M5cK3B/Wq1R2f4XsQQ8t7qJGCNMaBU3itDDKNZapeGMjrlE4A9CKOh2RPQsKlrsGjJdgGL4YdfDiT1LiF0pD/KL/t5Fw8Z8GMndzA+hmpvQ+hgufvd8n2ZzK/SIK6RmfC9BabdlvuQ0rr3/EdQJ5yIgk3WtWv73bOWDopqHerEAcaNl8KjCu5n2vokAidqEh0/O30yQQB9PAFMRqEwapLVcDSnaJXr6OQnAAm9tIoWaEaIF8ZJH3Rmn0COs8xZqYFyFv/pP3gVy3Mw+VL3aIkEf/eZ3IQWYNf7fiKPvIdNVsmghmeoQRqCp4E0dpn2XDSEGx5i5oq7ynSDCPky4GahiLtpe3vas+Z+lzD5hAAZmlX0G615b4hWjAViTEQU+CMXhmpj7eVpT9co+1oKmqtbDvHr7uCJIf4d17kC13u/8yjgl2SaCjRWx94wh8su/h6h9Xw0qewITbfM32pFRiH+R5bc=--fxg4Lf+qmYeuvyZp--qVZ+ELV0FRzBb69MUAqOsQ==
+1
View File
@@ -0,0 +1 @@
Tc514fYVlm1c2c81KUFmuAGZ9rSZT7XfeGUEdTEvJNDD6JE9IghKbAAGgc6AalIt2/VxzSV6AGvNagb0/qlJrMOFeSJLqzufKt1YoLRUZtSUZDtzmI/2WSzju0Tyu4za6rnLEu+WvDc0NM8KZEv/TwwUbSOwoEVDQmava/iuWyvFr5sOH1wNr2kwYbErwDpThKcYqacI4CdvgpgXc//3T0Z5vsVCs6d8723DymM0KkSK9so8WrcCesk2fmhM1K2hQV7QdckaVuOL0JEj1AI7KzdJm7REoeLwRwaBeVDkqDNo2fKxwZ2XmU2ViJOozPsowx+324wQQZjbsf/0ohtDkgjbfMmA2tjkuNJdJFwIIc5VyvILhVs5WQIJHSXmabhUKxzKRCRZNBSaadLGCb7/AGfULKvh2vP1BWqm80EkDmhHm42ucJW1vA377y9UYWVcdvqWtp5m2F7HlXqi9Uw/+kFV2X6Jw1c07FsCu35TrUb5u8G7mu/866GBgf5VlfDVs1BA3aru05CtcsXi2qKMw0II7fZps/4XpYCB+VehXAyLi2ABVeTnqSfF7HwasXvfD3Jgh7zJLD0hudNDpu6xkmoyeksVBn034dmQf0TwAIKmNjEKMDxgAG50T2JhnEvHZEnqzGl6O8if8DKM5rPgt3XEU3zN/U3yA8T0eTyFdGlWbYK7Ergu4TcGo3WPk6bJhTGOHQ9puD89t6JGHbKDYW77w44SHv+C42flmNkzcndXlpvZvnEafDYeW6ofRC4eeTRX4+WKL4CYhlel78+twD6Mh0MV42MYM2rmtl66NHN8fWxyfoyjrNcXveKVAsGMatB0+dOglq6GmOiXRAt/I6D+7QYZLyaUhYNmmiuZXF4=--H6KWPnHc9qUQE5AI--PN98cg9t9ZhxMUEax/Mfgg==
+2 -2
View File
@@ -63,5 +63,5 @@ production: &production
database: storage/production_queue.sqlite3
migrations_paths: db/queue_migrate
beta:
<<: *production
beta: *production
staging: *production
+58
View File
@@ -0,0 +1,58 @@
servers:
web:
hosts:
- fizzy-staging-app-01
jobs:
hosts:
- fizzy-staging-app-01
web-replica:
hosts:
- fizzy-staging-app-101
proxy: true
proxy:
ssl: false
ssh:
user: app
env:
clear:
RAILS_ENV: staging
x-beamer-accessory: &beamer-accessory
image: basecamp/beamer
registry:
username: bcbot
password:
- BASECAMP_REGISTRY_PASSWORD
options:
user: 1000 # Match the UID of the Rails app, for volume permissions
volumes:
- fizzy:/storage
accessories:
beamer-primary:
<<: *beamer-accessory
service: beamer-primary
cmd: beamer primary --remove-after=1h --dir=/storage --disable-tls
port: 5000:80
roles:
- web
beamer-replica:
<<: *beamer-accessory
service: beamer-replica
cmd: beamer replica --primary=http://fizzy-staging-app-01:5000/ --dir=/storage
roles:
- web-replica
load-balancer:
image: basecamp/kamal-proxy:lb
roles:
- web
options:
publish:
- 80:80
- 443:443
+5
View File
@@ -0,0 +1,5 @@
require_relative "production"
Rails.application.configure do
config.action_mailer.default_url_options = { host: "%{tenant}.fizzy.37signals-staging.com" }
end
+1 -1
View File
@@ -1,4 +1,4 @@
if (Rails.env.production? || Rails.env.beta?) && ENV["SKIP_TELEMETRY"].blank?
if !Rails.env.local? && ENV["SKIP_TELEMETRY"].blank?
Sentry.init do |config|
config.dsn = "https://ca338fb1fe6f677d6aeec2336a86f0ee@o33603.ingest.us.sentry.io/4508093839179776"
config.breadcrumbs_logger = %i[ active_support_logger http_logger ]
+3 -3
View File
@@ -1,10 +1,10 @@
Rails.application.config.after_initialize do
# in production, we're using a two-level subdomain like "tenant.fizzy.37signals.com", but in
# development and beta it's only a single-level subdomain.
# in production and staging, we're using a two-level subdomain like "tenant.fizzy.37signals.com".
# in development and beta it's only a single-level subdomain.
Rails.application.config.active_record_tenanted.tenant_resolver = ->(request) do
next nil if request.path == "/up"
tld_length = request.domain == "37signals.com" ? 2 : 1
tld_length = [ "37signals.com", "37signals-staging.com" ].include?(request.domain) ? 2 : 1
request.subdomain(tld_length).presence || "return-404" # this is a quick fix for now
end
end
+5 -11
View File
@@ -8,14 +8,8 @@ default: &default
processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
polling_interval: 0.1
development:
<<: *default
test:
<<: *default
production: &production
<<: *default
beta:
<<: *production
development: *default
test: *default
beta: *default
staging: *default
production: *default
+2 -2
View File
@@ -15,5 +15,5 @@ production: &production
class: DeleteUnusedTagsJob
schedule: every day at 04:00
beta:
<<: *production
beta: *production
staging: *production