gitshark

Clone repository

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

← Commits

๐Ÿ› (api): Fix Renovate-blocking gaps found in end-to-end run

0ab5769cdc48e4173882b7e1fb1c52a5e57fa444 ยท Michael Hainz ยท 2026-07-21T12:45:13Z

Changes

11 files changed, +209 -15

MODIFY README.md +1 -1
diff --git a/README.md b/README.md
index 5803e01..0ab834e 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
16 16 [for users](docs/users/organisations.md), [for admins](docs/admins/organisations.md)
17 17 - Clone/fetch/push over `https://<host>/git/<owner>/<repo>.git`
18 18 - anonymous read on public repositories
19 - - push and private read authenticate with **personal access tokens** (HTTP Basic password)
19 + - push and private read authenticate with **personal access tokens** (HTTP Basic username or password, like a GitHub PAT)
20 20 - Clone/fetch/push over SSH โ€” scp shorthand `git@<host>:<owner>/<repo>.git` on the standard
21 21 port, or `ssh://git@<host>:<port>/<owner>/<repo>.git` when the advertised port isn't 22
22 22 - public-key authentication only; keys managed per user in the UI
MODIFY docs/README.md +3 -0
diff --git a/docs/README.md b/docs/README.md
index 53f72cd..8a0c426 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -63,6 +63,9 @@
63 63 - **[CI/CD runners](admins/ci-runners.md)** โ€” register Forgejo/Gitea runners via
64 64 the `runner.v1` Connect endpoints: admin handles, registration tokens, the
65 65 `/api/actions` paths, reverse-proxy notes, and the runner tables.
66 +- **[Renovate](admins/renovate.md)** โ€” point Renovate's `gitea` platform driver
67 + at git-shark's Gitea-compatible `/api/v1` for automated dependency-update PRs:
68 + token, config, and current limitations.
66 69
67 70 ### For maintainers
68 71
MODIFY docs/admins/getting-started.md +1 -1
diff --git a/docs/admins/getting-started.md b/docs/admins/getting-started.md
index 7c9b062..a2ca5a9 100644
--- a/docs/admins/getting-started.md
+++ b/docs/admins/getting-started.md
@@ -452,5 +452,5 @@
452 452 | 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. |
453 453 | 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). |
454 454 | 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). |
455 -| `git push` over HTTP rejected | Use a personal access token (from *Access tokens*) as the Basic-auth password, not your OIDC password. |
455 +| `git push` over HTTP rejected | Use a personal access token (from *Access tokens*) as the Basic-auth password (or username โ€” either works, like a GitHub PAT), not your OIDC password. |
456 456 | 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. |
ADD docs/admins/renovate.md +72 -0
diff --git a/docs/admins/renovate.md b/docs/admins/renovate.md
new file mode 100644
index 0000000..572a457
--- /dev/null
+++ b/docs/admins/renovate.md
@@ -0,0 +1,72 @@
1 +# Running Renovate against git-shark
2 +
3 +git-shark's `/api/v1` is [Gitea-compatible](../maintainers/gitea-api.md), so
4 +[Renovate](https://docs.renovatebot.com/) drives it with its stock `gitea`
5 +platform driver โ€” no git-shark-specific plugin. Renovate opens, updates, and
6 +merges dependency-update pull requests on repositories hosted here.
7 +
8 +## Prerequisites
9 +
10 +- A personal access token for the account Renovate acts as (create one under
11 + **Settings โ†’ Access tokens**). The account needs write access to the target
12 + repositories (owner or collaborator).
13 +- Renovate reaches both the API host and the git host โ€” for a typical
14 + deployment that is the same public origin (`https://gitshark.example.com`),
15 + with the API under `/api/v1` and git under `/git/<owner>/<repo>.git`.
16 +
17 +## Configuration
18 +
19 +Point Renovate's `gitea` platform at the `/api/v1` endpoint. Minimal
20 +self-hosted config:
21 +
22 +```json
23 +{
24 + "platform": "gitea",
25 + "endpoint": "https://gitshark.example.com/api/v1",
26 + "token": "gs_your_access_token",
27 + "repositories": ["alice/deptest"],
28 + "onboarding": false,
29 + "dependencyDashboard": false,
30 + "requireConfig": "optional"
31 +}
32 +```
33 +
34 +Or entirely by environment variable:
35 +
36 +```
37 +RENOVATE_PLATFORM=gitea
38 +RENOVATE_ENDPOINT=https://gitshark.example.com/api/v1
39 +RENOVATE_TOKEN=gs_your_access_token
40 +RENOVATE_REPOSITORIES=alice/deptest
41 +RENOVATE_ONBOARDING=false
42 +RENOVATE_DEPENDENCY_DASHBOARD=false
43 +LOG_LEVEL=debug renovate
44 +```
45 +
46 +Notes and current limitations:
47 +
48 +- **Do not set `RENOVATE_GIT_URL=endpoint`.** git-shark serves git under
49 + `/git/<owner>/<repo>.git`, not at the API path; leave `git-url` unset so
50 + Renovate clones from the repository's `clone_url` (which carries the correct
51 + path). Renovate injects the token as the Basic username, which git-shark
52 + accepts.
53 +- **`dependencyDashboard: false`** โ€” the dashboard needs issue open/closed
54 + mapping and issue-comment endpoints that are not implemented yet.
55 +- **`onboarding: false`** โ€” pin the target repositories in `repositories`
56 + rather than relying on an onboarding PR / autodiscovery.
57 +- **Labels and commit statuses are stubs**: labels are always empty and the
58 + combined commit status is reported all-clear, so Renovate treats branches as
59 + passing. There is no real CI gating yet.
60 +- Release-notes retrieval logs a warning without a `github.com` token; it does
61 + not block PR creation.
62 +
63 +## Verifying
64 +
65 +With `LOG_LEVEL=debug`, a successful run clones the repo, extracts the
66 +dependency manifest, pushes a `renovate/<dep>-<range>` branch, and opens a pull
67 +request. Confirm via the UI (the repository's merge requests) or the API:
68 +
69 +```
70 +curl -H "Authorization: token gs_your_access_token" \
71 + https://gitshark.example.com/api/v1/repos/alice/deptest/pulls?state=open
72 +```
MODIFY docs/maintainers/gitea-api.md +24 -4
diff --git a/docs/maintainers/gitea-api.md b/docs/maintainers/gitea-api.md
index 1c16ffd..8d2a02d 100644
--- a/docs/maintainers/gitea-api.md
+++ b/docs/maintainers/gitea-api.md
@@ -23,7 +23,8 @@
23 23
24 24 | Concern | Type | Notes |
25 25 |---|---|---|
26 -| Auth | `ApiTokenAuthFilter` | Accepts both `Authorization: Bearer <PAT>` and the Gitea-style `Authorization: token <PAT>`; same personal access tokens as git-over-HTTP |
26 +| Auth (REST) | `ApiTokenAuthFilter` | Accepts both `Authorization: Bearer <PAT>` and the Gitea-style `Authorization: token <PAT>`; same personal access tokens as git-over-HTTP |
27 +| Auth (git) | `GitBasicAuthFilter` | Basic auth for git-over-HTTP: the PAT is accepted as the **password or the username** (Renovate clones with the token in the username, empty password โ€” like a GitHub PAT) |
27 28 | DTOs | `ApiModels` | All response records are Gitea-shaped; snake_case fields via `@JsonProperty`. **Shared with the MCP tools** โ€” reshaping the REST body reshapes MCP tool output too (accepted; see decisions) |
28 29 | Surrogate ids | `GiteaIds` | Folds a `UUID` PK into a stable non-negative `long` for Gitea's int64 `id` |
29 30 | Version probe | `VersionApiResource` | `GET /api/v1/version`; string from `gitshark.gitea-api.version` |
@@ -52,7 +53,12 @@
52 53 - **Fields git-shark has no feature for are hard-coded:** `archived` and `mirror`
53 54 are always false (no archive feature; push-mirrors are outbound, not incoming
54 55 mirrors), and the `allow_*` merge flags advertise merge commits only, matching
55 - the one merge strategy the merge service implements.
56 + the one merge strategy the merge service implements. `default_merge_style` is
57 + reported as `merge` and is **load-bearing, not cosmetic**: Renovate picks a
58 + merge method by running `default_merge_style` first through a `.find`, and its
59 + `isAllowed` *throws* on an unrecognized style โ€” so omitting the field (leaving
60 + it undefined on the wire) makes Renovate block the whole repository with
61 + "unknown merge style" before it ever checks `allow_merge_commits`.
56 62 - **`mergeable` is a placeholder** = "the pull is open", not a real conflict
57 63 check. Computing true mergeability needs a live trial-merge per pull; Renovate
58 64 only needs a hint, and the actual `POST {number}/merge` still rejects a real
@@ -98,5 +104,19 @@
98 104 priority).
99 105 - Issue open/closed mapping + issue-comment REST endpoints (dependency dashboard);
100 106 deferred โ€” run Renovate with `dependencyDashboard: false`.
101 -- A real Renovate `LOG_LEVEL=debug` end-to-end run to validate JSON fidelity
102 - (field-name mismatches fail silently).
107 +
108 +## Validation
109 +
110 +A real Renovate `LOG_LEVEL=debug` run (`platform: gitea`, `endpoint:
111 +http://localhost:8080/api/v1`, `git-url` unset so it clones via `clone_url`)
112 +against a seeded repo with an outdated npm dependency **opened dependency PRs
113 +end to end**, and merging one via `POST /pulls/{number}/merge` advanced `main`.
114 +Two field-fidelity bugs surfaced and were fixed as part of this:
115 +
116 +- **`default_merge_style` must be present** (see decisions) โ€” Renovate blocked
117 + the whole repo without it.
118 +- **git auth must accept the token as the Basic username** โ€” Renovate clones
119 + with the PAT in the username and an empty password.
120 +
121 +The npm registry lookup for the dependency needs outbound network; Release-notes
122 +retrieval warns without a `github.com` token but does not block PR creation.
MODIFY src/main/java/de/workaround/api/ApiModels.java +3 -3
diff --git a/src/main/java/de/workaround/api/ApiModels.java b/src/main/java/de/workaround/api/ApiModels.java
index 5f574ee..85456fd 100644
--- a/src/main/java/de/workaround/api/ApiModels.java
+++ b/src/main/java/de/workaround/api/ApiModels.java
@@ -51,7 +51,7 @@
51 51 @JsonProperty("clone_url") String cloneUrl, @JsonProperty("html_url") String htmlUrl, RepositoryView parent,
52 52 PermissionsView permissions, @JsonProperty("allow_merge_commits") boolean allowMergeCommits,
53 53 @JsonProperty("allow_rebase") boolean allowRebase, @JsonProperty("allow_squash_merge") boolean allowSquashMerge,
54 - Instant createdAt)
54 + @JsonProperty("default_merge_style") String defaultMergeStyle, Instant createdAt)
55 55 {
56 56 /**
57 57 * A shallow projection used only as a fork's {@code parent}: DB-derived fields only, no git read or URLs.
@@ -61,7 +61,7 @@
61 61 {
62 62 return new RepositoryView(GiteaIds.of(repo.id), repo.name, repo.ownerHandle() + "/" + repo.name,
63 63 OwnerView.of(repo), repo.description, repo.visibility == Repository.Visibility.PRIVATE,
64 - repo.parent != null, false, false, false, null, null, null, null, null, true, false, false,
64 + repo.parent != null, false, false, false, null, null, null, null, null, true, false, false, "merge",
65 65 repo.createdAt);
66 66 }
67 67
@@ -76,7 +76,7 @@
76 76 RepositoryView parent = showParent && fork ? shallow(repo.parent) : null;
77 77 return new RepositoryView(GiteaIds.of(repo.id), repo.name, repo.ownerHandle() + "/" + repo.name,
78 78 OwnerView.of(repo), repo.description, repo.visibility == Repository.Visibility.PRIVATE, fork, false,
79 - false, empty, defaultBranch, cloneUrl, htmlUrl, parent, permissions, true, false, false,
79 + false, empty, defaultBranch, cloneUrl, htmlUrl, parent, permissions, true, false, false, "merge",
80 80 repo.createdAt);
81 81 }
82 82 }
MODIFY src/main/java/de/workaround/dev/DevDataSeeder.java +36 -0
diff --git a/src/main/java/de/workaround/dev/DevDataSeeder.java b/src/main/java/de/workaround/dev/DevDataSeeder.java
index 70bf64e..b5f9a05 100644
--- a/src/main/java/de/workaround/dev/DevDataSeeder.java
+++ b/src/main/java/de/workaround/dev/DevDataSeeder.java
@@ -11,6 +11,8 @@
11 11 import de.workaround.git.IssueService;
12 12 import de.workaround.git.MergeRequestCommentService;
13 13 import de.workaround.git.MergeRequestService;
14 +import de.workaround.http.AccessTokenService;
15 +import de.workaround.model.AccessToken;
14 16 import de.workaround.model.Issue;
15 17 import de.workaround.model.MergeRequest;
16 18 import de.workaround.model.Repository;
@@ -36,6 +38,13 @@
36 38 private static final String DEMO_REPO = "demo";
37 39 private static final String DEMO_BRANCH = "feature";
38 40
41 + /**
42 + * A fixed personal access token seeded for the demo user so local API/Renovate testing needs no UI login.
43 + * DEV ONLY: seeded exclusively under the {@code %dev} profile (guarded by {@code gitshark.dev.seed-data},
44 + * true only in {@code %dev}), so it never exists in a production database.
45 + */
46 + public static final String DEV_ACCESS_TOKEN = "gs_dev-only-local-renovate-token-0123456789";
47 +
39 48 @Inject
40 49 GitRepositoryService repositories;
41 50
@@ -54,6 +63,12 @@
54 63 @Inject
55 64 IssueService issueService;
56 65
66 + @Inject
67 + AccessTokenService accessTokens;
68 +
69 + @Inject
70 + AccessToken.Repo accessTokenRepo;
71 +
57 72 @ConfigProperty(name = "gitshark.dev.seed-data", defaultValue = "false")
58 73 boolean enabled;
59 74
@@ -98,6 +113,27 @@
98 113
99 114 seedMergeRequest(alice, demo, bare);
100 115 seedIssues(alice, demo);
116 + seedDevAccessToken(alice);
117 + }
118 +
119 + /**
120 + * Seeds the fixed {@link #DEV_ACCESS_TOKEN} for the given user so local API and Renovate testing needs no
121 + * UI login. Idempotent: skipped when the user already has any token. DEV ONLY โ€” see {@link #DEV_ACCESS_TOKEN}.
122 + */
123 + @Transactional
124 + public void seedDevAccessToken(User user)
125 + {
126 + if (!accessTokenRepo.findByUser(user).isEmpty())
127 + {
128 + return;
129 + }
130 + AccessToken token = new AccessToken();
131 + token.user = user;
132 + token.label = "dev (local API/Renovate testing)";
133 + token.tokenHash = accessTokens.hash(DEV_ACCESS_TOKEN);
134 + token.persist();
135 + LOG.warnf("Seeded DEV access token for %s: %s (DEV ONLY โ€” never enable gitshark.dev.seed-data in production)",
136 + user.username, DEV_ACCESS_TOKEN);
101 137 }
102 138
103 139 /**
MODIFY src/main/java/de/workaround/http/GitBasicAuthFilter.java +34 -6
diff --git a/src/main/java/de/workaround/http/GitBasicAuthFilter.java b/src/main/java/de/workaround/http/GitBasicAuthFilter.java
index c34e92d..f62d9e8 100644
--- a/src/main/java/de/workaround/http/GitBasicAuthFilter.java
+++ b/src/main/java/de/workaround/http/GitBasicAuthFilter.java
@@ -2,7 +2,9 @@
2 2
3 3 import java.io.IOException;
4 4 import java.nio.charset.StandardCharsets;
5 +import java.util.ArrayList;
5 6 import java.util.Base64;
7 +import java.util.List;
6 8 import java.util.Optional;
7 9
8 10 import de.workaround.model.User;
@@ -16,8 +18,9 @@
16 18 import jakarta.servlet.http.HttpServletResponseWrapper;
17 19
18 20 /**
19 - * HTTP Basic authentication for Git smart HTTP. The Basic password is a personal access
20 - * token; the username part is informational only (like GitHub PATs). Requests without
21 + * HTTP Basic authentication for Git smart HTTP. The personal access token may be supplied as the
22 + * Basic password (the git default, with any username) or as the Basic username (as Renovate and
23 + * other Gitea clients send it) โ€” either position is accepted, like a GitHub PAT. Requests without
21 24 * credentials continue anonymously โ€” the GitServlet decides whether anonymous is enough.
22 25 * Invalid credentials are rejected immediately. All 401 responses carry a Basic challenge
23 26 * so Git clients prompt for credentials and retry.
@@ -37,7 +40,7 @@
37 40 String header = request.getHeader("Authorization");
38 41 if (header != null && header.regionMatches(true, 0, "Basic ", 0, 6))
39 42 {
40 - Optional<User> user = decodePassword(header).flatMap(tokenService::authenticate);
43 + Optional<User> user = authenticate(header);
41 44 if (user.isEmpty())
42 45 {
43 46 response.setHeader("WWW-Authenticate", CHALLENGE);
@@ -49,18 +52,43 @@
49 52 chain.doFilter(request, new ChallengeOn401(response));
50 53 }
51 54
52 - private static Optional<String> decodePassword(String header)
55 + /** Tries the token in both Basic positions โ€” password first (git default), then username. */
56 + private Optional<User> authenticate(String header)
57 + {
58 + for (String candidate : credentialCandidates(header))
59 + {
60 + Optional<User> user = tokenService.authenticate(candidate);
61 + if (user.isPresent())
62 + {
63 + return user;
64 + }
65 + }
66 + return Optional.empty();
67 + }
68 +
69 + private static List<String> credentialCandidates(String header)
53 70 {
54 71 try
55 72 {
56 73 String decoded = new String(Base64.getDecoder().decode(header.substring(6).trim()),
57 74 StandardCharsets.UTF_8);
58 75 int colon = decoded.indexOf(':');
59 - return colon >= 0 ? Optional.of(decoded.substring(colon + 1)) : Optional.empty();
76 + String username = colon >= 0 ? decoded.substring(0, colon) : decoded;
77 + String password = colon >= 0 ? decoded.substring(colon + 1) : "";
78 + List<String> candidates = new ArrayList<>(2);
79 + if (!password.isEmpty())
80 + {
81 + candidates.add(password);
82 + }
83 + if (!username.isEmpty())
84 + {
85 + candidates.add(username);
86 + }
87 + return candidates;
60 88 }
61 89 catch (IllegalArgumentException e)
62 90 {
63 - return Optional.empty();
91 + return List.of();
64 92 }
65 93 }
66 94
MODIFY src/test/java/de/workaround/api/RepositoryApiTest.java +1 -0
diff --git a/src/test/java/de/workaround/api/RepositoryApiTest.java b/src/test/java/de/workaround/api/RepositoryApiTest.java
index 6ef23a9..ae00c09 100644
--- a/src/test/java/de/workaround/api/RepositoryApiTest.java
+++ b/src/test/java/de/workaround/api/RepositoryApiTest.java
@@ -47,6 +47,7 @@
47 47 .body("private", is(false))
48 48 .body("description", equalTo("gadgets"))
49 49 .body("default_branch", equalTo("main"))
50 + .body("default_merge_style", equalTo("merge"))
50 51 .body("clone_url", org.hamcrest.Matchers.endsWith("/git/" + owner.username + "/widgets.git"))
51 52 .body("permissions.admin", is(true))
52 53 .extract().header("Location");
MODIFY src/test/java/de/workaround/dev/DevDataSeederTest.java +16 -0
diff --git a/src/test/java/de/workaround/dev/DevDataSeederTest.java b/src/test/java/de/workaround/dev/DevDataSeederTest.java
index b50f1f3..b2025b9 100644
--- a/src/test/java/de/workaround/dev/DevDataSeederTest.java
+++ b/src/test/java/de/workaround/dev/DevDataSeederTest.java
@@ -10,9 +10,11 @@
10 10 import de.workaround.git.GitRepositoryService;
11 11 import de.workaround.git.IssueService;
12 12 import de.workaround.git.MergeRequestCommentService;
13 +import de.workaround.http.AccessTokenService;
13 14 import de.workaround.model.Issue;
14 15 import de.workaround.model.MergeRequest;
15 16 import de.workaround.model.Repository;
17 +import de.workaround.model.User;
16 18 import io.quarkus.test.junit.QuarkusTest;
17 19 import jakarta.inject.Inject;
18 20
@@ -41,6 +43,20 @@
41 43 @Inject
42 44 IssueService issues;
43 45
46 + @Inject
47 + AccessTokenService tokens;
48 +
49 + @Test
50 + void seedsAFixedDevAccessTokenForAliceIdempotently()
51 + {
52 + seeder.seed();
53 + seeder.seed();
54 +
55 + User authenticated = tokens.authenticate(DevDataSeeder.DEV_ACCESS_TOKEN).orElse(null);
56 + assertTrue(authenticated != null, "the seeded dev token must authenticate");
57 + assertEquals("alice", authenticated.username);
58 + }
59 +
44 60 @Test
45 61 void seedsAliceWithDemoRepoContainingACommit()
46 62 {
MODIFY src/test/java/de/workaround/http/GitSmartHttpTest.java +18 -0
diff --git a/src/test/java/de/workaround/http/GitSmartHttpTest.java b/src/test/java/de/workaround/http/GitSmartHttpTest.java
index 43c3c2b..bac42f7 100644
--- a/src/test/java/de/workaround/http/GitSmartHttpTest.java
+++ b/src/test/java/de/workaround/http/GitSmartHttpTest.java
@@ -174,6 +174,24 @@
174 174 }
175 175
176 176 @Test
177 + void tokenSuppliedAsUsernameAuthenticates() throws Exception
178 + {
179 + // Renovate (and Gitea clients) put the PAT in the Basic username with an empty password;
180 + // git-shark must accept the token in either position, like a GitHub PAT.
181 + User owner = persistUser();
182 + Repository repo = createRepo(owner, "token-as-username", Repository.Visibility.PRIVATE);
183 + seedCommit(service.repositoryPath(repo));
184 +
185 + String token = createToken(owner);
186 + Path target = Files.createTempDirectory("token-username");
187 + try (Git clone = cloneOver(httpUrl(owner, "token-as-username"), target,
188 + new UsernamePasswordCredentialsProvider(token, "")))
189 + {
190 + assertTrue(Files.exists(target.resolve("README.md")));
191 + }
192 + }
193 +
194 + @Test
177 195 void revokedTokenIsRejected() throws Exception
178 196 {
179 197 User owner = persistUser();

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog