feat: build and publish release images from version tags
Tagging v* builds both images on Gitea Actions and pushes them to the instance registry, then smoke-tests the published stack. The frontend read its backend URL through import.meta.env, which Vite resolves at build time, so an image could only point at whatever the build host had; it now reads it at runtime.
This commit is contained in:
@@ -1,6 +1,15 @@
|
|||||||
# Change this secret to something random
|
# Change this secret to something random
|
||||||
CHITAI_TOKEN_SECRET=secret
|
CHITAI_TOKEN_SECRET=secret
|
||||||
|
|
||||||
|
# Which release to run. Pin this to a tag (e.g. 0.1.0) for a real deployment so a
|
||||||
|
# restart cannot silently move you to a newer image.
|
||||||
|
CHITAI_VERSION=latest
|
||||||
|
|
||||||
|
# The URL you reach the app on, exactly as it appears in the browser. Logging in fails
|
||||||
|
# with a 403 if this does not match, because SvelteKit rejects the form POST as
|
||||||
|
# cross-origin. Include the port unless it is the scheme default.
|
||||||
|
CHITAI_ORIGIN=http://localhost:3000
|
||||||
|
|
||||||
# Setup the path to your initial library (optional)
|
# Setup the path to your initial library (optional)
|
||||||
CHITAI_DEFAULT_LIBRARY_NAME=Books
|
CHITAI_DEFAULT_LIBRARY_NAME=Books
|
||||||
CHITAI_DEFAULT_LIBRARY_PATH="libraries/books"
|
CHITAI_DEFAULT_LIBRARY_PATH="libraries/books"
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
# Builds and publishes the two container images from a version tag.
|
||||||
|
#
|
||||||
|
# Tagging v1.2.3 publishes chitai-backend and chitai-frontend as 1.2.3, 1.2, 1 and latest.
|
||||||
|
# A prerelease tag (v1.2.3-rc.1) publishes only 1.2.3-rc.1 and leaves latest alone, which
|
||||||
|
# makes -rc tags a safe way to exercise this workflow.
|
||||||
|
#
|
||||||
|
# Note that `uses: docker/...` does not mean github.com here the way it would on GitHub. Gitea
|
||||||
|
# resolves a bare reference against the instance's DEFAULT_ACTIONS_URL, which defaults to
|
||||||
|
# gitea.com; all five actions below are mirrored there at these tags. If that setting is ever
|
||||||
|
# pointed somewhere without them, set it to `github` in app.ini rather than editing this file.
|
||||||
|
#
|
||||||
|
# Requires a runner with a working Docker daemon (a docker:dind sidecar, or host mode with
|
||||||
|
# the socket mounted) and, for the smoke job, the compose plugin.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.jaroszew.ski
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
# The two images are independent artifacts; don't cancel a good build for a bad one.
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- component: backend
|
||||||
|
context: ./backend
|
||||||
|
- component: frontend
|
||||||
|
context: ./frontend
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN || secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
|
- id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ github.repository }}-${{ matrix.component }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.title=chitai-${{ matrix.component }}
|
||||||
|
org.opencontainers.image.source=https://git.jaroszew.ski/${{ github.repository }}
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ${{ matrix.context }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
# Relies on the runner's cache server. If it is disabled, drop these two lines —
|
||||||
|
# a cold build of both images is only a few minutes.
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
smoke:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN || secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
|
# Boots the stack from the images that were just pushed, rather than rebuilding them.
|
||||||
|
# This is what catches migrations failing from entrypoint.sh, the frontend being unable
|
||||||
|
# to reach the backend, and a missing runtime environment variable.
|
||||||
|
- name: Boot the published images
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
cp .env.prod-example .env
|
||||||
|
echo "CHITAI_VERSION=${TAG#v}" >> .env
|
||||||
|
mkdir -p libraries
|
||||||
|
docker compose pull backend frontend
|
||||||
|
docker compose up -d --wait --wait-timeout 180
|
||||||
|
curl -fsS http://localhost:8000/healthcheck
|
||||||
|
curl -fsS http://localhost:3000/login > /dev/null
|
||||||
|
|
||||||
|
- name: Tear down
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
docker compose logs --no-color || true
|
||||||
|
docker compose down -v || true
|
||||||
@@ -26,12 +26,18 @@ eBook library management application.
|
|||||||
|
|
||||||
### Installation (Production Deployment)
|
### Installation (Production Deployment)
|
||||||
|
|
||||||
1. Clone the repository: `git clone <repository_url>` (Replace `<repository_url>` with the actual URL)
|
1. Clone the repository: `git clone https://git.jaroszew.ski/patrick/chitai.git`
|
||||||
2. Navigate to the project root: `cd chitai`
|
2. Navigate to the project root: `cd chitai`
|
||||||
3. Build the Docker images: `docker compose build`
|
3. Copy the example environment file: `cp .env.prod-example .env`
|
||||||
4. Copy the example environment file and configure: `cp .env.prod-example .env`
|
4. Edit `.env`. At minimum set `CHITAI_TOKEN_SECRET` to something random, and set
|
||||||
5. Run the Docker containers in detached mode: `docker compose up -d`
|
`CHITAI_ORIGIN` to the URL you will reach the app on — logging in fails with a 403 if it
|
||||||
6. Access the frontend at: `http://localhost:3000/`
|
does not match. Pin `CHITAI_VERSION` to a release tag if you would rather not track `latest`.
|
||||||
|
5. Pull the released images: `docker compose pull`
|
||||||
|
6. Run the Docker containers in detached mode: `docker compose up -d`
|
||||||
|
7. Access the frontend at: `http://localhost:3000/`
|
||||||
|
|
||||||
|
To build the images from source instead of pulling them, run `docker compose build` in place of
|
||||||
|
step 5.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
|
image: git.jaroszew.ski/patrick/chitai-backend:${CHITAI_VERSION:-latest}
|
||||||
|
|
||||||
|
# Only used when the image above is not present locally. Run `docker compose build` to
|
||||||
|
# build from source instead of pulling a release.
|
||||||
build: ./backend
|
build: ./backend
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
@@ -53,6 +57,8 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
image: git.jaroszew.ski/patrick/chitai-frontend:${CHITAI_VERSION:-latest}
|
||||||
|
|
||||||
build: ./frontend
|
build: ./frontend
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
@@ -66,6 +72,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
VITE_BACKEND_API_URL: ${CHITAI_API_URL}
|
VITE_BACKEND_API_URL: ${CHITAI_API_URL}
|
||||||
|
|
||||||
|
# Must match the URL the browser uses, or SvelteKit rejects form POSTs as cross-origin.
|
||||||
|
ORIGIN: ${CHITAI_ORIGIN:-http://localhost:3000}
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
# Building release images in CI
|
||||||
|
|
||||||
|
Design for publishing `chitai-backend` and `chitai-frontend` container images from a tagged
|
||||||
|
release, on Gitea Actions, to the Gitea package registry, for `linux/amd64`.
|
||||||
|
|
||||||
|
## What exists today
|
||||||
|
|
||||||
|
- `origin` is Gitea 1.27 at `git.jaroszew.ski`. The repo is **public**, with `has_actions` and
|
||||||
|
`has_packages` both true. There is no `.gitea/` or `.github/` directory and no CI of any kind.
|
||||||
|
- There are **no tags** in the repository. `backend/pyproject.toml` says `0.1.0`,
|
||||||
|
`frontend/package.json` says `0.0.1`; nothing reads either.
|
||||||
|
- `backend/Dockerfile` and `frontend/Dockerfile` are both multi-stage and both already build a
|
||||||
|
runnable image. `docker-compose.yml` builds them from source with `build: ./backend` and
|
||||||
|
`build: ./frontend` — there is no `image:` key, so there is nothing for a user to pull.
|
||||||
|
|
||||||
|
So the work is not "make the images build" — they build. It is "make an image built on one machine
|
||||||
|
correct on another", then automate producing one per tag.
|
||||||
|
|
||||||
|
## The blocker: the frontend image hardcodes the backend URL
|
||||||
|
|
||||||
|
This has to be fixed before publishing an image is meaningful.
|
||||||
|
|
||||||
|
`frontend/src/lib/server/config.ts` reads the backend URL through `import.meta.env`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const BACKEND_API_URL = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:8000';
|
||||||
|
```
|
||||||
|
|
||||||
|
Vite replaces `import.meta.env.VITE_*` **at build time**, including in the SSR bundle. The current
|
||||||
|
build output in `frontend/build/` shows exactly what that produces:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// frontend/build/server/chunks/config-BvKh7uym.js
|
||||||
|
const BACKEND_API_URL = "http://localhost:8000";
|
||||||
|
```
|
||||||
|
|
||||||
|
`frontend/Dockerfile` sets no `VITE_BACKEND_API_URL` before `pnpm run build`, so the string baked
|
||||||
|
into any image built from it is `http://localhost:8000`. The `VITE_BACKEND_API_URL: ${CHITAI_API_URL}`
|
||||||
|
entry under the compose `frontend` service is therefore **dead** — it sets a process environment
|
||||||
|
variable that nothing reads, in a process whose value was decided at build time. Inside the
|
||||||
|
container `localhost:8000` is the frontend's own port, not the backend.
|
||||||
|
|
||||||
|
Two ways out, and only one of them is right for a published image:
|
||||||
|
|
||||||
|
- **Build arg.** `ARG VITE_BACKEND_API_URL` in the build stage. This works, but it makes the image
|
||||||
|
specific to one deployment's topology — CI would bake `http://backend:8000` and anyone whose
|
||||||
|
service is named differently gets an image that cannot be repointed. Wrong for a release artifact.
|
||||||
|
- **Runtime env, recommended.** Read it through SvelteKit's dynamic env, which is `process.env` at
|
||||||
|
request time:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
export const BACKEND_API_URL = env.VITE_BACKEND_API_URL || 'http://localhost:8000';
|
||||||
|
```
|
||||||
|
|
||||||
|
`$lib/server/config.ts` is server-only and its one consumer (`$lib/server/api.ts`) is too, so
|
||||||
|
`$env/dynamic/private` is available everywhere it is used. The compose entry then starts working
|
||||||
|
as written, and the same image serves any deployment.
|
||||||
|
|
||||||
|
Worth renaming the variable to `CHITAI_API_URL` at the same time — the `VITE_` prefix now means
|
||||||
|
the opposite of what it does — but that is a follow-up, not a prerequisite. If you do rename it,
|
||||||
|
the fallback in `.env.prod-example` and the compose `environment:` block move with it.
|
||||||
|
|
||||||
|
Same class of problem, same fix window: `frontend/Dockerfile` sets `ENV ORIGIN=http://localhost:3000`.
|
||||||
|
That one *is* read at runtime by `adapter-node`, so it can be overridden — but nothing overrode it,
|
||||||
|
and adapter-node rejects cross-origin form POSTs when `ORIGIN` does not match the browser's, so
|
||||||
|
every deployment behind a real domain 403s on its first login. Now set as
|
||||||
|
`ORIGIN: ${CHITAI_ORIGIN:-http://localhost:3000}` on the compose `frontend` service, with a
|
||||||
|
documented `CHITAI_ORIGIN` in `.env.prod-example`.
|
||||||
|
|
||||||
|
**Both are done.** The built module now reads `private_env.VITE_BACKEND_API_URL`, and the published
|
||||||
|
image was verified by running it with `VITE_BACKEND_API_URL` pointed at a throwaway listener: the
|
||||||
|
container's `GET /access/me` arrived there rather than at `localhost:8000`.
|
||||||
|
|
||||||
|
## Toolchain pinning
|
||||||
|
|
||||||
|
Two reproducibility gaps that a release pipeline turns from cosmetic into real, because CI builds
|
||||||
|
from a clean container every time and your laptop does not.
|
||||||
|
|
||||||
|
- **pnpm has no pin.** `frontend/package.json` has no `packageManager` field, so `corepack enable`
|
||||||
|
followed by `pnpm install` resolves to whatever version corepack considers current on the day the
|
||||||
|
build runs. `pnpm-lock.yaml` is `lockfileVersion: 9.0`, i.e. pnpm 9/10; a future pnpm 11 could
|
||||||
|
refuse it, and `--frozen-lockfile` would fail a release for reasons unrelated to the release. Add
|
||||||
|
`"packageManager": "pnpm@<version from your nix shell>"` and let corepack honour it.
|
||||||
|
- **`pnpm-workspace.yaml` is never copied into the image.** It carries `onlyBuiltDependencies`
|
||||||
|
(`esbuild`, `@tailwindcss/oxide`), and pnpm 10 blocks postinstall scripts that are not listed
|
||||||
|
there. Both install stages in `frontend/Dockerfile` copy only `pnpm-lock.yaml` and `package.json`,
|
||||||
|
so the container install runs under different rules than the local one. Copy it alongside
|
||||||
|
`package.json` in the `prod-deps` and `build` stages so the two agree.
|
||||||
|
|
||||||
|
Both are one-line changes and both belong before the first tag, not after. **Both are done** —
|
||||||
|
`packageManager` is pinned to the dev shell's `pnpm@11.20.0`, and `pnpm-workspace.yaml` is copied
|
||||||
|
into the `prod-deps` and `build` stages. `docker compose build` was re-run against the change.
|
||||||
|
|
||||||
|
## Prerequisites on the Gitea instance
|
||||||
|
|
||||||
|
Verify these before writing the workflow; each one fails the job in a way that looks like a bug in
|
||||||
|
the workflow.
|
||||||
|
|
||||||
|
1. **A registered `act_runner`.** Gitea Actions is enabled instance-side but does nothing without a
|
||||||
|
runner. Register one against the repo or the instance with the label `ubuntu-latest`.
|
||||||
|
2. **The runner needs a Docker daemon.** This is the single most common failure for image-building
|
||||||
|
workflows on Gitea. `act_runner` in docker mode runs each job inside a container that has no
|
||||||
|
daemon of its own. Either run a `docker:dind` sidecar next to the runner and set
|
||||||
|
`DOCKER_HOST=tcp://docker:2376` (with TLS certs shared over a volume), or run the runner in host
|
||||||
|
mode with the socket mounted. The dind sidecar is the safer of the two — mounting the host socket
|
||||||
|
into job containers gives any workflow root on the runner host.
|
||||||
|
3. **Action resolution.** A bare `uses: docker/build-push-action@v6` does not mean github.com here.
|
||||||
|
Gitea resolves it against `[actions] DEFAULT_ACTIONS_URL`, which defaults to `https://gitea.com`.
|
||||||
|
That is fine as it stands — `actions/checkout@v4`, `docker/setup-buildx-action@v3`,
|
||||||
|
`docker/login-action@v3`, `docker/metadata-action@v5` and `docker/build-push-action@v6` are all
|
||||||
|
mirrored on gitea.com at those tags (verified 2026-08-17). Worth knowing because it is where an
|
||||||
|
action reference resolves from if that setting is ever changed; `DEFAULT_ACTIONS_URL = github`
|
||||||
|
in `app.ini` is the fix if so. Writing full `https://` URLs in `uses:` also works on Gitea but
|
||||||
|
is invalid syntax on GitHub Actions, so it would cost portability for no gain.
|
||||||
|
4. **A registry credential.** Gitea auto-injects `secrets.GITEA_TOKEN`, but whether it carries
|
||||||
|
package-write scope has varied across versions. Try it first; if the push 401s, create a personal
|
||||||
|
access token with `write:package` and store it as the repo secret `REGISTRY_TOKEN`. The workflow
|
||||||
|
below reads `REGISTRY_TOKEN` with a fallback to the automatic token.
|
||||||
|
5. **Package visibility.** Gitea ties package visibility to the owner rather than offering a
|
||||||
|
per-package toggle. The repo is public, so anonymous pulls should work — confirm with a
|
||||||
|
`docker pull` from a logged-out machine after the first release, because the README will tell
|
||||||
|
people to do exactly that.
|
||||||
|
|
||||||
|
## The workflow
|
||||||
|
|
||||||
|
Written as **`.gitea/workflows/release.yml`**, triggered by tags matching `v*`. Notes on the choices
|
||||||
|
made there:
|
||||||
|
|
||||||
|
- **`github.*` context, not `gitea.*`.** Both exist on Gitea; the third-party actions read the
|
||||||
|
`GITHUB_*` environment anyway, and using it keeps the file portable if the repo is ever mirrored.
|
||||||
|
`github.repository` is `patrick/chitai`, so the images are
|
||||||
|
`git.jaroszew.ski/patrick/chitai-backend` and `…/chitai-frontend`.
|
||||||
|
- **Tags produced from `v1.2.3`:** `1.2.3`, `1.2`, `1`, and `latest`. `metadata-action`'s default
|
||||||
|
`latest=auto` flavour adds `latest` only for a non-prerelease semver, so `v0.2.0-rc.1` publishes
|
||||||
|
`0.2.0-rc.1` and leaves `latest` where it was. That is what makes release-candidate tags a safe
|
||||||
|
way to exercise the pipeline.
|
||||||
|
- **`fail-fast: false`** so a frontend failure does not cancel a backend build that was going to
|
||||||
|
succeed. The two images are independent artifacts; a half-published release is easier to reason
|
||||||
|
about than a cancelled one.
|
||||||
|
- **`type=gha` cache** relies on `act_runner`'s built-in cache server exporting
|
||||||
|
`ACTIONS_CACHE_URL` / `ACTIONS_RUNTIME_TOKEN`. If your runner has caching disabled, buildx warns
|
||||||
|
and continues, or errors depending on version — just delete the two `cache-*` lines. The
|
||||||
|
Dockerfiles' `--mount=type=cache` blocks do nothing across ephemeral runners either way, and a
|
||||||
|
cold build of both images is a few minutes.
|
||||||
|
|
||||||
|
## Gating: what the release job should and should not run
|
||||||
|
|
||||||
|
Do **not** put `pytest`, `pnpm check` or `pnpm lint` in front of the push. Per `TODO.md` and
|
||||||
|
`frontend/AGENTS.md` those are all currently red: `ruff check src/` reports 114 errors,
|
||||||
|
`pnpm check` has a documented baseline of 30 errors, `pnpm lint` 131. Wiring them into the release
|
||||||
|
path means the first tag fails for reasons that have nothing to do with the release, and the
|
||||||
|
predictable response is to disable the gate. Cleaning those up is worthwhile and is its own task —
|
||||||
|
`TODO.md` already tracks it as "Nothing gates formatting, linting or types".
|
||||||
|
|
||||||
|
Also skip `pytest` here specifically: it needs Docker for `pytest-databases`, which means
|
||||||
|
docker-in-docker-in-docker on the runner, for a suite that tests code paths the image build does not
|
||||||
|
affect.
|
||||||
|
|
||||||
|
What *is* worth gating on is that the images actually start, which is the failure mode a release
|
||||||
|
introduces and which nothing else catches. That is the `smoke` job in the same workflow: it copies
|
||||||
|
`.env.prod-example`, pins `CHITAI_VERSION` to the tag, `docker compose pull`s the images that were
|
||||||
|
just pushed and brings the stack up with `--wait`, then curls both healthchecks.
|
||||||
|
|
||||||
|
This is cheap and it covers the three things that break a release image: migrations failing to apply
|
||||||
|
from `entrypoint.sh`, the frontend being unable to reach the backend (the baked-URL bug above —
|
||||||
|
`--wait` fails because the frontend never goes healthy), and a missing runtime env var. It tests the
|
||||||
|
artifact that was pushed, not a rebuild of it.
|
||||||
|
|
||||||
|
`--wait` depends on healthchecks. `db` declares one inline in `docker-compose.yml`, and the backend
|
||||||
|
image carries a `HEALTHCHECK` in its Dockerfile which compose inherits. The frontend had neither, so
|
||||||
|
one was added to `frontend/Dockerfile` — in the image rather than in compose, matching the backend
|
||||||
|
and covering anyone running the image without compose. It probes `/login` with **node's global
|
||||||
|
`fetch`, not curl**, because `node:24-slim` ships no curl and adding one for a healthcheck is a
|
||||||
|
package and a CVE surface for nothing. It reads `PORT` so it keeps working if the port is
|
||||||
|
overridden.
|
||||||
|
|
||||||
|
Note the smoke job runs `cp .env.prod-example .env`, which is destructive on a developer machine —
|
||||||
|
it is safe only because a CI checkout has no `.env`. Don't run those lines locally.
|
||||||
|
|
||||||
|
## Making the images consumable
|
||||||
|
|
||||||
|
The images are pointless if `docker-compose.yml` still builds from source. Both services now carry
|
||||||
|
`image:` **and** keep `build:` — compose pulls when the image is absent, and `docker compose build`
|
||||||
|
still builds from source and tags the result under the same name:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backend:
|
||||||
|
image: git.jaroszew.ski/patrick/chitai-backend:${CHITAI_VERSION:-latest}
|
||||||
|
build: ./backend
|
||||||
|
```
|
||||||
|
|
||||||
|
`CHITAI_VERSION` and `CHITAI_ORIGIN` are documented in `.env.prod-example`, and the README's install
|
||||||
|
steps now copy and edit `.env` *before* pulling — the pull cannot resolve `${CHITAI_VERSION}` until
|
||||||
|
that file exists, so the old ordering would have silently fetched `latest`.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Adopt `vMAJOR.MINOR.PATCH`. The tag is the source of truth; `metadata-action` derives everything
|
||||||
|
from it and the OCI labels record it. There is no tag today, so the first release is a decision —
|
||||||
|
`v0.1.0` matches `backend/pyproject.toml` and is the honest number for an app with an open
|
||||||
|
"any authenticated user can delete any library" item.
|
||||||
|
|
||||||
|
Do not automate bumping `pyproject.toml` and `package.json` from the tag. It requires CI to commit
|
||||||
|
back to the repo, and neither version is read by anything. Either keep a two-line manual checklist
|
||||||
|
(bump both, commit, tag) or delete `frontend/package.json`'s version field and treat the backend's
|
||||||
|
as the product version. Bumping by hand is the smaller cost.
|
||||||
|
|
||||||
|
## Order of work
|
||||||
|
|
||||||
|
- [x] **Runtime config.** `frontend/src/lib/server/config.ts` → `$env/dynamic/private`; `ORIGIN` on
|
||||||
|
the compose frontend service; `CHITAI_ORIGIN` in `.env.prod-example`.
|
||||||
|
- [x] **Toolchain pins.** `packageManager` in `frontend/package.json`; `pnpm-workspace.yaml` copied
|
||||||
|
in both `frontend/Dockerfile` install stages.
|
||||||
|
- [x] **The workflow.** `.gitea/workflows/release.yml`, both jobs.
|
||||||
|
- [x] **Consumable images.** `image:` keys in `docker-compose.yml`, frontend `HEALTHCHECK`,
|
||||||
|
`CHITAI_VERSION`, README install steps.
|
||||||
|
- [ ] **Stand up `act_runner` with a working Docker daemon.** Not something the repo can carry.
|
||||||
|
Prove it with a throwaway workflow running `docker version` before trusting the release one.
|
||||||
|
- [ ] **Tag `v0.1.0-rc.1`.** Confirm two images land in the registry, that `latest` was *not* moved,
|
||||||
|
and that the smoke job goes green.
|
||||||
|
- [ ] **Confirm an anonymous `docker pull`** from a logged-out machine, since the README tells people
|
||||||
|
to do exactly that.
|
||||||
|
- [ ] **Tag `v0.1.0`.**
|
||||||
|
|
||||||
|
Everything the repository can hold is in place; what remains is instance-side and needs a real tag
|
||||||
|
to exercise. Verified locally along the way: `docker compose build` succeeds against both changed
|
||||||
|
Dockerfiles, `docker compose config` resolves the image names, and the built frontend image
|
||||||
|
honours `VITE_BACKEND_API_URL` at runtime and reports `healthy` to Docker.
|
||||||
|
|
||||||
|
## Deferred, deliberately
|
||||||
|
|
||||||
|
- **Multi-arch.** amd64 only for now. Adding `linux/arm64` under QEMU roughly triples the job and the
|
||||||
|
Python and pnpm installs are exactly the workload emulation is worst at; a native arm runner and a
|
||||||
|
fan-out/merge workflow is the answer if it is ever needed.
|
||||||
|
- **Building on `main`.** An `edge` tag from every push to `main` is a two-line addition to the same
|
||||||
|
workflow (`on: push: branches: [main]` plus `type=raw,value=edge,enable={{is_default_branch}}`).
|
||||||
|
Left out because it doubles registry churn for a deployment target that does not exist yet.
|
||||||
|
- **PR CI.** Formatting, lint, type and test gates are a separate workflow on a separate trigger and
|
||||||
|
should not be mixed into the release path — see the gating section.
|
||||||
|
- **Gitea Releases.** A job creating a release object with generated notes is nice-to-have and can
|
||||||
|
be added once the tags mean something.
|
||||||
|
- **Signing / SBOM / provenance.** `build-push-action` can emit provenance and an SBOM, and cosign
|
||||||
|
can sign the digests. Worth it if the images are ever consumed by anyone other than you; not worth
|
||||||
|
the key management before then.
|
||||||
|
- **`backend/.dockerignore` is thin.** It excludes `.git`, `__pycache__` and the ruff cache, but not
|
||||||
|
`.venv/`, `.postgres/`, `libraries/`, `covers/` or `tests/`. None of those reach the image — the
|
||||||
|
Dockerfile copies specific paths — and none exist in a CI checkout since they are gitignored, so
|
||||||
|
this does not affect the pipeline. It does make local builds slower than they need to be.
|
||||||
+7
-2
@@ -11,7 +11,9 @@ WORKDIR /app
|
|||||||
|
|
||||||
FROM base AS prod-deps
|
FROM base AS prod-deps
|
||||||
|
|
||||||
COPY pnpm-lock.yaml ./
|
# pnpm-workspace.yaml carries onlyBuiltDependencies; without it pnpm blocks the postinstall
|
||||||
|
# scripts it lists, so the install here would not match a local one.
|
||||||
|
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/pnpm/store \
|
RUN --mount=type=cache,target=/pnpm/store \
|
||||||
pnpm fetch --frozen-lockfile
|
pnpm fetch --frozen-lockfile
|
||||||
@@ -25,7 +27,7 @@ RUN --mount=type=cache,target=/pnpm/store \
|
|||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
|
|
||||||
COPY pnpm-lock.yaml package.json ./
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/pnpm/store \
|
RUN --mount=type=cache,target=/pnpm/store \
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
@@ -54,4 +56,7 @@ EXPOSE 3000
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=3s --retries=5 --start-period=10s \
|
||||||
|
CMD ["node", "-e", "fetch(`http://127.0.0.1:${process.env.PORT || 3000}/login`).then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
|
||||||
|
|
||||||
CMD [ "node", "build" ]
|
CMD [ "node", "build" ]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "chitai-web",
|
"name": "chitai-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"packageManager": "pnpm@11.20.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
export const BACKEND_API_URL = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:8000';
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
|
// Read at runtime, not build time: `import.meta.env` is substituted by Vite during the build, so a
|
||||||
|
// published image would carry whatever the build host had — see docs/ci-release-pipeline.md.
|
||||||
|
export const BACKEND_API_URL = env.VITE_BACKEND_API_URL || 'http://localhost:8000';
|
||||||
|
|||||||
Reference in New Issue
Block a user