๐ (docs): Fix drift found by docs-vs-code audit
Changes
11 files changed, +144 -5
MODIFY
.github/workflows/jvm.yml
+20 -1
@@ -18,11 +18,30 @@
18
18
- name: Run JVM tests
19
19
run: ./mvnw -B test
20
20
21
+ jvm-image-smoke:
22
+ # The documented Compose setup relies on the image shipping /data mount points
23
+ # owned by the runtime UID (185) so freshly created named volumes inherit that
24
+ # ownership. Build the image and verify it before anything gets published.
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v7
28
+ - uses: actions/setup-java@v5
29
+ with:
30
+ distribution: temurin
31
+ java-version: '21'
32
+ cache: maven
33
+ - name: Build application
34
+ run: ./mvnw -B package -DskipTests
35
+ - name: Build JVM image
36
+ run: docker build -f src/main/docker/Dockerfile.jvm -t git-shark:smoke .
37
+ - name: Smoke-test /data volume ownership
38
+ run: ./src/main/docker/smoke-test-volumes.sh git-shark:smoke 185
39
+
21
40
jvm-image:
22
41
# Build the application and publish the JVM container image to GHCR after tests pass.
23
42
if: github.ref == 'refs/heads/main'
24
43
runs-on: ubuntu-latest
25
- needs: jvm-tests
44
+ needs: [jvm-tests, jvm-image-smoke]
26
45
permissions:
27
46
contents: read
28
47
packages: write
MODIFY
.github/workflows/native.yml
+7 -0
@@ -20,6 +20,13 @@
20
20
cache: maven
21
21
- name: Native build & integration tests
22
22
run: ./mvnw -B verify -Dnative -Dquarkus.native.container-build=true
23
+ # The documented Compose setup relies on the image shipping /data mount points
24
+ # owned by the runtime UID (1001 in the native image) so freshly created named
25
+ # volumes inherit that ownership. Verify before the image gets published.
26
+ - name: Build native image for smoke test
27
+ run: docker build -f src/main/docker/Dockerfile.native-micro -t git-shark:native-smoke .
28
+ - name: Smoke-test /data volume ownership
29
+ run: ./src/main/docker/smoke-test-volumes.sh git-shark:native-smoke 1001
23
30
# Publish the native image to GHCR after the build and integration tests pass.
24
31
# The native executable is architecture-specific and was compiled on this amd64
25
32
# runner, so the image is published for linux/amd64 only (the multi-arch manifest
MODIFY
README.md
+3 -1
@@ -185,6 +185,7 @@
185
185
| `GITSHARK_REPO_IMAGE_ROOT` | `data/repo-images` | Root directory for uploaded per-repository images (persistent volume) |
186
186
| `GITSHARK_SSH_PORT` | `2222` | Embedded SSH server port |
187
187
| `GITSHARK_SSH_HOST_KEY` | `data/ssh/host-key` | Persisted SSH host key file |
188
+| `GITSHARK_ADMIN_HANDLES` | โ (empty = no admins) | Comma-separated handles allowed into `/admin/*` (CI runner management) |
188
189
| `QUARKUS_DATASOURCE_JDBC_URL` / `_USERNAME` / `_PASSWORD` | โ (Dev Services in dev/test) | PostgreSQL connection |
189
190
| `QUARKUS_OIDC_AUTH_SERVER_URL` / `_CLIENT_ID` / `_CREDENTIALS_SECRET` | โ (Keycloak Dev Services in dev/test) | OIDC provider |
190
191
| `QUARKUS_OIDC_AUTHENTICATION_STATE_SECRET` | โ (dev/test use a fixed dev secret) | Encrypts the OIDC state cookie carrying the PKCE `code_verifier`; โฅ 32 chars, stable across pods |
@@ -196,6 +197,7 @@
196
197
| `GITSHARK_FEDERATION_BASE_URL` | โ | Public HTTPS origin (e.g. `https://shark.example`); actor IDs derive from it and are permanent |
197
198
| `GITSHARK_FEDERATION_PEER_ALLOWLIST` | โ (empty = deny all) | Comma-separated peer hosts allowed to send/receive federation traffic |
198
199
| `GITSHARK_FEDERATION_MAX_ATTEMPTS` | `8` | Max delivery attempts before a queued activity is dead-lettered |
200
+| `GITSHARK_FEDERATION_USER_RESYNC_INTERVAL` | `5m` | How often followed remote users are re-scanned for new public repositories |
199
201
| `GITSHARK_FEDERATION_DEV_ALLOW_INSECURE` | `false` | **Dev/local only.** Lets the SSRF guard accept `http` + loopback/private targets so two instances can federate on one machine (peer allowlist still enforced). Never enable in production. |
200
202
201
203
> **TLS required in production:** personal access tokens travel as HTTP Basic credentials
@@ -245,7 +247,7 @@
245
247
246
248
| Store | What |
247
249
|---|---|
248
-| PostgreSQL | `users`, `repositories` (metadata), `repository_pins` (per-user pinned repositories), `ssh_keys` (public keys + fingerprints), `access_tokens` (SHA-256 hashes, labels, last-used), push-mirror tables (`push_mirror` with AES-GCM-encrypted secrets, `mirror_sync` queue), federation tables (`federation_keys`, `remote_actors`, `repository_followers`, `federation_outbox`, `federation_inbox`, `federation_delivery`) |
250
+| PostgreSQL | `users`, `repositories` (metadata), `repository_pins` (per-user pinned repositories), `ssh_keys` (public keys + fingerprints), `access_tokens` (SHA-256 hashes, labels, last-used), push-mirror tables (`push_mirror` with AES-GCM-encrypted secrets, `mirror_sync` queue), federation tables (`federation_keys`, `remote_actors`, `repository_followers`, `remote_follows`, `remote_user_follows`, `received_pushes`, `federation_outbox`, `federation_inbox`, `federation_delivery`) |
249
251
| Filesystem (`GITSHARK_STORAGE_ROOT`) | Bare Git repositories |
250
252
| Filesystem (`GITSHARK_AVATAR_ROOT`) | Uploaded profile pictures, one file per user (UUID-named) |
251
253
| Filesystem (`GITSHARK_SSH_HOST_KEY`) | SSH host key |
MODIFY
docs/admins/getting-started.md
+8 -2
@@ -228,7 +228,12 @@
228
228
229
229
- **Separate volumes for `/data/repositories`, `/data/avatars`, `/data/repo-images`,
230
230
and `/data/ssh`** so Docker creates each mount point with the right ownership โ no
231
- init container or `mkdir` needed. The SSH host key is generated on first boot and
231
+ init container or `mkdir` needed. This works because the image ships those
232
+ directories pre-owned by the runtime user, and Docker copies that ownership into a
233
+ freshly created named volume. Volumes first created with an image from before this
234
+ fix are root-owned and stay that way โ see
235
+ [Persistent data](persistent-data.md#fixing-root-owned-data-volumes) for the
236
+ one-time repair. The SSH host key is generated on first boot and
232
237
persists across restarts (so client `known_hosts` entries stay valid). All four mounts
233
238
(plus the database) are mandatory for a stateful deployment โ see
234
239
[Persistent data](persistent-data.md) for what each holds and what breaks without it.
@@ -424,7 +429,8 @@
424
429
| Boot fails on OIDC discovery | `QUARKUS_OIDC_AUTH_SERVER_URL` wrong/unreachable, or IdP demands HTTPS the app can't reach. |
425
430
| IdP rejects login with a `redirect_uri` error | The client's registered redirect URI doesn't match the fixed callback `https://<host>/login` (Step 2). Deployments set up before the silent-refresh change registered `https://<host>/` โ add/replace it with `/login`. |
426
431
| App exits complaining about secret length | `*_STATE_SECRET` shorter than 32 chars. Regenerate with `openssl rand -hex 16`. |
427
-| SSH host key changed after redeploy | The `ssh` volume wasn't persisted โ confirm it's a named volume, not a throwaway mount. |
432
+| SSH host key changed after redeploy | The `ssh` volume wasn't persisted โ confirm it's a named volume, not a throwaway mount. If the volume **is** there but stays empty and the logs show `Failed (AccessDeniedException) to write EC key`, the mount points are root-owned (volumes created with a pre-fix image) โ see the row below. |
433
+| Repository creation or image/avatar upload fails; app logs show `Permission denied` or `AccessDeniedException` under `/data` | The `/data` volumes were first created by an image that didn't ship those directories, so their mount points are root-owned and the app user (UID 185, or 1001 for the native image) can't write. One-time fix: `docker compose exec --user 0 app chown -R 185:0 /data && docker compose restart app` (use `1001:0` for the native image). Details in [Persistent data](persistent-data.md#fixing-root-owned-data-volumes). |
428
434
| Profile pictures disappear after redeploy / render as broken images | The `avatars` volume wasn't mounted, so uploads landed in the container layer. Add the volume and `GITSHARK_AVATAR_ROOT` as in Step 5 โ retrofit steps in [Persistent data](persistent-data.md#upgrading-a-deployment-created-before-profile-pictures). |
429
435
| `git push` over HTTP rejected | Use a personal access token (from *Access tokens*) as the Basic-auth password, not your OIDC password. |
430
436
| Schema validation error at start | DB not empty / migrated by a different tool. git-shark's Flyway owns the schema; start from an empty database. |
MODIFY
docs/admins/persistent-data.md
+22 -0
@@ -68,6 +68,28 @@
68
68
layer); affected users re-upload under *Settings โ Profile*, or remove the picture
69
69
there to fall back to the initials badge.
70
70
71
+## Fixing root-owned /data volumes
72
+
73
+The reference Compose file relies on Docker copying ownership from the image into each
74
+freshly created named volume. Images from before the `/data`-ownership fix didn't ship
75
+the `/data` directories, so volumes first created with such an image have root-owned
76
+mount points that the app user (UID `185`, or `1001` for the native image) cannot
77
+write. Ownership is only copied when a volume is **first** created, so upgrading the
78
+image alone does not repair existing volumes.
79
+
80
+Symptoms: repository creation and avatar/repository-image uploads fail, the SSH host
81
+key changes on every restart (it can't be persisted), and the app logs show
82
+`Permission denied` or `AccessDeniedException` under `/data`.
83
+
84
+One-time repair on the running deployment:
85
+
86
+```bash
87
+docker compose exec --user 0 app chown -R 185:0 /data # native image: 1001:0
88
+docker compose restart app
89
+```
90
+
91
+Verify afterwards that `/data/ssh/host-key` exists and survives a restart.
92
+
71
93
## Backups
72
94
73
95
Back up all five stores together and consistently โ a DB dump that references git
MODIFY
docs/maintainers/forgefed.md
+1 -1
@@ -44,7 +44,7 @@
44
44
Web UI: `web/FollowingResource` + Qute template (`/following` page). Persistence
45
45
in `model/` (`FederationKey`, `RemoteActor`, `RepositoryFollower`,
46
46
`RemoteFollow`, `RemoteUserFollow`, `ReceivedPush`, `InboxActivity`,
47
-`OutboxActivity`, `FederationDelivery`), schema in `db/migration/V2__federation.sql`,
47
+`OutboxActivity`, `DeliveryTask`), schema in `db/migration/V2__federation.sql`,
48
48
`V9__federation_following.sql`, and `V20__federation_user_follows.sql`.
49
49
50
50
## Actor model
MODIFY
src/main/docker/Dockerfile.jvm
+9 -0
@@ -84,6 +84,15 @@
84
84
85
85
ENV LANGUAGE='en_US:en'
86
86
87
+# The documented deployment mounts named volumes at these /data paths. They must
88
+# exist in the image owned by the runtime UID so Docker copies that ownership into
89
+# freshly created volumes โ otherwise every mount point is root-owned and the app
90
+# cannot persist the SSH host key or write repositories, avatars, or repo images.
91
+USER root
92
+RUN mkdir -p /data/repositories /data/avatars /data/repo-images /data/ssh \
93
+ && chown -R 185:0 /data \
94
+ && chmod -R g+rwX /data
95
+USER 185
87
96
88
97
# We make four distinct layers so if there are application changes the library layers can be re-used
89
98
COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
MODIFY
src/main/docker/Dockerfile.legacy-jar
+9 -0
@@ -84,6 +84,15 @@
84
84
85
85
ENV LANGUAGE='en_US:en'
86
86
87
+# The documented deployment mounts named volumes at these /data paths. They must
88
+# exist in the image owned by the runtime UID so Docker copies that ownership into
89
+# freshly created volumes โ otherwise every mount point is root-owned and the app
90
+# cannot persist the SSH host key or write repositories, avatars, or repo images.
91
+USER root
92
+RUN mkdir -p /data/repositories /data/avatars /data/repo-images /data/ssh \
93
+ && chown -R 185:0 /data \
94
+ && chmod -R g+rwX /data
95
+USER 185
87
96
88
97
COPY target/lib/* /deployments/lib/
89
98
COPY target/*-runner.jar /deployments/quarkus-run.jar
MODIFY
src/main/docker/Dockerfile.native
+7 -0
@@ -21,6 +21,13 @@
21
21
RUN chown 1001 /work \
22
22
&& chmod "g+rwX" /work \
23
23
&& chown 1001:root /work
24
+# The documented deployment mounts named volumes at these /data paths. They must
25
+# exist in the image owned by the runtime UID so Docker copies that ownership into
26
+# freshly created volumes โ otherwise every mount point is root-owned and the app
27
+# cannot persist the SSH host key or write repositories, avatars, or repo images.
28
+RUN mkdir -p /data/repositories /data/avatars /data/repo-images /data/ssh \
29
+ && chown -R 1001:root /data \
30
+ && chmod -R g+rwX /data
24
31
COPY --chown=1001:root --chmod=0755 target/*-runner /work/application
25
32
26
33
EXPOSE 8080
MODIFY
src/main/docker/Dockerfile.native-micro
+7 -0
@@ -24,6 +24,13 @@
24
24
RUN chown 1001 /work \
25
25
&& chmod "g+rwX" /work \
26
26
&& chown 1001:root /work
27
+# The documented deployment mounts named volumes at these /data paths. They must
28
+# exist in the image owned by the runtime UID so Docker copies that ownership into
29
+# freshly created volumes โ otherwise every mount point is root-owned and the app
30
+# cannot persist the SSH host key or write repositories, avatars, or repo images.
31
+RUN mkdir -p /data/repositories /data/avatars /data/repo-images /data/ssh \
32
+ && chown -R 1001:root /data \
33
+ && chmod -R g+rwX /data
27
34
COPY --chown=1001:root --chmod=0755 target/*-runner /work/application
28
35
29
36
EXPOSE 8080
ADD
src/main/docker/smoke-test-volumes.sh
+51 -0
@@ -0,0 +1,51 @@
1
+#!/usr/bin/env sh
2
+# Smoke test: the documented Compose setup mounts named volumes at the /data
3
+# paths below and relies on Docker copying ownership from the image into a
4
+# freshly created volume. That only works when the paths exist in the image
5
+# and are owned by the runtime UID โ this script fails the build when they
6
+# don't, which used to leave every /data mount root-owned and unwritable
7
+# (no persisted SSH host key, repository creation failing at first write).
8
+#
9
+# Usage: smoke-test-volumes.sh <image> <runtime-uid>
10
+# e.g. smoke-test-volumes.sh git-shark:local 185 # JVM image
11
+# smoke-test-volumes.sh git-shark:native 1001 # native image
12
+set -eu
13
+
14
+IMAGE="${1:?usage: smoke-test-volumes.sh <image> <runtime-uid>}"
15
+UID_EXPECTED="${2:?usage: smoke-test-volumes.sh <image> <runtime-uid>}"
16
+
17
+DATA_DIRS="data/repositories data/avatars data/repo-images data/ssh"
18
+
19
+# --- 1. Static check: paths exist in the image, owned by the runtime UID. ---
20
+# Works for every image, including the shell-less native-micro base, because
21
+# it inspects the exported filesystem instead of executing anything inside.
22
+CONTAINER="$(docker create "$IMAGE")"
23
+trap 'docker rm -f "$CONTAINER" >/dev/null' EXIT
24
+LISTING="$(docker export "$CONTAINER" | tar -tv)"
25
+
26
+FAILED=0
27
+for DIR in $DATA_DIRS; do
28
+ LINE="$(printf '%s\n' "$LISTING" | grep -E "[[:space:]]$DIR/?$" || true)"
29
+ if [ -z "$LINE" ]; then
30
+ echo "FAIL: /$DIR missing from image $IMAGE" >&2
31
+ FAILED=1
32
+ elif ! printf '%s\n' "$LINE" | grep -qE "[[:space:]]$UID_EXPECTED/"; then
33
+ echo "FAIL: /$DIR not owned by uid $UID_EXPECTED: $LINE" >&2
34
+ FAILED=1
35
+ fi
36
+done
37
+[ "$FAILED" -eq 0 ] || exit 1
38
+echo "OK: all /data paths present in image and owned by uid $UID_EXPECTED"
39
+
40
+# --- 2. Functional check: the runtime user can write through fresh volumes. ---
41
+# Anonymous volumes get the same ownership-copy treatment as the named volumes
42
+# in the documented docker-compose.yml. Skipped when the image has no shell.
43
+if printf '%s\n' "$LISTING" | grep -qE '[[:space:]](bin|usr/bin)/sh( |$)'; then
44
+ docker run --rm \
45
+ -v /data/repositories -v /data/avatars -v /data/repo-images -v /data/ssh \
46
+ --entrypoint "" "$IMAGE" \
47
+ sh -c 'touch /data/repositories/.rw /data/avatars/.rw /data/repo-images/.rw /data/ssh/.rw'
48
+ echo "OK: runtime user can write all /data mount points"
49
+else
50
+ echo "SKIP: image has no shell; static ownership check only"
51
+fi