From d5fe1dc18e8f9e58ba912e51aa0b1d471d4e0747 Mon Sep 17 00:00:00 2001 From: JounQin Date: Thu, 29 Oct 2026 21:12:29 +0000 Subject: [PATCH 01/20] docs: settle the sidecar compose filename and layout Use docker-compose.yml in the deployment directory, move the tracked example to the root, and document the deployment tree and --code conventions. --- ...26-09-30-git-credential-broker-verified.md | 4 +-- .changeset/compose-layout.md | 6 +++++ .gitignore | 4 --- DEVELOPMENT.md | 5 ++-- README.md | 26 ++++++++++++++----- ....snippet.yml => docker-compose.example.yml | 2 +- packages/host/src/commands/init.ts | 4 +-- 7 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 .changeset/compose-layout.md rename examples/docker-compose.snippet.yml => docker-compose.example.yml (99%) diff --git a/.agents/notes/verified/architecture/2026-09-30-git-credential-broker-verified.md b/.agents/notes/verified/architecture/2026-09-30-git-credential-broker-verified.md index b6cd4126..5f67b894 100644 --- a/.agents/notes/verified/architecture/2026-09-30-git-credential-broker-verified.md +++ b/.agents/notes/verified/architecture/2026-09-30-git-credential-broker-verified.md @@ -14,7 +14,7 @@ ``` 宿主 NAS /volume1/docker/git-cred-broker/ - docker-compose.broker.yml sidecar 定义 + docker-compose.yml sidecar 定义 app/ 代码(挂载之外,agent 写不到) app.pem App 私钥 0600 ← 唯一长期凭据 broker.config.json 白名单/策略 0600 @@ -125,7 +125,7 @@ git-credential-broker probe --socket /home/app/.dsh/git-broker/broker.sock \ git-credential-broker diagnose --config /volume1/docker/git-cred-broker/broker.config.json # sidecar -docker compose -f /volume1/docker/git-cred-broker/docker-compose.broker.yml ps|logs|restart|down +docker compose -f /volume1/docker/git-cred-broker/docker-compose.yml ps|logs|restart|down # 每次签发 tail -f /volume1/docker/git-cred-broker/log/audit.jsonl diff --git a/.changeset/compose-layout.md b/.changeset/compose-layout.md new file mode 100644 index 00000000..470c6ca8 --- /dev/null +++ b/.changeset/compose-layout.md @@ -0,0 +1,6 @@ +--- +'gcb-host': patch +--- + +Name the rendered sidecar file `docker-compose.yml` in the deployment directory and document the +deployment layout. diff --git a/.gitignore b/.gitignore index b65ca21e..64eb32e2 100644 --- a/.gitignore +++ b/.gitignore @@ -19,10 +19,6 @@ node_modules/ # GIT_BROKER_CONFIG at it, so nothing here needs a personal copy. /broker.config.yml /broker.config.yaml -# `compose` output, which the README tells you to redirect to this name inside the repo. It -# carries one host's deployment paths, so it is per-deployment, not source. -/docker-compose.broker.yml - # The person's refresh token, if anyone keeps one here by mistake. user.refresh user.token diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 820d73b1..8c9f7665 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -87,7 +87,7 @@ As an image, build the host stage of the root Dockerfile: ```sh docker build --target host -t gcb-host:local . -gcb compose --image gcb-host:local > docker-compose.broker.yml +gcb compose --image gcb-host:local > docker-compose.yml ``` Then keep the rendered `sh -c` wrapper (it applies `GCB_PROXY`) and change only the final @@ -185,6 +185,7 @@ scripts/postbuild.mjs the shebang and exec bit on the two bins test/unit/ policy, JWT, provider, helper, broker, config, commands, stage, daemon test/e2e/push.test.ts a real push over authenticated smart HTTP test/e2e/packages.test.ts npm pack and install into separate client/host prefixes -examples/ configuration and compose snippets +examples/ configuration snippets +docker-compose.example.yml tracked sidecar example .agents/notes/ the design, its review, the verified record, environment facts ``` diff --git a/README.md b/README.md index a7d48219..1eee02ec 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ the default sidecar fetches `gcb-host` with `npx` at start, while `--code` runs registry access at boot. ```sh -DEPLOY=/srv/git-cred-broker # broker.config.yml, app.pem and log/ live here +DEPLOY=/srv/git-cred-broker # the deployment directory SOCKET_DIR=/srv/git-broker-socket # a host directory the pushing container already mounts gcb init --mode sidecar --dir "$DEPLOY" \ @@ -167,8 +167,20 @@ gcb init --mode sidecar --dir "$DEPLOY" \ --allow owner/repo --client-id Iv23li… --app-id 123456 gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" \ - > "$DEPLOY/docker-compose.broker.yml" -docker compose -f "$DEPLOY/docker-compose.broker.yml" up -d + > "$DEPLOY/docker-compose.yml" +docker compose -f "$DEPLOY/docker-compose.yml" up -d +``` + +A deployment directory holds the config, key, log and compose file; `app/` is only a convention +for a staged commit, and `--code` accepts any directory outside every mount. + +```text +$DEPLOY/ + broker.config.yml + app.pem + log/ + docker-compose.yml + app/ optional staged commit ``` Nothing above has to name the shared directories. A host process asks the container runtime — docker, @@ -186,7 +198,8 @@ instead, run `gcb stage --to "$DEPLOY/app"`, install its dependencies in that di root you intend to export, or pass `--repo `: it defaults to the current directory and exports that repository's `HEAD`, never the working tree. `stage` is export-only: it does not install dependencies or build, and it never runs the commit's code. It leaves `STAGED.json` beside the code -recording the commit. +recording the commit; `--code` does not require that file, and the directory may have any name outside +the mounts. ## Quick start: host process @@ -272,7 +285,7 @@ app's **private key**, so run it where the key is. On a host-process deployment a sidecar it is inside the container: ```sh -docker compose -f docker-compose.broker.yml exec gcb \ +docker compose -f "$DEPLOY/docker-compose.yml" exec gcb \ npx --yes --package gcb-host gcb \ diagnose --config /etc/git-cred-broker/broker.config.yml ``` @@ -583,7 +596,8 @@ packages/client/src/ credential helper, setup, probe, logs, pr, gh packages/host/src/ YAML config, providers, broker, daemon and host commands packages/*/bin/ the two installed gcb bins (client and host) test/ unit tests, plus an end-to-end push over authenticated smart HTTP -examples/ configuration and compose snippets +examples/ configuration snippets +docker-compose.example.yml tracked sidecar example DEVELOPMENT.md building from source, tests, releasing .agents/notes/ the design, its review, and the measured environment facts ``` diff --git a/examples/docker-compose.snippet.yml b/docker-compose.example.yml similarity index 99% rename from examples/docker-compose.snippet.yml rename to docker-compose.example.yml index a66ee43e..6ac26e10 100644 --- a/examples/docker-compose.snippet.yml +++ b/docker-compose.example.yml @@ -3,7 +3,7 @@ # `gcb compose` prints the sidecar in 1b, already filled in for your paths: # # gcb compose --socket-dir \ -# > docker-compose.broker.yml +# > docker-compose.yml # # Generate it rather than retyping it. The mount targets inside the container must match the # paths `init --mode sidecar` recorded, and both commands read them from one definition, so a diff --git a/packages/host/src/commands/init.ts b/packages/host/src/commands/init.ts index 70b81acb..89c92008 100644 --- a/packages/host/src/commands/init.ts +++ b/packages/host/src/commands/init.ts @@ -604,7 +604,7 @@ export async function runInit(argv: readonly string[]): Promise { if (result.previous) { say('Restart the broker to pick this up:'); if (result.mode === 'sidecar') { - say(' docker compose -f docker-compose.broker.yml up -d # sidecar'); + say(` docker compose -f ${path.dirname(result.keyPath)}/docker-compose.yml up -d # sidecar`); } else { say(` gcb daemon --config ${result.configPath} # host process`); } @@ -628,7 +628,7 @@ export async function runInit(argv: readonly string[]): Promise { say( ` gcb compose --dir ${path.dirname(result.keyPath)} --config ${result.configPath} --socket-dir \\`, ); - say(' > docker-compose.broker.yml && docker compose -f docker-compose.broker.yml up -d'); + say(` > ${path.dirname(result.keyPath)}/docker-compose.yml && docker compose -f ${path.dirname(result.keyPath)}/docker-compose.yml up -d`); } else { say('Then start the broker as a host process:'); say(` gcb daemon --config ${result.configPath} --check # validate, bind nothing`); From e894aacf0e64319a430a6d0a750a2d33a08961f5 Mon Sep 17 00:00:00 2001 From: JounQin Date: Thu, 29 Oct 2026 21:21:58 +0000 Subject: [PATCH 02/20] docs: use the compose filename relative to the deployment directory --- README.md | 2 +- packages/host/src/commands/init.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1eee02ec..a5c0b6a3 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ app's **private key**, so run it where the key is. On a host-process deployment a sidecar it is inside the container: ```sh -docker compose -f "$DEPLOY/docker-compose.yml" exec gcb \ +cd "$DEPLOY" && docker compose -f docker-compose.yml exec gcb \ npx --yes --package gcb-host gcb \ diagnose --config /etc/git-cred-broker/broker.config.yml ``` diff --git a/packages/host/src/commands/init.ts b/packages/host/src/commands/init.ts index 89c92008..a86b602b 100644 --- a/packages/host/src/commands/init.ts +++ b/packages/host/src/commands/init.ts @@ -604,7 +604,7 @@ export async function runInit(argv: readonly string[]): Promise { if (result.previous) { say('Restart the broker to pick this up:'); if (result.mode === 'sidecar') { - say(` docker compose -f ${path.dirname(result.keyPath)}/docker-compose.yml up -d # sidecar`); + say(` cd ${path.dirname(result.keyPath)} && docker compose -f docker-compose.yml up -d # sidecar`); } else { say(` gcb daemon --config ${result.configPath} # host process`); } @@ -628,7 +628,8 @@ export async function runInit(argv: readonly string[]): Promise { say( ` gcb compose --dir ${path.dirname(result.keyPath)} --config ${result.configPath} --socket-dir \\`, ); - say(` > ${path.dirname(result.keyPath)}/docker-compose.yml && docker compose -f ${path.dirname(result.keyPath)}/docker-compose.yml up -d`); + say(` > ${path.dirname(result.keyPath)}/docker-compose.yml`); + say(` cd ${path.dirname(result.keyPath)} && docker compose -f docker-compose.yml up -d`); } else { say('Then start the broker as a host process:'); say(` gcb daemon --config ${result.configPath} --check # validate, bind nothing`); From 95a6c6501497f63fdcf80e156d129c63e8289554 Mon Sep 17 00:00:00 2001 From: JounQin Date: Thu, 29 Oct 2026 21:23:46 +0000 Subject: [PATCH 03/20] docs: rely on compose's default docker-compose.yml --- README.md | 4 ++-- docker-compose.example.yml | 2 ++ packages/host/src/commands/init.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a5c0b6a3..11d95476 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ gcb init --mode sidecar --dir "$DEPLOY" \ gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" \ > "$DEPLOY/docker-compose.yml" -docker compose -f "$DEPLOY/docker-compose.yml" up -d +cd "$DEPLOY" && docker compose up -d ``` A deployment directory holds the config, key, log and compose file; `app/` is only a convention @@ -285,7 +285,7 @@ app's **private key**, so run it where the key is. On a host-process deployment a sidecar it is inside the container: ```sh -cd "$DEPLOY" && docker compose -f docker-compose.yml exec gcb \ +cd "$DEPLOY" && docker compose exec gcb \ npx --yes --package gcb-host gcb \ diagnose --config /etc/git-cred-broker/broker.config.yml ``` diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 6ac26e10..dc857c64 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -5,6 +5,8 @@ # gcb compose --socket-dir \ # > docker-compose.yml # +# Then, from that directory: docker compose up -d +# # Generate it rather than retyping it. The mount targets inside the container must match the # paths `init --mode sidecar` recorded, and both commands read them from one definition, so a # hand-edited copy is the one thing that can silently drift. diff --git a/packages/host/src/commands/init.ts b/packages/host/src/commands/init.ts index a86b602b..9a86497b 100644 --- a/packages/host/src/commands/init.ts +++ b/packages/host/src/commands/init.ts @@ -604,7 +604,7 @@ export async function runInit(argv: readonly string[]): Promise { if (result.previous) { say('Restart the broker to pick this up:'); if (result.mode === 'sidecar') { - say(` cd ${path.dirname(result.keyPath)} && docker compose -f docker-compose.yml up -d # sidecar`); + say(` cd ${path.dirname(result.keyPath)} && docker compose up -d # sidecar`); } else { say(` gcb daemon --config ${result.configPath} # host process`); } @@ -629,7 +629,7 @@ export async function runInit(argv: readonly string[]): Promise { ` gcb compose --dir ${path.dirname(result.keyPath)} --config ${result.configPath} --socket-dir \\`, ); say(` > ${path.dirname(result.keyPath)}/docker-compose.yml`); - say(` cd ${path.dirname(result.keyPath)} && docker compose -f docker-compose.yml up -d`); + say(` cd ${path.dirname(result.keyPath)} && docker compose up -d`); } else { say('Then start the broker as a host process:'); say(` gcb daemon --config ${result.configPath} --check # validate, bind nothing`); From 84a7ea4954b36efe1c0d46340c5b37d774896b0d Mon Sep 17 00:00:00 2001 From: JounQin Date: Thu, 29 Oct 2026 21:28:40 +0000 Subject: [PATCH 04/20] docs: quote deployment paths and note the legacy compose file --- DEVELOPMENT.md | 2 +- packages/host/src/commands/init.ts | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8c9f7665..d5995c76 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -87,7 +87,7 @@ As an image, build the host stage of the root Dockerfile: ```sh docker build --target host -t gcb-host:local . -gcb compose --image gcb-host:local > docker-compose.yml +gcb compose --image gcb-host:local > /tmp/gcb-compose.yml ``` Then keep the rendered `sh -c` wrapper (it applies `GCB_PROXY`) and change only the final diff --git a/packages/host/src/commands/init.ts b/packages/host/src/commands/init.ts index 9a86497b..3195dbe6 100644 --- a/packages/host/src/commands/init.ts +++ b/packages/host/src/commands/init.ts @@ -28,6 +28,11 @@ import { type DeploymentMode, } from '../config.ts'; import { parseAllowEntry } from '../policy.ts'; + +/** Quote one value as a single shell word, including paths with spaces. */ +function shellQuote(value: string): string { + return `'${value.replaceAll("'", "'\\''")}'`; +} import type { BrokerConfig, GithubAppHostConfig } from 'gcb-core'; import { brokerPaths, @@ -604,7 +609,14 @@ export async function runInit(argv: readonly string[]): Promise { if (result.previous) { say('Restart the broker to pick this up:'); if (result.mode === 'sidecar') { - say(` cd ${path.dirname(result.keyPath)} && docker compose up -d # sidecar`); + const dir = path.dirname(result.keyPath); + const legacy = path.join(dir, 'docker-compose.broker.yml'); + const compose = path.join(dir, 'docker-compose.yml'); + if (fs.existsSync(legacy) && !fs.existsSync(compose)) { + say('Rename the old sidecar file once:'); + say(` mv ${shellQuote(legacy)} ${shellQuote(compose)}`); + } + say(` cd ${shellQuote(dir)} && docker compose up -d # sidecar`); } else { say(` gcb daemon --config ${result.configPath} # host process`); } @@ -626,10 +638,10 @@ export async function runInit(argv: readonly string[]): Promise { if (result.mode === 'sidecar') { say('Then start the sidecar, which mounts those paths exactly as recorded:'); say( - ` gcb compose --dir ${path.dirname(result.keyPath)} --config ${result.configPath} --socket-dir \\`, + ` gcb compose --dir ${shellQuote(path.dirname(result.keyPath))} --config ${shellQuote(result.configPath)} --socket-dir \\`, ); - say(` > ${path.dirname(result.keyPath)}/docker-compose.yml`); - say(` cd ${path.dirname(result.keyPath)} && docker compose up -d`); + say(` > ${shellQuote(path.join(path.dirname(result.keyPath), 'docker-compose.yml'))}`); + say(` cd ${shellQuote(path.dirname(result.keyPath))} && docker compose up -d`); } else { say('Then start the broker as a host process:'); say(` gcb daemon --config ${result.configPath} --check # validate, bind nothing`); From b7fd05a89415bcc9d8c9e84d6c1e040554070681 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 01:28:58 +0000 Subject: [PATCH 05/20] docs: render the dev compose file in the deployment directory --- DEVELOPMENT.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d5995c76..8b138094 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -87,7 +87,8 @@ As an image, build the host stage of the root Dockerfile: ```sh docker build --target host -t gcb-host:local . -gcb compose --image gcb-host:local > /tmp/gcb-compose.yml +gcb compose --image gcb-host:local > docker-compose.yml +docker compose config # parse the rendered file ``` Then keep the rendered `sh -c` wrapper (it applies `GCB_PROXY`) and change only the final @@ -106,6 +107,9 @@ Then keep the rendered `sh -c` wrapper (it applies `GCB_PROXY`) and change only exec gcb daemon --config /etc/git-cred-broker/broker.config.yml ``` +Run it from that deployment directory with `docker compose up -d`. The config path defaults to +`./broker.config.yml`, so this is a deployment directory, not the repository root. + ## Tests ```sh From 151efb948e3a6516e0575de22c16f75983823de0 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 01:32:20 +0000 Subject: [PATCH 06/20] docs: use the shared GHCR dev tag for the local host image --- DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8b138094..0c25f97b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -86,8 +86,8 @@ gcb --version As an image, build the host stage of the root Dockerfile: ```sh -docker build --target host -t gcb-host:local . -gcb compose --image gcb-host:local > docker-compose.yml +docker build --target host -t ghcr.io/code-vaults/gcb-host:dev . +gcb compose --image ghcr.io/code-vaults/gcb-host:dev > docker-compose.yml docker compose config # parse the rendered file ``` From dc4adcaedccf215eb95e2d807d3e8078adbd8e39 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 01:33:52 +0000 Subject: [PATCH 07/20] refactor: drop the legacy compose filename notice --- packages/host/src/commands/init.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/host/src/commands/init.ts b/packages/host/src/commands/init.ts index 3195dbe6..67c7eafe 100644 --- a/packages/host/src/commands/init.ts +++ b/packages/host/src/commands/init.ts @@ -609,14 +609,7 @@ export async function runInit(argv: readonly string[]): Promise { if (result.previous) { say('Restart the broker to pick this up:'); if (result.mode === 'sidecar') { - const dir = path.dirname(result.keyPath); - const legacy = path.join(dir, 'docker-compose.broker.yml'); - const compose = path.join(dir, 'docker-compose.yml'); - if (fs.existsSync(legacy) && !fs.existsSync(compose)) { - say('Rename the old sidecar file once:'); - say(` mv ${shellQuote(legacy)} ${shellQuote(compose)}`); - } - say(` cd ${shellQuote(dir)} && docker compose up -d # sidecar`); + say(` cd ${shellQuote(path.dirname(result.keyPath))} && docker compose up -d # sidecar`); } else { say(` gcb daemon --config ${result.configPath} # host process`); } From 99efea337d968240de2b4b847a7cc0186d38ba22 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 01:41:32 +0000 Subject: [PATCH 08/20] docs: make the development compose workflow use its deployment directory Build from the checkout, render into $DEPLOY, and move the shellQuote helper below the imports. --- DEVELOPMENT.md | 5 +++-- packages/host/src/commands/init.ts | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0c25f97b..62864e7f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -87,8 +87,9 @@ As an image, build the host stage of the root Dockerfile: ```sh docker build --target host -t ghcr.io/code-vaults/gcb-host:dev . -gcb compose --image ghcr.io/code-vaults/gcb-host:dev > docker-compose.yml -docker compose config # parse the rendered file +DEPLOY=/path/to/deployment +gcb compose --dir "$DEPLOY" --image ghcr.io/code-vaults/gcb-host:dev > "$DEPLOY/docker-compose.yml" +cd "$DEPLOY" && docker compose config # parse the rendered file ``` Then keep the rendered `sh -c` wrapper (it applies `GCB_PROXY`) and change only the final diff --git a/packages/host/src/commands/init.ts b/packages/host/src/commands/init.ts index 67c7eafe..b2bd5ff7 100644 --- a/packages/host/src/commands/init.ts +++ b/packages/host/src/commands/init.ts @@ -29,10 +29,6 @@ import { } from '../config.ts'; import { parseAllowEntry } from '../policy.ts'; -/** Quote one value as a single shell word, including paths with spaces. */ -function shellQuote(value: string): string { - return `'${value.replaceAll("'", "'\\''")}'`; -} import type { BrokerConfig, GithubAppHostConfig } from 'gcb-core'; import { brokerPaths, @@ -44,6 +40,11 @@ import { import type { BrokerPaths } from './deployment.ts'; import { fail, insideMountedPath, isInsideContainer, parseArgs, say, writeSecretFile } from 'gcb-core'; +/** Quote one value as a single shell word, including paths with spaces. */ +function shellQuote(value: string): string { + return `'${value.replaceAll("'", "'\\''")}'`; +} + /** The environment `init` reasons about, injectable so the guard rails can be tested. */ export interface InitEnvironment { /** Whether this process is inside the container. */ From 0e5eb02444a2d830435378240fd6e393c519a4d8 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 01:42:16 +0000 Subject: [PATCH 09/20] docs: define the deployment directory in the diagnose snippet --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 11d95476..08bee020 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,7 @@ app's **private key**, so run it where the key is. On a host-process deployment a sidecar it is inside the container: ```sh +DEPLOY=/srv/git-cred-broker cd "$DEPLOY" && docker compose exec gcb \ npx --yes --package gcb-host gcb \ diagnose --config /etc/git-cred-broker/broker.config.yml From d2801eadd9750fb0f6ab445c1847318df46f435d Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 02:35:02 +0000 Subject: [PATCH 10/20] docs: pass the required socket directory in the dev compose example --- DEVELOPMENT.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 62864e7f..3c9f2cba 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -88,7 +88,9 @@ As an image, build the host stage of the root Dockerfile: ```sh docker build --target host -t ghcr.io/code-vaults/gcb-host:dev . DEPLOY=/path/to/deployment -gcb compose --dir "$DEPLOY" --image ghcr.io/code-vaults/gcb-host:dev > "$DEPLOY/docker-compose.yml" +SOCKET_DIR=/path/to/socket +gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" --image ghcr.io/code-vaults/gcb-host:dev \ + > "$DEPLOY/docker-compose.yml" cd "$DEPLOY" && docker compose config # parse the rendered file ``` From 518c409fdfd9878a5c5da76fa434fed603d714bb Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 03:11:45 +0000 Subject: [PATCH 11/20] docs: pass the pusher uid and gid in the dev compose example --- DEVELOPMENT.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 3c9f2cba..fc1270a8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -89,8 +89,10 @@ As an image, build the host stage of the root Dockerfile: docker build --target host -t ghcr.io/code-vaults/gcb-host:dev . DEPLOY=/path/to/deployment SOCKET_DIR=/path/to/socket -gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" --image ghcr.io/code-vaults/gcb-host:dev \ - > "$DEPLOY/docker-compose.yml" +PUSHER_UID=1000 # uid of the container that pushes +PUSHER_GID=1000 # gid of the container that pushes +gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" --user "$PUSHER_UID:$PUSHER_GID" \ + --image ghcr.io/code-vaults/gcb-host:dev > "$DEPLOY/docker-compose.yml" cd "$DEPLOY" && docker compose config # parse the rendered file ``` From 0c294c4ca965abb8180a9d4bfd29379bdfa7d7bd Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 03:48:08 +0000 Subject: [PATCH 12/20] feat: default probe and log host to github.com --- packages/client/src/commands/logs.ts | 5 ++--- packages/client/src/commands/probe.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/client/src/commands/logs.ts b/packages/client/src/commands/logs.ts index 16bad269..7620a482 100644 --- a/packages/client/src/commands/logs.ts +++ b/packages/client/src/commands/logs.ts @@ -24,7 +24,7 @@ export const LOGS_USAGE = `Usage: gcb logs [options] Read a workflow job log through the broker, without the credential ever reaching this side. --socket Broker socket (default: $GIT_BROKER_SOCKET or the socket file) - --host Host as configured, e.g. github.com [required] + --host Host as configured (default: github.com) --repo Repository the job belongs to [required] --job Workflow job id, which is also the check run id [required] --proxy Use this proxy for this request @@ -49,13 +49,12 @@ export async function runLogs(argv: readonly string[]): Promise { } const socketPath = resolveSocketPath(args.value('socket'), process.env); - const host = args.value('host'); + const host = args.value('host') ?? 'github.com'; const repo = args.value('repo'); const rawJob = args.value('job'); if (!socketPath) { fail('no broker socket: pass --socket, set GIT_BROKER_SOCKET, or run `gcb setup`'); } - if (!host) fail('--host is required (e.g. github.com)'); if (!repo) fail('--repo is required (e.g. owner/name)'); if (!rawJob) fail('--job is required (the workflow job id, which is also the check run id)'); const jobId = Number(rawJob); diff --git a/packages/client/src/commands/probe.ts b/packages/client/src/commands/probe.ts index 1ab911b9..1c040d38 100644 --- a/packages/client/src/commands/probe.ts +++ b/packages/client/src/commands/probe.ts @@ -15,7 +15,7 @@ export const PROBE_USAGE = `Usage: gcb probe [options] Ask the broker for a credential without pushing. Prints a fingerprint, never the credential. --socket Broker socket (default: $GIT_BROKER_SOCKET or the socket file) - --host Host as git reports it, e.g. github.com [required] + --host Host as git reports it (default: github.com) --repo Repository path [required] --protocol

