10 Commits

Author SHA1 Message Date
jeanGaston abd3daea7e chore: bump version to 1.0.3
Publish Docker image / build-and-push (push) Successful in 4m23s
2026-07-30 17:35:59 +02:00
jeanGaston 0da6dc7810 fix(prisma): track migration SQL files in git
migration.sql was gitignored, so CI builds a Docker image from a
fresh clone with empty migration directories. prisma migrate deploy
then has nothing to apply, leaving the database schema-less on first
launch (P2021: table does not exist).
2026-07-30 17:32:40 +02:00
jeanGaston d8d152e249 chore: bump version to 1.0.2
Publish Docker image / build-and-push (push) Successful in 4m46s
2026-07-30 16:40:56 +02:00
jeanGaston 75ea9b7416 fix(docker): fix data volume ownership on fresh bind mounts
/app/data is a bind mount, so the chown baked into the image is
overwritten by the host directory's ownership at runtime. On a fresh
prod deploy Docker creates ./data as root-owned, but the container ran
as the non-root nextjs user, so Prisma couldn't open lovelope.db
("unable to open database file").

Add an entrypoint that starts as root, chowns /app/data, then drops to
nextjs via su-exec before running migrations and starting the server.
2026-07-30 16:40:45 +02:00
jeanGaston cd36fc5723 docs: document the dev/tag release process in GIT_WORKFLOW.md
Spells out the two-channel CI trigger (dev branch for test builds, v*
tags for releases) as an actual step-by-step process: keeping dev in
sync with main/master, bumping package.json, tagging, and the
required Gitea repo secrets/variables.
2026-07-30 16:35:45 +02:00
jeanGaston 3987a4e43e chore: bump version to 1.0.1
Publish Docker image / build-and-push (push) Successful in 3m22s
2026-07-30 15:47:12 +02:00
jeanGaston 5ca66f3ed7 chore: rename database file from askedout.db to lovelope.db
Publish Docker image / build-and-push (push) Successful in 3m34s
Leftover naming from before the app was renamed to lovelope. Renamed
the local data/askedout.db file to data/lovelope.db to match (data/
is gitignored, so this doesn't touch version control).
2026-07-30 15:46:21 +02:00
jeanGaston c36d146fef chore: bump version to 1.0.0 for first release
Publish Docker image / build-and-push (push) Successful in 3m39s
2026-07-30 15:28:37 +02:00
jeanGaston 3ac68a88d0 docs: lead Quick start with the published GHCR image
Publish Docker image / build-and-push (push) Successful in 3m31s
Running the app no longer requires cloning the repo: pull the
published ghcr.io image directly via docker-compose. Move the
build-from-source flow (existing docker-compose.yml + Dockerfile) to
a secondary section for contributors.
2026-07-30 12:03:29 +02:00
jeanGaston 94b02198dd ci: build images from a dev branch and release tags, not every commit
Every push to master was triggering a build+push, since that's where
day-to-day commits land. Switch to two deliberate channels instead:
pushing to dev produces a rolling :dev tag for testing, and pushing a
v* tag produces :latest + the version tag for real releases. Plain
commits/merges to master/main no longer build anything on their own.
2026-07-30 12:03:20 +02:00
11 changed files with 198 additions and 24 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# SQLite database file (mounted on a persistent volume in Docker)
DATABASE_URL="file:/app/data/askedout.db"
DATABASE_URL="file:/app/data/lovelope.db"
# Public base URL (used in QR codes and share links)
APP_URL="http://localhost:3000"
+24 -6
View File
@@ -1,8 +1,12 @@
name: Publish Docker image
# Two channels, not one-image-per-commit:
# - push to `dev` -> rolling `:dev` tag, for testing in-progress work
# - push tag `v*` -> `:latest` + the version tag, for actual releases
# Plain commits/merges to master/main build nothing on their own.
on:
push:
branches: [main, master]
branches: [dev]
tags: ['v*']
permissions:
@@ -20,6 +24,24 @@ jobs:
- name: Compute lowercase image name
run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- name: Compute image tags
run: |
if [ "${{ github.ref_type }}" = "tag" ]; then
VERSION="${{ github.ref_name }}"
TAGS="${{ vars.REGISTRY_HOST }}/${IMAGE_NAME}:latest
${{ vars.REGISTRY_HOST }}/${IMAGE_NAME}:${VERSION}
ghcr.io/${IMAGE_NAME}:latest
ghcr.io/${IMAGE_NAME}:${VERSION}"
else
TAGS="${{ vars.REGISTRY_HOST }}/${IMAGE_NAME}:dev
ghcr.io/${IMAGE_NAME}:dev"
fi
{
echo "TAGS<<TAGS_EOF"
echo "$TAGS"
echo "TAGS_EOF"
} >> "$GITHUB_ENV"
- name: Log in to Gitea container registry
uses: docker/login-action@v3
with:
@@ -39,8 +61,4 @@ jobs:
with:
context: .
push: true
tags: |
${{ vars.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}:latest
${{ vars.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
ghcr.io/${{ env.IMAGE_NAME }}:latest
ghcr.io/${{ env.IMAGE_NAME }}:${{ github.sha }}
tags: ${{ env.TAGS }}
-3
View File
@@ -38,8 +38,5 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
# prisma
prisma/migrations/*/migration.sql
# local Claude Code tooling (skills, agent config) - not part of the app
/.claude
+4 -4
View File
@@ -28,7 +28,7 @@ RUN npm run build
# ── Stage 3: runner ────────────────────────────────────────────────────────────
FROM node:20-alpine AS runner
RUN apk add --no-cache libc6-compat openssl
RUN apk add --no-cache libc6-compat openssl su-exec
WORKDIR /app
ENV NODE_ENV=production
@@ -48,11 +48,11 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
USER nextjs
COPY --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Run migrations then start the server
CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"]
# Fix ownership of the bind-mounted /app/data volume, then drop to nextjs
ENTRYPOINT ["./docker-entrypoint.sh"]
+61 -3
View File
@@ -46,10 +46,68 @@ Types: `feat`, `fix`, `refactor`, `style`, `docs`, `chore`, `test`.
- Never force-push `main`. Force-pushing a feature branch you own (after a
rebase) is fine; never force-push a branch someone else might be using.
## Tags / releases
## Releases & Docker images
Tag production releases as `v<major>.<minor>.<patch>` (semver) once this
project has a deployment cadence worth marking. Not required for every merge.
CI (`.gitea/workflows/docker-publish.yml`) never builds an image from a
plain commit or merge to `main`/`master` — only from two deliberate
triggers, each producing tags on both the Gitea registry and GHCR:
| Trigger | Produces | Purpose |
|------------------|--------------------------|-------------------------------------|
| push to `dev` | `:dev` | test a build before releasing |
| push a `v*` tag | `:latest` + `:vX.Y.Z` | ship an actual release |
### Testing a build (`dev`)
`dev` is a disposable pointer, not a branch with history of its own — it
should always match `main`/`master` exactly. Bring it up to date and push
whenever you want a fresh test image:
```
git checkout dev
git merge main --ff-only
git push origin dev
```
If `dev` has drifted and won't fast-forward (e.g. after a rebase, or a
commit landed only on `dev` by mistake), reset it instead of merging —
`dev` has no unique history worth preserving:
```
git checkout dev
git reset --hard main
git push origin dev --force-with-lease
```
### Shipping a release (tag)
1. Bump the version in `package.json` on `main`/`master` (a plain commit —
builds nothing on its own):
```
# edit package.json: "version": "X.Y.Z"
git add package.json
git commit -m "chore: bump version to X.Y.Z"
git push origin main
```
2. Tag that commit and push the tag — this is what actually triggers the
release build:
```
git tag vX.Y.Z
git push origin vX.Y.Z
```
3. Never move or re-push an existing tag once it's out (it may already be
pulled/deployed). If you need to fix something after tagging, bump to
the next patch version and tag again instead.
Use `v<major>.<minor>.<patch>` (semver) for tags.
### Required repo secrets/variables (Gitea → Settings → Actions)
- `vars.REGISTRY_HOST` — the Gitea instance hostname (no protocol), used to
tag/push to the built-in Gitea container registry.
- `secrets.REGISTRY_TOKEN` — a Gitea PAT with `write:package` scope.
- `secrets.GHCR_TOKEN` — a GitHub PAT with `write:packages` scope, used to
also push to `ghcr.io`.
## What not to commit
+36 -5
View File
@@ -10,7 +10,40 @@ No accounts, no friction, just a sweet little link. Everything personal is encry
## Quick start
You need [Docker](https://www.docker.com/) installed. That's it.
You need [Docker](https://www.docker.com/) installed. That's it — no clone required, the image is published on GHCR.
Create a `docker-compose.yml`:
```yaml
services:
app:
image: ghcr.io/jeangaston/lovelope:latest
ports:
- "3000:3000"
environment:
DATABASE_URL: "file:/app/data/lovelope.db"
APP_URL: "http://localhost:3000"
GIPHY_API_KEY: "${GIPHY_API_KEY:-}"
volumes:
- ./data:/app/data
restart: unless-stopped
```
Then:
```bash
docker compose up -d
```
Open **http://localhost:3000**. Your data lives in `./data/lovelope.db` on your machine.
Want GIF search to work? Grab a free key at [developers.giphy.com](https://developers.giphy.com/), export it as `GIPHY_API_KEY` before running `docker compose up` (or drop it in a `.env` file next to `docker-compose.yml`), then restart. Without it, the app still runs fine, GIF search is just disabled.
Pin a specific version instead of always tracking `latest` by using a release tag, e.g. `ghcr.io/jeangaston/lovelope:v1.0.0`.
### Build from source
If you'd rather build the image yourself instead of pulling from GHCR:
```bash
git clone https://github.com/jeangaston/lovelope.git && cd lovelope
@@ -18,9 +51,7 @@ cp .env.example .env
docker compose up --build
```
Open **http://localhost:3000**. Everything (the app and its database) runs in one container; your data lives in `./data/askedout.db` on your machine.
Want GIF search to work? Grab a free key at [developers.giphy.com](https://developers.giphy.com/) and put it in `.env` as `GIPHY_API_KEY`, then restart. Without it, the app still runs fine, GIF search is just disabled.
This uses the [docker-compose.yml](docker-compose.yml) in the repo, which builds from the local [Dockerfile](Dockerfile) instead of pulling a published image.
## Features
@@ -52,7 +83,7 @@ Browsers never send the part after `#` to any server, so that key never reaches
| Variable | Required | Description |
|---|---|---|
| `DATABASE_URL` | ✅ | SQLite file path, default `file:/app/data/askedout.db` works out of the box with Docker |
| `DATABASE_URL` | ✅ | SQLite file path, default `file:/app/data/lovelope.db` works out of the box with Docker |
| `APP_URL` | ✅ | Base URL used to build share links, e.g. `http://localhost:3000` |
| `GIPHY_API_KEY` | optional | Enables GIF search. Free key at [developers.giphy.com](https://developers.giphy.com/), leave blank to disable |
+1 -1
View File
@@ -6,7 +6,7 @@ services:
ports:
- "3000:3000"
environment:
DATABASE_URL: "file:/app/data/askedout.db"
DATABASE_URL: "file:/app/data/lovelope.db"
APP_URL: "http://localhost:3000"
NODE_ENV: "development"
GIPHY_API_KEY: "${GIPHY_API_KEY:-}"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
set -e
chown -R nextjs:nodejs /app/data
exec su-exec nextjs sh -c "npx prisma migrate deploy && node server.js"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "lovelope",
"version": "0.1.0",
"version": "1.0.3",
"private": true,
"license": "MIT",
"scripts": {
@@ -0,0 +1,62 @@
-- CreateTable
CREATE TABLE "Proposal" (
"id" TEXT NOT NULL PRIMARY KEY,
"slug" TEXT NOT NULL,
"manageToken" TEXT NOT NULL,
"senderName" TEXT NOT NULL,
"recipientName" TEXT NOT NULL,
"title" TEXT NOT NULL,
"message" TEXT NOT NULL,
"theme" TEXT NOT NULL DEFAULT 'sunset',
"gradientFrom" TEXT,
"gradientVia" TEXT,
"gradientTo" TEXT,
"gifUrl" TEXT,
"evasiveNo" BOOLEAN NOT NULL DEFAULT false,
"status" TEXT NOT NULL DEFAULT 'draft',
"expiresAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "ActivityOption" (
"id" TEXT NOT NULL PRIMARY KEY,
"proposalId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"emoji" TEXT NOT NULL DEFAULT '🎉',
"order" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "ActivityOption_proposalId_fkey" FOREIGN KEY ("proposalId") REFERENCES "Proposal" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "TimeSlot" (
"id" TEXT NOT NULL PRIMARY KEY,
"activityOptionId" TEXT NOT NULL,
"label" TEXT NOT NULL,
"startsAt" TEXT,
CONSTRAINT "TimeSlot_activityOptionId_fkey" FOREIGN KEY ("activityOptionId") REFERENCES "ActivityOption" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Response" (
"id" TEXT NOT NULL PRIMARY KEY,
"proposalId" TEXT NOT NULL,
"selectedActivityId" TEXT,
"selectedTimeSlotId" TEXT,
"answer" TEXT NOT NULL,
"note" TEXT,
"respondedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Response_proposalId_fkey" FOREIGN KEY ("proposalId") REFERENCES "Proposal" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "Response_selectedActivityId_fkey" FOREIGN KEY ("selectedActivityId") REFERENCES "ActivityOption" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Response_selectedTimeSlotId_fkey" FOREIGN KEY ("selectedTimeSlotId") REFERENCES "TimeSlot" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Proposal_slug_key" ON "Proposal"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Proposal_manageToken_key" ON "Proposal"("manageToken");
-- CreateIndex
CREATE UNIQUE INDEX "Response_proposalId_key" ON "Response"("proposalId");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ActivityOption" ADD COLUMN "location" TEXT;