M0: Docker + Fabled-Git CI (typecheck + lint + test + build)
- 2-stage Dockerfile (node:22 build Vue -> python:3.12-slim runtime); CMD runs `alembic upgrade head` then hypercorn (rule 82). - .forgejo/workflows/ci.yml on ci-python:3.14: typecheck (vue-tsc) + lint (ruff check src/) + test (uv venv + pytest, DB-free) + gated buildx push with the rule-46 tag scheme (:dev/:latest/:<sha>). No local integration lane yet. - ci-requirements.md (rule 39); docker-compose for a local app+Postgres stack. - Real README with layout, dev instructions, and the milestone arc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -0,0 +1,157 @@
|
|||||||
|
# CI runs first; build only proceeds if lint + typecheck pass.
|
||||||
|
#
|
||||||
|
# Push to dev: typecheck + lint + test + build :dev + :<sha>
|
||||||
|
# Push to main: typecheck + lint + test + build :latest + :<sha>
|
||||||
|
# Tag v* (release): typecheck + lint + test + build :latest + :<version> + :<sha>
|
||||||
|
#
|
||||||
|
# main is the production line, so a merge to main rebuilds and moves :latest to its
|
||||||
|
# tip (family rule 46) — no version release required. The :<sha> image is the
|
||||||
|
# immutable rollback unit for every build.
|
||||||
|
#
|
||||||
|
# Required secrets (repo -> Settings -> Secrets -> Actions):
|
||||||
|
# REGISTRY_USER -- Forgejo username
|
||||||
|
# REGISTRY_TOKEN -- Forgejo PAT with write:packages scope
|
||||||
|
name: CI & Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev, main]
|
||||||
|
tags: ["v*"]
|
||||||
|
paths:
|
||||||
|
- "src/**"
|
||||||
|
- "frontend/**"
|
||||||
|
- "tests/**"
|
||||||
|
- "pyproject.toml"
|
||||||
|
- "alembic/**"
|
||||||
|
- "alembic.ini"
|
||||||
|
- "Dockerfile"
|
||||||
|
- ".forgejo/workflows/ci.yml"
|
||||||
|
|
||||||
|
# Cancel older runs on the same branch when a newer push lands. Tag runs get their
|
||||||
|
# own group implicitly and are never cancelled.
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.fabledsword.com
|
||||||
|
IMAGE: git.fabledsword.com/bvandeusen/thoughtsync
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
typecheck:
|
||||||
|
name: TypeScript typecheck
|
||||||
|
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: python-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
working-directory: frontend
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: npx vue-tsc --noEmit
|
||||||
|
working-directory: frontend
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Python lint
|
||||||
|
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: python-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# ruff is pre-installed in the ci-runner base image.
|
||||||
|
- name: Lint
|
||||||
|
run: ruff check src/
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Python tests
|
||||||
|
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: python-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Create virtual environment
|
||||||
|
run: uv venv /opt/venv
|
||||||
|
|
||||||
|
- name: Install package with dev deps
|
||||||
|
run: uv pip install --python /opt/venv/bin/python -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: /opt/venv/bin/python -m pytest tests/ -q
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build & push image
|
||||||
|
# Build gates on lint + typecheck. The `test` job runs in parallel for
|
||||||
|
# visibility but does not block dev image builds (DB-backed integration
|
||||||
|
# testing happens against the dev image manually, not on every push).
|
||||||
|
needs: [typecheck, lint]
|
||||||
|
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: python-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Generate image tags and version
|
||||||
|
id: tags
|
||||||
|
# run: steps execute under busybox sh (family rule 81), so use POSIX `case`,
|
||||||
|
# NOT bash `[[ ]]`.
|
||||||
|
run: |
|
||||||
|
TAGS="${{ env.IMAGE }}:${{ github.sha }}"
|
||||||
|
BUILD_VERSION="dev"
|
||||||
|
case "${{ github.ref }}" in
|
||||||
|
refs/heads/dev)
|
||||||
|
TAGS="$TAGS,${{ env.IMAGE }}:dev"
|
||||||
|
;;
|
||||||
|
refs/heads/main)
|
||||||
|
# Production line: :latest tracks main's tip (rule 46). No :main tag;
|
||||||
|
# the :<sha> above is the rollback unit. Version label = short sha.
|
||||||
|
TAGS="$TAGS,${{ env.IMAGE }}:latest"
|
||||||
|
BUILD_VERSION="$(echo ${{ github.sha }} | cut -c1-7)"
|
||||||
|
;;
|
||||||
|
refs/tags/*)
|
||||||
|
TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}"
|
||||||
|
BUILD_VERSION="${{ github.ref_name }}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo "value=$TAGS" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_version=$BUILD_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Free disk space
|
||||||
|
run: |
|
||||||
|
docker system prune -af || true
|
||||||
|
docker builder prune --keep-storage 5g -f || true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Log in to Forgejo registry
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
provenance: false
|
||||||
|
tags: ${{ steps.tags.outputs.value }}
|
||||||
|
build-args: BUILD_VERSION=${{ steps.tags.outputs.build_version }}
|
||||||
|
cache-from: type=registry,ref=${{ env.IMAGE }}:cache
|
||||||
|
cache-to: type=registry,ref=${{ env.IMAGE }}:cache,mode=max,ignore-error=true
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# Stage 1: build the Vue frontend (also type-checks via `vue-tsc` in the build script).
|
||||||
|
FROM node:22-alpine AS build-frontend
|
||||||
|
WORKDIR /build
|
||||||
|
COPY frontend/package.json frontend/package-lock.json* ./
|
||||||
|
RUN npm ci --quiet
|
||||||
|
COPY frontend/ .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Python runtime.
|
||||||
|
FROM python:3.12-slim AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml .
|
||||||
|
COPY src/ src/
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
|
pip install .
|
||||||
|
|
||||||
|
# Bake the built SPA into the package's static dir (served by app.py). PYTHONPATH
|
||||||
|
# points at /app/src so the runtime imports this source tree (with static/ present),
|
||||||
|
# not the pip-installed copy.
|
||||||
|
COPY --from=build-frontend /build/dist/ src/thoughtsync/static/
|
||||||
|
COPY alembic.ini .
|
||||||
|
COPY alembic/ alembic/
|
||||||
|
|
||||||
|
ENV PYTHONPATH=/app/src
|
||||||
|
|
||||||
|
ARG BUILD_VERSION=dev
|
||||||
|
ENV APP_VERSION=$BUILD_VERSION
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
# Run migrations, then serve. Family convention (rule 82): schema is built by real
|
||||||
|
# migrations, never metadata.create_all.
|
||||||
|
CMD ["sh", "-c", "alembic upgrade head && hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"]
|
||||||
@@ -1,3 +1,58 @@
|
|||||||
# thoughtsync
|
# ThoughtSync
|
||||||
|
|
||||||
Self-hosted personal thought-capture web app (FabledSword family) — a Google-Keep-style masonry post-it board for fast idea capture, growing into a lightweight second brain. Quart + Vue 3 + PostgreSQL.
|
Self-hosted personal thought-capture web app in the **FabledSword** family — a
|
||||||
|
Google-Keep-style **masonry post-it board** for capturing disparate thoughts in
|
||||||
|
under a second, designed to grow into a lightweight second brain (labels, search,
|
||||||
|
`[[wiki-links]]`, graph).
|
||||||
|
|
||||||
|
- **Stack:** Quart (async Python) + Vue 3 + PostgreSQL, Docker, Fabled-Git CI.
|
||||||
|
- **Auth:** native email + password (signed-cookie sessions). Multi-user with the
|
||||||
|
family owner + direct-share + group-share ACL.
|
||||||
|
- **Web-first.** An Android companion is a later, conditional milestone.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/thoughtsync/ Quart app (app factory, auth, models, ACL, config, db)
|
||||||
|
alembic/ async migrations (schema built via `alembic upgrade head`)
|
||||||
|
tests/ DB-free unit tests (pytest)
|
||||||
|
frontend/ Vue 3 + Vite + TypeScript + Tailwind SPA
|
||||||
|
Dockerfile 2-stage build (Vue -> python:3.12-slim runtime)
|
||||||
|
docker-compose.yml local app + Postgres stack
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Backend (needs a Postgres reachable at `THOUGHTSYNC_DATABASE_URL`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
alembic upgrade head
|
||||||
|
hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend (proxies `/api` to `:5000`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev # http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run the whole stack in Docker:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
- **M0 — Foundation & Identity** *(in progress)*: Quart+Vue+Postgres skeleton,
|
||||||
|
native auth, sharing-ACL spine, Fabled-Git CI.
|
||||||
|
- M1 — Capture Core: note model + masonry board (quick-add, colors, pin,
|
||||||
|
edit-in-place, archive, trash).
|
||||||
|
- M2 — Organize: labels/tags, search, checklists, attachments.
|
||||||
|
- M3 — Second-brain seeds: `[[wiki-links]]`, backlinks, graph view, reminders.
|
||||||
|
- M4 — Launch hardening: responsive/PWA, settings UI, polish.
|
||||||
|
|
||||||
|
Work happens on `dev`; `main` is protected (PR-only).
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# CI Requirements — ThoughtSync
|
||||||
|
|
||||||
|
> Spec lives in [`docs/process.md`](https://git.fabledsword.com/bvandeusen/CI-runner/src/branch/main/docs/process.md)
|
||||||
|
> in the CI-Runner repo.
|
||||||
|
|
||||||
|
## Runtime image
|
||||||
|
|
||||||
|
```
|
||||||
|
git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
|
```
|
||||||
|
|
||||||
|
Selected via `container.image` (not a `runs-on` label) on all four jobs in
|
||||||
|
`.forgejo/workflows/ci.yml`: typecheck (Vue/TS), lint (ruff), test (pytest),
|
||||||
|
build (docker buildx).
|
||||||
|
|
||||||
|
## Image deps used
|
||||||
|
|
||||||
|
- python 3.12+ (the runtime `Dockerfile` targets python:3.12-slim; tests run on
|
||||||
|
the image's 3.14 — both >=3.12, so results stay representative)
|
||||||
|
- node 24 — `npm ci` + `vue-tsc` in the typecheck job, and the frontend builder
|
||||||
|
stage inside the production `Dockerfile`. (Also required by the JS-based
|
||||||
|
`actions/checkout` action — a Node-less runner fails every job at checkout.)
|
||||||
|
- ruff — lint job runs `ruff check src/` with zero install overhead
|
||||||
|
- uv — test job creates the venv (`uv venv /opt/venv`) and installs the package
|
||||||
|
with dev deps
|
||||||
|
- docker CLI + buildx — build job pushes the dev/release image to the Forgejo
|
||||||
|
registry
|
||||||
|
|
||||||
|
## Per-job tool installs
|
||||||
|
|
||||||
|
Nothing installed at job time beyond what the image provides — all four jobs run
|
||||||
|
entirely on `ci-python:3.14`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **No `actions/cache`.** Deliberately omitted for npm/uv: it's a GitHub-fetched
|
||||||
|
JS action and on a cold runner concurrent jobs race fetching it. We lean on the
|
||||||
|
pinned `ci-python` image's pre-installed toolchain instead; `npm ci` / `uv pip
|
||||||
|
install` cold cost is a non-blocker.
|
||||||
|
- Build gates on `typecheck` + `lint` only. The `test` job runs in parallel for
|
||||||
|
visibility but does not block the dev image push. DB-backed / integration tests
|
||||||
|
run against the dev image manually — ThoughtSync's unit tests are DB-free (no
|
||||||
|
Postgres service lane in CI yet).
|
||||||
|
- `dev` push -> `:dev` + `:<sha>`; `v*` tag -> `:latest` + `:<version>` + `:<sha>`
|
||||||
|
(family rule 46).
|
||||||
|
- The production runtime `Dockerfile` tracks python:3.12 so test results stay
|
||||||
|
representative of the deployed image.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Local two-service stack (app + Postgres). Provided for convenience — per family
|
||||||
|
# rule 12 the agent does NOT start this; run it yourself with `docker compose up`.
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: thoughtsync
|
||||||
|
POSTGRES_PASSWORD: thoughtsync
|
||||||
|
POSTGRES_DB: thoughtsync
|
||||||
|
volumes:
|
||||||
|
- thoughtsync-db:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U thoughtsync"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://thoughtsync:thoughtsync@db:5432/thoughtsync
|
||||||
|
THOUGHTSYNC_DATA_DIR: /data
|
||||||
|
volumes:
|
||||||
|
- thoughtsync-data:/data
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
thoughtsync-db:
|
||||||
|
thoughtsync-data:
|
||||||
Reference in New Issue
Block a user