Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
37 changes: 37 additions & 0 deletions .github/mergify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
pull_request_rules:
- name: Approve and merge dependabot PRs when all checks pass
conditions:
- and:
- author=dependabot[bot]
- label!=wontfix
actions:
review:
type: APPROVE
message: Automatically approving dependabot
merge:
method: merge
- name: Merge when all builds pass and the PR has been approved
conditions:
- and:
- "#approved-reviews-by>=1"
- "#changes-requested-reviews-by=0"
actions:
merge:
method: merge
- name: Ask for reviews
conditions:
- -closed
- -draft
- -author=dependabot[bot]
actions:
request_reviews:
teams:
- devs
- name: Assign the PR to its author
conditions:
- author!=dependabot[bot]
actions:
assign:
users:
- "{{author}}"
16 changes: 8 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@675dd7ba1b06c8786a1480d89c384f5620a42647
with:
ruby-version: .ruby-version
bundler-cache: true
Expand All @@ -27,10 +27,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@675dd7ba1b06c8786a1480d89c384f5620a42647
with:
ruby-version: .ruby-version
bundler-cache: true
Expand All @@ -51,10 +51,10 @@ jobs:
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg

- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@675dd7ba1b06c8786a1480d89c384f5620a42647
env:
REDIS_URL: redis://localhost:6379/0
with:
Expand All @@ -77,10 +77,10 @@ jobs:
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg

- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@675dd7ba1b06c8786a1480d89c384f5620a42647
env:
REDIS_URL: redis://localhost:6379/0
with:
Expand Down
57 changes: 57 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: Deploy

on:
workflow_dispatch:
workflow_run:
workflows: ["Build and publish container image to GHCR"]
types: [completed]
branches: [main]

concurrency:
group: deploy-production
cancel-in-progress: false

jobs:
deploy:
name: Deploy to production
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
environment:
name: production
timeout-minutes: 30
permissions:
packages: read

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Setup Ruby
uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
with:
ruby-version: "3.4"

- name: Install Kamal
run: gem install kamal

- name: Setup SSH agent
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd
with:
ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}

- name: Add server to known hosts
run: ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}

- name: Deploy with Kamal
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_DOMAIN: ${{ secrets.DEPLOY_DOMAIN }}
DEPLOY_USER: ${{ secrets.DEPLOY_USERNAME }}
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
SSL_CERTIFICATE_PEM: ${{ secrets.SSL_CERTIFICATE_PEM }}
SSL_PRIVATE_KEY_PEM: ${{ secrets.SSL_PRIVATE_KEY_PEM }}
run: kamal deploy --skip-push
25 changes: 13 additions & 12 deletions .github/workflows/publish-image.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
name: Build and publish container image to GHCR

on:
Expand Down Expand Up @@ -43,13 +44,13 @@ jobs:
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f

- name: Log in to GHCR
uses: docker/login-action@v3.5.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand All @@ -66,7 +67,7 @@ jobs:

- name: Extract Docker metadata (tags, labels) with arch suffix
id: meta
uses: docker/metadata-action@v5.8.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
with:
images: ${{ steps.vars.outputs.canonical }}
tags: |
Expand All @@ -81,10 +82,11 @@ jobs:
suffix=-${{ matrix.arch }}
labels: |
org.opencontainers.image.source=${{ env.SOURCE_URL }}
service=campfire

- name: Build and push (${{ matrix.platform }})
id: build
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: .
file: Dockerfile
Expand All @@ -102,7 +104,7 @@ jobs:

- name: Attest image provenance (per-arch)
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v3.0.0
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
with:
subject-name: ${{ steps.vars.outputs.canonical }}
subject-digest: ${{ steps.build.outputs.digest }}
Expand All @@ -119,10 +121,10 @@ jobs:
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Set up Docker Buildx (for imagetools)
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f

- name: Log in to GHCR
uses: docker/login-action@v3.5.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand All @@ -139,7 +141,7 @@ jobs:

- name: Compute base tags (no suffix)
id: meta
uses: docker/metadata-action@v5.8.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ${{ steps.vars.outputs.canonical }}
tags: |
Expand All @@ -154,6 +156,7 @@ jobs:
latest=false
labels: |
org.opencontainers.image.source=${{ env.SOURCE_URL }}
service=campfire

- name: Create multi-arch manifests
shell: bash
Expand Down Expand Up @@ -185,7 +188,7 @@ jobs:
done <<< "$tags"

