gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

๐Ÿ“ (docs): Fix drift found by docs-vs-code audit

131a50d7661709a97cc5216ab9a7f5622b61542c ยท Michael Hainz ยท 2026-07-16T20:12:05Z

Changes

11 files changed, +144 -5

MODIFY .github/workflows/jvm.yml +20 -1
diff --git a/.github/workflows/jvm.yml b/.github/workflows/jvm.yml
index 2886f3b..306ff8d 100644
--- a/.github/workflows/jvm.yml
+++ b/.github/workflows/jvm.yml
@@ -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
diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml
index fb9fd8a..5c4229f 100644
--- a/.github/workflows/native.yml
+++ b/.github/workflows/native.yml
@@ -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
diff --git a/README.md b/README.md
index 4df8f23..de7772a 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/docs/admins/getting-started.md b/docs/admins/getting-started.md
index d4a87b9..f178e42 100644
--- a/docs/admins/getting-started.md
+++ b/docs/admins/getting-started.md
@@ -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
diff --git a/docs/admins/persistent-data.md b/docs/admins/persistent-data.md
index fbdd276..b55de66 100644
--- a/docs/admins/persistent-data.md
+++ b/docs/admins/persistent-data.md
@@ -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
diff --git a/docs/maintainers/forgefed.md b/docs/maintainers/forgefed.md
index e7343f4..0e7de65 100644
--- a/docs/maintainers/forgefed.md
+++ b/docs/maintainers/forgefed.md
@@ -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
diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm
index 2e52723..dae043c 100644
--- a/src/main/docker/Dockerfile.jvm
+++ b/src/main/docker/Dockerfile.jvm
@@ -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
diff --git a/src/main/docker/Dockerfile.legacy-jar b/src/main/docker/Dockerfile.legacy-jar
index 94ee321..9f87bc4 100644
--- a/src/main/docker/Dockerfile.legacy-jar
+++ b/src/main/docker/Dockerfile.legacy-jar
@@ -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
diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native
index 03d9470..b8606f3 100644
--- a/src/main/docker/Dockerfile.native
+++ b/src/main/docker/Dockerfile.native
@@ -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
diff --git a/src/main/docker/Dockerfile.native-micro b/src/main/docker/Dockerfile.native-micro
index 1c687f8..72dc21c 100644
--- a/src/main/docker/Dockerfile.native-micro
+++ b/src/main/docker/Dockerfile.native-micro
@@ -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
diff --git a/src/main/docker/smoke-test-volumes.sh b/src/main/docker/smoke-test-volumes.sh
new file mode 100755
index 0000000..0bfe025
--- /dev/null
+++ b/src/main/docker/smoke-test-volumes.sh
@@ -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

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog