Add installer, CI, ARM build and test updates

Introduce a one‑line Docker installer and environment examples, add CI and docker validation workflows, and update docs and tests.

Key changes:
- Add docker/install.sh installer that bootstraps /opt/synapsis, downloads compose files, optionally installs Docker, and generates secrets.
- Add top-level .env.example and docker/.env.example entries for shared storage and local development.
- Add GitHub Actions CI (ci.yml) with type checks, targeted vitest runs, build and docker-compose validation.
- Update existing docker workflow to set up QEMU and build multi‑arch images (amd64, arm64).
- Update README and docker/README to use the installer, point to the new repo, adjust local setup instructions, and note shared S3 env vars.
- Update docker-compose comments and include shared S3 env variables and defaults.
- Update .gitignore to ignore site-work mirrors.
- Tests: expand supported bot source types (add brave_news, youtube), increase POST_MAX_LENGTH from 400 to 600, add minimal user handle in mention handler tests, and add mocks & test adjustments in scheduler tests to keep them unit-scoped.

These changes simplify installation, add CI coverage (including Docker config validation), enable multi‑arch builds, and align tests with expanded bot source/validation behavior.
This commit is contained in:
cyph3rasi
2026-03-07 16:11:37 -08:00
parent a686db38b0
commit ae2c34df6c
14 changed files with 350 additions and 48 deletions
+32
View File
@@ -0,0 +1,32 @@
# Synapsis local development environment
# Copy to .env and adjust values for your machine.
# Required: local database
DATABASE_URL=postgres://postgres:postgres@localhost:5432/synapsis
# Required: used for sessions, node key encryption, and bot API key encryption
AUTH_SECRET=replace-with-a-long-random-secret
# Required for admin access. Register with one of these emails locally.
ADMIN_EMAILS=admin@example.com
# Recommended: local node identity
NEXT_PUBLIC_NODE_DOMAIN=localhost:3000
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Optional: node metadata shown before a node record exists in the database
# NEXT_PUBLIC_NODE_NAME=Synapsis Local
# NEXT_PUBLIC_NODE_DESCRIPTION=Local development node
# NEXT_PUBLIC_ACCENT_COLOR=#FFFFFF
# Optional: bot limits
# BOT_MAX_PER_USER=5
# Optional: app-level storage fallback. Most media storage is user-owned and can be
# configured in the app, so these are only needed if you want a shared default.
# STORAGE_ENDPOINT=
# STORAGE_REGION=
# STORAGE_BUCKET=
# STORAGE_ACCESS_KEY=
# STORAGE_SECRET_KEY=
# STORAGE_PUBLIC_BASE_URL=
+56
View File
@@ -0,0 +1,56 @@
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run type-check
- name: Targeted bot regression checks
run: >
AUTH_SECRET=ci-test-secret
npx vitest run
src/lib/bots/encryption.test.ts
src/lib/bots/contentSource.test.ts
src/lib/bots/posting.property.test.ts
src/lib/bots/mentionHandler.property.test.ts
src/lib/bots/scheduler.test.ts
- name: Production build
run: npm run build
docker-validation:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate production compose config
run: docker compose --env-file docker/.env.example config > /tmp/synapsis-compose.txt
- name: Validate source-build compose config
run: docker compose -f docker/docker-compose.yml --env-file docker/.env.example config > /tmp/synapsis-build-compose.txt
- name: Build application image
run: docker build -f docker/Dockerfile .
+4 -1
View File
@@ -29,6 +29,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -61,4 +64,4 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
no-cache: true
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
+3
View File
@@ -51,3 +51,6 @@ next-env.d.ts
.vscode/
.kiro/
.env.dev
# local website/docs mirrors
site-work/
+8 -13
View File
@@ -11,19 +11,14 @@ Synapsis is an open-source, federated social network built for the decentralized
Run your personal Synapsis node with a single command:
```bash
# 1. Download and start
mkdir -p /opt/synapsis && cd /opt/synapsis
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/caddy-entrypoint.sh
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example
cp .env.example .env
# 1. Bootstrap the deployment directory
curl -fsSL https://synapsis.social/install.sh | bash
# 2. Edit .env with your domain and secrets (takes 2 minutes)
nano .env
# Use a host-only domain (no scheme), e.g. synapsis.example.com
# 2. Edit /opt/synapsis/.env with your domain and admin email
nano /opt/synapsis/.env
# 3. Start your node
cd /opt/synapsis
docker compose up -d
```
@@ -117,7 +112,7 @@ Want to hack on Synapsis? Here's how to run it locally:
```bash
# 1. Clone the repository
git clone https://github.com/cyph3rasi/synapsis.git
git clone https://github.com/GnosysLabs/Synapsis.git
cd synapsis
# 2. Install dependencies
@@ -128,13 +123,13 @@ cp .env.example .env
# Edit .env with your local database and storage settings
# 4. Set up the database
npx drizzle-kit push
npm run db:push
# 5. Run the development server
npm run dev
```
Visit `http://localhost:3000` — the app will redirect you to `/install` for initial setup.
Visit `http://localhost:3000` and register with an email listed in `ADMIN_EMAILS`. Local setup no longer uses a dedicated `/install` route.
### Tech Stack
+10 -10
View File
@@ -2,15 +2,10 @@
# Uses pre-built image from GitHub Container Registry
#
# Quick Start:
# 1. Download files to a new directory:
# mkdir -p /opt/synapsis && cd /opt/synapsis
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/caddy-entrypoint.sh
# curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example
# cp .env.example .env
# 2. Configure (edit with your domain and secrets): nano .env
# 3. Start your node: docker compose up -d
# 1. Bootstrap the deployment directory:
# curl -fsSL https://synapsis.social/install.sh | bash
# 2. Configure (edit with your domain and admin email): nano /opt/synapsis/.env
# 3. Start your node: cd /opt/synapsis && docker compose up -d
#
# Services:
# - Synapsis Next.js application (from GHCR)
@@ -75,7 +70,12 @@ services:
# Port configuration (auto or specific port)
PORT: ${PORT:-3000}
# S3 Storage public URL (for custom S3 providers like R2, B2, Contabo)
# Shared S3 storage configuration
STORAGE_ENDPOINT: ${STORAGE_ENDPOINT:-}
STORAGE_REGION: ${STORAGE_REGION:-us-east-1}
STORAGE_BUCKET: ${STORAGE_BUCKET:-}
STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY:-}
STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY:-}
STORAGE_PUBLIC_BASE_URL: ${STORAGE_PUBLIC_BASE_URL:-}
# Node environment
+13
View File
@@ -43,3 +43,16 @@ PORT=auto
# Maximum AI bots per user (default: 5)
# BOT_MAX_PER_USER=5
# ===========================================
# OPTIONAL: Shared S3 Storage
# ===========================================
# These are only needed if you want app-level shared storage defaults.
# Most media storage is configured per user inside the app.
# STORAGE_ENDPOINT=
# STORAGE_REGION=us-east-1
# STORAGE_BUCKET=
# STORAGE_ACCESS_KEY=
# STORAGE_SECRET_KEY=
# STORAGE_PUBLIC_BASE_URL=
+16 -19
View File
@@ -7,13 +7,9 @@ Production Docker deployment using pre-built images from GitHub Container Regist
## 🚀 Quick Start
```bash
mkdir -p /opt/synapsis && cd /opt/synapsis
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/Caddyfile
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/caddy-entrypoint.sh
curl -O https://raw.githubusercontent.com/cyph3rasi/synapsis/main/docker/.env.example
cp .env.example .env
nano .env # Add your domain and secrets
curl -fsSL https://synapsis.social/install.sh | bash
nano /opt/synapsis/.env # Add your domain and admin email
cd /opt/synapsis
docker compose up -d
```
@@ -27,14 +23,7 @@ Your node is live at `https://your-domain.com` with automatic SSL.
|-------------|---------|
| **Server** | 2GB RAM, 2 CPU cores, 20GB SSD (minimum) |
| **Domain** | A domain or subdomain pointing to your server |
| **Docker** | Version 24.0+ with Docker Compose 2.20+ |
**Install Docker (Ubuntu/Debian):**
```bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
```
| **Docker** | Installed automatically by `install.sh` when missing on supported Linux hosts |
---
@@ -53,6 +42,15 @@ Optional (advanced):
- `NEXT_PUBLIC_NODE_DOMAIN` to override the node domain (defaults to `DOMAIN`)
- `NEXT_PUBLIC_APP_URL` to override the public app URL used by background jobs (auto-derived from the node domain)
- `ALLOW_LOCALHOST=1` to allow `localhost` in production containers for local testing
- Shared S3 storage env vars are available if you want app-level fallback storage
Optional shared storage env vars:
- `STORAGE_ENDPOINT`
- `STORAGE_REGION`
- `STORAGE_BUCKET`
- `STORAGE_ACCESS_KEY`
- `STORAGE_SECRET_KEY`
- `STORAGE_PUBLIC_BASE_URL`
**Port Configuration:**
- `PORT=auto` (default) — Automatically finds an available port between 3000-3020
@@ -131,8 +129,7 @@ docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
# Verify image exists
docker pull ghcr.io/cyph3rasi/synapsis:latest
# Check available tags at:
# https://github.com/cyph3rasi/synapsis/pkgs/container/synapsis
# Check the published package tags in GitHub Container Registry
```
---
@@ -166,9 +163,9 @@ echo "0 2 * * * /opt/synapsis/backup.sh" | sudo crontab -
To build locally instead of using pre-built images:
```bash
git clone https://github.com/cyph3rasi/synapsis.git
git clone https://github.com/GnosysLabs/Synapsis.git
cd synapsis/docker
docker compose -f docker-compose.build.yml up -d --build
docker compose up -d --build
```
---
+154
View File
@@ -0,0 +1,154 @@
#!/bin/sh
set -eu
REPO="${REPO:-GnosysLabs/Synapsis}"
REF="${REF:-}"
INSTALL_DIR="${1:-${INSTALL_DIR:-/opt/synapsis}}"
REPO_API="https://api.github.com/repos/${REPO}"
PUBLIC_INSTALL_URL="${PUBLIC_INSTALL_URL:-https://synapsis.social/install.sh}"
resolve_ref() {
if [ -n "${REF}" ]; then
REF_SOURCE="custom override"
return
fi
echo "🔎 Resolving latest Synapsis release"
latest_tag=$(
curl -fsSL "${REPO_API}/releases/latest" 2>/dev/null \
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
| head -n 1
)
if [ -n "${latest_tag}" ]; then
REF="${latest_tag}"
REF_SOURCE="latest GitHub release"
return
fi
REF="main"
REF_SOURCE="main fallback"
echo "⚠️ Could not resolve the latest release, falling back to ${REF}" >&2
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "❌ Required command not found: $1" >&2
exit 1
fi
}
download_file() {
source_path="$1"
target_path="$2"
echo "⬇️ Downloading ${source_path}"
curl -fsSL "${RAW_BASE}/${source_path}" -o "${target_path}"
}
set_env_value() {
file="$1"
key="$2"
value="$3"
escaped_value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g')
sed -i.bak -E "s|^${key}=.*$|${key}=${escaped_value}|" "$file"
rm -f "${file}.bak"
}
generate_db_password() {
openssl rand -base64 24 | tr -d '\n' | tr '/+' '_-' | cut -c1-32
}
install_docker_if_needed() {
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
echo "🐳 Docker is already installed"
return
fi
if [ "$(id -u)" -ne 0 ]; then
echo "❌ Docker is not installed and this installer is not running as root." >&2
echo " Re-run it as root or with sudo so Synapsis can install Docker for you." >&2
exit 1
fi
echo "🐳 Docker not found, installing Docker"
curl -fsSL https://get.docker.com | sh
if command -v systemctl >/dev/null 2>&1; then
systemctl enable --now docker >/dev/null 2>&1 || true
elif command -v service >/dev/null 2>&1; then
service docker start >/dev/null 2>&1 || true
fi
if ! command -v docker >/dev/null 2>&1; then
echo "❌ Docker installation did not finish successfully." >&2
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo "⚠️ Docker was installed, but 'docker compose' is not available yet." >&2
echo " Install the Docker Compose plugin before starting Synapsis." >&2
fi
}
require_command curl
require_command chmod
require_command mkdir
require_command cp
resolve_ref
RAW_BASE="https://raw.githubusercontent.com/${REPO}/${REF}"
echo "========================================"
echo " Synapsis Docker Installer"
echo "========================================"
echo " Repo: ${REPO}"
echo " Ref: ${REF} (${REF_SOURCE})"
echo " Install dir: ${INSTALL_DIR}"
echo "========================================"
install_docker_if_needed
mkdir -p "${INSTALL_DIR}"
download_file "docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"
download_file "docker/Caddyfile" "${INSTALL_DIR}/Caddyfile"
download_file "docker/caddy-entrypoint.sh" "${INSTALL_DIR}/caddy-entrypoint.sh"
download_file "docker/.env.example" "${INSTALL_DIR}/.env.example"
chmod 755 "${INSTALL_DIR}/caddy-entrypoint.sh"
if [ ! -f "${INSTALL_DIR}/.env" ]; then
cp "${INSTALL_DIR}/.env.example" "${INSTALL_DIR}/.env"
echo "📝 Created ${INSTALL_DIR}/.env"
if command -v openssl >/dev/null 2>&1; then
set_env_value "${INSTALL_DIR}/.env" "AUTH_SECRET" "$(openssl rand -hex 32)"
set_env_value "${INSTALL_DIR}/.env" "DB_PASSWORD" "$(generate_db_password)"
echo "🔐 Generated AUTH_SECRET and DB_PASSWORD"
else
echo "⚠️ openssl not found, leaving placeholder secrets in .env"
fi
if [ -n "${DOMAIN:-}" ]; then
set_env_value "${INSTALL_DIR}/.env" "DOMAIN" "${DOMAIN}"
echo "🌐 Set DOMAIN=${DOMAIN}"
fi
if [ -n "${ADMIN_EMAILS:-}" ]; then
set_env_value "${INSTALL_DIR}/.env" "ADMIN_EMAILS" "${ADMIN_EMAILS}"
echo "📧 Set ADMIN_EMAILS=${ADMIN_EMAILS}"
fi
else
echo "📝 Existing ${INSTALL_DIR}/.env found, leaving it unchanged"
fi
echo ""
echo "Next steps:"
echo " 1. Review ${INSTALL_DIR}/.env"
echo " 2. Start Synapsis:"
echo " cd ${INSTALL_DIR} && docker compose up -d"
echo ""
echo "One-line usage examples:"
echo " curl -fsSL ${PUBLIC_INSTALL_URL} | bash"
echo " curl -fsSL ${PUBLIC_INSTALL_URL} | DOMAIN=synapsis.example.com ADMIN_EMAILS=you@example.com bash"
+4 -2
View File
@@ -750,11 +750,13 @@ describe('Feature: bot-system, Property 14: Multiple Source Types Per Bot', () =
);
});
it('SUPPORTED_SOURCE_TYPES contains all three types', () => {
it('SUPPORTED_SOURCE_TYPES contains the core and extended source types', () => {
expect(SUPPORTED_SOURCE_TYPES).toContain('rss');
expect(SUPPORTED_SOURCE_TYPES).toContain('reddit');
expect(SUPPORTED_SOURCE_TYPES).toContain('news_api');
expect(SUPPORTED_SOURCE_TYPES.length).toBe(3);
expect(SUPPORTED_SOURCE_TYPES).toContain('brave_news');
expect(SUPPORTED_SOURCE_TYPES).toContain('youtube');
expect(SUPPORTED_SOURCE_TYPES.length).toBe(5);
});
});
+3 -1
View File
@@ -433,7 +433,9 @@ describe('Constants', () => {
expect(SUPPORTED_SOURCE_TYPES).toContain('rss');
expect(SUPPORTED_SOURCE_TYPES).toContain('reddit');
expect(SUPPORTED_SOURCE_TYPES).toContain('news_api');
expect(SUPPORTED_SOURCE_TYPES.length).toBe(4);
expect(SUPPORTED_SOURCE_TYPES).toContain('brave_news');
expect(SUPPORTED_SOURCE_TYPES).toContain('youtube');
expect(SUPPORTED_SOURCE_TYPES.length).toBe(5);
});
@@ -52,6 +52,9 @@ export const __addBot = (handle: string, userId: string) => {
id,
userId,
handle: handle.toLowerCase(),
user: {
handle: handle.toLowerCase(),
},
name: `Bot ${handle}`,
personalityConfig: JSON.stringify({
systemPrompt: 'Test bot',
+1 -1
View File
@@ -454,7 +454,7 @@ describe('Feature: bot-system, Property 38: Post Content Validation', () => {
it('validation constants are properly defined (Requirement 11.5)', async () => {
// Verify constants are reasonable
expect(POST_MAX_LENGTH).toBe(400);
expect(POST_MAX_LENGTH).toBe(600);
expect(POST_MIN_LENGTH).toBe(1);
expect(MAX_URLS_PER_POST).toBe(5);
+43 -1
View File
@@ -586,6 +586,22 @@ vi.mock('./rateLimiter', () => ({
canPost: vi.fn(async () => ({ allowed: true })),
}));
// Mock post creation so scheduler tests stay unit-scoped
vi.mock('./posting', () => ({
triggerPost: vi.fn(async () => ({
success: true,
post: {
id: 'post-1',
content: 'Generated post',
},
})),
}));
// Mock source fetching to avoid exercising fetcher behavior here
vi.mock('./contentFetcher', () => ({
fetchAllSourcesForBot: vi.fn(async () => undefined),
}));
import {
hasUnprocessedContent,
getNextUnprocessedContent,
@@ -742,7 +758,7 @@ describe('processScheduledPosts', () => {
expect(result.details[0].status).toBe('skipped_rate_limit');
});
it('should skip bots when no content available', async () => {
it('should post original content when a bot has no active sources', async () => {
const { db } = await import('@/db');
const { canPost } = await import('./rateLimiter');
@@ -760,6 +776,32 @@ describe('processScheduledPosts', () => {
vi.mocked(canPost).mockResolvedValue({ allowed: true });
vi.mocked(db.query.botContentSources.findMany).mockResolvedValue([]);
const result = await processScheduledPosts();
expect(result.processed).toBe(1);
expect(result.details[0].status).toBe('posted');
});
it('should skip bots when active sources have no content available', async () => {
const { db } = await import('@/db');
const { canPost } = await import('./rateLimiter');
vi.mocked(db.query.bots.findMany).mockResolvedValue([
{
id: 'bot-1',
scheduleConfig: JSON.stringify({
type: 'interval',
intervalMinutes: 30,
}),
lastPostAt: null,
} as any,
]);
vi.mocked(canPost).mockResolvedValue({ allowed: true });
vi.mocked(db.query.botContentSources.findMany).mockResolvedValue([
{ id: 'source-1' } as any,
]);
vi.mocked(db.query.botContentItems.findFirst).mockResolvedValue(undefined);
const result = await processScheduledPosts();
expect(result.skipped).toBe(1);
expect(result.details[0].status).toBe('skipped_no_content');