- name: Install Cosign
uses: sigstore/cosign-installer@v3.9.2
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad

- name: Cosign sign all tags (keyless OIDC)
shell: bash
Expand All @@ -198,5 +201,3 @@ jobs:
echo "Signing $tag"
cosign sign --yes "$tag"
done <<< "$tags"


3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
# Ignore production envs
/.env

# Ignore Kamal secrets
/.kamal/secrets

# Ignore dartsass builds
app/assets/builds/*

Expand Down
77 changes: 77 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# CLAUDE.md

This file provides guidance to AI Agents when working with code in this repository.

## Project Overview

Campfire is a Rails 8 web-based chat application (fork of Once Campfire). Single-tenant deployment with rooms, direct messages, file attachments, search, Web Push notifications, @mentions, and bot API integrations.

## Development Commands

```bash
# Setup
bin/setup # Install deps, configure DB, start Redis

# Run application
bin/rails server # Start Rails on :3000
bin/dev # Start all Procfile processes (web + redis + workers)

# Testing
bin/rails test # Run unit/functional tests
bin/rails test test/models/user_test.rb # Run single test file
bin/rails test test/models/user_test.rb:42 # Run specific test line
bin/rails test:system # Run browser tests (Capybara/Selenium)

# Full CI suite (runs locally)
bin/ci

# Code quality
bin/rubocop # Ruby linting (Rails Omakase style)
bin/brakeman # Security analysis
bin/bundler-audit # Gem vulnerability scan
bin/importmap audit # JS dependency check

# Database
bin/rails db:prepare # Idempotent DB setup
bin/rails db:seed:replant # Reset + reseed with test data
```

## CI Pipeline Steps

Defined in `config/ci.rb`:
1. Setup
2. Style: Ruby (rubocop)
3. Security: Gem audit, Importmap audit, Brakeman
4. Tests: Rails tests, System tests, Seeds validation
5. Signoff (via `gh signoff`)

## Architecture

**Stack:** Rails 8.2, Ruby 3.4.5, SQLite3, Redis, Puma, Resque

**Real-time:** ActionCable WebSocket channels + Turbo broadcasts + Stimulus controllers

**Key Models:**
- `Account` - Singleton per instance (settings, branding)
- `User` - Application users
- `Room` - Chat rooms (public/private with access controls)
- `Message` - Messages within rooms
- `Membership` - User participation in rooms
- `Webhook` - Bot integration endpoints

**Background Jobs:** Resque with resque-pool (`app/jobs/`)

**Key Directories:**
- `app/channels/` - ActionCable WebSocket channels for live updates
- `app/jobs/` - Resque background jobs (Web Push, link unfurling, media processing)
- `lib/restricted_http/` - SSRF protection utilities
- `lib/web_push/` - Push notification connection pooling
- `script/admin/` - Admin utilities (e.g., `create-vapid-key`)

## Deployment

Blue-green deployment via GitHub Actions. Docker multi-arch images (amd64 + arm64) published to GHCR.

**Production env vars:** `SECRET_KEY_BASE`, `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `SSL_DOMAIN` (or `DISABLE_SSL`), `SENTRY_DSN`

**Data persistence:** Mount volume to `/rails/storage` (SQLite DB + file attachments)
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
48 changes: 48 additions & 0 deletions config/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Kamal deployment configuration for Campfire
# Documentation: https://kamal-deploy.org

service: campfire

image: etchteam/once-campfire

builder:
arch: amd64

servers:
web:
hosts:
- <%= ENV["DEPLOY_HOST"] %>
options:
env: SECRET_KEY_BASE="$(cat /root/campfire-secret-key)"

proxy:
ssl:
certificate_pem: <%= ENV["SSL_CERTIFICATE_PEM"] %>
private_key_pem: <%= ENV["SSL_PRIVATE_KEY_PEM"] %>
host: <%= ENV["DEPLOY_DOMAIN"] %>
app_port: 5555
healthcheck:
path: /up
interval: 10
timeout: 30

registry:
server: ghcr.io
username: <%= ENV["KAMAL_REGISTRY_USERNAME"] %>
password: <%= ENV["KAMAL_REGISTRY_PASSWORD"] %>

env:
clear:
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: "true"

volumes:
- campfire-storage:/rails/storage

ssh:
user: <%= ENV["DEPLOY_USER"] %>

aliases:
console: app exec -i --reuse "bin/rails console"
logs: app logs -f
shell: app exec -i --reuse "bash"