Merge pull request #1697 from basecamp/open-source-split

Open source / SaaS split
This commit is contained in:
Jorge Manrubia
2025-11-28 16:06:15 +01:00
committed by GitHub
120 changed files with 1586 additions and 1672 deletions
+40
View File
@@ -0,0 +1,40 @@
name: Checks
on:
pull_request:
jobs:
security:
name: Security
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Gem audit
run: bin/bundler-audit check --update
- name: Importmap audit
run: bin/importmap audit
- name: Brakeman audit
run: bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Lint code for consistent style
run: bin/rubocop
+11
View File
@@ -0,0 +1,11 @@
name: CI (OSS)
on:
pull_request:
if: github.event.pull_request.head.repo.full_name != github.repository
jobs:
test:
uses: ./.github/workflows/test.yml
with:
saas: false
+18
View File
@@ -0,0 +1,18 @@
name: CI (SaaS)
on:
push:
jobs:
test_oss:
name: Test (OSS)
uses: ./.github/workflows/test.yml
with:
saas: false
test_saas:
name: Test (SaaS)
uses: ./.github/workflows/test.yml
with:
saas: true
secrets:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
+67
View File
@@ -0,0 +1,67 @@
name: Test
on:
workflow_call:
inputs:
saas:
type: boolean
required: true
secrets:
GH_TOKEN:
required: false
jobs:
test:
name: Tests (${{ matrix.mode }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- mode: SQLite
db_adapter: sqlite
- mode: MySQL
db_adapter: mysql
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: fizzy_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
env:
RAILS_ENV: test
DATABASE_ADAPTER: ${{ matrix.db_adapter }}
${{ inputs.saas && 'SAAS' || 'SAAS_DISABLED' }}: ${{ inputs.saas && '1' || '' }}
BUNDLE_GEMFILE: ${{ inputs.saas && 'Gemfile.saas' || 'Gemfile' }}
MYSQL_HOST: 127.0.0.1
MYSQL_PORT: 3306
MYSQL_USER: root
FIZZY_DB_HOST: 127.0.0.1
FIZZY_DB_PORT: 3306
BUNDLE_GITHUB__COM: ${{ inputs.saas && format('x-access-token:{0}', secrets.GH_TOKEN) || '' }}
steps:
- name: Install system packages
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Run tests
run: bin/rails db:setup test
- name: Run system tests
run: bin/rails test:system
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
MESSAGE="$KAMAL_PERFORMER deployed $KAMAL_SERVICE_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
bin/notify_dash_of_deployment "$MESSAGE" $KAMAL_VERSION $KAMAL_PERFORMER $CURRENT_BRANCH $KAMAL_DESTINATION $KAMAL_RUNTIME
if [[ $CURRENT_BRANCH == "main" && $KAMAL_DESTINATION == "production" ]]; then
gh release create $KAMAL_SERVICE_VERSION --target $KAMAL_VERSION --generate-notes 2> /dev/null || true
RELEASE_URL=$(gh release view $KAMAL_SERVICE_VERSION --json url,body --jq .url)
RELEASE_BODY=$(gh release view $KAMAL_SERVICE_VERSION --json url,body --jq .body)
bin/broadcast_to_bc "$MESSAGE "$'\n'"$RELEASE_URL "$'\n'"$RELEASE_BODY"
else
bin/broadcast_to_bc "$MESSAGE"
fi
-37
View File
@@ -1,37 +0,0 @@
#!/usr/bin/env ruby
def exit_with_error(message)
$stderr.puts message
exit 1
end
def check_branch
if ENV["KAMAL_DESTINATION"] == "production"
current_branch = `git branch --show-current`.strip
if current_branch != "main"
exit_with_error "Only the `main` branch should be deployed to production, current branch is #{current_branch}. If this is expected, try again with `SKIP_GIT_CHECKS=1` prepended to the command"
end
end
end
def check_for_uncommitted_changes
if `git status --porcelain`.strip.length != 0
exit_with_error "You have uncommitted changes, aborting"
end
end
def check_local_and_remote_heads_match
remote_head = `git ls-remote origin --tags $(git branch --show-current) | cut -f1 | head -1`.strip
local_head = `git rev-parse HEAD`.strip
if local_head != remote_head
exit_with_error "Remote HEAD #{remote_head}, differs from local HEAD #{local_head}, aborting"
end
end
unless ENV["SKIP_GIT_CHECKS"]
check_branch
check_for_uncommitted_changes
check_local_and_remote_heads_match
end
-82
View File
@@ -1,82 +0,0 @@
#!/usr/bin/env bash
# Validate hostnames are FQDNs ending in -int.37signals.com
if command -v yq >/dev/null 2>&1; then
declare -A SUGGESTIONS
while IFS= read -r host; do
if [[ ! $host =~ -int\.37signals\.com$ ]]; then
if [[ $host =~ -4[0-9]{2}$ ]]; then
SUGGESTIONS["$host"]="$host.df-ams-int.37signals.com"
elif [[ $host =~ -1[0-9]{2}$ ]]; then
SUGGESTIONS["$host"]="$host.df-iad-int.37signals.com"
else
SUGGESTIONS["$host"]="$host.sc-chi-int.37signals.com"
fi
fi
done < <(bin/kamal config -d "${KAMAL_DESTINATION:-production}" 2>/dev/null | yq -r '.":hosts"[]')
if [ ${#SUGGESTIONS[@]} -gt 0 ]; then
echo "Unqualified hostnames found in config/deploy.${KAMAL_DESTINATION:-production}.yml:" >&2
echo "" >&2
echo "Update to use fully-qualified hostnames:" >&2
for host in "${!SUGGESTIONS[@]}"; do
echo " $host → ${SUGGESTIONS[$host]}" >&2
done
exit 1
fi
fi
# Verify Tailscale connection and SSH authentication before deploying.
tailscale_cmd() {
if command -v tailscale >/dev/null 2>&1; then
tailscale "$@"
elif [ -f "/Applications/Tailscale.app/Contents/MacOS/Tailscale" ]; then
env TAILSCALE_BE_CLI=1 /Applications/Tailscale.app/Contents/MacOS/Tailscale "$@"
else
return 1
fi
}
on_tailscale() {
tailscale_cmd status --json 2>/dev/null | jq -e '.Self.Online' >/dev/null 2>&1
}
# Check Tailscale connection
if ! on_tailscale; then
echo "" >&2
echo "You must be connected to Tailscale to deploy." >&2
echo "" >&2
echo "→ Connect to Tailscale and try again" >&2
echo "" >&2
exit 1
fi
# Verify SSH access
echo "Deploying via Tailscale. Verifying SSH access…" >&2
TEST_HOST="fizzy-app-101"
SSH_OUTPUT=$(ssh -o ConnectTimeout=5 "app@$TEST_HOST" true 2>&1)
SSH_EXIT=$?
echo "$SSH_OUTPUT" >&2
if echo "$SSH_OUTPUT" | grep -q "Permission denied"; then
GITHUB_USER=$(gh api user 2>/dev/null | jq -r '.login // "unknown"')
GITHUB_KEYS_URL="https://github.com/${GITHUB_USER}.keys"
echo "" >&2
echo "ERROR: SSH authentication failed" >&2
echo "" >&2
echo "You must deploy with an SSH key that's on your GitHub account." >&2
echo "" >&2
echo "→ Verify your public key is at $GITHUB_KEYS_URL" >&2
echo " Add it at https://github.com/settings/keys if not" >&2
echo "" >&2
echo "Note that SSH keys are pulled from GitHub every 5 minutes, so if you've" >&2
echo "just added a new key to GitHub, try again in five." >&2
echo "" >&2
exit 1
fi
exit $SSH_EXIT
-12
View File
@@ -1,12 +0,0 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
DASH_BASIC_AUTH_SECRET=$(kamal secrets extract DASH_BASIC_AUTH_SECRET $SECRETS)
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
MYSQL_ALTER_PASSWORD=$(kamal secrets extract MYSQL_ALTER_PASSWORD $SECRETS)
MYSQL_ALTER_USER=$(kamal secrets extract MYSQL_ALTER_USER $SECRETS)
MYSQL_APP_PASSWORD=$(kamal secrets extract MYSQL_APP_PASSWORD $SECRETS)
MYSQL_APP_USER=$(kamal secrets extract MYSQL_APP_USER $SECRETS)
MYSQL_READONLY_PASSWORD=$(kamal secrets extract MYSQL_READONLY_PASSWORD $SECRETS)
MYSQL_READONLY_USER=$(kamal secrets extract MYSQL_READONLY_USER $SECRETS)
-6
View File
@@ -1,6 +0,0 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
DASH_BASIC_AUTH_SECRET=$(kamal secrets extract DASH_BASIC_AUTH_SECRET $SECRETS)
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
-12
View File
@@ -1,12 +0,0 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
DASH_BASIC_AUTH_SECRET=$(kamal secrets extract DASH_BASIC_AUTH_SECRET $SECRETS)
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
MYSQL_ALTER_PASSWORD=$(kamal secrets extract MYSQL_ALTER_PASSWORD $SECRETS)
MYSQL_ALTER_USER=$(kamal secrets extract MYSQL_ALTER_USER $SECRETS)
MYSQL_APP_PASSWORD=$(kamal secrets extract MYSQL_APP_PASSWORD $SECRETS)
MYSQL_APP_USER=$(kamal secrets extract MYSQL_APP_USER $SECRETS)
MYSQL_READONLY_PASSWORD=$(kamal secrets extract MYSQL_READONLY_PASSWORD $SECRETS)
MYSQL_READONLY_USER=$(kamal secrets extract MYSQL_READONLY_USER $SECRETS)
-12
View File
@@ -1,12 +0,0 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER)
GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
DASH_BASIC_AUTH_SECRET=$(kamal secrets extract DASH_BASIC_AUTH_SECRET $SECRETS)
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
MYSQL_ALTER_PASSWORD=$(kamal secrets extract MYSQL_ALTER_PASSWORD $SECRETS)
MYSQL_ALTER_USER=$(kamal secrets extract MYSQL_ALTER_USER $SECRETS)
MYSQL_APP_PASSWORD=$(kamal secrets extract MYSQL_APP_PASSWORD $SECRETS)
MYSQL_APP_USER=$(kamal secrets extract MYSQL_APP_USER $SECRETS)
MYSQL_READONLY_PASSWORD=$(kamal secrets extract MYSQL_READONLY_PASSWORD $SECRETS)
MYSQL_READONLY_USER=$(kamal secrets extract MYSQL_READONLY_USER $SECRETS)
+3 -61
View File
@@ -137,72 +137,14 @@ Key recurring tasks (via `config/recurring.yml`):
- Search records denormalized for performance
- Models in `app/models/search/`
## Production Observability
Grafana MCP tools provide access to production metrics and logs for performance analysis.
### Datasources
| Name | UID | Use |
|------|-----|-----|
| Thanos (Prometheus) | `PC96415006F908B67` | Metrics, latencies |
| Loki | `e38bdfea-097e-47fa-a7ab-774fd2487741` | Application logs |
### Key Metrics
- `rails_request_duration_seconds_bucket:rate1m:sum_by_app:quantiles{app="fizzy"}` - Request latency percentiles
- `rails_request_total:rate1m:sum_by_controller_action{app="fizzy"}` - Request rates by endpoint
- `fizzy_replica_wait_seconds` - Database replica consistency wait times
### Loki Log Labels and Query Patterns
**Base label selector:**
```logql
{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"}
```
**Useful JSON fields:** `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller`, `url_path`, `authentication_identity_id`, `http_response_status_code`
**Query patterns:**
- Filter by fields: `{labels} | field_name = "value"`
- Multiple field filters: `{labels} | field1 = "value1" | field2 = "value2"`
- Reduce returned labels: `{labels} | filters | keep field1,field2,field3` (reduces label payload)
- Minimize log line content: `{labels} | filters | line_format "{{.field_name}}"` (replaces raw log line)
- Combine both for minimal tokens: `{labels} | filters | keep field1,field2 | line_format "{{.field1}}"`
- **Important:** Fields are pre-parsed by the OTel collector. Don't use string search (`|=`) when filtering structured fields
- **Important:** Do NOT use `| json` - it will cause JSONParserErr since fields are already parsed as labels
**Token management (CRITICAL):**
- Always probe with `limit: 3` first to check response size before running larger queries
- Aggregations return time series (many data points), not single values - can explode token usage
- NEVER use `sum by (field)` - returns a time series per unique value, easily exceeds token limits
- For breakdowns by field: fetch raw logs with `| keep field | line_format "{{.field}}"` and count client-side
**Aggregations for statistics (use instead of fetching raw logs):**
- `mcp__grafana__query_loki_logs` returns limited results (default 10, max ~100) and large responses get truncated; use aggregations for statistics on large datasets
- Count: `sum(count_over_time({labels} | filters [12h]))`
- Percentiles: `quantile_over_time(0.95, {labels} | filters | unwrap field_name | __error__="" [12h]) by ()`
- Average: `avg_over_time({labels} | filters | unwrap field_name | __error__="" [12h]) by ()`
- Min/Max: `min_over_time(...)` / `max_over_time(...)`
- The `| unwrap field_name | __error__=""` pattern extracts numeric values from pre-parsed labels
- Use `by ()` or wrap in `sum()` to avoid cardinality limits
**Documentation:** For advanced LogQL syntax (aggregations, pattern matching, etc.), consult https://grafana.com/docs/loki/latest/query/
### Instrumentation
Yabeda-based metrics exported at `:9394/metrics`. Config in `config/initializers/yabeda.rb`.
## Tools
### Chrome MCP (Local Dev)
URL: `http://fizzy.localhost:3006`
Login: david@37signals.com (passwordless magic link auth - check rails console for link)
Use Chrome MCP tools to interact with the running dev app for UI testing and debugging.
### Sentry Error Tracking
Organization: `basecamp` | Project: `fizzy` | Region: `https://us.sentry.io`
Use Sentry MCP tools to investigate production errors:
- `search_issues` - Find grouped issues by natural language query
- `get_issue_details` - Get full stacktrace and context for a specific issue
- `analyze_issue_with_seer` - AI-powered root cause analysis with code fix suggestions
Use Chrome MCP tools to interact with the running dev app for UI testing and debugging.``
## Coding style
+40 -41
View File
@@ -1,77 +1,76 @@
# syntax=docker/dockerfile:1
# check=error=true
# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t fizzy .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name fizzy fizzy
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.7
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
# Rails app lives here
WORKDIR /rails
# Set production environment
# Install base packages
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Set production environment variables and enable jemalloc for reduced memory usage and latency.
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
BUNDLE_WITHOUT="development" \
LD_PRELOAD="/usr/local/lib/libjemalloc.so"
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install -y --no-install-recommends -y build-essential pkg-config git libvips libyaml-dev libssl-dev && \
apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Install application gems
COPY Gemfile Gemfile.lock .ruby-version ./
COPY lib/bootstrap.rb ./lib/bootstrap.rb
COPY gems ./gems/
RUN --mount=type=secret,id=GITHUB_TOKEN --mount=type=cache,id=fizzy-permabundle-${RUBY_VERSION},sharing=locked,target=/permabundle \
gem install bundler && \
BUNDLE_PATH=/permabundle BUNDLE_GITHUB__COM="$(cat /run/secrets/GITHUB_TOKEN):x-oauth-basic" bundle install && \
cp -a /permabundle/. "$BUNDLE_PATH"/ && \
bundle clean --force && \
rm -rf "$BUNDLE_PATH"/ruby/*/bundler/gems/*/.git && \
find "$BUNDLE_PATH" -type f \( -name '*.gem' -o -iname '*.a' -o -iname '*.o' -o -iname '*.h' -o -iname '*.c' -o -iname '*.hpp' -o -iname '*.cpp' \) -delete && \
bundle exec bootsnap precompile --gemfile
COPY Gemfile Gemfile.lock vendor ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
bundle exec bootsnap precompile -j 1 --gemfile
# Copy application code
COPY . .
# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/
# Precompile bootsnap code for faster boot times.
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
RUN bundle exec bootsnap precompile -j 1 app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage for app image
FROM base
# Install packages needed for deployment
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools sqlite3 libjemalloc-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
USER 1000:1000
# Copy built artifacts: gems, application
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails
# Run and own only the runtime files as a non-root user for security
RUN useradd rails --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER rails:rails
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --chown=rails:rails --from=build /rails /rails
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
# Ruby GC tuning values pulled from Autotuner recommendations
ENV RUBY_GC_HEAP_0_INIT_SLOTS=692636 \
RUBY_GC_HEAP_1_INIT_SLOTS=175943 \
RUBY_GC_HEAP_2_INIT_SLOTS=148807 \
RUBY_GC_HEAP_3_INIT_SLOTS=9169 \
RUBY_GC_HEAP_4_INIT_SLOTS=3054 \
RUBY_GC_MALLOC_LIMIT=33554432 \
RUBY_GC_MALLOC_LIMIT_MAX=67108864 \
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
# Start the server by default, this can be overwritten at runtime
EXPOSE 80 443 9394
# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
+1 -1
View File
@@ -18,7 +18,7 @@ RUN apt-get update -qq && \
# Install application gems
COPY Gemfile Gemfile.lock .ruby-version ./
COPY lib/bootstrap.rb ./lib/bootstrap.rb
COPY lib/fizzy.rb ./lib/fizzy.rb
COPY gems ./gems/
RUN --mount=type=secret,id=GITHUB_TOKEN --mount=type=cache,id=fizzy-devbundle-${RUBY_VERSION},sharing=locked,target=/devbundle \
gem install bundler foreman && \
+3 -22
View File
@@ -1,7 +1,7 @@
source "https://rubygems.org"
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
gem "rails", github: "rails/rails", branch: "main"
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
# Assets & front end
gem "importmap-rails"
@@ -36,22 +36,9 @@ gem "net-http-persistent"
gem "mittens"
gem "useragent", bc: "useragent"
# Telemetry, logging, and operations
gem "mission_control-jobs"
gem "sentry-ruby"
gem "sentry-rails"
gem "rails_structured_logging", bc: "rails-structured-logging"
gem "yabeda"
gem "yabeda-actioncable"
gem "yabeda-activejob", github: "basecamp/yabeda-activejob", branch: "bulk-and-scheduled-jobs"
gem "yabeda-gc"
gem "yabeda-http_requests"
gem "yabeda-prometheus-mmap"
gem "yabeda-puma-plugin"
gem "yabeda-rails"
gem "webrick" # required for yabeda-prometheus metrics server
gem "prometheus-client-mmap", "~> 1.3"
# Operations
gem "autotuner"
gem "mission_control-jobs"
gem "benchmark" # indirect dependency, being removed from Ruby 3.5 stdlib so here to quash warnings
group :development, :test do
@@ -75,9 +62,3 @@ group :test do
gem "vcr"
gem "mocha"
end
require_relative "lib/bootstrap"
unless Bootstrap.oss_config?
eval_gemfile "gems/fizzy-saas/Gemfile"
gem "fizzy-saas", path: "gems/fizzy-saas"
end
+1 -129
View File
@@ -1,36 +1,9 @@
GIT
remote: https://github.com/basecamp/queenbee-plugin
revision: eb01c697de1ad028afc65cc7d9b5345a7a8e849f
ref: eb01c697de1ad028afc65cc7d9b5345a7a8e849f
specs:
queenbee (3.2.0)
activeresource
builder
rexml
GIT
remote: https://github.com/basecamp/rails-structured-logging
revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6
specs:
rails_structured_logging (0.2.1)
json
rails (>= 6.0.0)
GIT
remote: https://github.com/basecamp/useragent
revision: 433ca320a42db1266c4b89df74d0abdb9a880c5e
specs:
useragent (0.16.11)
GIT
remote: https://github.com/basecamp/yabeda-activejob.git
revision: 684973f77ff01d8b3dd75874538fae55961e15e6
branch: bulk-and-scheduled-jobs
specs:
yabeda-activejob (0.6.0)
rails (>= 6.1)
yabeda (~> 0.6)
GIT
remote: https://github.com/rails/rails.git
revision: b22cb0c0cb39b103d816a66d560991acf0a57163
@@ -132,31 +105,13 @@ GIT
tsort (>= 0.2)
zeitwerk (~> 2.6)
PATH
remote: gems/fizzy-saas
specs:
fizzy-saas (0.1.0)
queenbee
rails (>= 8.1.0.beta1)
rails_structured_logging
GEM
remote: https://rubygems.org/
specs:
action_text-trix (2.1.15)
railties
activemodel-serializers-xml (1.0.3)
activemodel (>= 5.0.0.a)
activesupport (>= 5.0.0.a)
builder (~> 3.1)
activeresource (6.2.0)
activemodel (>= 7.0)
activemodel-serializers-xml (~> 1.0)
activesupport (>= 7.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
anyway_config (2.7.2)
ruby-next-core (~> 1.0)
ast (2.4.3)
autotuner (1.1.0)
aws-eventstream (1.4.0)
@@ -216,7 +171,6 @@ GEM
reline (>= 0.3.8)
dotenv (3.1.8)
drb (2.2.3)
dry-initializer (3.2.0)
ed25519 (1.4.0)
erb (6.0.0)
erubi (1.13.1)
@@ -259,7 +213,7 @@ GEM
json (2.16.0)
jwt (3.1.2)
base64
kamal (2.8.2)
kamal (2.9.0)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
@@ -352,31 +306,6 @@ GEM
prettyprint
prettyprint (0.2.0)
prism (1.6.0)
prometheus-client-mmap (1.3.0)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-arm64-darwin)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-x86_64-darwin)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-x86_64-linux-gnu)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-x86_64-linux-musl)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
propshaft (1.3.1)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
@@ -408,9 +337,6 @@ GEM
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rainbow (3.1.1)
rake (13.3.1)
rake-compiler-dock (1.9.1)
rb_sys (0.9.117)
rake-compiler-dock (= 1.9.1)
rdoc (6.16.0)
erb
psych (>= 4.0.0)
@@ -453,7 +379,6 @@ GEM
rubocop (>= 1.72)
rubocop-performance (>= 1.24)
rubocop-rails (>= 2.30)
ruby-next-core (1.1.2)
ruby-progressbar (1.13.0)
ruby-vips (2.2.5)
ffi (~> 1.12)
@@ -467,15 +392,6 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
sentry-rails (6.1.2)
railties (>= 5.2.0)
sentry-ruby (~> 6.1.2)
sentry-ruby (6.1.2)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
sniffer (0.5.0)
anyway_config (>= 1.0)
dry-initializer (~> 3)
solid_cable (3.0.12)
actioncable (>= 7.2)
activejob (>= 7.2)
@@ -539,7 +455,6 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.8.0)
base64
@@ -547,33 +462,6 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yabeda (0.14.0)
anyway_config (>= 1.0, < 3)
concurrent-ruby
dry-initializer
yabeda-actioncable (0.2.2)
actioncable (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
yabeda (~> 0.8)
yabeda-gc (0.4.0)
yabeda (~> 0.6)
yabeda-http_requests (0.3.0)
anyway_config (>= 1.3, < 3.0)
sniffer
yabeda
yabeda-prometheus-mmap (0.4.0)
prometheus-client-mmap
yabeda (~> 0.10)
yabeda-puma-plugin (0.9.0)
json
puma
yabeda (~> 0.5)
yabeda-rails (0.10.0)
activesupport
anyway_config (>= 1.3, < 3)
railties
yabeda (~> 0.8)
zeitwerk (2.7.3)
PLATFORMS
@@ -585,7 +473,6 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
activeresource
autotuner
aws-sdk-s3
bcrypt (~> 3.1.7)
@@ -596,7 +483,6 @@ DEPENDENCIES
capybara
debug
faker
fizzy-saas!
geared_pagination (~> 1.2)
image_processing (~> 1.14)
importmap-rails
@@ -609,20 +495,15 @@ DEPENDENCIES
mocha
net-http-persistent
platform_agent
prometheus-client-mmap (~> 1.3)
propshaft
puma (>= 5.0)
queenbee!
rack-mini-profiler
rails!
rails_structured_logging!
redcarpet
rouge
rqrcode
rubocop-rails-omakase
selenium-webdriver
sentry-rails
sentry-ruby
solid_cable (>= 3.0)
solid_cache (~> 1.0)
solid_queue (~> 1.2)
@@ -636,15 +517,6 @@ DEPENDENCIES
web-console
web-push
webmock
webrick
yabeda
yabeda-actioncable
yabeda-activejob!
yabeda-gc
yabeda-http_requests
yabeda-prometheus-mmap
yabeda-puma-plugin
yabeda-rails
BUNDLED WITH
2.7.2
+24
View File
@@ -0,0 +1,24 @@
# This Gemfile extends the base Gemfile with SaaS-specific dependencies
eval_gemfile "Gemfile"
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
gem "activeresource", require: "active_resource"
gem "queenbee", bc: "queenbee-plugin"
gem "fizzy-saas", bc: "fizzy-saas"
gem "rails_structured_logging", bc: "rails-structured-logging"
gem "sentry-ruby"
gem "sentry-rails"
# Telemetry
gem "yabeda"
gem "yabeda-actioncable"
gem "yabeda-activejob", github: "basecamp/yabeda-activejob", branch: "bulk-and-scheduled-jobs"
gem "yabeda-gc"
gem "yabeda-http_requests"
gem "yabeda-prometheus-mmap"
gem "yabeda-puma-plugin"
gem "yabeda-rails"
gem "webrick" # required for yabeda-prometheus metrics server
gem "prometheus-client-mmap", "~> 1.3"
+662
View File
@@ -0,0 +1,662 @@
GIT
remote: https://github.com/basecamp/fizzy-saas
revision: 7f392bbbf9f5170d334b6ee2f6d240569bd157ed
specs:
fizzy-saas (0.1.0)
prometheus-client-mmap
queenbee
rails (>= 8.1.0.beta1)
rails_structured_logging
sentry-rails
sentry-ruby
yabeda
yabeda-actioncable
yabeda-activejob
yabeda-gc
yabeda-http_requests
yabeda-prometheus-mmap
yabeda-puma-plugin
yabeda-rails
GIT
remote: https://github.com/basecamp/queenbee-plugin
revision: 15faf03a876c5e66b67753d2e1ddb24f1eb5abb2
specs:
queenbee (3.2.0)
activeresource
builder
rexml
GIT
remote: https://github.com/basecamp/rails-structured-logging
revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6
specs:
rails_structured_logging (0.2.1)
json
rails (>= 6.0.0)
GIT
remote: https://github.com/basecamp/useragent
revision: 433ca320a42db1266c4b89df74d0abdb9a880c5e
specs:
useragent (0.16.11)
GIT
remote: https://github.com/basecamp/yabeda-activejob.git
revision: 684973f77ff01d8b3dd75874538fae55961e15e6
branch: bulk-and-scheduled-jobs
specs:
yabeda-activejob (0.6.0)
rails (>= 6.1)
yabeda (~> 0.6)
GIT
remote: https://github.com/rails/rails.git
revision: 4f7ab01bb5d6be78c7447dbb230c55027d08ae34
branch: main
specs:
actioncable (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
activejob (= 8.2.0.alpha)
activerecord (= 8.2.0.alpha)
activestorage (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
mail (>= 2.8.0)
actionmailer (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
actionview (= 8.2.0.alpha)
activejob (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.2.0.alpha)
actionview (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.2.0.alpha)
action_text-trix (~> 2.1.15)
actionpack (= 8.2.0.alpha)
activerecord (= 8.2.0.alpha)
activestorage (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.2.0.alpha)
activesupport (= 8.2.0.alpha)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.2.0.alpha)
activesupport (= 8.2.0.alpha)
globalid (>= 0.3.6)
activemodel (8.2.0.alpha)
activesupport (= 8.2.0.alpha)
activerecord (8.2.0.alpha)
activemodel (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
timeout (>= 0.4.0)
activestorage (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
activejob (= 8.2.0.alpha)
activerecord (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
marcel (~> 1.0)
activesupport (8.2.0.alpha)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
rails (8.2.0.alpha)
actioncable (= 8.2.0.alpha)
actionmailbox (= 8.2.0.alpha)
actionmailer (= 8.2.0.alpha)
actionpack (= 8.2.0.alpha)
actiontext (= 8.2.0.alpha)
actionview (= 8.2.0.alpha)
activejob (= 8.2.0.alpha)
activemodel (= 8.2.0.alpha)
activerecord (= 8.2.0.alpha)
activestorage (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
bundler (>= 1.15.0)
railties (= 8.2.0.alpha)
railties (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6)
GEM
remote: https://rubygems.org/
specs:
action_text-trix (2.1.15)
railties
activemodel-serializers-xml (1.0.3)
activemodel (>= 5.0.0.a)
activesupport (>= 5.0.0.a)
builder (~> 3.1)
activeresource (6.2.0)
activemodel (>= 7.0)
activemodel-serializers-xml (~> 1.0)
activesupport (>= 7.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
anyway_config (2.7.2)
ruby-next-core (~> 1.0)
ast (2.4.3)
autotuner (1.1.0)
aws-eventstream (1.4.0)
aws-partitions (1.1187.0)
aws-sdk-core (3.239.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.205.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
base64 (0.3.0)
bcrypt (3.1.20)
bcrypt_pbkdf (1.1.1)
benchmark (0.5.0)
bigdecimal (3.3.1)
bindex (0.8.1)
bootsnap (1.19.0)
msgpack (~> 1.2)
brakeman (7.1.1)
racc
builder (3.3.0)
bundler-audit (0.9.2)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.5)
crack (1.0.1)
bigdecimal
rexml
crass (1.0.6)
date (3.5.0)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
dotenv (3.1.8)
drb (2.2.3)
dry-initializer (3.2.0)
ed25519 (1.4.0)
erb (6.0.0)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
faker (3.5.2)
i18n (>= 1.8.11, < 2)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl)
fugit (1.12.1)
et-orbi (~> 1.4)
raabro (~> 1.4)
geared_pagination (1.2.0)
activesupport (>= 5.0)
addressable (>= 2.5.0)
globalid (1.3.0)
activesupport (>= 6.1)
hashdiff (1.2.1)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.2.2)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.1)
irb (1.15.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.14.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
jmespath (1.6.2)
json (2.16.0)
jwt (3.1.2)
base64
kamal (2.9.0)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
concurrent-ruby (~> 1.2)
dotenv (~> 3.1)
ed25519 (~> 1.4)
net-ssh (~> 7.3)
sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3)
zeitwerk (>= 2.6.18, < 3.0)
language_server-protocol (3.17.0.5)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lexxy (0.1.20.beta)
rails (>= 8.0.2)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.1.0)
matrix (0.4.3)
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
minitest (5.26.2)
mission_control-jobs (1.1.0)
actioncable (>= 7.1)
actionpack (>= 7.1)
activejob (>= 7.1)
activerecord (>= 7.1)
importmap-rails (>= 1.2.1)
irb (~> 1.13)
railties (>= 7.1)
stimulus-rails
turbo-rails
mittens (0.3.0)
mocha (2.8.2)
ruby2_keywords (>= 0.0.5)
msgpack (1.8.0)
net-http-persistent (4.0.6)
connection_pool (~> 2.2, >= 2.2.4)
net-imap (0.5.12)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-scp (4.1.0)
net-ssh (>= 2.6.5, < 8.0.0)
net-sftp (4.0.0)
net-ssh (>= 5.0.0, < 8.0.0)
net-smtp (0.5.1)
net-protocol
net-ssh (7.3.0)
nio4r (2.7.5)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-musl)
racc (~> 1.4)
openssl (3.3.2)
ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.10.0)
ast (~> 2.4.1)
racc
platform_agent (1.0.1)
activesupport (>= 5.2.0)
useragent (~> 0.16.3)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.6.0)
prometheus-client-mmap (1.3.0)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-aarch64-linux-gnu)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-aarch64-linux-musl)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-x86_64-linux-gnu)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-x86_64-linux-musl)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
propshaft (1.3.1)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.2.6)
date
stringio
public_suffix (6.0.2)
puma (7.1.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.4)
rack-mini-profiler (4.0.1)
rack (>= 1.2.0)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rainbow (3.1.1)
rake (13.3.1)
rake-compiler-dock (1.9.1)
rb_sys (0.9.117)
rake-compiler-dock (= 1.9.1)
rdoc (6.15.1)
erb
psych (>= 4.0.0)
tsort
redcarpet (3.6.1)
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
rexml (3.4.4)
rouge (4.6.1)
rqrcode (3.1.0)
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.0)
rubocop (1.81.7)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.48.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rails (2.34.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rails-omakase (1.1.0)
rubocop (>= 1.72)
rubocop-performance (>= 1.24)
rubocop-rails (>= 2.30)
ruby-next-core (1.1.2)
ruby-progressbar (1.13.0)
ruby-vips (2.2.5)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyzip (3.2.2)
securerandom (0.4.1)
selenium-webdriver (4.38.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
sentry-rails (6.1.1)
railties (>= 5.2.0)
sentry-ruby (~> 6.1.1)
sentry-ruby (6.1.1)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
sniffer (0.5.0)
anyway_config (>= 1.0)
dry-initializer (~> 3)
solid_cable (3.0.12)
actioncable (>= 7.2)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_cache (1.0.10)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.2.4)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11)
railties (>= 7.1)
thor (>= 1.3.1)
sqlite3 (2.8.0-aarch64-linux-gnu)
sqlite3 (2.8.0-aarch64-linux-musl)
sqlite3 (2.8.0-arm-linux-gnu)
sqlite3 (2.8.0-arm-linux-musl)
sqlite3 (2.8.0-x86_64-linux-gnu)
sqlite3 (2.8.0-x86_64-linux-musl)
sshkit (1.24.0)
base64
logger
net-scp (>= 1.1.2)
net-sftp (>= 2.1.2)
net-ssh (>= 2.8.0)
ostruct
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.8)
thor (1.4.0)
thruster (0.1.16)
thruster (0.1.16-aarch64-linux)
thruster (0.1.16-x86_64-linux)
timeout (0.4.4)
trilogy (2.9.0)
tsort (0.2.0)
turbo-rails (2.0.20)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uri (1.1.1)
vcr (6.3.1)
base64
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
web-push (3.0.2)
jwt (~> 3.0)
openssl (~> 3.0)
webmock (3.26.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yabeda (0.14.0)
anyway_config (>= 1.0, < 3)
concurrent-ruby
dry-initializer
yabeda-actioncable (0.2.2)
actioncable (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
yabeda (~> 0.8)
yabeda-gc (0.4.0)
yabeda (~> 0.6)
yabeda-http_requests (0.3.0)
anyway_config (>= 1.3, < 3.0)
sniffer
yabeda
yabeda-prometheus-mmap (0.4.0)
prometheus-client-mmap
yabeda (~> 0.10)
yabeda-puma-plugin (0.9.0)
json
puma
yabeda (~> 0.5)
yabeda-rails (0.10.0)
activesupport
anyway_config (>= 1.3, < 3)
railties
yabeda (~> 0.8)
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
activeresource
autotuner
aws-sdk-s3
bcrypt (~> 3.1.7)
benchmark
bootsnap
brakeman
bundler-audit
capybara
debug
faker
fizzy-saas!
geared_pagination (~> 1.2)
image_processing (~> 1.14)
importmap-rails
jbuilder
kamal
letter_opener
lexxy
mission_control-jobs
mittens
mocha
net-http-persistent
platform_agent
prometheus-client-mmap (~> 1.3)
propshaft
puma (>= 5.0)
queenbee!
rack-mini-profiler
rails!
rails_structured_logging!
redcarpet
rouge
rqrcode
rubocop-rails-omakase
selenium-webdriver
sentry-rails
sentry-ruby
solid_cable (>= 3.0)
solid_cache (~> 1.0)
solid_queue (~> 1.2)
sqlite3 (>= 2.0)
stimulus-rails
thruster
trilogy (~> 2.9)
turbo-rails
useragent!
vcr
web-console
web-push
webmock
webrick
yabeda
yabeda-actioncable
yabeda-activejob!
yabeda-gc
yabeda-http_requests
yabeda-prometheus-mmap
yabeda-puma-plugin
yabeda-rails
BUNDLED WITH
2.7.2
+10
View File
@@ -0,0 +1,10 @@
# O'Saasy License Agreement
Copyright © 2025, 37signals LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
2. No licensee or downstream recipient may use the Software (including any modified or derivative versions) to directly compete with the original Licensor by offering it to third parties as a hosted, managed, or Software-as-a-Service (SaaS) product or cloud service where the primary value of the service is the functionality of the Software itself.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+42 -29
View File
@@ -1,22 +1,29 @@
# Fizzy
## Setting up for development
This is the source code of [Fizzy](https://fizzy.do/), the Kanban tracking tool for issues and ideas by [37signals](https://37signals.com).
First get everything installed and configured with:
## Development
bin/setup
### Setting up
If you'd like to load fixtures:
First, get everything installed and configured with:
bin/rails db:fixtures:load
```sh
bin/setup
bin/setup --reset # Reset the database and seed it
```
And then run the development server:
bin/dev
```sh
bin/dev
```
You'll be able to access the app in development at http://fizzy.localhost:3006
You'll be able to access the app in development at http://fizzy.localhost:3006.
## Running tests
To login, enter `david@37signals.com` and grab the verification code from the browser console to sign in.
### Running tests
For fast feedback loops, unit tests can be run with:
@@ -26,47 +33,53 @@ The full continuous integration tests can be run with:
bin/ci
### Tests
### Database configuration
Fizzy supports SQLite (default, recommended for most scenarios) and MySQL. You can switch adapters with the `DATABASE_ADAPTER` environment variable.
```sh
DATABASE_ADAPTER=mysql bin/rails
DATABASE_ADAPTER=mysql bin/test
bin/ci # Runs tests against both SQLite and MySQL
```
### Outbound Emails
#### Development
You can view email previews at http://fizzy.localhost:3006/rails/mailers.
You can enable or disable [`letter_opener`](https://github.com/ryanb/letter_opener) to
open sent emails automatically with:
You can enable or disable [`letter_opener`](https://github.com/ryanb/letter_opener) to open sent emails automatically with:
bin/rails dev:email
Under the hood, this will create or remove `tmp/email-dev.txt`.
## Environments
## Deployment
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.
We recommend [Kamal](https://kamal-deploy.org/) for deploying Fizzy. This project comes with a vanilla Rails template, you can find our production setup in [`fizzy-saas`](https://github.com/basecamp/fizzy-saas).
### Beta
### Web Push Notifications
Beta is primarily intended for testing product features.
Fizzy uses VAPID (Voluntary Application Server Identification) keys to send browser push notifications. You'll need to generate a key pair and set these environment variables:
Beta tenant is:
- `VAPID_PRIVATE_KEY`
- `VAPID_PUBLIC_KEY`
- https://fizzy-beta.37signals.com
Generate them with the `web-push` gem:
This environment uses local disk for Active Storage.
```ruby
vapid_key = WebPush.generate_key
puts "VAPID_PRIVATE_KEY=#{vapid_key.private_key}"
puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
```
### Staging
## SaaS gem
Staging is primarily intended for testing infrastructure changes.
37signals bundles Fizzy with [`fizzy-saas`](https://github.com/basecamp/fizzy-saas), a companion gem that links Fizzy with our billing system and contains our production setup.
- https://fizzy.37signals-staging.com/
This gem depends on some private git repositories and it is not meant to be used by third parties. But we hope it can serve as inspiration for anyone wanting to run fizzy on their own infrastructure.
This environment uses a FlashBlade bucket for blob storage, and shares nothing with Production. We may periodically copy data here from production.
## License
Fizzy is released under the [O'Saasy License](LICENSE.md).
### Production
- https://app.fizzy.do/
This environment uses a FlashBlade bucket for blob storage.
+35 -38
View File
@@ -135,56 +135,53 @@ class SomeModule
end
```
## CRUD operations from controllers
## CRUD controllers
In general, we favor a vanilla Rails approach to CRUD operations. We create and update models from Rails controllers passing the parameters directly to the model constructor or update method. We do not use services or form objects to handle these operations.
There are exceptional scenarios where we need to perform more complex operations, and we use form objects or higher-level service methods to handle them. We use the same pattern for both creations and updates.
Related to this, we prefer to avoid [nested attributes](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html). If you find yourself wanting to use `accepts_nested_attributes_for`, that's a good smell that you might want to consider using a form object instead.
As an example, you can check how we create and update messages in HEY's: `MessagesController`:
We model web endpoints as CRUD operations on resources (REST). When an action doesn't map cleanly to a standard CRUD verb, we introduce a new resource rather than adding custom actions.
```ruby
class MessagesController < ApplicationController
# Bad
resources :cards do
post :close
post :reopen
end
# Good
resources :cards do
resource :closure
end
```
## Controller and model interactions
In general, we favor a [vanilla Rails](https://dev.37signals.com/vanilla-rails-is-plenty/) approach with thin controllers directly invoking a rich domain model. We don't use services or other artifacts to connect the two.
Invoking plain Active Record operations is totally fine:
```ruby
class Cards::CommentsController < ApplicationController
def create
@entry = Entry.enter \
new_message,
on: new_topic,
status: :drafted,
address: entry_addressed_param,
scheduled_delivery_at: entry_scheduled_delivery_at_param,
scheduled_bubble_up_on: entry_scheduled_bubble_up_on_param
respond_to_saved_entry @entry
end
def update
previously_scheduled = @entry.scheduled_delivery
@entry.revise \
message_params,
status: :drafted,
is_delivery_imminent: !entry_status_param.drafted?,
address: entry_addressed_param,
scheduled_delivery_at: entry_scheduled_delivery_at_param,
scheduled_bubble_up_on: entry_scheduled_bubble_up_on_param
respond_to_saved_entry(@entry, previously_scheduled: previously_scheduled)
@comment = @card.comments.create!(comment_params)
end
end
```
class Entry < ApplicationRecord
def self.enter(*args, **kwargs)
Entry::Enter.new(*args, **kwargs).perform
end
For more complex behavior, we prefer clear, intention-revealing model APIs that controllers call directly:
def revise(*args, **kwargs)
Entry::Revise.new(self, *args, **kwargs).perform
```ruby
class Cards::GoldnessesController < ApplicationController
def create
@card.gild
end
end
```
When justified, it is fine to use services or form objects, but don't treat those as special artifacts:
```ruby
Signup.new(email_address: email_address).create_identity
```
## Run async operations in jobs
As a general rule, we write shallow job classes that delegate the logic itself to domain models:
@@ -1,6 +1,4 @@
class Admin::StatsController < AdminController
disallow_account_scope
layout "public"
def show
+1
View File
@@ -1,3 +1,4 @@
class AdminController < ApplicationController
disallow_account_scope
before_action :ensure_staff
end
@@ -4,7 +4,6 @@ class ApplicationController < ActionController::Base
include CurrentRequest, CurrentTimezone, SetPlatform
include RequestForgeryProtection
include TurboFlash, ViewTransitions
include Saas
include RoutingHeaders
etag { "v1" }
@@ -81,7 +81,6 @@ module Authentication
end
def set_current_session(session)
logger.struct " Authorized Identity##{session.identity.id}", authentication: { identity: { id: session.identity.id } }
Current.session = session
cookies.signed.permanent[:session_token] = { value: session.signed_id, httponly: true, same_site: :lax }
end
@@ -3,6 +3,7 @@ module Authorization
included do
before_action :ensure_can_access_account, if: -> { Current.account.present? && authenticated? }
before_action :ensure_only_staff_can_access_non_production_remote_environments, if: :authenticated?
end
class_methods do
@@ -29,6 +30,10 @@ module Authorization
redirect_to session_menu_url(script_name: nil) if Current.user.blank? || !Current.user.active?
end
def ensure_only_staff_can_access_non_production_remote_environments
head :forbidden unless Rails.env.local? || Rails.env.production? || Current.identity.staff?
end
def redirect_existing_user
redirect_to root_path if Current.user
end
@@ -42,7 +42,7 @@ module RequestForgeryProtection
Rails.logger.info "CSRF protection check: " + info.map { it.join(" ") }.join(", ")
if (origin && token) != sec_fetch_site
if Fizzy.saas? && (origin && token) != sec_fetch_site
Sentry.capture_message "CSRF protection mismatch", level: :info, extra: { info: info }
end
end
-11
View File
@@ -1,11 +0,0 @@
module Saas
extend ActiveSupport::Concern
included do
helper_method :signups_allowed?
end
def signups_allowed?
defined?(Signup) && defined?(saas)
end
end
+15 -7
View File
@@ -19,13 +19,9 @@ class SessionsController < ApplicationController
def create
if identity = Identity.find_by_email_address(email_address)
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
redirect_to session_magic_link_path
elsif signups_allowed?
Signup.new(email_address: email_address).create_identity
session[:return_to_after_authenticating] = saas.new_signup_completion_path
redirect_to session_magic_link_path
handle_existing_user(identity)
elsif
handle_new_signup
end
end
@@ -38,4 +34,16 @@ class SessionsController < ApplicationController
def email_address
params.expect(:email_address)
end
def handle_existing_user(identity)
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
redirect_to session_magic_link_path
end
def handle_new_signup
Signup.new(email_address: email_address).create_identity
session[:return_to_after_authenticating] = new_signup_completion_path
redirect_to session_magic_link_path
end
end
+6
View File
@@ -11,6 +11,7 @@ class Account < ApplicationRecord
has_many_attached :uploads
before_create :assign_external_account_id
after_create :create_join_code
validates :name, presence: true
@@ -35,4 +36,9 @@ class Account < ApplicationRecord
def system_user
users.where(role: :system).first!
end
private
def assign_external_account_id
self.external_account_id ||= ExternalIdSequence.next
end
end
@@ -0,0 +1,26 @@
# Provides sequential IDs for +external_account_id+ when creating accounts without one.
class Account::ExternalIdSequence < ApplicationRecord
class << self
def next
with_lock do |sequence|
sequence.increment!(:value).value
end
end
def value
first&.value
end
private
def with_lock
transaction do
sequence = lock.first_or_create!(value: initial_value)
yield sequence
end
end
def initial_value
Account.maximum(:external_account_id) || 0
end
end
end
@@ -4,20 +4,13 @@ class Signup
include ActiveModel::Validations
attr_accessor :full_name, :email_address, :identity
attr_reader :queenbee_account, :account, :user
attr_reader :account, :user
with_options on: :completion do
validates_presence_of :full_name, :identity
end
def initialize(...)
@full_name = nil
@email_address = nil
@account = nil
@user = nil
@queenbee_account = nil
@identity = nil
super
@email_address = @identity.email_address if @identity
@@ -31,13 +24,12 @@ class Signup
def complete
if valid?(:completion)
begin
create_queenbee_account
@tenant = create_tenant
create_account
true
rescue => error
destroy_account
destroy_queenbee_account
handle_account_creation_error(error)
errors.add(:base, "Something went wrong, and we couldn't create your account. Please give it another try.")
Rails.error.report(error, severity: :error)
@@ -52,22 +44,20 @@ class Signup
end
private
def create_queenbee_account
@account_name = AccountNameGenerator.new(identity: identity, name: full_name).generate
@queenbee_account = Queenbee::Remote::Account.create!(queenbee_account_attributes)
@tenant = queenbee_account.id.to_s
# Override to customize the handling of external accounts associated to the account.
def create_tenant
nil
end
def destroy_queenbee_account
@queenbee_account&.cancel
@queenbee_account = nil
# Override to inject custom handling for account creation errors
def handle_account_creation_error(error)
end
def create_account
@account = Account.create_with_admin_user(
account: {
external_account_id: @tenant,
name: @account_name
name: generate_account_name
},
owner: {
name: full_name,
@@ -78,6 +68,11 @@ class Signup
@account.setup_customer_template
end
def generate_account_name
AccountNameGenerator.new(identity: identity, name: full_name).generate
end
def destroy_account
@account&.destroy!
@@ -86,28 +81,6 @@ class Signup
@tenant = nil
end
def queenbee_account_attributes
{}.tap do |attributes|
attributes[:product_name] = "fizzy"
attributes[:name] = @account_name
attributes[:owner_name] = full_name
attributes[:owner_email] = email_address
attributes[:trial] = true
attributes[:subscription] = subscription_attributes
attributes[:remote_request] = request_attributes
# # TODO: Terms of Service
# attributes[:terms_of_service] = true
# We've confirmed the email
attributes[:auto_allow] = true
# Tell Queenbee to skip the request to create a local account. We've created it ourselves.
attributes[:skip_remote] = true
end
end
def subscription_attributes
subscription = FreeV1Subscription
+5 -7
View File
@@ -24,13 +24,11 @@
<p class="margin-none-block-start">You dont have any Fizzy accounts.</p>
<% end %>
<% if signups_allowed? %>
<div class="margin-block-start">
<%= link_to saas.new_signup_completion_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %>
<span>Sign up for a new Fizzy account</span>
<% end %>
</div>
<% end %>
<div class="margin-block-start">
<%= link_to new_signup_completion_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %>
<span>Sign up for a new Fizzy account</span>
<% end %>
</div>
</div>
<% end %>
@@ -3,10 +3,10 @@
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
<h1 class="txt-x-large font-weight-black margin-block-end"><%= @page_title %></h1>
<%= form_with model: @signup, url: saas.signup_completion_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %>
<%= form_with model: @signup, url: signup_completion_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %>
<%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name…", autofocus: true, required: true %>
<p>Youre one step away. Just enter your name to get your own Fizzy account.</p>
<p>You're one step away. Just enter your name to get your own Fizzy account.</p>
<% if @signup.errors.any? %>
<div class="margin-block-half">
@@ -3,7 +3,7 @@
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
<h1 class="txt-xx-large margin-block-end-double">Sign up</h1>
<%= form_with model: @signup, url: saas.signup_path, scope: "signup", class: "flex flex-column gap", data: { turbo: false, controller: "form" } do |form| %>
<%= form_with model: @signup, url: signup_path, scope: "signup", class: "flex flex-column gap", data: { turbo: false, controller: "form" } do |form| %>
<%= form.email_field :email_address, class: "input", autocomplete: "username", placeholder: "Email address", required: true %>
<% if @signup.errors.any? %>
+17
View File
@@ -22,6 +22,23 @@ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this
end
require "rubygems"
require_relative "../lib/fizzy"
Fizzy.configure_bundle
require "bundler/setup"
if Fizzy.saas?
gem_path = Gem::Specification.find_by_name("fizzy-saas").gem_dir
deploy_config = File.join(gem_path, "config", "deploy.yml")
unless ARGV.include?("-c") || ARGV.include?("--config-file")
if ARGV.empty? || ARGV.first.start_with?("-")
ARGV.unshift("-c", deploy_config)
else
ARGV.insert(1, "-c", deploy_config)
end
end
end
load Gem.bin_path("kamal", "kamal")
+10 -9
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env ruby
require_relative "../lib/bootstrap"
if !Bootstrap.oss_config?
# default from rails/test_unit/runner.rb but adding the saas gem test files
ENV["DEFAULT_TEST"] = "{gems/fizzy-saas/,}test/**/*_test.rb"
ENV["DEFAULT_TEST_EXCLUDE"] = "{gems/fizzy-saas/,}test/{system,dummy,fixtures}/**/*_test.rb"
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../lib/fizzy"
Fizzy.configure_bundle
require_relative "../config/boot"
require "rails/commands"
Fizzy::Saas.append_test_paths if Fizzy.saas? && Rails.env.test?
+54 -12
View File
@@ -8,6 +8,14 @@ app_root="$(
)"
export PATH="$app_root/bin:$PATH"
if [ -e tmp/saas.txt ]; then
export SAAS=1
fi
if [ -n "$SAAS" ]; then
export BUNDLE_GEMFILE="Gemfile.saas"
fi
# Install gum if needed
if ! command -v gum &>/dev/null; then
echo
@@ -50,6 +58,24 @@ needs_seeding() {
fi
}
setup_database() {
local adapter="$1"
local reset="$2"
local label="${adapter:+ ($adapter)}"
local env_cmd="${adapter:+env DATABASE_ADAPTER=$adapter}"
if [ "$reset" = "true" ]; then
step "Resetting the database$label" $env_cmd rails db:reset
else
step "Preparing the database$label" $env_cmd rails db:prepare
if needs_seeding; then
step "Seeding the database$label" $env_cmd rails db:seed
fi
fi
}
echo
gum style --foreground 153 " ˚ ∘ ∘ ˚ "
gum style --foreground 111 --bold " ∘˚˳°∘° 𝒻𝒾𝓏𝓏𝓎 °∘°˳˚∘ "
@@ -71,22 +97,38 @@ step "Set up gh-signoff" bash -c "gh extension install basecamp/gh-signoff || gh
bundle config set --local auto_install true
step "Installing RubyGems" bundle install
if [ -e tmp/minio-dev.txt ]; then
step "Starting Docker services" bash -c "[ -d ~/Work/basecamp/docker-dev ] && git -C ~/Work/basecamp/docker-dev pull || gh repo clone basecamp/docker-dev ~/Work/basecamp/docker-dev && ~/Work/basecamp/docker-dev/setup minio"
step "Configuring MinIO" bin/minio-setup
if [ -n "$SAAS" ]; then
saas_setup=$(bundle show fizzy-saas)/bin/setup
source "$saas_setup"
else
if ! nc -z localhost 3306 2>/dev/null; then
if docker ps -aq -f name=fizzy-mysql | grep -q .; then
step "Starting MySQL" docker start fizzy-mysql
else
step "Setting up MySQL" bash -c '
docker pull mysql:8.4
docker run -d \
--name fizzy-mysql \
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes \
-p 3306:3306 \
mysql:8.4
echo "MySQL is starting… (it may take a few seconds)"
'
fi
fi
fi
step "Starting mysql" bash -c "[ -d ~/Work/basecamp/docker-dev ] && git -C ~/Work/basecamp/docker-dev pull || gh repo clone basecamp/docker-dev ~/Work/basecamp/docker-dev && ~/Work/basecamp/docker-dev/setup mysql80"
reset_flag=""
[[ $* == *--reset* ]] && reset_flag="true"
if [[ $* == *--reset* ]]; then
step "Resetting the database" rails db:reset
if [ -n "$SAAS" ]; then
for adapter in saas sqlite; do
setup_database "$adapter" "$reset_flag"
done
else
step "Preparing the database" rails db:prepare
if needs_seeding; then
step "Seeding the database" rails db:seed
fi
for adapter in sqlite mysql; do
setup_database "$adapter" "$reset_flag"
done
fi
step "Cleaning up logs and tempfiles" rails log:clear tmp:clear
+4
View File
@@ -1,5 +1,7 @@
require_relative "boot"
require "rails/all"
require_relative "../lib/fizzy"
Bundler.require(*Rails.groups)
module Fizzy
@@ -22,5 +24,7 @@ module Fizzy
config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
config.mission_control.jobs.http_basic_auth_enabled = false
end
end
+15 -6
View File
@@ -1,18 +1,27 @@
# Run using bin/ci
require_relative "../lib/fizzy"
OSS_ENV = "SAAS=false BUNDLE_GEMFILE=Gemfile"
SAAS_ENV = "SAAS=true BUNDLE_GEMFILE=Gemfile.saas"
CI.run do
step "Setup", "bin/setup --skip-server"
step "Style: Ruby", "bin/rubocop"
step "Security: Gem audit", "bin/bundler-audit check --update"
step "Security: Gem audit", "bin/bundler-audit check --update"
step "Security: Importmap audit", "bin/importmap audit"
step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
step "Security: Gitleaks audit", "bin/gitleaks-audit"
step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
step "Security: Gitleaks audit", "bin/gitleaks-audit"
step "Tests: Rails: SaaS config", "bin/rails test"
step "Tests: Rails: OSS config", "OSS_CONFIG=1 bin/rails test"
step "Tests: System", "bin/rails test:system"
if Fizzy.saas?
step "Tests: SaaS", "#{SAAS_ENV} bin/rails test:all"
step "Tests: SQLite", "#{OSS_ENV} DATABASE_ADAPTER=sqlite bin/rails test:all"
else
step "Tests: MySQL", "#{OSS_ENV} DATABASE_ADAPTER=mysql bin/rails test:all"
step "Tests: SQLite", "#{OSS_ENV} DATABASE_ADAPTER=sqlite bin/rails test:all"
end
if success?
step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
+1 -1
View File
@@ -1 +1 @@
6Ciuxv75imDwiIXYs8lVxsDClQq+76RRr9gvVSxbDNpQhhuETHtMZZPiK2yfHtnFwwm/CrkpWhIRy+ZUt+9bQjnz3J3dMwPmsHdmKTnxUeiKuj+7k5QPKDHc0G1kfpxRANsB10WEcjt5IeoNcNxTrXYbja3NX2OhHYOFWMW/jLpGIXRPnBw0+JgQElKQI42vm1APix/9DRwqrM7TmgjXjJw4q4C7WeSgc4Y7+sHF5T2wJrtkdmExgD7X+9BTI7FOHkpwmQIYEWe34P1/BSFgFcy6tgwI/5U74QUcsj/dEgtKADukLQK9eMPLbfbJS4X7WUD1bDrOnmix5WQy0kdERq4NGSzdvumOhOY4LKYqS3otnPuTIZObbduLxPY9mgd8VFXfev8EjCrrVcVLVNUvtqTz+ri4fYAaHGTRQXttV2oOKtAik8JAggSrY3W5zZGWPEknDD704jyzi4ZRhJ2goMf+vvNZeH6pcQeWlLrIBxrfghGDrhDiHYFqaub2+2qeHhhkenOGtsttHiG4lpfcgmEa2Sr6ADdcxcmOi9qMt17LFmvplw2jR13aIUfglw4K2mzaikCUkDbgKRPqjKpbwkKbdJOaGFx69Oa4ZtiXX+ecKSgGhxhyIOyg7c8NtGeUNAZbjVEzdG8jqj+WBPLzozvpE+3y/dHgQ8MXegf5fsoov/2cpO2HK9tAepgjPyfAU7Rl5nzLrbGPsvgZ1/VRWr/Li6baZBVKrdZnWrjqLLfgqT28deVIHfwrlspifKq2wtm0eIyNGDs3Sm9wIYc1H2QMBe0zVly9rpQBnD8f901CzFQ4PN9ZIn1ZxdoNsdPdc/26mNHwa1UvVyPxgfLMBhlofRRpLNXLh1HOEzBYxuoHtJ3f86G0UwcxkhG7dTxbm8ujQ8RmYfHfgOEBJET/P7rILflXEtRsKPfkNjZqHIt0AeEjpnA5ZmSYZrpsjAQ8FnblgCj32/QfKBsW7MIvJ18w2AOap+z7v9RyJTB9Pq93KdR232xXg7U9sX/qUMI3j6+Gx0URrTFlWCDXpq8QYK+BN/vqUfe39rSxKh/qy+JCbk1B/Ftd52Cn+gsG/fHnVaKyCK04UZSrYllqg82gMrl6A39v8PAiD/x1VFxiZzSm/oi8rBAVI9elOzxXzSr0lDUr+w0iXmiJUzSzLkgtd6nf74cjEmitZQqRRgo9/YANJQkh2HiABHuevHsBgcebw+FOM4VO9y4lljY/Ex98echppRjZs4DgJYK2/R4uQBOMBTlxuqdwuSGodYSlmI5kW43bZGTTA5KJa7w1DRmpVhQfoReWg5EDxifXh2sUkFqTMOejOauQm5fjN9RxnXA1c7b7mxpvBgkepUEI+o+AcbzNlAXG/orcm9zlt2sC8YoDR1T01blYEhoAM6IwqXCXt6ODxKEgMGQ/S3EhDWQQ989lR2gmoMjjbkwxU75pT/SDWJi/A2kFBC+e952Al+iybFxaUM9VpNfPrLB2iNIOO5McQJCH/EjD8+/ZfhhSin5fAl0GFFZyyk0Shd0+TWkuJk0mtAf3ev1sohzGFieCcB5Shj4oNcqWTvPisb4DCoO2AOnEo3adNPA3zCDRvSWv+rwsXEzqW2w2Wav4cAV8VqyKj/6NYIbvwbjwD3ojnpB41705+OekhFubgTLLCnk3+tbNgaLlBcnUawuHW4akZezmUCUoOYoLRzGEQwxCGMG6Hn9lNDw46Uyv7VsggpaNVORTeYGX1Om25QcDM+OBoYQynD24gg==--UIlvvCt/ukIZVxPZ--en6RIr54eZ9nuOvN+prX4w==
JwcXSv6qlqJvbzfHX3v0TB1WvqgI8CEqVRuR4m760EUTJDe6k6NeX1b41P33Zr341GT/LSyxnlxU5psfx4TxvSZhnJRv4e/YuAr04hFIhWnKnjvJtkwDQRgaZH689O4vgGVnQIEPln/DGag=--Q4Xa+Y5602cPApVn--s3PqHeC1cQ/7Fg2dTJ1w4w==
+1 -1
View File
@@ -1 +1 @@
lbv40Nq+bDCBc6DGVp3Q+CBuKLAt26v1vkKhOuUBJ/hPr07N98H13tejmM+C1GHF8DUJsTTsmSLYdYgelng0GDo9qhSpC/k+thZ1+Ne5pmZh00sH58PwItUNPBtLluqlpolRfNrJGMK3kjprWyTCbznAfGV6BZoT7Hbv12ZIri2bKsuTvsPZiIYLLAe2a2CWrNWjs4RezZbGWzbJpIyj+RIj3GKhGNCWNFhq79HyeRChaZNxioMaAgWBfAYKvXvkR2MuKEaspJM3lXnpnyxqVKWEWz4orhTCvlqbZchESDLF7hAuIkzXCXEC2EHpeEbzD0F52K/mLzT+kmi8dI+eLVimPJn0PZslccsHNAnXXHwvLvLOPFwzPz9N0jjGihu6yZKE1vl5r1dzo9mVWVTnsI5Z/g5SAmOtY+DB+pzssQdXSLFQZ/l2JcWvgYA9wCQyQbyGLEeirGe/QeCQciTkE+Z/bj7b+20rBgsRQIyHxNdzWDsmi4onJ75lIz6PA+1X4AIkwnAqUVzuBDwB+VxdwbzRZZeURMLBVzs76Ta/qSemIpuZ782cyt6MRmeYuAT6RMccnG/zwdvPHtpJ6C8fqKyOu+XkU2a49Yx/D6l/uvVtwiOayHTCwn2QmGYDlnniCBDJCzp0y0Y1M4d3XxAdV61XEL6rISMuM0mjdRZL3PYTpR8mrrflnYA4kFrLUnA+hEmcOq8stHSL1RYe83PI4dj6l1/hzQ6up2cGNBk+to7FXpQOhkuYV2+9xu+4PnJnl261G1D/v8zoWV+0OIn4ySC1o/t8BkvoYvFzDJkDIGYwNV6tTdhgitlZ3ljohwTj2HIclr0pZVwptQDJpe/ZYbpxq13uN0Xi6QdAWHJcUWzt3jZnX7N07nY34NW7DhM/D5SSDMkZbjPIPBS4t+n/lShh--MofA9sfR6Z6UuGQ2--jtVAHhYAxdvdXOJ/vb06Ug==
vWcWTHpq+ngYMhjXPNuAdQoX0IWy18O+vXn8Ny//U81BzG0tZIw30hOJ2MYx9yqgG9TMo8skPDE8fYGjHjuNCmp0CAgay6tcJzDtue+8l7nosbVBhQDkdW4GGAs8zRzVevQFNVXiYggQBeY=--vOZV11N8QeP6HBLz--enCNPwzwui/5QILC4bceBg==
+1 -1
View File
@@ -1 +1 @@
z0tKJ9f/hCNXOi7b6lZlfzmFm8HAGNafiluMTcRlKmFxTm5ygs6fk0N0JNnJyrxsv6huye3/mptfrqkMOHRQcWzmZQyWDmayLxtmghRkpXmX92QZ8DiSs6jgWFX9zKuMPEW9eFpuLjstF1khPskg70SYVXBc3cTRJbyx5JGhnv1c3qOBfN4PKGYvTWsxacoQ356LYfu3haezmiS0yPzREo1rhFdwNyveV8uZ1QX1ZNZFp+WN5RXeiM632kL4R73KhwkbGBSHj5z9xxl0v4BzLrGM+Jf3VsJFmhYXS10mN+XOsNELCHUaM1wQOqMNHegBfupTzwur6Q3R154XP7rrBLquFT3wkAlkrI8ox+NRJgnGQ/yCc3PM8zhxgdrLzmr9s35eByKRCMsuIkF0WSfcBeWpCfNQaeucw10fuKirlI+G5JsX6HaRfAea3dUvVsuKCeutKn2/YPmw7Qodat2G9sZgUhBOYRzDcmzWaUyudciAMXRzNfoTZqngwFXDvfcAlmq4FrnucnfAsMB/TtqrZzV/td4nPfQLpxZCCl7v3hIggQUJl7gmdR335J6wJ4r8i0eTQjTw+j/Xm8wF1AEHpwxz2DOg8BjAI+/F8Lgqc+YDQCEYINYn+bwy5eMUv7H3aLDGrF3ZKnsEwWj62Q5LtHF/Fi7ZIOWgQyk4d5lLc4+T6hx4qe9iqu29LlduIxwJRG1dWPgNxuikPM2hcl6dQP8ebP9dNpdeeMCGylzR/kZjro7mAawxgIKEdOvWE6NJs8xGkjV2JjqWu7ufiIpk+jurUBlO5mIJuZhVvRumpMgcxVwqXWjJRt5BTVT/qcuLdaPcYY9hUt3TQrqjSGIt5bSFjcCFEfY15DxXsIVe84Oi4KYQNmuM4h2ub1zfYQYYH4UaGg7bLbb2OeQIIkkTwXXAR6o3BKJWtskOD3GTdamk3gKhudhm+BOgbOavoiE4hpvv/4ajjl8OGseGOAjC4bj8hpNnhezjQFRe2pK1515hi8iyYQ2tEScGy4wmsAbSaoA54+tHki0yeLUF8oJ9GTr9UCwnz8nUhQmfrS4VJO6Q/J4MXRCon+mAt3cyHaC6z3RISREw0a4aO3T2PGvUBVDEP0kfhDJmZXM5Of0RdFX27pwaQUPo3hqzQ+BmyenGqUonC1nPd/npojmRWXan9v5iMRKRx8qEBfhm+WQPEQuJqi5szm8pla6gUtNKaL6P4zhJOnIZiKxpYSlnezlMWNof/xcOb5BqYeEYQJ9FTvIxGrmN7OxQFlfk21z+n1OJI82TKIaKB/Oo+bsTtta8BuXONJUtsRSP9HcqMlMnwq2We4jn8JLFf/9O0s+iQtB6um7EAcHkE48jBHkMVzJVWB4/9Z8xe4DIwrg9BZH6UNL/pc/K5/dOeW8QoOPceBWmHP9BGoWFVqmNmEwPeNmnQZ5H/zxGPD94oG5YfiUMfMrOtIiAwS3Qqbmr+nAa5eNe4Ij7/i5nlV5gJ8y3NSNQvjico4iqBU1WAEHL6bHV--JcYezF+/1oGWkoz5--lepA3Pi+iuzJTgNRR4ax8Q==
ZZgOiLwd51WsQRpTwuCdPmDqmErel30sv5ZO+hOk7PrdqSgKuA7SCM3BaaM01gusiWCAq2760SmjTrrhaEBPkydR2dDERxPNcWe+zvqYklczYY8Qs4DqqLq3L8O89REO5GFvlbRulDQlO0w=--LC7VCBgZNnfp05RJ--svimv/2SjVLEJfVrBxC87Q==
+1 -1
View File
@@ -1 +1 @@
tTZW5futjROLhOOU4sULsj8f7nCQ73Z7Pmh/3HT2Y1hxhz7MxKOREq2ZjwEFPhL2/c/zim/C9PzgNql6MDjtUzYmd26Q+2HzFSS2JpFBqU7DnEgL6mWC7x5s9feTG61ilOhmlLIwZRgNvugwRAUTlCv2KZ8R5iaUviPcwf6fl4dOhM7pW2Fx8MhuJQBSbjkoqisyniIzdCvgsE2s4qIkxBz+qrbjVNhmyTvl/jdttjTcIYlubCOlELZdbT4zNsitcs/8xBTBlxsR4Vt9rBcwr+EPrhQfpvGVKnSleeiatc6EOi0kAniqbjwJFkcpJ95PrU3yhV8TP7RAzHpOIhYHSwDcrQS2ltP9z7fVP2CRicpWEIh7iFR79JtIsp1cYH6pbx08ZAnKKeu240SKkKA/wZH5BzyOrVaTHwQ5q18U8hzWon6loMiQMV9+/jRZtb8rOTHU4IpUi/oM7shUZoHxccBq68ZxpclLEDZs7CLNytXbRq9oaCqyg5tW5qW+Lws+4iRlmR43hTq2SHJ+eMjq4N8WxAdecDGbgPdNFaoY6Ymw9YEwsYiWzHx7fRC0EUYckbUPEccT6b8IuYTJg/QeQQ8HVenboM9U/9Cd6SODR4Vdyn6fGkFmc7mfPke01naCUEconH7RL0EB38yz/MMqW355PhsaZqsv7PlCE7KJIxkejp3gwiBwP8petq4OazxZY2MJahYLwvMQwFt35qu9RJVAMiAMN9DLvRQu7w+oNA0g3OSGOwnIzHtjKMRks3W4YNEgwMGrq3YzI11EmORB/1F3t+UDDN8GOxxGgBmIdVrFHwSazrw5igqv/kTFsrzxSHW1ja3/U27JVYOSGDBMZ5TlAH7CSFD6SgbDHreauN/O0zuesTH9QnkUARhOHw75dfTEvacjy6uIcO9GmrRConJGWzJdZj5joBROhCtoflcn9jfce3W6YJfzHgveUxPnRU1bhaNULGTW/lA+cclDmm1jL2AhpDYzk1hq6/I5we0fY9/VNMgFnm+c/qrkjrXChFNCT4w+EU+n7TaXJ5ocWdCMTyM2/RSnm1dBX8wGoygryOYEnHM+6GDeX64vfGSnWZ2yfpsKrAzn7zVFWAY6Ah2YEXm+pQdUPB4CYwvg6A1liaCvF71P2p9SZgX79YEwGvsODMzrBavxVMIlVa92jtOZ23yoOjfR8gWleqeyoAOCN2JaM4rJfPrzZrs5LKDfYtgpR4Jg36LjD1UKWp3Kab+kHOpxzkPOgq6v8px3Rk3xaGADwCPnE+4qSTFByYaHCdx6FNeQQit3jSEq4h6WNeVkrKnZ6ybCnd7LwBKiu9uQURB6jar7t4IqM6kYThN9J7q/oxp3HS5+TkNT+1Org13CuNL9iF3M6n+R/U/vJCCRUMbM9+O8cYHEt5MJ875qBD1NRxj4UbVCcv5rDXhAFq+GuJqJTLN2+NPWVJ2QhZk1bhmwxIsZJ5vlWTMUXoOZOxUJrLqPK+v8h2x6G8J8Cc0WpzsHAl1B8peN7TxXJgcOyaVkHbPr2CYao/z7mG7D+5SIHcqcbNUB09Nh4iMVbQa1SEpTnWPqum1EwXkmH8QjUcj+IMsXcNWTVkaAfAsme50fMS8XGLiOU9AGfV5QcryaJQ6w250ewQ==--IKMr2eXgh2p/VO+z--2sCRrfBLg9TEjHH93qFmcw==
eqnqN1H7D3SMoWZAS61s1U+Km/tbbbb+1ofw+B+7AlD+Y6aNLBUuIbm6qSZPZ2L4bEl49RG9H3tLKtaawGWbqCCePygCZMw2wMlCt0Y08hKR1IAesRsJdMLRJUUn6vvTywXIm6leO/OdQlU=--H1k6MN/bYQAb3Kre--9jfEjn3753Brzy/63b56SA==
+1 -1
View File
@@ -1 +1 @@
OCWdPtq1nxi6erX3KVmolRjjazqRbSQ102v9t/MUwHy2Af7CztQt+S0N2fGBNnGb0QTNGbzAlETRVUo7trc7+pjO8w7lyuHS/eKO84wuuPh7qRkrZfwxF2R0D2jmcaCa2S2Hfs8Ws7xFN2j9xCdjWJKd4khP4OygyLFK9wah9fWW+QvMl1evgnSA8Vtko1Eh+/bBQC4Lb3/tlsjeqfv/N229oPywD6uM6XfRsSS0/tuCp3w1MPhs2lb1ZfTwt4y5Dsk+r0YoUUr0CDPUAPSohmDJn5++8NiipIt7CQoGe72M4dp7WZ4eQAiprID8QIWAQ2I8RdvwbJdjFRKjcjgeB7vYiLUTRgmav8q/7QDk+JxWCU5oQxDmVex63CJ+sZ8NI94EnyppqHqihyLDzViwbBHkOmWj3WsgPOQHNjwhX+LwMeJ8UkWjAJlA7q59EqEDvPPN7MAgpunPuRXL6YXsS6UBpUmpof6fwMyhHbnFpRgTx0QyUFW3Ta7cs2JWCST2PXI5lGQKJBKIhcLB60bO1NtJYtX3xlheywnbNUzvsBTQO3j+ezPxcy07REvW1NfXQInpAgOudsZ4ScW0mblwiO5v/rLGJN5cMUNWAg==--8NVEpFCH347bJZoD--6fw4f/Fad/N8XQH6oTwpQg==
kd4RhBwzDS97pb8kht6DOzdg/NFgRQj0sAezzV7q7wiqk4pAMT2v5pZTJoq3pz0f8URB7nXK1KeHI6ceqN/9GKWMqpwyUyeM8A7LbCK5ffBiaJZH2YFMtKEnX0topl87sEDNlMawY0OT6ySi4KhLkrQMyEGqJx3XXmS1U4aGfF2P7i3GTGOjPpLtPntzSVT62cLU7/GSfDvXdqiW/WDRNvCpQJDqw+J1DdnlZtq+A6lo+B42o2clGHOVw09MY9INHuhfcscQfR035exSkRZ3wsvqDay3fez+D3xvDXOreyRrlCUpDfAxuXHPgavnYuPF73Xhg5Ov48B3EW9RLNHH5Nl57M/laxPvhkSa73IK6VPL9BSv9osW--MmZ9291ZbCvv0tqX--yLAI3tXJEW5rJoA/2KMk+Q==
+20
View File
@@ -0,0 +1,20 @@
default: &default
adapter: trilogy
host: <%= ENV.fetch("MYSQL_HOST", "127.0.0.1") %>
port: <%= ENV.fetch("MYSQL_PORT", "3306") %>
username: <%= ENV.fetch("MYSQL_USER", "root") %>
password: <%= ENV["MYSQL_PASSWORD"] %>
pool: 50
timeout: 5000
development:
<<: *default
database: fizzy_development
test:
<<: *default
database: fizzy_test
production:
<<: *default
database: fizzy_production
+22
View File
@@ -0,0 +1,22 @@
default: &default
adapter: sqlite3
pool: 5
timeout: 5000
development:
primary:
<<: *default
database: storage/development.sqlite3
schema_dump: schema_sqlite.rb
test:
primary:
<<: *default
database: storage/test.sqlite3
schema_dump: schema_sqlite.rb
production:
primary:
<<: *default
database: storage/production.sqlite3
schema_dump: schema_sqlite.rb
+6 -121
View File
@@ -1,126 +1,11 @@
<%
database_adapter = ENV.fetch("DATABASE_ADAPTER", "mysql")
use_sqlite = database_adapter == "sqlite"
require_relative "../lib/fizzy"
if ENV["MIGRATE"].present?
mysql_app_user_key = "MYSQL_ALTER_USER"
mysql_app_password_key = "MYSQL_ALTER_PASSWORD"
max_execution_time_ms = 0 # No limit
config_path = if Fizzy.saas?
gem_path = Gem::Specification.find_by_name("fizzy-saas").gem_dir
File.join(gem_path, "config", "database.yml")
else
mysql_app_user_key = "MYSQL_APP_USER"
mysql_app_password_key = "MYSQL_APP_PASSWORD"
max_execution_time_ms = 5_000
File.join(__dir__, "database.#{Fizzy.db_adapter}.yml")
end
mysql_app_user = ENV[mysql_app_user_key]
mysql_app_password = ENV[mysql_app_password_key]
%>
default: &default
<% if use_sqlite %>
adapter: sqlite3
pool: 5
timeout: 5000
<% else %>
adapter: trilogy
host: <%= ENV.fetch "FIZZY_DB_HOST", "127.0.0.1" %>
port: <%= ENV.fetch "FIZZY_DB_PORT", 3306 %>
pool: 50
timeout: 5000
variables:
transaction_isolation: READ-COMMITTED
max_execution_time: <%= max_execution_time_ms %>
<% end %>
development:
<% if use_sqlite %>
primary:
<<: *default
database: storage/development.sqlite3
schema_dump: schema_sqlite.rb
<% else %>
primary:
<<: *default
database: fizzy_development
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
replica:
<<: *default
database: fizzy_development
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
replica: true
cable:
<<: *default
database: development_cable
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
migrations_paths: db/cable_migrate
cache:
<<: *default
database: development_cache
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
migrations_paths: db/cache_migrate
queue:
<<: *default
database: development_queue
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
migrations_paths: db/queue_migrate
<% end %>
# 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:
<% if use_sqlite %>
primary:
<<: *default
database: storage/test.sqlite3
schema_dump: schema_sqlite.rb
<% else %>
primary:
<<: *default
database: fizzy_test
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
replica:
<<: *default
database: fizzy_test
port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %>
replica: true
<% end %>
production: &production
primary:
<<: *default
database: fizzy_production
host: <%= ENV["MYSQL_DATABASE_HOST"] %>
username: <%= mysql_app_user %>
password: <%= mysql_app_password %>
replica:
<<: *default
database: fizzy_production
host: <%= ENV["MYSQL_DATABASE_REPLICA_HOST"] %>
username: <%= ENV["MYSQL_READONLY_USER"] %>
password: <%= ENV["MYSQL_READONLY_PASSWORD"] %>
replica: true
cable:
<<: *default
database: fizzy_solidcable_production
host: <%= ENV["MYSQL_SOLID_CABLE_HOST"] %>
username: <%= mysql_app_user %>
password: <%= mysql_app_password %>
migrations_paths: db/cable_migrate
queue:
<<: *default
database: fizzy_solidqueue_production
host: <%= ENV["MYSQL_SOLID_QUEUE_HOST"] %>
username: <%= mysql_app_user %>
password: <%= mysql_app_password %>
migrations_paths: db/queue_migrate
cache:
<<: *default
database: fizzy_solidcache_production
host: <%= ENV["MYSQL_SOLID_CACHE_HOST"] %>
username: <%= mysql_app_user %>
password: <%= mysql_app_password %>
migrations_paths: db/cache_migrate
beta: *production
staging: *production
<%= ERB.new(File.read(config_path)).result %>
-53
View File
@@ -1,53 +0,0 @@
servers:
web:
hosts:
- fizzy-beta-app-01.sc-chi-int.37signals.com: sc_chi
- fizzy-beta-app-101.df-iad-int.37signals.com: df_iad
labels:
otel_scrape_enabled: true
# we don't run the jobs role in beta
allow_empty_roles: true
proxy:
ssl: false
ssh:
user: app
env:
clear:
RAILS_ENV: beta
MYSQL_DATABASE_HOST: fizzy-mysql-primary
MYSQL_DATABASE_REPLICA_HOST: fizzy-mysql-replica
MYSQL_SOLID_CABLE_HOST: fizzy-mysql-primary
MYSQL_SOLID_QUEUE_HOST: fizzy-mysql-primary
MYSQL_SOLID_CACHE_HOST: fizzy-beta-solidcache-db-101
secret:
- RAILS_MASTER_KEY
- MYSQL_ALTER_PASSWORD
- MYSQL_ALTER_USER
- MYSQL_APP_PASSWORD
- MYSQL_APP_USER
- MYSQL_READONLY_PASSWORD
- MYSQL_READONLY_USER
tags:
sc_chi: {}
df_iad:
PRIMARY_DATACENTER: true
df_ams: {}
accessories:
load-balancer:
image: basecamp/kamal-proxy:lb
host: fizzy-beta-lb-01.sc-chi-int.37signals.com
labels:
otel_role: load-balancer
otel_service: fizzy-load-balancer
otel_scrape_enabled: true
options:
publish:
- 80:80
- 443:443
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
-72
View File
@@ -1,72 +0,0 @@
servers:
web:
hosts:
- fizzy-app-01.sc-chi-int.37signals.com: sc_chi
- fizzy-app-02.sc-chi-int.37signals.com: sc_chi
- fizzy-app-101.df-iad-int.37signals.com: df_iad
- fizzy-app-102.df-iad-int.37signals.com: df_iad
- fizzy-app-401.df-ams-int.37signals.com: df_ams
- fizzy-app-402.df-ams-int.37signals.com: df_ams
labels:
otel_scrape_enabled: true
jobs:
hosts:
- fizzy-jobs-101.df-iad-int.37signals.com: df_iad
- fizzy-jobs-102.df-iad-int.37signals.com: df_iad
labels:
otel_scrape_enabled: true
proxy:
ssl: false
ssh:
user: app
env:
clear:
RAILS_ENV: production
MYSQL_DATABASE_HOST: fizzy-mysql-primary
MYSQL_DATABASE_REPLICA_HOST: fizzy-mysql-replica
MYSQL_SOLID_CABLE_HOST: fizzy-mysql-primary
MYSQL_SOLID_QUEUE_HOST: fizzy-mysql-primary
secret:
- RAILS_MASTER_KEY
- MYSQL_ALTER_PASSWORD
- MYSQL_ALTER_USER
- MYSQL_APP_PASSWORD
- MYSQL_APP_USER
- MYSQL_READONLY_PASSWORD
- MYSQL_READONLY_USER
tags:
sc_chi:
MYSQL_SOLID_CACHE_HOST: fizzy-solidcache-db-01.sc-chi-int.37signals.com
df_iad:
MYSQL_SOLID_CACHE_HOST: fizzy-solidcache-db-101.df-iad-int.37signals.com
PRIMARY_DATACENTER: true
df_ams:
MYSQL_SOLID_CACHE_HOST: fizzy-solidcache-db-401.df-ams-int.37signals.com
accessories:
load-balancer:
image: basecamp/kamal-proxy:lb
hosts:
- fizzy-lb-101.df-iad-int.37signals.com
- fizzy-lb-102.df-iad-int.37signals.com
- fizzy-lb-01.sc-chi-int.37signals.com
- fizzy-lb-02.sc-chi-int.37signals.com
- fizzy-lb-401.df-ams-int.37signals.com
- fizzy-lb-402.df-ams-int.37signals.com
labels:
otel_role: load-balancer
otel_service: fizzy-load-balancer
otel_scrape_enabled: true
options:
publish:
- 80:80
- 443:443
# NFS mount for certificates
# See https://3.basecamp.com/2914079/buckets/37331921/todos/9180260061
mount: type=volume,src=certificates,dst=/certificates,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/fizzy-production-certificates,"volume-opt=o=addr=purestorage.sc-chi-int.37signals.com,nfsvers=3,rw,noatime,nconnect=8,soft,timeo=30,retrans=2"
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
-69
View File
@@ -1,69 +0,0 @@
servers:
web:
hosts:
- fizzy-staging-app-101.df-iad-int.37signals.com: df_iad
- fizzy-staging-app-102.df-iad-int.37signals.com: df_iad
- fizzy-staging-app-01.sc-chi-int.37signals.com: sc_chi
- fizzy-staging-app-02.sc-chi-int.37signals.com: sc_chi
- fizzy-staging-app-401.df-ams-int.37signals.com: df_ams
- fizzy-staging-app-402.df-ams-int.37signals.com: df_ams
labels:
otel_scrape_enabled: true
jobs:
hosts:
- fizzy-staging-jobs-101.df-iad-int.37signals.com: df_iad
- fizzy-staging-jobs-102.df-iad-int.37signals.com: df_iad
labels:
otel_scrape_enabled: true
proxy:
ssl: false
ssh:
user: app
env:
clear:
RAILS_ENV: staging
MYSQL_DATABASE_HOST: fizzy-staging-mysql-primary
MYSQL_DATABASE_REPLICA_HOST: fizzy-staging-mysql-replica
MYSQL_SOLID_CABLE_HOST: fizzy-staging-mysql-primary
MYSQL_SOLID_QUEUE_HOST: fizzy-staging-mysql-primary
secret:
- RAILS_MASTER_KEY
- MYSQL_ALTER_PASSWORD
- MYSQL_ALTER_USER
- MYSQL_APP_PASSWORD
- MYSQL_APP_USER
- MYSQL_READONLY_PASSWORD
- MYSQL_READONLY_USER
tags:
sc_chi:
MYSQL_SOLID_CACHE_HOST: fizzy-staging-solidcache-db-01.sc-chi-int.37signals.com
df_iad:
MYSQL_SOLID_CACHE_HOST: fizzy-staging-solidcache-db-101.df-iad-int.37signals.com
PRIMARY_DATACENTER: true
df_ams:
MYSQL_SOLID_CACHE_HOST: fizzy-staging-solidcache-db-401.df-ams-int.37signals.com
accessories:
load-balancer:
image: basecamp/kamal-proxy:lb
hosts:
- fizzy-staging-lb-01.sc-chi-int.37signals.com
- fizzy-staging-lb-101.df-iad-int.37signals.com
- fizzy-staging-lb-401.df-ams-int.37signals.com
labels:
otel_role: load-balancer
otel_service: fizzy-load-balancer
otel_scrape_enabled: true
options:
publish:
- 80:80
- 443:443
# NFS mount for certificates
# See https://3.basecamp.com/2914079/buckets/37331921/todos/9180260061
mount: type=volume,src=certificates,dst=/certificates,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/fizzy-staging-certificates,"volume-opt=o=addr=purestorage.sc-chi-int.37signals.com,nfsvers=3,rw,noatime,nconnect=8,soft,timeo=30,retrans=2"
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
+107 -21
View File
@@ -1,34 +1,120 @@
# Name of your application. Used to uniquely configure containers.
service: fizzy
image: basecamp/fizzy
asset_path: /rails/public/assets
# Name of the container image (use your-user/app-name on external registries).
image: fizzy
# Deploy to these servers.
servers:
jobs:
cmd: bin/jobs
web:
- 192.168.0.1
# job:
# hosts:
# - 192.168.0.1
# cmd: bin/jobs
volumes:
- fizzy:/rails/storage
proxy:
ssl: true
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
#
# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb!
#
# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer).
#
# proxy:
# ssl: true
# host: app.example.com
# Where you keep your container images.
registry:
server: registry.37signals.com
username: robot$harbor-bot
password:
- BASECAMP_REGISTRY_PASSWORD
# Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
server: localhost:5555
builder:
arch: amd64
secrets:
- GITHUB_TOKEN
remote: ssh://app@docker-builder-102
local: <%= ENV.fetch("KAMAL_BUILDER_LOCAL", "true") %>
# Needed for authenticated registries.
# username: your-user
# Always use an access token rather than real password when possible.
# password:
# - KAMAL_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
secret:
- RAILS_MASTER_KEY
clear:
# Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
# When you start using multiple servers, you should split out job processing to a dedicated machine.
SOLID_QUEUE_IN_PUMA: true
# Set number of processes dedicated to Solid Queue (default: 1)
# JOB_CONCURRENCY: 3
# Set number of cores available to the application on each server (default: 1).
# WEB_CONCURRENCY: 2
# Match this to any external database server to configure Active Record correctly
# Use fizzy-db for a db accessory server on same machine via local kamal docker network.
# DB_HOST: 192.168.0.2
# Log everything from Rails
# RAILS_LOG_LEVEL: debug
# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
console: app exec -i --reuse "bin/rails console"
ssh: app exec -i --reuse /bin/bash
console: app exec --interactive --reuse "bin/rails console"
shell: app exec --interactive --reuse "bash"
logs: app logs -f
dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"
# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
- "fizzy_storage:/rails/storage"
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets
# Configure the image builder.
builder:
arch: amd64
# # Build image via remote server (useful for faster amd64 builds on arm64 computers)
# remote: ssh://docker@docker-builder-server
#
# # Pass arguments and secrets to the Docker build process
# args:
# RUBY_VERSION: ruby-3.4.7
# secrets:
# - GITHUB_TOKEN
# - RAILS_MASTER_KEY
# Use a different ssh user than root
# ssh:
# user: app
# Use accessory services (secrets come from .kamal/secrets).
# accessories:
# db:
# image: mysql:8.0
# host: 192.168.0.2
# # Change to 3306 to expose port to the world instead of just local network.
# port: "127.0.0.1:3306:3306"
# env:
# clear:
# MYSQL_ROOT_HOST: '%'
# secret:
# - MYSQL_ROOT_PASSWORD
# files:
# - config/mysql/production.cnf:/etc/mysql/my.cnf
# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
# directories:
# - data:/var/lib/mysql
# redis:
# image: valkey/valkey:8
# host: 192.168.0.2
# port: 6379
# directories:
# - data:/data
-4
View File
@@ -93,8 +93,4 @@ Rails.application.configure do
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue, reading: :queue } }
end
if Rails.root.join("tmp/structured-logging.txt").exist?
config.structured_logging.logger = ActiveSupport::Logger.new("log/structured-development.log")
end
end
-6
View File
@@ -32,9 +32,6 @@ Rails.application.configure do
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
# config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :purestorage
# Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil
# config.action_cable.url = "wss://example.com/cable"
@@ -55,9 +52,6 @@ Rails.application.configure do
# Suppress unstructured log lines
config.log_level = :fatal
# Structured JSON logging
config.structured_logging.logger = ActiveSupport::Logger.new(STDOUT)
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
-2
View File
@@ -1,2 +0,0 @@
require "bootstrap"
Rails.application.config.x.oss_config = Bootstrap.oss_config?
+4 -14
View File
@@ -7,18 +7,8 @@ Autotuner.enabled = true
# or somewhere else!
Autotuner.reporter = proc do |report|
Rails.logger.info "GCAUTOTUNE: #{report}"
Sentry.capture_message "Autotuner suggestion", level: :info, extra: { report: report.to_s }
end
# # This (optional) callback is called to provide metrics that can give you
# # insights about the performance of your app. It's recommended to send this
# # data to your observability service (e.g. Datadog, Prometheus, New Relic, etc).
# # Use a metric type that would allow you to calculate the average and percentiles.
# # On Datadog this would be the distribution type. On Prometheus this would be
# # the histogram type.
# Autotuner.metrics_reporter = proc do |metrics|
# # stats is a hash of metric name (string) to integer value.
# metrics.each do |key, val|
# StatsD.gauge(key, val)
# end
# end
if Fizzy.saas?
Sentry.capture_message "Autotuner suggestion", level: :info, extra: { report: report.to_s }
end
end
+1 -3
View File
@@ -1,5 +1,3 @@
Rails.application.config.before_initialize do
# We don't want normal tenanted authentication on mission control.
# Note that we're using HTTP basic auth configured via credentials.
MissionControl::Jobs.base_controller_class = "ActionController::Base"
MissionControl::Jobs.base_controller_class = "AdminController"
end
-9
View File
@@ -1,9 +0,0 @@
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 ]
config.send_default_pii = false
config.release = ENV["GIT_REVISION"]
config.excluded_exceptions += [ "ActiveRecord::ConcurrentMigrationError" ]
end
end
-5
View File
@@ -1,5 +0,0 @@
SolidQueue.on_start do
Process.warmup
Yabeda::Prometheus::Exporter.start_metrics_server!
end
-7
View File
@@ -1,7 +0,0 @@
ActiveSupport.on_load(:action_controller_base) do
before_action do
if Current.account.present?
logger.try(:struct, account: { queenbee_id: Current.account.external_account_id })
end
end
end
+2 -2
View File
@@ -1,4 +1,4 @@
Rails.application.configure do
config.x.vapid.private_key = ENV.fetch("VAPID_PRIVATE_KEY", Rails.application.credentials.dig(:vapid, :private_key))
config.x.vapid.public_key = ENV.fetch("VAPID_PUBLIC_KEY", Rails.application.credentials.dig(:vapid, :public_key))
config.x.vapid.private_key = ENV["VAPID_PRIVATE_KEY"]
config.x.vapid.public_key = ENV["VAPID_PUBLIC_KEY"]
end
-15
View File
@@ -1,15 +0,0 @@
require "prometheus/client/support/puma"
Prometheus::Client.configuration.logger = Rails.logger
Prometheus::Client.configuration.pid_provider = Prometheus::Client::Support::Puma.method(:worker_pid_provider)
Yabeda::Rails.config.controller_name_case = :camel
Yabeda::ActiveJob.install!
require "yabeda/solid_queue"
Yabeda::SolidQueue.install!
Yabeda::ActionCable.configure do |config|
# Fizzy relies primarily on Turbo::StreamsChannel
config.channel_class_name = "ActionCable::Channel::Base"
end
+7 -5
View File
@@ -10,12 +10,14 @@ plugin :tmp_restart
# Run Solid Queue with Puma
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
# Expose Prometheus metrics at http://0.0.0.0:9394/metrics.
# Expose Prometheus metrics at http://0.0.0.0:9394/metrics (SaaS only).
# In dev, overridden to http://127.0.0.1:9306/metrics in .mise.toml.
control_uri = Rails.env.local? ? "unix://tmp/pumactl.sock" : "auto"
activate_control_app control_uri, no_token: true
plugin :yabeda
plugin :yabeda_prometheus
if Fizzy.saas?
control_uri = Rails.env.local? ? "unix://tmp/pumactl.sock" : "auto"
activate_control_app control_uri, no_token: true
plugin :yabeda
plugin :yabeda_prometheus
end
if !Rails.env.local?
# Because we expect fewer I/O waits than Rails apps that connect to the
+4
View File
@@ -1,3 +1,5 @@
<% require_relative "../lib/fizzy" %>
production: &production
# Application functionality: notifications and summaries
deliver_bundled_notifications:
@@ -26,10 +28,12 @@ production: &production
command: "MagicLink.cleanup"
schedule: every 4 hours
<% if Fizzy.saas? %>
# Metrics
yabeda_actioncable:
command: "Yabeda::ActionCable.measure"
schedule: every 60 seconds
<% end %>
beta: *production
staging: *production
+6 -4
View File
@@ -155,6 +155,12 @@ Rails.application.routes.draw do
end
end
get "/signup/new", to: redirect("/session/new")
namespace :signup do
resource :completion, only: %i[ new create ]
end
resource :landing
namespace :my do
@@ -224,10 +230,6 @@ Rails.application.routes.draw do
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
get "service-worker" => "pwa#service_worker"
unless Rails.application.config.x.oss_config
mount Fizzy::Saas::Engine, at: "/", as: "saas"
end
namespace :admin do
mount MissionControl::Jobs::Engine, at: "/jobs"
get "stats", to: "stats#show"
+18
View File
@@ -0,0 +1,18 @@
test:
service: Disk
root: <%= Rails.root.join("tmp/storage/files") %>
local:
service: Disk
root: <%= Rails.root.join("storage", Rails.env, "files") %>
devminio:
service: S3
bucket: fizzy-dev-activestorage
endpoint: "http://minio.localhost:39000"
force_path_style: true
request_checksum_calculation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support
response_checksum_validation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support
region: us-east-1 # default region required for signer
access_key_id: minioadmin
secret_access_key: minioadmin
+10 -32
View File
@@ -1,33 +1,11 @@
test:
service: Disk
root: <%= Rails.root.join("tmp/storage/files") %>
<%
require_relative "../lib/fizzy"
local:
service: Disk
root: <%= Rails.root.join("storage", Rails.env, "files") %>
devminio:
service: S3
bucket: fizzy-dev-activestorage
endpoint: "http://minio.localhost:39000"
force_path_style: true
request_checksum_calculation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support
response_checksum_validation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support
region: us-east-1 # default region required for signer
access_key_id: minioadmin
secret_access_key: minioadmin
# We have "development", "staging", and "production" buckets configured. Note that we don't have a
# "beta" bucket. (As of 2025-06-01.)
<% pure_env = Rails.env.beta? ? "production" : Rails.env %>
purestorage:
service: S3
bucket: fizzy-<%= pure_env %>-activestorage
endpoint: "https://storage.basecamp.com"
ssl_verify_peer: false # FIXME: using self-signed cert internally
force_path_style: true
request_checksum_calculation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support
response_checksum_validation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support
region: us-east-1 # default region required for signer
access_key_id: <%= Rails.application.credentials.dig(:active_storage, :purestorage_service, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:active_storage, :purestorage_service, :secret_access_key) %>
config_path = if Fizzy.saas?
gem_path = Gem::Specification.find_by_name("fizzy-saas").gem_dir
File.join(gem_path, "config", "storage.yml")
else
File.join(__dir__, "storage.oss.yml")
end
%>
<%= ERB.new(File.read(config_path)).result %>
@@ -0,0 +1,9 @@
class CreateAccountExternalIdSequences < ActiveRecord::Migration[8.0]
def change
create_table :account_external_id_sequences, id: :uuid do |t|
t.bigint :value, null: false, default: 0
t.index :value, unique: true
end
end
end
Generated
+6 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2025_11_25_130010) do
ActiveRecord::Schema[8.2].define(version: 2025_11_27_000001) do
create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.datetime "accessed_at"
t.uuid "account_id", null: false
@@ -25,6 +25,11 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_25_130010) do
t.index ["user_id"], name: "index_accesses_on_user_id"
end
create_table "account_external_id_sequences", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "value", default: 0, null: false
t.index ["value"], name: "index_account_external_id_sequences_on_value", unique: true
end
create_table "account_join_codes", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.string "code", null: false
+6 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2025_11_25_130010) do
ActiveRecord::Schema[8.2].define(version: 2025_11_27_000001) do
create_table "accesses", id: :uuid, force: :cascade do |t|
t.datetime "accessed_at"
t.uuid "account_id", null: false
@@ -25,6 +25,11 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_25_130010) do
t.index ["user_id"], name: "index_accesses_on_user_id"
end
create_table "account_external_id_sequences", id: :uuid, force: :cascade do |t|
t.bigint "value", default: 0, null: false
t.index ["value"], name: "index_account_external_id_sequences_on_value", unique: true
end
create_table "account_join_codes", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.string "code", limit: 255, null: false
-12
View File
@@ -1,12 +0,0 @@
version: 2
updates:
- package-ecosystem: bundler
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
-69
View File
@@ -1,69 +0,0 @@
name: CI
on:
pull_request:
push:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
env:
RUBY_VERSION: ruby-3.4.5
RUBOCOP_CACHE_ROOT: tmp/rubocop
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ env.RUBY_VERSION }}
bundler-cache: true
- name: Prepare RuboCop cache
uses: actions/cache@v4
env:
DEPENDENCIES_HASH: ${{ hashFiles('**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }}
with:
path: ${{ env.RUBOCOP_CACHE_ROOT }}
key: rubocop-${{ runner.os }}-${{ env.RUBY_VERSION }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }}
restore-keys: |
rubocop-${{ runner.os }}-${{ env.RUBY_VERSION }}-${{ env.DEPENDENCIES_HASH }}-
- name: Lint code for consistent style
run: bin/rubocop -f github
test:
runs-on: ubuntu-latest
# services:
# redis:
# image: valkey/valkey:8
# ports:
# - 6379:6379
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ruby-3.4.5
bundler-cache: true
- name: Run tests
env:
RAILS_ENV: test
# RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
# REDIS_URL: redis://localhost:6379/0
run: bin/rails db:test:prepare test
- name: Keep screenshots from failed system tests
uses: actions/upload-artifact@v4
if: failure()
with:
name: screenshots
path: ${{ github.workspace }}/tmp/screenshots
if-no-files-found: ignore
-10
View File
@@ -1,10 +0,0 @@
/.bundle/
/doc/
/log/*.log
/pkg/
/tmp/
/test/dummy/db/*.sqlite3
/test/dummy/db/*.sqlite3-*
/test/dummy/log/*.log
/test/dummy/storage/
/test/dummy/tmp/
-8
View File
@@ -1,8 +0,0 @@
# Omakase Ruby styling for Rails
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
# Overwrite or add rules to create your own house style
#
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
# Layout/SpaceInsideArrayLiteralBrackets:
# Enabled: false
-6
View File
@@ -1,6 +0,0 @@
source "https://rubygems.org"
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
# 37id and Queenbee integration
gem "queenbee", bc: "queenbee-plugin", ref: "eb01c697de1ad028afc65cc7d9b5345a7a8e849f"
gem "activeresource", require: "active_resource" # needed by queenbee
-69
View File
@@ -1,69 +0,0 @@
GIT
remote: https://github.com/basecamp/queenbee-plugin
revision: eb01c697de1ad028afc65cc7d9b5345a7a8e849f
ref: eb01c697de1ad028afc65cc7d9b5345a7a8e849f
specs:
queenbee (3.2.0)
activeresource
builder
rexml
GEM
remote: https://rubygems.org/
specs:
activemodel (8.0.2.1)
activesupport (= 8.0.2.1)
activemodel-serializers-xml (1.0.3)
activemodel (>= 5.0.0.a)
activesupport (>= 5.0.0.a)
builder (~> 3.1)
activeresource (6.1.4)
activemodel (>= 6.0)
activemodel-serializers-xml (~> 1.0)
activesupport (>= 6.0)
activesupport (8.0.2.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
base64 (0.3.0)
benchmark (0.4.1)
bigdecimal (3.2.3)
builder (3.3.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
drb (2.2.3)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
logger (1.7.0)
minitest (5.25.5)
rexml (3.4.4)
securerandom (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uri (1.0.3)
PLATFORMS
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
activeresource
queenbee!
BUNDLED WITH
2.7.0
-28
View File
@@ -1,28 +0,0 @@
# Fizzy::Saas
Short description and motivation.
## Usage
How to use my plugin.
## Installation
Add this line to your application's Gemfile:
```ruby
gem "fizzy-saas"
```
And then execute:
```bash
$ bundle
```
Or install it yourself as:
```bash
$ gem install fizzy-saas
```
## Contributing
Contribution directions go here.
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
-8
View File
@@ -1,8 +0,0 @@
require "bundler/setup"
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
load "rails/tasks/engine.rake"
load "rails/tasks/statistics.rake"
require "bundler/gem_tasks"
@@ -1,15 +0,0 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/
@@ -1,13 +0,0 @@
class Subscription < Queenbee::Subscription
SHORT_NAMES = %w[ FreeV1 ]
def self.short_name
name.demodulize
end
class FreeV1 < Subscription
property :proper_name, "Free Subscription"
property :price, 0
property :frequency, "yearly"
end
end
@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Fizzy saas</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= yield :head %>
<%= stylesheet_link_tag "fizzy/saas/application", media: "all" %>
</head>
<body>
<%= yield %>
</body>
</html>
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails gems
# installed from the root of your application.
ENGINE_ROOT = File.expand_path("..", __dir__)
ENGINE_PATH = File.expand_path("../lib/fizzy/saas/engine", __dir__)
APP_PATH = File.expand_path("../test/dummy/config/application", __dir__)
# Set up gems listed in the Gemfile.
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
require "rails/all"
require "rails/engine/commands"
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"
# explicit rubocop config increases performance slightly while avoiding config confusion.
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
load Gem.bin_path("rubocop", "rubocop")
-9
View File
@@ -1,9 +0,0 @@
Fizzy::Saas::Engine.routes.draw do
get "/signup/new", to: redirect("/session/new")
namespace :signup do
resource :completion, only: %i[ new create ]
end
Queenbee.routes(self)
end
-27
View File
@@ -1,27 +0,0 @@
require_relative "lib/fizzy/saas/version"
Gem::Specification.new do |spec|
spec.name = "fizzy-saas"
spec.version = Fizzy::Saas::VERSION
spec.authors = [ "Mike Dalessio" ]
spec.email = [ "mike@37signals.com" ]
spec.homepage = "TODO"
spec.summary = "TODO: Summary of Fizzy::Saas."
spec.description = "TODO: Description of Fizzy::Saas."
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host"
# to allow pushing to a single host or delete this section to allow pushing to any host.
spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
spec.files = Dir.chdir(File.expand_path(__dir__)) do
Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
end
spec.add_dependency "rails", ">= 8.1.0.beta1"
spec.add_dependency "queenbee"
spec.add_dependency "rails_structured_logging"
end
-7
View File
@@ -1,7 +0,0 @@
require "fizzy/saas/version"
require "fizzy/saas/engine"
module Fizzy
module Saas
end
end
-31
View File
@@ -1,31 +0,0 @@
require_relative "metrics"
require_relative "transaction_pinning"
module Fizzy
module Saas
class Engine < ::Rails::Engine
# moved from config/initializers/queenbee.rb
Queenbee.host_app = Fizzy
initializer "fizzy_saas.transaction_pinning" do |app|
if ActiveRecord::Base.replica_configured?
app.config.middleware.insert_after(
ActiveRecord::Middleware::DatabaseSelector,
TransactionPinning::Middleware
)
end
end
config.to_prepare do
Queenbee::Subscription.short_names = Subscription::SHORT_NAMES
Queenbee::ApiToken.token = Rails.application.credentials.dig(:queenbee_api_token)
Subscription::SHORT_NAMES.each do |short_name|
const_name = "#{short_name}Subscription"
::Object.send(:remove_const, const_name) if ::Object.const_defined?(const_name)
::Object.const_set const_name, Subscription.const_get(short_name, false)
end
end
end
end
end
-13
View File
@@ -1,13 +0,0 @@
Yabeda.configure do
SHORT_HISTOGRAM_BUCKETS = [ 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5 ]
group :fizzy do
counter :replica_stale,
comment: "Number of requests served from a stale replica"
histogram :replica_wait,
unit: :seconds,
comment: "Time spent waiting for replica to catch up with transaction",
buckets: SHORT_HISTOGRAM_BUCKETS
end
end
@@ -1,65 +0,0 @@
module TransactionPinning
class Middleware
SESSION_KEY = :last_txn
DEFAULT_MAX_WAIT = 0.25
def initialize(app)
@app = app
@timeout = Rails.application.config.x.transaction_pinning&.timeout&.to_f || DEFAULT_MAX_WAIT
end
def call(env)
request = ActionDispatch::Request.new(env)
replica_metrics = {}
if ApplicationRecord.current_role == :reading
wait_for_replica_catchup(request, replica_metrics)
end
status, headers, body = @app.call(env)
headers.merge!(replica_metrics.transform_values(&:to_s))
if ApplicationRecord.current_role == :writing
capture_transaction_id(request)
end
[ status, headers, body ]
end
private
def wait_for_replica_catchup(request, replica_metrics)
if last_txn = request.session[SESSION_KEY].presence
has_transaction = tracking_replica_wait_time(replica_metrics) do
replica_has_transaction(last_txn)
end
unless has_transaction
Yabeda.fizzy.replica_stale.increment
replica_metrics["X-Replica-Stale"] = true
end
end
end
def capture_transaction_id(request)
request.session[SESSION_KEY] = ApplicationRecord.connection.show_variable("global.gtid_executed")
end
def replica_has_transaction(txn)
sql = ApplicationRecord.sanitize_sql_array([ "SELECT WAIT_FOR_EXECUTED_GTID_SET(?, ?)", txn, @timeout ])
ApplicationRecord.connection.select_value(sql) == 0
rescue => e
Sentry.capture_exception(e, extra: { gtid: txn })
true # Treat as if we're up to date, since we don't know
end
def tracking_replica_wait_time(replica_metrics)
started_at = Time.current
Yabeda.fizzy.replica_wait.measure do
yield
end.tap do
replica_metrics["X-Replica-Wait"] = Time.current - started_at
end
end
end
end
@@ -1,5 +0,0 @@
module Fizzy
module Saas
VERSION = "0.1.0"
end
end
@@ -1,4 +0,0 @@
# desc "Explaining what the task does"
# task :fizzy_saas do
# # Task goes here
# end
View File
View File

Some files were not shown because too many files have changed in this diff Show More