docs: setup files and the client, shared and CI docs describe what is (#6277)
.dockerignore, .gitignore, deploy/initdb, ci-requirements.md, client/README.md, shared/README.md and two web/scripts test names: dates, issue/rule/spike ids and the stories behind each exclusion removed. Corrected: - client/README.md: the dev lane bundles its own APK; :main no longer exists; main bundles the recorded sidecar instead of re-deriving the version name. Its smoke-test step wrote a bare "v0.2.0", which the server rejects as a sidecar that is not JSON; it now writes the JSON shape, which the README documents. - ci-requirements.md: one workflow, not four; the android lane builds no debug APK; main reads the recorded sidecar. - .gitignore: CI fills client/ on every image build, not tag releases only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-6
@@ -9,12 +9,8 @@ web/build
|
|||||||
# The Android client — built by its own job, never from this context. The APK
|
# The Android client — built by its own job, never from this context. The APK
|
||||||
# reaches the image through client/, downloaded as a CI artifact, so nothing
|
# reaches the image through client/, downloaded as a CI artifact, so nothing
|
||||||
# here reads android/ sources.
|
# here reads android/ sources.
|
||||||
#
|
# Excluded so the Gradle project neither bloats the context nor busts the
|
||||||
# This block named `flutter_client/` until 2026-09-10 and lost its PATTERN when
|
# `COPY . .` layer on every Android-only change.
|
||||||
# that tree was deleted, leaving a comment describing an exclusion that was no
|
|
||||||
# longer happening. android/ never took its place, so 4.1 MB of Gradle project
|
|
||||||
# has been entering the context and busting the `COPY . .` layer on every
|
|
||||||
# Android-only change.
|
|
||||||
android/
|
android/
|
||||||
|
|
||||||
# Local `make build` output — an 18 MB binary the image never uses, since the
|
# Local `make build` output — an 18 MB binary the image never uses, since the
|
||||||
|
|||||||
+6
-9
@@ -12,13 +12,11 @@
|
|||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
# `make build` output. bin/minstrel was tracked until 2026-09-10 — an 18 MB
|
# `make build` output — an 18 MB binary that is never committed.
|
||||||
# binary committed by accident, last refreshed by a commit about web test
|
|
||||||
# mocks, and re-dirtied by every local build since.
|
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
# Bundled Android APK + version sidecar (#397). Populated by CI for
|
# Bundled Android APK + version sidecar. Populated by CI on every image
|
||||||
# tag releases; never committed. README in client/ explains the flow.
|
# build; never committed. README in client/ explains the flow.
|
||||||
client/minstrel.apk
|
client/minstrel.apk
|
||||||
client/minstrel.apk.version
|
client/minstrel.apk.version
|
||||||
|
|
||||||
@@ -44,10 +42,9 @@ go.work.sum
|
|||||||
docs/superpowers/
|
docs/superpowers/
|
||||||
|
|
||||||
# Per-machine Claude Code settings + remember-skill memory artifacts +
|
# Per-machine Claude Code settings + remember-skill memory artifacts +
|
||||||
# project-level Claude/AI instruction files. All operator-scoped; the
|
# project-level Claude/AI instruction files. All per-machine;
|
||||||
# canonical project memory lives under ~/.claude/projects/ outside the
|
# CLAUDE.md/AGENTS.md/GEMINI.md are tool-specific working notes, not
|
||||||
# repo. CLAUDE.md/AGENTS.md/GEMINI.md stay on the operator's machine
|
# project artifacts.
|
||||||
# only — they're tool-specific working notes, not project artifacts.
|
|
||||||
.claude/
|
.claude/
|
||||||
.remember/
|
.remember/
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
|||||||
+17
-32
@@ -5,27 +5,20 @@
|
|||||||
|
|
||||||
## Runtime images
|
## Runtime images
|
||||||
|
|
||||||
Minstrel's four workflows consume two CI images:
|
Minstrel's one workflow, `.gitea/workflows/release.yml`, consumes two CI images:
|
||||||
|
|
||||||
```
|
```
|
||||||
git.fabledsword.com/bvandeusen/ci-go:1.26
|
git.fabledsword.com/bvandeusen/ci-go:1.26
|
||||||
git.fabledsword.com/bvandeusen/ci-android:36
|
git.fabledsword.com/bvandeusen/ci-android:36
|
||||||
```
|
```
|
||||||
|
|
||||||
- `ci-go:1.26` — the `go`, `integration` and `web` lanes, `release-assets`, and the container build (`image-release`). All CI lives in one workflow, `.gitea/workflows/release.yml`, so every publishing job can depend on every lane (rule 177).
|
- `ci-go:1.26` — the `go`, `integration` and `web` lanes, `release-assets`, and the container build (`image-release`). All CI lives in that one workflow so every publishing job can depend on every lane.
|
||||||
- `golang:1.26-bookworm` (Docker Hub) — the `govulncheck` lane. Deliberately the same image as the Dockerfile's builder stage rather than `ci-go`, so the standard library it checks is the one in the shipped binary. Keep the two in step.
|
- `golang:1.26-bookworm` (Docker Hub) — the `govulncheck` lane. Deliberately the same image as the Dockerfile's builder stage rather than `ci-go`, so the standard library it checks is the one in the shipped binary. Keep the two in step.
|
||||||
- `ci-android:36` — native Kotlin/Compose client: ktlint + detekt + unit tests + debug APK (the `android` lane), and the signed release APK (`android-release`).
|
- `ci-android:36` — native Kotlin/Compose client: ktlint + detekt + unit tests (the `android` lane), and the signed release APK (`android-release`).
|
||||||
|
|
||||||
**`ci-flutter` is no longer consumed, and `ci-flutter` can now be retired.**
|
**No job uses `ci-flutter`**; nothing in this repo needs a Flutter toolchain.
|
||||||
The M8 rewrite replaced the Flutter client with the native Android app and
|
The `flutter-ci` runner *label* is still used: the Android jobs schedule on it
|
||||||
`flutter.yml` was removed; `ci-android` took its place. `flutter_client/`
|
while pulling `ci-android:36`, per the label/image split below.
|
||||||
itself was deleted on 2026-08-16, which was the condition CI-Runner was
|
|
||||||
waiting on before dropping the image — nothing in this repo needs a Flutter
|
|
||||||
toolchain any more.
|
|
||||||
|
|
||||||
Note this does **not** mean the `flutter-ci` runner *label* goes: the Android
|
|
||||||
jobs still schedule on it while pulling `ci-android:36`, per the label/image
|
|
||||||
split below. The label is a scheduling handle, not a toolchain assertion.
|
|
||||||
|
|
||||||
## Image deps used
|
## Image deps used
|
||||||
|
|
||||||
@@ -43,8 +36,7 @@ split below. The label is a scheduling handle, not a toolchain assertion.
|
|||||||
error. The workflows also set `JAVA_TOOL_OPTIONS=--enable-native-access=ALL-UNNAMED`
|
error. The workflows also set `JAVA_TOOL_OPTIONS=--enable-native-access=ALL-UNNAMED`
|
||||||
to silence Gradle's launcher-JVM restricted-method warning.
|
to silence Gradle's launcher-JVM restricted-method warning.
|
||||||
- **Android SDK + cmdline-tools + build-tools 36.0.0** — APK assembly + signing.
|
- **Android SDK + cmdline-tools + build-tools 36.0.0** — APK assembly + signing.
|
||||||
No NDK: the native client has no C/C++ sources (this is why it isn't on
|
No NDK: the app has no C/C++ sources.
|
||||||
`ci-flutter`).
|
|
||||||
- **ktlint + detekt** — `./gradlew ktlintCheck` and `./gradlew detekt` in
|
- **ktlint + detekt** — `./gradlew ktlintCheck` and `./gradlew detekt` in
|
||||||
the `android` lane. Image pins track `android/gradle/libs.versions.toml` so local
|
the `android` lane. Image pins track `android/gradle/libs.versions.toml` so local
|
||||||
and CI checks agree.
|
and CI checks agree.
|
||||||
@@ -61,32 +53,25 @@ None.
|
|||||||
- **Label/image split.** Workflows keep `runs-on: go-ci` / `runs-on: flutter-ci` as the scheduling label per the [`ci-runners.md`](https://…/FabledRulebook/ci-runners.md) "label = scheduling handle, image = `container.image`" pattern. The labels are intentional handles, not toolchain assertions — which is why the Android jobs still schedule on `flutter-ci` while pulling `ci-android:36`. Switch them to `android-ci` if that runner label is ever registered; nothing breaks either way.
|
- **Label/image split.** Workflows keep `runs-on: go-ci` / `runs-on: flutter-ci` as the scheduling label per the [`ci-runners.md`](https://…/FabledRulebook/ci-runners.md) "label = scheduling handle, image = `container.image`" pattern. The labels are intentional handles, not toolchain assertions — which is why the Android jobs still schedule on `flutter-ci` while pulling `ci-android:36`. Switch them to `android-ci` if that runner label is ever registered; nothing breaks either way.
|
||||||
- **Integration-job docker-socket dependency.** The `integration` lane uses the runner's shared docker socket (`/var/run/docker.sock`) to bridge-IP-discover the per-job Postgres service container by name + network intersection — the dev compose's `minstrel-postgres-*` containers are explicitly skipped as belt-and-suspenders. Depends on `act_runner.valid_volumes` whitelisting the socket; if that ever stops auto-mounting, integration tests fail at the `docker inspect` step.
|
- **Integration-job docker-socket dependency.** The `integration` lane uses the runner's shared docker socket (`/var/run/docker.sock`) to bridge-IP-discover the per-job Postgres service container by name + network intersection — the dev compose's `minstrel-postgres-*` containers are explicitly skipped as belt-and-suspenders. Depends on `act_runner.valid_volumes` whitelisting the socket; if that ever stops auto-mounting, integration tests fail at the `docker inspect` step.
|
||||||
- **Go toolchain pin.** `go.mod` is on `go 1.25.0` because `golang.org/x/crypto v0.51.0` declares 1.25 as its minimum. `ci-go:1.26` satisfies this with headroom. Future `x/crypto` bumps that move the Go floor should be paired with an image-tag bump in this file + the workflows.
|
- **Go toolchain pin.** `go.mod` is on `go 1.25.0` because `golang.org/x/crypto v0.51.0` declares 1.25 as its minimum. `ci-go:1.26` satisfies this with headroom. Future `x/crypto` bumps that move the Go floor should be paired with an image-tag bump in this file + the workflows.
|
||||||
- **In-app update channel — `needs:`, not polling.** `release.yml`'s `image-release` job declares `needs: [android-release]`, so on tag pushes the signed APK is guaranteed present before the image build starts — no polling window, no race. (The old cross-workflow polling against `flutter.yml` is gone with that workflow.) On non-tag `main` pushes `android-release` is skipped and `image-release` instead pulls the most recent release's APK and reconstructs its exact `versionName`, so `:latest` never ships without an update channel. It degrades to an empty `client/` — never a wrong version — if no release, asset, or tag commit-count can be resolved.
|
- **In-app update channel — `needs:`, not polling.** `release.yml`'s `image-release` job declares `needs: [android-release]`, so on dev and tag pushes the signed APK is guaranteed present before the image build starts — no polling window, no race. On `main` pushes `android-release` is skipped and `image-release` instead pulls the most recent release's APK and the version sidecar recorded beside it, so `:latest` never ships without an update channel. It degrades to an empty `client/` — never a wrong version — if no release or APK asset can be resolved.
|
||||||
- **Cache server reachability.** The `web` lane does NOT use `cache: 'npm'` on `actions/setup-node` — the Gitea Actions cache server isn't reachable from this runner's container network and `setup-node` was burning ~4m41s on ETIMEDOUT before failing open. With the migration to `ci-go:1.26`, `setup-node` is removed entirely (Node is in the image). The cache concern reappears if a future change re-introduces a network-dependent action.
|
- **Cache server reachability.** The `web` lane does NOT use `cache: 'npm'` on `actions/setup-node` — the Gitea Actions cache server isn't reachable from this runner's container network, so `setup-node` would spend minutes on ETIMEDOUT before failing open. `setup-node` is not used at all (Node is in the image). The cache concern reappears if a change introduces a network-dependent action.
|
||||||
- **Artifacts — stock `actions/upload-artifact@v7` and `actions/download-artifact@v8`; never `@v3`.**
|
- **Artifacts — stock `actions/upload-artifact@v7` and `actions/download-artifact@v8`; never `@v3`.**
|
||||||
```yaml
|
```yaml
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
```
|
```
|
||||||
Stock works on this forge since the runner moved to gitea/runner 3.x, which
|
Stock works on this forge because gitea/runner 3.x edits the actions'
|
||||||
edits the actions' client-side `isGhes()` refusal out of their bundles. Proven
|
client-side `isGhes()` refusal out of their bundles; upload v4–v7 and
|
||||||
on 2026-09-10 for upload v4–v7 and download v4–v8 (Scribe spike #3843). Until
|
download v4–v8 are proven.
|
||||||
then this repo pinned SHA mirrors of the Forgejo project's forks, because
|
|
||||||
upstream threw on the hostname before it opened a connection (Scribe 2255).
|
|
||||||
|
|
||||||
`@v3` is still broken: it reports success, and Gitea serves artifacts back only
|
`@v3` is still broken: it reports success, and Gitea serves artifacts back only
|
||||||
through the v4 API (`content_encoding = application/zip`), so a v3 upload is
|
through the v4 API (`content_encoding = application/zip`), so a v3 upload is
|
||||||
stored but invisible to every retrieval path. That is how 72 unreachable
|
stored but invisible to every retrieval path.
|
||||||
artifacts accumulated on this repo (Scribe 2270).
|
|
||||||
|
|
||||||
**Pairing no longer needs managing.** This entry used to pin upload v5 against
|
**The upload and download majors need not match.** Every download major
|
||||||
download v6 so both bundled `@actions/artifact` ^4.0.0, warning that a mismatch
|
v4–v8 reads the artifacts of every upload major v4–v7, by name and by
|
||||||
across `release.yml`'s producer/consumer pair would list empty. Tested, and not
|
pattern; the only protocol break is v3 → v4. Every CI-runner image carries
|
||||||
true on this instance: every download major v4–v8 read the artifacts of every
|
Node 24, and the runner runs actions with the image's `node`.
|
||||||
upload major v4–v7, by name and by pattern (CI-runner run 6312). The only real
|
|
||||||
protocol break is v3 → v4. node24 is no longer a concern either — every
|
|
||||||
CI-runner image carries Node 24 and the runner runs actions with the image's
|
|
||||||
`node`.
|
|
||||||
|
|
||||||
Upload steps set `if-no-files-found: error` rather than the default `warn`, so
|
Upload steps set `if-no-files-found: error` rather than the default `warn`, so
|
||||||
an upload that matches nothing fails its own job instead of failing the
|
an upload that matches nothing fails its own job instead of failing the
|
||||||
|
|||||||
+38
-25
@@ -1,45 +1,58 @@
|
|||||||
# Bundled client assets
|
# Bundled client assets
|
||||||
|
|
||||||
Holds the Android APK (`minstrel.apk` + `minstrel.apk.version`) that the
|
Holds the Android APK (`minstrel.apk`) and its version sidecar
|
||||||
server serves via `/api/client/version` and `/api/client/apk` for the
|
(`minstrel.apk.version`) that the server serves via `/api/client/version`
|
||||||
in-app update flow (#397).
|
and `/api/client/apk` for the in-app update flow.
|
||||||
|
|
||||||
## Production
|
## Production
|
||||||
|
|
||||||
CI populates this directory in `release.yml` (no Flutter — the client is
|
CI fills this directory in `release.yml` before every image build:
|
||||||
native Android, built by the `android-release` job):
|
|
||||||
|
|
||||||
- **Tag release (`v*`):** the `android-release` job builds + signs the
|
- **Dev push (`:dev`) and tag release (`v*`):** the `android-release` job
|
||||||
APK, uploads it as a workflow artifact, and attaches it to the Gitea
|
builds and signs the APK and uploads it as a workflow artifact;
|
||||||
release as `minstrel-<TAG>.apk`. The `image-release` job
|
`image-release` downloads it here as `minstrel.apk` and writes the
|
||||||
(`needs: android-release`) downloads that artifact into this directory
|
sidecar from that build's version outputs. A tag release also attaches
|
||||||
as `minstrel.apk` and writes the computed versionName to
|
both files to the Gitea release.
|
||||||
`minstrel.apk.version`.
|
- **Main push (`:latest` + `:<sha>`):** no APK is built, but the image
|
||||||
- **Main push (non-tag `:latest` / `:main`):** no APK is built, but the
|
still moves `:latest`, so `image-release` downloads the most recent
|
||||||
image still moves `:latest`, so `image-release` pulls the most-recent
|
release's APK and the sidecar recorded beside it. `:latest` keeps its
|
||||||
release's signed APK from the Gitea API and reconstructs its exact
|
update channel between releases.
|
||||||
versionName — keeping the in-app update channel alive on `:latest`
|
|
||||||
instead of stripping it until the next tag.
|
|
||||||
|
|
||||||
Either way, `docker buildx build` bakes both files into the image via
|
Either way, `docker buildx build` bakes both files into the image via
|
||||||
`COPY client/ /app/client/`.
|
`COPY client/ /app/client/`.
|
||||||
|
|
||||||
|
## The sidecar
|
||||||
|
|
||||||
|
`minstrel.apk.version` is JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"name": "2026.10.23.1200", "code": 3549060, "channel": "dev"}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `name` is the version name the app shows.
|
||||||
|
- `code` is the ordering key Android installs by. `null` means unknown,
|
||||||
|
and clients then compare names.
|
||||||
|
- `channel` says which lane built it.
|
||||||
|
|
||||||
|
A sidecar that is not valid JSON, or has no `name`, is an error rather
|
||||||
|
than a blank version.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Empty directory works fine — the endpoints return 404 and the Android
|
An empty directory works fine: the endpoints return 404 and the Android
|
||||||
update banner stays hidden ("no update channel" graceful degradation).
|
update banner stays hidden.
|
||||||
|
|
||||||
To smoke-test the update flow locally, drop a real APK + a version
|
To smoke-test the update flow locally, drop a real APK and a sidecar into
|
||||||
file into this directory:
|
this directory:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cp /path/to/app-release.apk client/minstrel.apk
|
cp /path/to/app-release.apk client/minstrel.apk
|
||||||
echo "v0.2.0" > client/minstrel.apk.version
|
echo '{"name":"2099.01.01.0000","code":null,"channel":"dev"}' > client/minstrel.apk.version
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why a directory and not embed.FS?
|
## Why a directory and not embed.FS?
|
||||||
|
|
||||||
The APK is 30-60 MB. Embedding bloats the Go binary and slows
|
The APK is 30-60 MB. Embedding bloats the Go binary and slows `go build`
|
||||||
`go build` for everyone, even when the APK isn't being changed.
|
for everyone, even when the APK isn't being changed. File-on-disk also
|
||||||
File-on-disk also lets operators override at runtime via volume
|
lets an admin override it at runtime with a volume mount on
|
||||||
mount on `/app/client/` if they want to ship their own build.
|
`/app/client/` to ship their own build.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
-- Runs once, only on a fresh Postgres data volume (Docker entrypoint
|
-- Runs once, only on a fresh Postgres data volume (Docker entrypoint
|
||||||
-- initdb hook). Creates the dedicated integration-test database so
|
-- initdb hook). Creates the dedicated integration-test database so
|
||||||
-- `go test` never truncates the operator's dev `minstrel` DB (Fable
|
-- `go test` never truncates the dev `minstrel` DB. For an already-initialised volume this script does NOT run;
|
||||||
-- #339). For an already-initialised volume this script does NOT run;
|
|
||||||
-- `make test-integration` creates the DB idempotently instead.
|
-- `make test-integration` creates the DB idempotently instead.
|
||||||
CREATE DATABASE minstrel_test OWNER minstrel;
|
CREATE DATABASE minstrel_test OWNER minstrel;
|
||||||
|
|||||||
+2
-6
@@ -3,12 +3,8 @@
|
|||||||
`fabledsword.tokens.json` is the canonical statement of the FabledSword
|
`fabledsword.tokens.json` is the canonical statement of the FabledSword
|
||||||
palette — the dark, light and flat cohorts in one machine-readable place.
|
palette — the dark, light and flat cohorts in one machine-readable place.
|
||||||
|
|
||||||
It lived under `flutter_client/shared/` until that client was deleted
|
The Android theme (`FabledSwordTokens.kt`) names it as its source of
|
||||||
(2026-08-16). It was never Flutter-specific: the Android theme
|
truth, and the web client's CSS custom properties carry the same values.
|
||||||
(`FabledSwordTokens.kt`) names it as its source of truth, and the web
|
|
||||||
client's CSS custom properties carry the same values. It moved here rather
|
|
||||||
than going with the client, because losing the one place all three cohorts
|
|
||||||
are written down together would have been collateral damage.
|
|
||||||
|
|
||||||
Nothing generates from it today — both clients hold their own transcription,
|
Nothing generates from it today — both clients hold their own transcription,
|
||||||
so this is the reference they must agree with, not a build input. If the
|
so this is the reference they must agree with, not a build input. If the
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { findCss, findRawText, findTailwind, scan } from './check-tint-contrast.
|
|||||||
|
|
||||||
const web = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
const web = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
||||||
describe('hue-on-own-tint check (#3150)', () => {
|
describe('hue-on-own-tint check', () => {
|
||||||
test('no source paints a hue as text on a tint of itself', () => {
|
test('no source paints a hue as text on a tint of itself', () => {
|
||||||
expect(scan(resolve(web, 'src'), web)).toEqual([]);
|
expect(scan(resolve(web, 'src'), web)).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -42,7 +42,7 @@ describe('hue-on-own-tint check (#3150)', () => {
|
|||||||
expect(findRawText(`.msg { color: var(--fs-error); }`)[0].fix).toBe('var(--fs-error-fg)');
|
expect(findRawText(`.msg { color: var(--fs-error); }`)[0].fix).toBe('var(--fs-error-fg)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('raw accent text is caught too, including hover: variants (#5318)', () => {
|
test('raw accent text is caught too, including hover: variants', () => {
|
||||||
expect(findRawText(`<a class="text-sm text-accent hover:underline">`)[0].fix).toBe('text-accent-fg');
|
expect(findRawText(`<a class="text-sm text-accent hover:underline">`)[0].fix).toBe('text-accent-fg');
|
||||||
expect(findRawText(`<a class="hover:text-accent">`)).toHaveLength(1);
|
expect(findRawText(`<a class="hover:text-accent">`)).toHaveLength(1);
|
||||||
expect(findRawText(`.pending { color: var(--fs-accent); }`)[0].fix).toBe('var(--fs-accent-fg)');
|
expect(findRawText(`.pending { color: var(--fs-accent); }`)[0].fix).toBe('var(--fs-accent-fg)');
|
||||||
|
|||||||
Reference in New Issue
Block a user