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
33 changes: 33 additions & 0 deletions .github/workflows/worktree-dev-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Worktree development tooling

on:
push:
branches: [main]
paths:
- 'scripts/worktree-dev/**'
- '.gitignore'
- 'Makefile'
- '.github/workflows/worktree-dev-test.yml'
pull_request:
paths:
- 'scripts/worktree-dev/**'
- '.gitignore'
- 'Makefile'
- '.github/workflows/worktree-dev-test.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
lifecycle:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Check syntax
run: |
bash -n scripts/worktree-dev/*.sh scripts/worktree-dev/talebound-worktree-dev
python3 -m py_compile scripts/worktree-dev/*.py
- name: Verify concurrent worktree isolation and cleanup
run: scripts/worktree-dev/test-concurrent-worktrees.sh
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
.DS_Store
.worktrees/

# Generated local worktree configuration and runtime state
.talebound/
.local-dev/
.env.worktree
backend/app.env.worktree
frontend/.env.worktree
__pycache__/
49 changes: 49 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,52 @@
- Prefer an existing suitable worktree. Otherwise branch from fetched `origin/main`
with an explicit task branch and destination path, using `--no-track`.
Use `git worktree add` if a managed tool cannot honor the required location.

## Local worktree development

- Read `scripts/worktree-dev/README.md` before changing local lifecycle tooling.
- On Linux/WSL, bootstrap a new worktree with
`scripts/worktree-dev/bootstrap-worktree.sh`. This installs locked frontend
dependencies, starts this worktree's data services, and applies migrations.
- Use `scripts/worktree-dev/talebound-worktree-dev up`, `status`, `logs`, and
`down` for the app lifecycle. Root Makefile aliases are also supported.
Do not create ad-hoc shared databases or use the legacy backend Compose file.
- `setup` and `up` are idempotent; neither replaces existing database data.
`down` retains data. Before `down --purge-data`, check `status` and the exact
current worktree path. Purge deletes this worktree's labelled data and cache
volumes. Never remove unrelated or unlabelled Docker resources.
- Every worktree has a separate PostgreSQL container/volume, Redis queue,
Mailpit inbox, auth key, and loopback address/ports. Open the frontend URL
reported by `status`; substituting `localhost` breaks origin/session isolation.
- `.talebound/`, `.env.worktree`, `backend/app.env.worktree`, and
`frontend/.env.worktree` are generated local state. Never commit them, print
their secret values, or copy them between worktrees. Registry/lock files live
in the primary clone's Git metadata and must not be edited while tooling runs.
- The launcher generates development configuration and does not copy another
checkout's environment. Email stays in Mailpit, Sentry is disabled, and
Cloudflare is unconfigured by default. Optional development image credentials
belong in `.talebound/integrations.env`; never add production fallbacks.
- Frontend changes hot reload. For backend changes, use
`scripts/worktree-dev/talebound-worktree-dev restart backend`: the container
snapshots the source so migrations do not write generated SQL into the checkout.
- Purge the current stack before removing a disposable worktree. Use
`git worktree remove` for Git-created worktrees. Preserve development data when
it has not been intentionally discarded.

## Validation

- For lifecycle, ownership, ports, or cleanup changes: run Bash syntax checks,
Python compilation, `status`, and
`scripts/worktree-dev/test-concurrent-worktrees.sh`. This creates disposable
worktrees under this workspace and verifies isolation and scoped cleanup.
- For backend changes, use `scripts/worktree-dev/talebound-worktree-dev migrate`
when needed, then `test-backend`. Short tests skip the legacy real-email test.
Tests write fixtures to the current worktree database; never point them at a
shared or production database.
- For frontend changes, run relevant lint/build checks and verify the affected
browser flow. Report failures honestly and do not suppress checks to make a
build appear green. Use `useAppSelector`/`useAppDispatch` from
`frontend/store/hooks.ts` for typed Redux access.
- For worktree authentication/email changes, verify signup, local email delivery,
verification, login, and isolation from another running worktree. Report the
actual commands/results and any untested behavior before handoff.
51 changes: 34 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,69 @@

NODE ?= node
FRONTEND_YARN = cd frontend && $(NODE) .yarn/releases/yarn-3.6.1.cjs
COMPOSE = docker compose -f backend/docker-compose.yaml
WORKTREE_DEV = scripts/worktree-dev/talebound-worktree-dev

.PHONY: help frontend-install frontend-dev frontend-build frontend-lint backend-dev backend-build backend-test backend-migrate api-generate services-up services-down
.PHONY: help dev-setup dev-up dev-down dev-status dev-logs frontend-install frontend-dev frontend-build frontend-lint backend-dev backend-build backend-test backend-migrate api-generate services-up services-down

help:
@echo "dev-setup Install dependencies and migrate this worktree's local database"
@echo "dev-up Set up and start this worktree's complete local app"
@echo "dev-down Stop this worktree, retaining data"
@echo "dev-status Show this worktree's URLs and services"
@echo "dev-logs Show recent logs for this worktree"
@echo "frontend-install Install frontend dependencies from the lockfile"
@echo "frontend-dev Start Next.js on port 4000"
@echo "frontend-dev Alias for dev-up (starts both applications)"
@echo "frontend-build Build the frontend"
@echo "frontend-lint Lint the frontend"
@echo "backend-dev Start the Go API"
@echo "backend-dev Alias for dev-up (starts both applications)"
@echo "backend-build Compile all Go packages"
@echo "backend-test Run Go tests (requires a migrated test database)"
@echo "backend-migrate Apply database migrations to the configured database"
@echo "backend-test Run Go short tests in this worktree's container and database"
@echo "backend-migrate Apply migrations to this worktree's database"
@echo "api-generate Generate frontend API types from the local Swagger schema"
@echo "services-up Start local PostgreSQL and Redis"
@echo "services-down Stop local Compose services, retaining volumes"
@echo "services-up Start this worktree's PostgreSQL, Redis, and local email inbox"
@echo "services-down Alias for dev-down"

dev-setup:
$(WORKTREE_DEV) setup

dev-up:
$(WORKTREE_DEV) up

dev-down:
$(WORKTREE_DEV) down

dev-status:
$(WORKTREE_DEV) status

dev-logs:
$(WORKTREE_DEV) logs

frontend-install:
$(FRONTEND_YARN) install --immutable

frontend-dev:
$(FRONTEND_YARN) dev
frontend-dev: dev-up

frontend-build:
$(FRONTEND_YARN) build

frontend-lint:
$(FRONTEND_YARN) lint

backend-dev:
$(MAKE) -C backend server
backend-dev: dev-up

backend-build:
cd backend && go build ./...

backend-test:
$(MAKE) -C backend test
$(WORKTREE_DEV) test-backend

backend-migrate:
$(MAKE) -C backend migrateup
$(WORKTREE_DEV) migrate

api-generate:
$(FRONTEND_YARN) generate

services-up:
$(COMPOSE) up -d postgres redis
$(WORKTREE_DEV) setup --services-only

services-down:
$(COMPOSE) down
services-down: dev-down
64 changes: 30 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,34 @@ The frontend and backend for Talebound, developed together in one repository.

## Development

Use Node.js 22, Go 1.23 (as declared in `backend/go.mod`), GNU Make, and Docker
with Compose. Yarn is checked into the frontend, so a global Yarn installation
is not required. Run `make` to see the available commands.
On Linux/WSL, use Bash, Python 3.9+, Git, and Docker with Compose v2. The supported
worktree launcher runs Node.js 22, Go 1.23, PostgreSQL 15, Redis 7, and Mailpit in
containers. No host Node, Go, Yarn, or Make installation is needed to run the app.

```sh
make frontend-install
make services-up
make backend-migrate
scripts/worktree-dev/bootstrap-worktree.sh
scripts/worktree-dev/talebound-worktree-dev up
scripts/worktree-dev/talebound-worktree-dev status
```

The backend loads `backend/app.env`. Put local overrides in the ignored
`backend/app.env.local`, which is loaded by the backend Makefile. Review the
database configuration before running migrations. The supplied development
database uses port 5432 and Redis uses port 6379.

Create `frontend/.env.local` using the settings described in
[the frontend README](frontend/README.md), including the API URL and the service
keys needed for features you use. The existing development CORS setup uses
`dev.talebound.net`; map it to `127.0.0.1` in your hosts file.

Start each application in its own terminal:

```sh
make backend-dev
make frontend-dev
```

The frontend runs at `http://dev.talebound.net:4000` and the HTTP API at
`http://dev.talebound.net:8080`. Email, image delivery, and other integrations
still need their own configuration. See [the backend README](backend/README.md)
for its existing development tools and workflow.
Each worktree gets its own database, Redis queue, email inbox, loopback address,
ports, and authentication key. `status` prints the exact app and inbox URLs.
Use those URLs: separate loopback addresses also keep browser cookies isolated.
No hosts-file edits or AWS credentials are required. Register an account in the
app and open the verification email in the local inbox.

Bootstrap is idempotent and stops after installing dependencies and applying
migrations; `up` starts both applications. `down` stops this worktree and retains
data. Generated `.env.worktree`, `backend/app.env.worktree`,
`frontend/.env.worktree`, and `.talebound/` are ignored local state. Do not copy
them between worktrees. Optional development Cloudflare settings go in
`.talebound/integrations.env`; uploads require those credentials.

See [the worktree development guide](scripts/worktree-dev/README.md) for lifecycle
commands, environment precedence, database access, cleanup, and validation.
GNU Make aliases (`make dev-up`, `make dev-status`, `make dev-down`) are optional.
The older `backend/Makefile` and `backend/docker-compose.yaml` contain historical
single-repository commands; use the root launcher for local worktree development.

## Validation and API generation

Expand All @@ -52,7 +49,9 @@ make backend-test
make api-generate
```

Backend tests require the migrated PostgreSQL database. API generation reads
Backend tests use the current worktree's migrated PostgreSQL database. The
frontend checks/API generator and `backend-build` use host Node.js 22/Go 1.23;
their container equivalents are documented in the worktree guide. API generation reads
`backend/doc/swagger/talebound.swagger.json` directly; it no longer needs a
running backend or the old development server. After changing protobuf API
definitions, regenerate the backend schema with `make -C backend proto` using
Expand All @@ -70,12 +69,9 @@ existing imports and protobuf package identities. Go builds use the local
client without changes. The Yarn lockfile installed with `--immutable`.
- GitHub Actions workflows passed `actionlint`; Compose configuration and root
Makefile command paths were checked.
- The imported frontend has existing build blockers: lint reports 28 errors
and one warning, and a separate build with lint disabled fails type checking
in `frontend/api/entities/useCreateEntityGroupContent.ts` because
`entityGroupId` can be undefined. The source and generated client are unchanged
by this migration. Frontend CI will report these failures until they are fixed;
lint and type checking have not been disabled in the build configuration.
- Frontend lint, TypeScript checking, and the production build pass with Node 22
and the checked-in Yarn lockfile. The inherited API/type mismatches and unused
code have been corrected; lint and type checking remain enabled.

## Builds and deployment

Expand Down
24 changes: 22 additions & 2 deletions backend/mail/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package mail
import (
"fmt"
"github.com/jordan-wright/email"
"net"
"net/smtp"
"strings"
)

const (
Expand All @@ -27,14 +29,25 @@ type AwsSesSender struct {
fromEmailAddress string
smtpUsername string
smtpPassword string
smtpEndpoint string
}

func NewAwsSesSender(name string, fromEmailAddress string, smtpUsername string, smtpPassword string) EmailSender {
return NewSMTPSender(name, fromEmailAddress, smtpServerAddress, smtpUsername, smtpPassword)
}

// NewSMTPSender accepts host:port, or a hostname using the submission port 587.
// An unauthenticated local SMTP sink needs neither username nor password.
func NewSMTPSender(name, fromEmailAddress, endpoint, smtpUsername, smtpPassword string) EmailSender {
if !strings.Contains(endpoint, ":") {
endpoint = net.JoinHostPort(endpoint, "587")
}
return &AwsSesSender{
name: name,
fromEmailAddress: fromEmailAddress,
smtpUsername: smtpUsername,
smtpPassword: smtpPassword,
smtpEndpoint: endpoint,
}
}

Expand All @@ -61,6 +74,13 @@ func (sender *AwsSesSender) SendEmail(
}
}

smtpAuth := smtp.PlainAuth("", sender.smtpUsername, sender.smtpPassword, smtpAuthAddress)
return e.Send(smtpServerAddress, smtpAuth)
host, _, err := net.SplitHostPort(sender.smtpEndpoint)
if err != nil {
return fmt.Errorf("invalid SMTP endpoint: %w", err)
}
var smtpAuth smtp.Auth
if sender.smtpUsername != "" || sender.smtpPassword != "" {
smtpAuth = smtp.PlainAuth("", sender.smtpUsername, sender.smtpPassword, host)
}
return e.Send(sender.smtpEndpoint, smtpAuth)
}
70 changes: 70 additions & 0 deletions backend/mail/smtp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package mail

import (
"bufio"
"fmt"
"net"
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestSendEmailToLocalSMTPWithoutAuthentication(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { listener.Close() })
message := make(chan string, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
return
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(5 * time.Second))
fmt.Fprint(conn, "220 localhost SMTP test\r\n")
scanner := bufio.NewScanner(conn)
var body strings.Builder
inData := false
for scanner.Scan() {
line := scanner.Text()
if inData {
if line == "." {
message <- body.String()
fmt.Fprint(conn, "250 accepted\r\n")
inData = false
} else {
body.WriteString(line + "\n")
}
continue
}
switch {
case strings.HasPrefix(line, "EHLO"), strings.HasPrefix(line, "HELO"):
fmt.Fprint(conn, "250 localhost\r\n")
case strings.HasPrefix(line, "MAIL FROM:"), strings.HasPrefix(line, "RCPT TO:"):
fmt.Fprint(conn, "250 OK\r\n")
case line == "DATA":
inData = true
fmt.Fprint(conn, "354 send message\r\n")
case line == "QUIT":
fmt.Fprint(conn, "221 bye\r\n")
return
default:
fmt.Fprint(conn, "500 unsupported command\r\n")
}
}
}()

sender := NewSMTPSender("Talebound", "noreply@talebound.test", listener.Addr().String(), "", "")
require.NoError(t, sender.SendEmail("Local verification", "<p>Verify your email</p>",
[]string{"developer@talebound.test"}, nil, nil, nil))
select {
case body := <-message:
require.Contains(t, body, "Subject: Local verification")
require.Contains(t, body, "developer@talebound.test")
require.Contains(t, body, "Verify your email")
case <-time.After(5 * time.Second):
t.Fatal("SMTP server did not receive the message")
}
}
Loading
Loading