Merge pull request #1697 from basecamp/open-source-split
Open source / SaaS split
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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 && \
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,13 +24,11 @@
|
||||
<p class="margin-none-block-start">You don’t 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 %>
|
||||
|
||||
|
||||
+2
-2
@@ -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>You’re 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? %>
|
||||
@@ -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")
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 @@
|
||||
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 @@
|
||||
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 @@
|
||||
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 @@
|
||||
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 @@
|
||||
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==
|
||||
@@ -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
|
||||
@@ -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
@@ -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 %>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
require "bootstrap"
|
||||
Rails.application.config.x.oss_config = Bootstrap.oss_config?
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
SolidQueue.on_start do
|
||||
Process.warmup
|
||||
|
||||
Yabeda::Prometheus::Exporter.start_metrics_server!
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,7 +0,0 @@
|
||||
require "fizzy/saas/version"
|
||||
require "fizzy/saas/engine"
|
||||
|
||||
module Fizzy
|
||||
module Saas
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user