https (default) or http for a host that opted in --proxy Use this proxy for this request @@ -40,12 +40,11 @@ export async function runProbe(argv: readonly string[]): Promise { } const socketPath = resolveSocketPath(args.value('socket'), process.env); - const host = args.value('host'); + const host = args.value('host') ?? 'github.com'; const repo = args.value('repo'); if (!socketPath) { fail('no broker socket: pass --socket, set GIT_BROKER_SOCKET, or run `gcb setup`'); } - if (!host) fail('--host is required (e.g. github.com)'); if (!repo) fail('--repo is required (e.g. owner/name)'); const protocol = args.value('protocol') ?? 'https'; From 9d2e9c44c133ab599dff5c8223e7bc3f6164bc65 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 03:55:13 +0000 Subject: [PATCH 13/20] docs: record the dev image deployment --- DEVELOPMENT.md | 59 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index fc1270a8..9685ab88 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -83,37 +83,48 @@ npm install -g /tmp/gcb-packs/gcb-core-*.tgz /tmp/gcb-packs/gcb-host-*.tgz gcb --version ``` -As an image, build the host stage of the root Dockerfile: +### Deploying the dev images + +The two dev tags are built from the current checkout and are never pushed: + +```sh +docker build --network=host --target host -t code-vaults/gcb-host:dev . +docker build --network=host --target client -t code-vaults/gcb-client:dev . +``` + +The `host` image carries `gcb` and `gh` and runs the broker. The `client` image is the +embedding source for an idsh-style base (`COPY --from=...`); it is not a second broker. + +Render a deployment for the host image on the host that runs it: ```sh -docker build --target host -t ghcr.io/code-vaults/gcb-host:dev . DEPLOY=/path/to/deployment -SOCKET_DIR=/path/to/socket -PUSHER_UID=1000 # uid of the container that pushes -PUSHER_GID=1000 # gid of the container that pushes -gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" --user "$PUSHER_UID:$PUSHER_GID" \ - --image ghcr.io/code-vaults/gcb-host:dev > "$DEPLOY/docker-compose.yml" -cd "$DEPLOY" && docker compose config # parse the rendered file +SOCKET_DIR=/path/to/shared/socket +PUSHER_UID=1000 +PUSHER_GID=1000 +gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" \ + --user "$PUSHER_UID:$PUSHER_GID" --image code-vaults/gcb-host:dev \ + > "$DEPLOY/docker-compose.yml" ``` -Then keep the rendered `sh -c` wrapper (it applies `GCB_PROXY`) and change only the final -`exec npx ...` to the binary the image installed, since the default one fetches the published package: - -```yaml - command: - - sh - - -c - - | - case "${GCB_PROXY-default}" in - default) ;; - "") unset http_proxy https_proxy all_proxy no_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY NODE_USE_ENV_PROXY ;; - *) export http_proxy="$GCB_PROXY" https_proxy="$GCB_PROXY" no_proxy=localhost,127.0.0.1 NODE_USE_ENV_PROXY=1 ;; - esac - exec gcb daemon --config /etc/git-cred-broker/broker.config.yml +Change only the generated final `exec npx ...` to `exec gcb daemon`, because the dev image already +installs the package; do **not** keep a staged `/opt/gcb` mount, or the image's code is ignored. +Then recreate from the deployment directory, choosing the proxy at runtime: + +```sh +cd "$DEPLOY" +GCB_PROXY=http://proxy.example:7890 docker compose up -d --force-recreate +docker compose logs --tail 20 gcb +``` + +Verify from the pushing side: + +```sh +gcb probe --repo owner/repo ``` -Run it from that deployment directory with `docker compose up -d`. The config path defaults to -`./broker.config.yml`, so this is a deployment directory, not the repository root. +`GCB_PROXY` is read at `docker compose up` time. An unset value keeps the host proxy variables, an +empty value goes direct, and a URL overrides them; changing it needs `--force-recreate`. ## Tests From 0afbe0a6a77362fbdf2763d2a3385ce54811c076 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 04:16:27 +0000 Subject: [PATCH 14/20] docs: require the sidecar to run as the pusher identity The socket is 0660 and created by the pusher uid/gid; make --user explicit in the quick start and init guidance. --- README.md | 8 +++++++- docker-compose.example.yml | 1 + packages/host/src/commands/init.ts | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 08bee020..32a34a34 100644 --- a/README.md +++ b/README.md @@ -161,16 +161,22 @@ registry access at boot. ```sh DEPLOY=/srv/git-cred-broker # the deployment directory SOCKET_DIR=/srv/git-broker-socket # a host directory the pushing container already mounts +PUSHER_UID=1000 # uid of the container that will use the socket +PUSHER_GID=1000 # gid of that container gcb init --mode sidecar --dir "$DEPLOY" \ --cert ~/Downloads/app.private-key.pem \ --allow owner/repo --client-id Iv23li… --app-id 123456 gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" \ - > "$DEPLOY/docker-compose.yml" + --user "$PUSHER_UID:$PUSHER_GID" > "$DEPLOY/docker-compose.yml" cd "$DEPLOY" && docker compose up -d ``` +`--user` must be the uid:gid of the container that will use the socket; the socket is mode 0660 and +created by that identity. A sidecar running as a different user cannot connect. If the pusher runs +as the same user as the sidecar, the default is correct. + A deployment directory holds the config, key, log and compose file; `app/` is only a convention for a staged commit, and `--code` accepts any directory outside every mount. diff --git a/docker-compose.example.yml b/docker-compose.example.yml index dc857c64..90d85315 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -57,6 +57,7 @@ services: container_name: git-cred-broker restart: unless-stopped # Must match the uid/gid of the container that will use the socket, or it is unusable. + # `compose --user ":"` renders this from the deployment. user: "1026:100" read_only: true tmpfs: diff --git a/packages/host/src/commands/init.ts b/packages/host/src/commands/init.ts index b2bd5ff7..9c83de2a 100644 --- a/packages/host/src/commands/init.ts +++ b/packages/host/src/commands/init.ts @@ -632,7 +632,7 @@ export async function runInit(argv: readonly string[]): Promise { if (result.mode === 'sidecar') { say('Then start the sidecar, which mounts those paths exactly as recorded:'); say( - ` gcb compose --dir ${shellQuote(path.dirname(result.keyPath))} --config ${shellQuote(result.configPath)} --socket-dir \\`, + ` gcb compose --dir ${shellQuote(path.dirname(result.keyPath))} --config ${shellQuote(result.configPath)} --socket-dir --user \\`, ); say(` > ${shellQuote(path.join(path.dirname(result.keyPath), 'docker-compose.yml'))}`); say(` cd ${shellQuote(path.dirname(result.keyPath))} && docker compose up -d`); From eb0bf14fbba1c4f8998eec306d8ef92375dc3a07 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 06:21:00 +0000 Subject: [PATCH 15/20] fix: complete the dev deployment docs and host default Add a gcb-client changeset, unify the local dev tags, show the exact image daemon command, and reject an empty --host. --- .changeset/compose-layout.md | 6 ++++-- DEVELOPMENT.md | 10 ++++++++-- Dockerfile | 4 ++-- README.md | 16 ++++++++-------- packages/client/src/commands/logs.ts | 1 + packages/client/src/commands/probe.ts | 1 + test/unit/commands.test.ts | 2 +- 7 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.changeset/compose-layout.md b/.changeset/compose-layout.md index 470c6ca8..f211c18e 100644 --- a/.changeset/compose-layout.md +++ b/.changeset/compose-layout.md @@ -1,6 +1,8 @@ --- +'gcb-client': patch 'gcb-host': patch --- -Name the rendered sidecar file `docker-compose.yml` in the deployment directory and document the -deployment layout. +Default `gcb probe` and `gcb logs` to `github.com`, name the rendered sidecar file +`docker-compose.yml` in the deployment directory, and document the deployment layout and the +`--user` requirement. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 9685ab88..36a559ae 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -107,8 +107,14 @@ gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" \ > "$DEPLOY/docker-compose.yml" ``` -Change only the generated final `exec npx ...` to `exec gcb daemon`, because the dev image already -installs the package; do **not** keep a staged `/opt/gcb` mount, or the image's code is ignored. +Change only the `exec npx --yes --package … gcb` prefix to `exec gcb`, keeping `daemon --config …`: + +```sh +exec gcb daemon --config /etc/git-cred-broker/broker.config.yml +``` + +The dev image already installs the package; do **not** keep a staged `/opt/gcb` mount, or the +image's code is ignored. Then recreate from the deployment directory, choosing the proxy at runtime: ```sh diff --git a/Dockerfile b/Dockerfile index e6b97170..8e851ecc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,8 @@ # host the broker, with gh, for running `gcb daemon` # # Local build: -# docker build --target client -t ghcr.io/code-vaults/gcb-client:dev . -# docker build --target host -t ghcr.io/code-vaults/gcb-host:dev . +# docker build --target client -t code-vaults/gcb-client:dev . +# docker build --target host -t code-vaults/gcb-host:dev . FROM node:24-slim AS build WORKDIR /src diff --git a/README.md b/README.md index 32a34a34..d6e807eb 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,11 @@ Keep the checkout outside every container mount. ## GitHub actions The action path is wired through the broker; the host image must include `gh` before these commands -can run. The container never receives the token. Build the image, then pass `--image ghcr.io/code-vaults/gcb-host:dev` to +can run. The container never receives the token. Build the image, then pass `--image code-vaults/gcb-host:dev` to the compose command in [Quick start](#quick-start-sidecar): ```sh -docker build --target host -t ghcr.io/code-vaults/gcb-host:dev . +docker build --target host -t code-vaults/gcb-host:dev . ``` The image's `CMD` starts `gcb daemon`, but `compose` still renders the registry-fetching `npx` @@ -626,12 +626,12 @@ The contract between the pushing container and the broker is deliberately small: ## Images -Two GHCR repositories mirror the two packages: +Two local dev images mirror the two packages; the release publishes the same repositories on GHCR: | Image | Contents | Use | |---|---|---| -| `ghcr.io/code-vaults/gcb-client:dev` | `gcb` client and Node runtime only | idsh base / `COPY --from` | -| `ghcr.io/code-vaults/gcb-host:dev` | broker, `gh`, and broker dependencies | run `gcb daemon` on the host | +| `code-vaults/gcb-client:dev` | `gcb` client and Node runtime only | idsh base / `COPY --from` | +| `code-vaults/gcb-host:dev` | broker, `gh`, and broker dependencies | run `gcb daemon` on the host | The release publishes both repositories with `latest`, `` and `sha-` tags. The `:dev` tags are local build tags and are not published; do not pull them from GHCR. @@ -639,8 +639,8 @@ The release publishes both repositories with `latest`, `` and `sha- { const socketPath = resolveSocketPath(args.value('socket'), process.env); const host = args.value('host') ?? 'github.com'; + if (host === '') fail('--host needs a value'); const repo = args.value('repo'); const rawJob = args.value('job'); if (!socketPath) { diff --git a/packages/client/src/commands/probe.ts b/packages/client/src/commands/probe.ts index 1c040d38..22f56799 100644 --- a/packages/client/src/commands/probe.ts +++ b/packages/client/src/commands/probe.ts @@ -41,6 +41,7 @@ export async function runProbe(argv: readonly string[]): Promise { const socketPath = resolveSocketPath(args.value('socket'), process.env); const host = args.value('host') ?? 'github.com'; + if (host === '') fail('--host needs a value'); const repo = args.value('repo'); if (!socketPath) { fail('no broker socket: pass --socket, set GIT_BROKER_SOCKET, or run `gcb setup`'); diff --git a/test/unit/commands.test.ts b/test/unit/commands.test.ts index 2b685bca..1c411f33 100644 --- a/test/unit/commands.test.ts +++ b/test/unit/commands.test.ts @@ -880,7 +880,7 @@ describe('socket discovery', () => { delete process.env['GIT_BROKER_SOCKET']; process.env['GIT_BROKER_SOCKET_FILE'] = socketFile; try { - const code = await runProbe(['--host', 'github.com', '--repo', 'a/one']); + const code = await runProbe(['--repo', 'a/one']); assert.equal(code, 3, 'it must use the path from the file and report the broker unreachable'); } finally { if (savedSocket === undefined) delete process.env['GIT_BROKER_SOCKET']; From 6a9ab370d2490bb354564972daa69135cb6dfe26 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 06:21:25 +0000 Subject: [PATCH 16/20] docs: use the same example pusher uid in both files --- docker-compose.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 90d85315..a72c17e0 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -58,7 +58,7 @@ services: restart: unless-stopped # Must match the uid/gid of the container that will use the socket, or it is unusable. # `compose --user ":"` renders this from the deployment. - user: "1026:100" + user: "1000:1000" read_only: true tmpfs: # npx needs somewhere writable for its cache; /tmp is a tmpfs above. From 154aa5e1a287ad83b2d2614cea3a83fdafb7d0fd Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 06:34:53 +0000 Subject: [PATCH 17/20] fix: reject a bare --host and document sidecar file ownership --- README.md | 5 +++++ packages/client/src/commands/logs.ts | 6 ++++-- packages/client/src/commands/probe.ts | 6 ++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d6e807eb..345582ab 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,11 @@ cd "$DEPLOY" && docker compose up -d created by that identity. A sidecar running as a different user cannot connect. If the pusher runs as the same user as the sidecar, the default is correct. +`init` writes the configuration and key as the invoking user, with the key mode 0600; the +sidecar's user must be able to read both and write `log/`. If `PUSHER_UID` differs from the user +running `init`, either run `init` as that uid or `chown` the config, key and log directory to it +(keep the key 0600). Do not make the key world-readable. + A deployment directory holds the config, key, log and compose file; `app/` is only a convention for a staged commit, and `--code` accepts any directory outside every mount. diff --git a/packages/client/src/commands/logs.ts b/packages/client/src/commands/logs.ts index 90203ffd..6031d830 100644 --- a/packages/client/src/commands/logs.ts +++ b/packages/client/src/commands/logs.ts @@ -49,8 +49,10 @@ export async function runLogs(argv: readonly string[]): Promise { } const socketPath = resolveSocketPath(args.value('socket'), process.env); - const host = args.value('host') ?? 'github.com'; - if (host === '') fail('--host needs a value'); + const hostArg = args.value('host'); + if (args.has('host') && hostArg === undefined) fail('--host needs a value'); + if (hostArg === '') fail('--host needs a value'); + const host = hostArg ?? 'github.com'; const repo = args.value('repo'); const rawJob = args.value('job'); if (!socketPath) { diff --git a/packages/client/src/commands/probe.ts b/packages/client/src/commands/probe.ts index 22f56799..18bdb409 100644 --- a/packages/client/src/commands/probe.ts +++ b/packages/client/src/commands/probe.ts @@ -40,8 +40,10 @@ export async function runProbe(argv: readonly string[]): Promise { } const socketPath = resolveSocketPath(args.value('socket'), process.env); - const host = args.value('host') ?? 'github.com'; - if (host === '') fail('--host needs a value'); + const hostArg = args.value('host'); + if (args.has('host') && hostArg === undefined) fail('--host needs a value'); + if (hostArg === '') fail('--host needs a value'); + const host = hostArg ?? 'github.com'; const repo = args.value('repo'); if (!socketPath) { fail('no broker socket: pass --socket, set GIT_BROKER_SOCKET, or run `gcb setup`'); From 20275aeb8292eba66d897e426c816759db9f15df Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 06:41:44 +0000 Subject: [PATCH 18/20] docs: include user.refresh in sidecar ownership --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 345582ab..b69a2df6 100644 --- a/README.md +++ b/README.md @@ -177,10 +177,13 @@ cd "$DEPLOY" && docker compose up -d created by that identity. A sidecar running as a different user cannot connect. If the pusher runs as the same user as the sidecar, the default is correct. -`init` writes the configuration and key as the invoking user, with the key mode 0600; the -sidecar's user must be able to read both and write `log/`. If `PUSHER_UID` differs from the user -running `init`, either run `init` as that uid or `chown` the config, key and log directory to it -(keep the key 0600). Do not make the key world-readable. +`init` writes the configuration and key as the invoking user, with the key mode 0600. If +`authorize` has run, it also writes `user.refresh` beside the key, mode 0600 and **read-write**: +the broker rotates that token on every exchange. The sidecar's user must read the config, key and +`user.refresh`, and write `log/` (including an existing audit file). If `PUSHER_UID` differs from +the user running `init`/`authorize`, either run both as that uid or `chown` the config, key, +`user.refresh`, the log directory and its audit file to it; re-run the `chown` after `authorize` +creates `user.refresh`. Keep the key and `user.refresh` at 0600; do not make them world-readable. A deployment directory holds the config, key, log and compose file; `app/` is only a convention for a staged commit, and `--code` accepts any directory outside every mount. From 0ed8dc6310777e664724dab27165db5970412169 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 06:50:24 +0000 Subject: [PATCH 19/20] test: cover bare and empty --host refusals --- test/unit/commands.test.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/unit/commands.test.ts b/test/unit/commands.test.ts index 1c411f33..f26adad1 100644 --- a/test/unit/commands.test.ts +++ b/test/unit/commands.test.ts @@ -891,6 +891,41 @@ describe('socket discovery', () => { }); }); +describe('host flag validation at the CLI boundary', () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gcb-host-home-')); + after(() => fs.rmSync(home, { recursive: true, force: true })); + + /** Run a client command as a process and return its refusal. */ + const refuse = (command: string, args: readonly string[]): { status: number; stderr: string } => { + try { + execFileSync('node', ['--conditions=development', 'packages/client/bin/gcb.js', command, ...args], { + cwd: path.resolve('.'), + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, HOME: home, GIT_BROKER_SOCKET: '', GIT_BROKER_SOCKET_FILE: '' }, + }); + return { status: 0, stderr: '' }; + } catch (error) { + const failed = error as { status?: number | null; stderr?: string }; + return { status: failed.status ?? -1, stderr: String(failed.stderr ?? '') }; + } + }; + + for (const command of ['probe', 'logs']) { + it(`${command} refuses a bare --host`, () => { + const result = refuse(command, ['--host']); + assert.equal(result.status, 1); + assert.match(result.stderr, /--host needs a value/); + }); + + it(`${command} refuses an empty --host`, () => { + const result = refuse(command, ['--host', '']); + assert.equal(result.status, 1); + assert.match(result.stderr, /--host needs a value/); + }); + } +}); + describe('config discovery', () => { it('prefers --config, then GIT_BROKER_CONFIG, then ./broker.config.yml', () => { assert.equal(resolveConfigPath('/a/b.json', {}), '/a/b.json'); From 5fa0def690fdf7b73217a497ff4e76a6e9273e5e Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 30 Oct 2026 07:50:19 +0000 Subject: [PATCH 20/20] test: assert probe defaults to github.com --- test/unit/commands.test.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/unit/commands.test.ts b/test/unit/commands.test.ts index f26adad1..aa58ac76 100644 --- a/test/unit/commands.test.ts +++ b/test/unit/commands.test.ts @@ -911,6 +911,38 @@ describe('host flag validation at the CLI boundary', () => { } }; + it('probe sends github.com when --host is omitted', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gcb-probe-default-')); + const socketPath = path.join(root, 'broker.sock'); + let request: Record | undefined; + const server = net.createServer((socket) => { + let buffer = ''; + socket.on('data', (chunk: Buffer) => { + buffer += chunk.toString('utf8'); + const newline = buffer.indexOf('\n'); + if (newline < 0) return; + request = parseMessage(buffer.slice(0, newline)) ?? undefined; + socket.end(encodeMessage({ ok: true, username: 'x-access-token', password: 'secret' })); + }); + }); + await new Promise((resolve) => server.listen(socketPath, resolve)); + const savedSocket = process.env['GIT_BROKER_SOCKET']; + const savedFile = process.env['GIT_BROKER_SOCKET_FILE']; + try { + process.env['GIT_BROKER_SOCKET'] = socketPath; + delete process.env['GIT_BROKER_SOCKET_FILE']; + assert.equal(await runProbe(['--repo', 'a/one']), 0); + assert.equal(request?.['host'], 'github.com'); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + if (savedSocket === undefined) delete process.env['GIT_BROKER_SOCKET']; + else process.env['GIT_BROKER_SOCKET'] = savedSocket; + if (savedFile === undefined) delete process.env['GIT_BROKER_SOCKET_FILE']; + else process.env['GIT_BROKER_SOCKET_FILE'] = savedFile; + fs.rmSync(root, { recursive: true, force: true }); + } + }); + for (const command of ['probe', 'logs']) { it(`${command} refuses a bare --host`, () => { const result = refuse(command, ['--host']);