Header-only control token, masked secrets, loopback default (v0.9.9)

Finishes the small items of the security audit in
docs/prs/2026-07-08-PR#000: TODO 3, 4 and 7.

The three mutating endpoints of BuildsApiController no longer accept the
control token as a `token` query parameter — only the X-GitTally-Token
header, which the bundled UI has always used. URLs end up in access logs,
proxy logs, browser history and Referer headers, and the token never
expires, so a historical log capture would yield a valid credential.

`config:print` masks git.token as `***` on both the raw and the --full
path and names the new --show-secrets flag in a leading YAML comment, so
the output stays parseable when piped. The setup script points at
--show-secrets where it used to steer the operator to the plain token.

`server.bindAddress` now defaults to 127.0.0.1: neither the UI nor the
API authenticates read access, so reaching GitTally should require the
host's reverse proxy. Existing .gittally.yml files keep their explicit
value; the managed nginx container needs `0.0.0.0` set deliberately,
which is noted in the release notes, docs/configuration.md and
docs/deployment.md.

Released as v0.9.9, which also carries the previous two commits.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-11 07:38:33 +02:00
co-authored by Claude
parent dea6770998
commit a4c995592f
12 changed files with 180 additions and 29 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ plugins {
group = "de.hoennig" group = "de.hoennig"
// bump at least the patch version for every deployment, so the UI footer // bump at least the patch version for every deployment, so the UI footer
// (BuildProperties) and --version identify what is actually running // (BuildProperties) and --version identify what is actually running
version = "0.9.8" version = "0.9.9"
java { java {
toolchain { toolchain {
+13 -2
View File
@@ -38,6 +38,9 @@ java -jar build/libs/gittally.jar config:print # only explicitly set val
java -jar build/libs/gittally.jar config:print --full # all values including defaults java -jar build/libs/gittally.jar config:print --full # all values including defaults
``` ```
`git.token` is masked as `***` by default, so the output can safely be shared or pasted.
Add `--show-secrets` to print it in clear text.
## `.gittally.yml` ## `.gittally.yml`
Values shown are the defaults. Values shown are the defaults.
@@ -48,8 +51,9 @@ server:
publicBaseUrl: https://ci.example.org/ publicBaseUrl: https://ci.example.org/
# HTTP port of the `server` subcommand (default 18080, like legacy) # HTTP port of the `server` subcommand (default 18080, like legacy)
port: 18080 port: 18080
# bind address of the `server` subcommand # bind address of the `server` subcommand; loopback only, because the UI and the API
bindAddress: 0.0.0.0 # are unauthenticated — set 0.0.0.0 only deliberately (see the note below)
bindAddress: 127.0.0.1
# optional Impressum (legal disclosure) link in the web UI footer; empty hides the link # optional Impressum (legal disclosure) link in the web UI footer; empty hides the link
impressumUrl: "" impressumUrl: ""
# Opt-in managed nginx+certbot Docker container for HTTPS, for hosts without # Opt-in managed nginx+certbot Docker container for HTTPS, for hosts without
@@ -172,6 +176,13 @@ branches:
- "04:00" - "04:00"
``` ```
### Notes on `server.bindAddress`
The default is `127.0.0.1`.
Neither the web UI nor the JSON API authenticates read access, and every page carries the control token that unlocks the build controls, so GitTally is meant to sit behind the host's reverse proxy rather than on a public interface.
Set `0.0.0.0` only deliberately — for the managed nginx container (which reaches GitTally over the Docker bridge, not over loopback), or when the proxy runs on another host.
Installations created before v0.9.9 have `bindAddress: 0.0.0.0` written into their `.gittally.yml` and keep it; the new default only applies where the key is absent or `init` writes a fresh file.
### Notes on `server.nginx` ### Notes on `server.nginx`
With `nginx.enabled`, the `server` subcommand also starts a managed nginx Docker container that serves GitTally over HTTPS (ADR 0005). With `nginx.enabled`, the `server` subcommand also starts a managed nginx Docker container that serves GitTally over HTTPS (ADR 0005).
+2 -2
View File
@@ -101,7 +101,7 @@ All GitTally configuration lives in the YAML files described in [configuration.m
## Reverse Proxy (nginx) ## Reverse Proxy (nginx)
Bind GitTally to localhost and set the public URL in `.gittally.yml`: Bind GitTally to localhost — the default since v0.9.9 — and set the public URL in `.gittally.yml`:
```yaml ```yaml
server: server:
@@ -197,5 +197,5 @@ All nginx and certificate failures are non-fatal warnings — the plain HTTP ser
`serverName` must be a public DNS name pointing at the host, reachable from the internet on port 80/443 (directly or via a port forward to `httpPort`/`httpsPort`), otherwise the ACME challenge fails. `serverName` must be a public DNS name pointing at the host, reachable from the internet on port 80/443 (directly or via a port forward to `httpPort`/`httpsPort`), otherwise the ACME challenge fails.
The nginx container cannot reach `localhost` of the host, so the proxy upstream defaults to `serverName`; set `server.nginx.upstreamHost` if the host is reachable under a different name from inside containers. The nginx container cannot reach `localhost` of the host, so the proxy upstream defaults to `serverName`; set `server.nginx.upstreamHost` if the host is reachable under a different name from inside containers.
With the managed nginx, keep `server.bindAddress: 0.0.0.0` (or an address reachable from the Docker network) — binding GitTally to `127.0.0.1` would make it unreachable for the proxy. With the managed nginx, set `server.bindAddress: 0.0.0.0` explicitly (or an address reachable from the Docker network) — the default `127.0.0.1` makes GitTally unreachable for the proxy container.
See [configuration.md](configuration.md) for all `server.nginx.*` keys. See [configuration.md](configuration.md) for all `server.nginx.*` keys.
@@ -65,7 +65,7 @@ Blast radius is limited to build-lifecycle operations (a DoS/integrity concern,
#### TODO 3 — Accept the control token via header only #### TODO 3 — Accept the control token via header only
- [ ] Remove the `token` query-parameter variant from the three mutating endpoints in [`BuildsApiController.kt:90,115,129`](../../src/main/kotlin/de/hoennig/gittally/server/BuildsApiController.kt); keep only the `X-GitTally-Token` header (which the bundled UI already uses). - [x] Remove the `token` query-parameter variant from the three mutating endpoints in [`BuildsApiController.kt:90,115,129`](../../src/main/kotlin/de/hoennig/gittally/server/BuildsApiController.kt); keep only the `X-GitTally-Token` header (which the bundled UI already uses).
**Background.** **Background.**
Tokens in URLs are routinely written to access logs, reverse-proxy logs, browser history, and the `Referer` header on outbound navigation. Tokens in URLs are routinely written to access logs, reverse-proxy logs, browser history, and the `Referer` header on outbound navigation.
@@ -74,8 +74,9 @@ The query-param path exists only for legacy convenience.
#### TODO 4 — Redact the Gitea token in `config:print` #### TODO 4 — Redact the Gitea token in `config:print`
- [ ] Mask `git.token` (and any future secret) by default in [`ConfigPrintCommand.kt:20-31`](../../src/main/kotlin/de/hoennig/gittally/commands/ConfigPrintCommand.kt); gate the plaintext value behind an explicit `--show-secrets` flag. - [x] Mask `git.token` (and any future secret) by default in [`ConfigPrintCommand.kt:20-31`](../../src/main/kotlin/de/hoennig/gittally/commands/ConfigPrintCommand.kt); gate the plaintext value behind an explicit `--show-secrets` flag.
- [ ] Update `tools/setup-gittally-instance:272`, which currently steers the operator to run `config:print --full` to view the token. Masked as `***` on both the `--full` and the raw path, with a leading YAML comment naming the flag, so the output stays parseable when piped.
- [x] Update `tools/setup-gittally-instance:272`, which currently steers the operator to run `config:print --full` to view the token.
**Background.** **Background.**
Both the `--full` and default branches print the token verbatim to stdout, landing it in terminal scrollback, `script(1)` captures, screen-shares, or CI logs. Both the `--full` and default branches print the token verbatim to stdout, landing it in terminal scrollback, `script(1)` captures, screen-shares, or CI logs.
@@ -112,7 +113,8 @@ Before this PR all build config was loaded from the primary checkout via `config
#### TODO 7 — Default `bindAddress` to `127.0.0.1` #### TODO 7 — Default `bindAddress` to `127.0.0.1`
- [ ] Change the default in [`GitTallyConfig.kt:21`](../../src/main/kotlin/de/hoennig/gittally/config/GitTallyConfig.kt) and the `init` template ([`InitCommand.kt:126`](../../src/main/kotlin/de/hoennig/gittally/commands/InitCommand.kt)) from `0.0.0.0` to `127.0.0.1`; require operators to opt into all-interfaces. - [x] Change the default in [`GitTallyConfig.kt:21`](../../src/main/kotlin/de/hoennig/gittally/config/GitTallyConfig.kt) and the `init` template ([`InitCommand.kt:126`](../../src/main/kotlin/de/hoennig/gittally/commands/InitCommand.kt)) from `0.0.0.0` to `127.0.0.1`; require operators to opt into all-interfaces.
Shipped as v0.9.9 with the migration note in the release notes, `docs/configuration.md` ("Notes on `server.bindAddress`") and `docs/deployment.md`: existing configs keep their explicit value, and the managed nginx now needs `0.0.0.0` set deliberately.
**Background.** **Background.**
The current default binds all interfaces, which — combined with the public read surface and token-in-HTML — exposes the whole UI and the control token to the network whenever the reverse proxy is forgotten. The current default binds all interfaces, which — combined with the public read surface and token-in-HTML — exposes the whole UI and the control token to the network whenever the reverse proxy is forgotten.
@@ -149,7 +151,7 @@ A small TOCTOU gap; `GitAskPass`'s atomic-at-creation approach is the pattern to
- TODO 2: full fix (gating the pages) versus documentation-only — the pages are the UI, so gating them needs a decision on how operators authenticate; the current implemented behavior is fully public. - TODO 2: full fix (gating the pages) versus documentation-only — the pages are the UI, so gating them needs a decision on how operators authenticate; the current implemented behavior is fully public.
- TODO 5: whether public read access is acceptable by design (it matches legacy) or should change; current behavior leaves all reads public. - TODO 5: whether public read access is acceptable by design (it matches legacy) or should change; current behavior leaves all reads public.
- TODO 6: the pinned set is settled (secrets + Gitea/server + `docker.enabled`/`docker.network`); the open point is whether `docker.env` should also be pinned, since a branch overriding it controls its own container's environment (currently proposed as worktree-overridable). - TODO 6: the pinned set is settled (secrets + Gitea/server + `docker.enabled`/`docker.network`); the open point is whether `docker.env` should also be pinned, since a branch overriding it controls its own container's environment (currently proposed as worktree-overridable).
- TODO 7: changing the default `bindAddress` is a behavior change for existing installs that rely on `0.0.0.0`; needs a migration note. - ~~TODO 7: changing the default `bindAddress` is a behavior change for existing installs that rely on `0.0.0.0`; needs a migration note.~~ Settled: the default changed in v0.9.9 and the migration note is in the release notes and both deployment docs.
## Additional Changes ## Additional Changes
@@ -17,16 +17,41 @@ class ConfigPrintCommand(
@Option(names = ["--full"], description = ["Include all defaults"]) @Option(names = ["--full"], description = ["Include all defaults"])
var full: Boolean = false var full: Boolean = false
@Option(names = ["--show-secrets"], description = ["Print secrets (git.token) in clear text instead of masked"])
var showSecrets: Boolean = false
override fun run() { override fun run() {
if (full) { if (full) {
print(configLoader.toYaml(configLoader.load())) val config = configLoader.load()
printMaskingNote(config.git.token)
print(configLoader.toYaml(if (showSecrets) config else config.copy(git = config.git.copy(token = MASK))))
} else { } else {
val raw = configLoader.loadRaw() val raw = configLoader.loadRaw()
if (raw.isEmpty()) { if (raw.isEmpty()) {
println("(no configuration files found)") println("(no configuration files found)")
} else { } else {
print(configLoader.toYaml(raw)) printMaskingNote(rawToken(raw))
print(configLoader.toYaml(if (showSecrets) raw else maskToken(raw)))
} }
} }
} }
/** A YAML comment, so the output stays parseable when piped into a file. */
private fun printMaskingNote(token: String?) {
if (!showSecrets && !token.isNullOrEmpty()) {
println("# git.token is masked — pass --show-secrets to print it")
}
}
private fun rawToken(raw: Map<String, Any?>): String? = (raw["git"] as? Map<*, *>)?.get("token") as? String
private fun maskToken(raw: Map<String, Any?>): Map<String, Any?> {
val git = raw["git"] as? Map<*, *> ?: return raw
if (rawToken(raw).isNullOrEmpty()) return raw
return raw + ("git" to git.entries.associate { (key, value) -> key to if (key == "token") MASK else value })
}
companion object {
private const val MASK = "***"
}
} }
@@ -125,8 +125,10 @@ class InitCommand(
publicBaseUrl: "" publicBaseUrl: ""
# HTTP port of the `server` subcommand # HTTP port of the `server` subcommand
port: 18080 port: 18080
# bind address of the `server` subcommand # bind address of the `server` subcommand; loopback only, because the UI and the
bindAddress: 0.0.0.0 # API are unauthenticated — use 0.0.0.0 only without a reverse proxy in front
# (and with the managed nginx below, which reaches GitTally from its container)
bindAddress: 127.0.0.1
# optional Impressum (legal disclosure) link in the web UI footer; empty hides the link # optional Impressum (legal disclosure) link in the web UI footer; empty hides the link
impressumUrl: "" impressumUrl: ""
# Opt-in managed nginx+certbot Docker container for HTTPS, for hosts without # Opt-in managed nginx+certbot Docker container for HTTPS, for hosts without
@@ -18,7 +18,12 @@ data class ServerConfig(
val publicBaseUrl: String = "", val publicBaseUrl: String = "",
/** HTTP port of the `server` subcommand; 18080 like the legacy artifact server. */ /** HTTP port of the `server` subcommand; 18080 like the legacy artifact server. */
val port: Int = 18080, val port: Int = 18080,
val bindAddress: String = "0.0.0.0", /**
* Loopback by default: the UI and the API are unauthenticated, so reaching them should
* require the host's reverse proxy. Set `0.0.0.0` explicitly to expose all interfaces —
* which the managed nginx container needs (see `docs/deployment.md`).
*/
val bindAddress: String = "127.0.0.1",
/** Optional Impressum (legal disclosure) link shown in the web UI footer; empty hides the link. */ /** Optional Impressum (legal disclosure) link shown in the web UI footer; empty hides the link. */
val impressumUrl: String = "", val impressumUrl: String = "",
val nginx: NginxConfig = NginxConfig(), val nginx: NginxConfig = NginxConfig(),
@@ -24,8 +24,10 @@ import java.nio.file.StandardOpenOption
/** /**
* JSON API over build results and running builds, replacing the legacy * JSON API over build results and running builds, replacing the legacy
* `/control/…` endpoints. Mutating endpoints are guarded by the control token * `/control/…` endpoints. Mutating endpoints are guarded by the control token,
* (header [TOKEN_HEADER] or parameter `token`), like the legacy cancel token. * like the legacy cancel token — in the header [TOKEN_HEADER] only: a token in
* the query string would end up in access logs, browser history and `Referer`
* headers, and it never expires.
*/ */
@RestController @RestController
class BuildsApiController( class BuildsApiController(
@@ -91,9 +93,8 @@ class BuildsApiController(
fun restart( fun restart(
@RequestParam branch: String, @RequestParam branch: String,
@RequestHeader(name = TOKEN_HEADER, required = false) headerToken: String?, @RequestHeader(name = TOKEN_HEADER, required = false) headerToken: String?,
@RequestParam(name = "token", required = false) paramToken: String?,
): ResponseEntity<Any> { ): ResponseEntity<Any> {
rejectBadToken(headerToken ?: paramToken)?.let { return it } rejectBadToken(headerToken)?.let { return it }
val commit = val commit =
repository.latestFor(branch)?.commit repository.latestFor(branch)?.commit
?: gitService.originHeadCommit(branch, workingDir) ?: gitService.originHeadCommit(branch, workingDir)
@@ -116,9 +117,8 @@ class BuildsApiController(
fun cancel( fun cancel(
@PathVariable artifactKey: String, @PathVariable artifactKey: String,
@RequestHeader(name = TOKEN_HEADER, required = false) headerToken: String?, @RequestHeader(name = TOKEN_HEADER, required = false) headerToken: String?,
@RequestParam(name = "token", required = false) paramToken: String?,
): ResponseEntity<Any> { ): ResponseEntity<Any> {
rejectBadToken(headerToken ?: paramToken)?.let { return it } rejectBadToken(headerToken)?.let { return it }
if (!buildExecutor.cancel(artifactKey)) { if (!buildExecutor.cancel(artifactKey)) {
return notFound("no queued or running build with artifact key '$artifactKey'") return notFound("no queued or running build with artifact key '$artifactKey'")
} }
@@ -130,9 +130,8 @@ class BuildsApiController(
fun delete( fun delete(
@PathVariable artifactKey: String, @PathVariable artifactKey: String,
@RequestHeader(name = TOKEN_HEADER, required = false) headerToken: String?, @RequestHeader(name = TOKEN_HEADER, required = false) headerToken: String?,
@RequestParam(name = "token", required = false) paramToken: String?,
): ResponseEntity<Any> { ): ResponseEntity<Any> {
rejectBadToken(headerToken ?: paramToken)?.let { return it } rejectBadToken(headerToken)?.let { return it }
if (!repository.delete(artifactKey)) { if (!repository.delete(artifactKey)) {
return notFound("no build with artifact key '$artifactKey'") return notFound("no build with artifact key '$artifactKey'")
} }
@@ -7,6 +7,22 @@
<div th:replace="~{fragments :: nav(${view})}"></div> <div th:replace="~{fragments :: nav(${view})}"></div>
<div class="panel release-notes"> <div class="panel release-notes">
<h2>v0.9.9 <span class="muted">— 2026-08-11</span></h2>
<ul>
<li><strong>Changed default:</strong> <code>server.bindAddress</code> is now <code>127.0.0.1</code>
instead of <code>0.0.0.0</code>, because neither the UI nor the API authenticates read access.
Existing <code>.gittally.yml</code> files keep whatever they set; with the managed nginx
container, <code>0.0.0.0</code> has to be set explicitly.</li>
<li>The control token is accepted in the <code>X-GitTally-Token</code> header only — the
<code>token</code> query parameter is gone, as URLs end up in access logs and browser history.</li>
<li><code>config:print</code> masks <code>git.token</code>; <code>--show-secrets</code> prints it.</li>
<li>Files holding secrets (the Gitea token written by <code>init</code>, the control token) are
created with mode <code>0600</code> right away instead of being <code>chmod</code>-ed afterwards,
the control token is compared in constant time, and git calls pass <code>--</code> before
branch names.</li>
<li>On narrow screens the page title and the repository name are stacked instead of wrapping.</li>
</ul>
<h2>v0.9.8 <span class="muted">— 2026-08-10</span></h2> <h2>v0.9.8 <span class="muted">— 2026-08-10</span></h2>
<ul> <ul>
<li>The artifact index also links report pages of directories without an <code>index.html</code>. <li>The artifact index also links report pages of directories without an <code>index.html</code>.
@@ -0,0 +1,74 @@
package de.hoennig.gittally.commands
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.config.GitConfig
import de.hoennig.gittally.config.GitTallyConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotContain
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
class ConfigPrintCommandTest : FunSpec() {
private val yamlWriter = ConfigLoader()
private val configLoader = mockk<ConfigLoader>()
private val command = ConfigPrintCommand(configLoader)
private val rawConfig =
mapOf<String, Any?>(
"git" to mapOf("account" to "ci-user", "token" to "s3cr3t-token"),
"server" to mapOf("port" to 18080),
)
init {
beforeEach {
clearMocks(configLoader)
every { configLoader.toYaml(any()) } answers { yamlWriter.toYaml(firstArg()) }
every { configLoader.loadRaw() } returns rawConfig
every { configLoader.load() } returns GitTallyConfig(git = GitConfig(account = "ci-user", token = "s3cr3t-token"))
command.full = false
command.showSecrets = false
}
test("masks the git token by default") {
val output = captureConsole { command.run() }.stdout
output shouldNotContain "s3cr3t-token"
output shouldContain "***"
output shouldContain "# git.token is masked"
output shouldContain "account: \"ci-user\""
}
test("masks the git token with --full as well") {
command.full = true
val output = captureConsole { command.run() }.stdout
output shouldNotContain "s3cr3t-token"
output shouldContain "***"
}
test("prints the git token with --show-secrets") {
command.showSecrets = true
val output = captureConsole { command.run() }.stdout
output shouldContain "s3cr3t-token"
output shouldNotContain "masked"
}
test("prints the git token with --full --show-secrets") {
command.full = true
command.showSecrets = true
captureConsole { command.run() }.stdout shouldContain "s3cr3t-token"
}
test("says nothing about masking when no token is configured") {
every { configLoader.loadRaw() } returns mapOf("server" to mapOf("port" to 18080))
captureConsole { command.run() }.stdout shouldNotContain "masked"
}
}
}
@@ -175,7 +175,7 @@ class BuildsApiControllerTest : FunSpec() {
runningBuild(liveLogFile).copy(branch = "fresh") runningBuild(liveLogFile).copy(branch = "fresh")
mockMvc mockMvc
.perform(post("/api/builds/restart").param("branch", "fresh").param("token", "secret")) .perform(post("/api/builds/restart").param("branch", "fresh").header(BuildsApiController.TOKEN_HEADER, "secret"))
.andExpect(status().isAccepted) .andExpect(status().isAccepted)
.andExpect(jsonPath("$.status").value("pending")) .andExpect(jsonPath("$.status").value("pending"))
@@ -187,7 +187,7 @@ class BuildsApiControllerTest : FunSpec() {
every { gitService.originHeadCommit("gone", any()) } returns null every { gitService.originHeadCommit("gone", any()) } returns null
mockMvc mockMvc
.perform(post("/api/builds/restart").param("branch", "gone").param("token", "secret")) .perform(post("/api/builds/restart").param("branch", "gone").header(BuildsApiController.TOKEN_HEADER, "secret"))
.andExpect(status().isNotFound) .andExpect(status().isNotFound)
} }
@@ -227,14 +227,30 @@ class BuildsApiControllerTest : FunSpec() {
every { buildExecutor.cancel("unknown-key") } returns false every { buildExecutor.cancel("unknown-key") } returns false
mockMvc mockMvc
.perform(post("/api/builds/known-key/cancel").param("token", "secret")) .perform(post("/api/builds/known-key/cancel").header(BuildsApiController.TOKEN_HEADER, "secret"))
.andExpect(status().isAccepted) .andExpect(status().isAccepted)
.andExpect(jsonPath("$.cancelled").value("known-key")) .andExpect(jsonPath("$.cancelled").value("known-key"))
mockMvc mockMvc
.perform(post("/api/builds/unknown-key/cancel").param("token", "secret")) .perform(post("/api/builds/unknown-key/cancel").header(BuildsApiController.TOKEN_HEADER, "secret"))
.andExpect(status().isNotFound) .andExpect(status().isNotFound)
} }
test("a token in the query string is not accepted — the header is the only way") {
mockMvc
.perform(post("/api/builds/restart").param("branch", "main").param("token", "secret"))
.andExpect(status().isForbidden)
mockMvc
.perform(post("/api/builds/some-key/cancel").param("token", "secret"))
.andExpect(status().isForbidden)
mockMvc
.perform(delete("/api/builds/some-key").param("token", "secret"))
.andExpect(status().isForbidden)
verify(exactly = 0) { buildExecutor.startBuild(any(), any(), any()) }
verify(exactly = 0) { buildExecutor.cancel(any()) }
verify(exactly = 0) { repository.delete(any()) }
}
test("cancel without token answers 403") { test("cancel without token answers 403") {
mockMvc mockMvc
.perform(post("/api/builds/some-key/cancel")) .perform(post("/api/builds/some-key/cancel"))
@@ -260,7 +276,7 @@ class BuildsApiControllerTest : FunSpec() {
every { repository.delete("unknown-key") } returns false every { repository.delete("unknown-key") } returns false
mockMvc mockMvc
.perform(delete("/api/builds/unknown-key").param("token", "secret")) .perform(delete("/api/builds/unknown-key").header(BuildsApiController.TOKEN_HEADER, "secret"))
.andExpect(status().isNotFound) .andExpect(status().isNotFound)
verify(exactly = 0) { artifactStore.prune(any()) } verify(exactly = 0) { artifactStore.prune(any()) }
+2 -1
View File
@@ -275,7 +275,8 @@ Done. GitTally config for $hostname is in place.
Verify and start on this host: Verify and start on this host:
cd $REPO_DIR cd $REPO_DIR
java -jar $JAR_PATH config:print --full # check the effective config + git.token java -jar $JAR_PATH config:print --full # check the effective config (git.token masked)
java -jar $JAR_PATH config:print --full --show-secrets # ... including git.token in clear text
java -jar $JAR_PATH server # or: java -jar $JAR_PATH init --systemd java -jar $JAR_PATH server # or: java -jar $JAR_PATH init --systemd
Note: GitTally binds to $hostname:${GITTALLY_ARTIFACT_SERVER_PORT:-18080} over plain HTTP. Note: GitTally binds to $hostname:${GITTALLY_ARTIFACT_SERVER_PORT:-18080} over plain HTTP.