From 432285fb0e320363e183915b6019aa90d5556177 Mon Sep 17 00:00:00 2001 From: Josh Rose <1677846+JoshTheWanderer@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:44:15 +0100 Subject: [PATCH 1/6] ci: lock down action versions --- .github/workflows/ci.yml | 16 ++++++++-------- .github/workflows/publish-image.yml | 23 +++++++++++------------ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ac6d9e1..7e32d571 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 @@ -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: @@ -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: diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index eb191342..f75acd84 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -1,3 +1,4 @@ +--- name: Build and publish container image to GHCR on: @@ -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 }} @@ -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: | @@ -84,7 +85,7 @@ jobs: - 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 @@ -102,7 +103,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 }} @@ -119,10 +120,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 }} @@ -139,7 +140,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: | @@ -185,7 +186,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 @@ -198,5 +199,3 @@ jobs: echo "Signing $tag" cosign sign --yes "$tag" done <<< "$tags" - - From d9cb8e6fa96967efe3c376e5b110b65f68b39a23 Mon Sep 17 00:00:00 2001 From: Josh Rose <1677846+JoshTheWanderer@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:55:24 +0100 Subject: [PATCH 2/6] ci: add an editorconfig --- .editorconfig | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4039ff11 --- /dev/null +++ b/.editorconfig @@ -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 From 0a3d53539008314b58059b6de5f89a537f7fb70c Mon Sep 17 00:00:00 2001 From: Josh Rose <1677846+JoshTheWanderer@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:19:24 +0100 Subject: [PATCH 3/6] docs: add agentic documentation --- AGENTS.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 78 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..31d795b9 --- /dev/null +++ b/AGENTS.md @@ -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) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md From 2a580c521e05f76dcde2984ad8eede2ab90bcce2 Mon Sep 17 00:00:00 2001 From: Josh Rose <1677846+JoshTheWanderer@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:19:41 +0100 Subject: [PATCH 4/6] ci: add mergify config --- .github/mergify.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/mergify.yml diff --git a/.github/mergify.yml b/.github/mergify.yml new file mode 100644 index 00000000..fb9f5104 --- /dev/null +++ b/.github/mergify.yml @@ -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}}" From ccf25ca103c26f8c18bc36f121593c1fbc49fb6f Mon Sep 17 00:00:00 2001 From: Josh Rose <1677846+JoshTheWanderer@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:38:42 +0100 Subject: [PATCH 5/6] build: add service tag for kamal deploys --- .github/workflows/publish-image.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index f75acd84..a3483e2e 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -82,6 +82,7 @@ jobs: suffix=-${{ matrix.arch }} labels: | org.opencontainers.image.source=${{ env.SOURCE_URL }} + service=campfire - name: Build and push (${{ matrix.platform }}) id: build @@ -155,6 +156,7 @@ jobs: latest=false labels: | org.opencontainers.image.source=${{ env.SOURCE_URL }} + service=campfire - name: Create multi-arch manifests shell: bash From 7dfccbb0c9c31af13d5858efdffb7a7051b64113 Mon Sep 17 00:00:00 2001 From: Josh Rose <1677846+JoshTheWanderer@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:39:01 +0100 Subject: [PATCH 6/6] ci: add kamal deployments --- .github/workflows/deploy.yml | 57 ++++++++++++++++++++++++++++++++++++ .gitignore | 3 ++ config/deploy.yml | 48 ++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 config/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..92b2463e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/.gitignore b/.gitignore index 7070965d..5738440d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,9 @@ # Ignore production envs /.env +# Ignore Kamal secrets +/.kamal/secrets + # Ignore dartsass builds app/assets/builds/* diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 00000000..0c23af4c --- /dev/null +++ b/config/deploy.yml @@ -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"