fix(docker): address git identity review

This commit is contained in:
Devvyn
2026-09-25 22:27:26 +08:00
parent 8d358aaa26
commit bf73a84732
10 changed files with 65 additions and 48 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"aicodeman": minor
---
Docker deployments can configure a static Git commit identity for server and Docker-case agent images.
+4 -4
View File
@@ -15,11 +15,11 @@ TZ=Australia/Perth
# this value rebuilds the image with a matching account.
CODEMAN_RUNTIME_USER=codeman
# Required for Git commits made by Codeman and Docker-case agents. These values
# Optional Git identity for commits made by Codeman and Docker-case agents. These values
# are written to each image's system Git configuration when it is rebuilt, so
# they remain available even when the runtime home directory is a fresh mount.
GIT_USER_NAME=
GIT_USER_EMAIL=
# deployments can configure a consistent default. Set both values together.
# GIT_USER_NAME=
# GIT_USER_EMAIL=
# Required. Persistent Codeman application data, CLI credentials, and session
# state are stored here on the host and mounted at the runtime account's home
+3 -1
View File
@@ -80,7 +80,9 @@ Compose passes the values to the Codeman server build, and to the server process
when it builds Docker-case agent images. Both images write the pair to Git's
system configuration during their build, so commits retain the same identity
after a container or agent image is recreated. Set both values together; an
image build with only one value fails rather than using a partial identity.
image build with only one value fails rather than using a partial identity. An
identity already present in `CODEMAN_APPDATA_PATH`'s `~/.gitconfig` overrides
the server image's system-level default.
Run `bash docker/Start-Codeman.sh` after changing the server values. Rebuild an
existing agent image with `node scripts/build-agent-image.mjs --no-cache` in the
+15 -14
View File
@@ -12,9 +12,6 @@
# writable even though the uid is not the baked 1000.
FROM node:22-bookworm-slim
ARG GIT_USER_EMAIL=
ARG GIT_USER_NAME=
# Base toolchain. `curl` is needed for the hook callbacks (`curl -sk $CODEMAN_API_URL`),
# `procps` for `ps`, `tmux` for the durable in-container session.
RUN apt-get update \
@@ -30,17 +27,6 @@ RUN apt-get update \
openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Docker cases run with a fresh, container-owned home directory. Configure Git
# at the system level during the build so the identity supplied in docker/.env
# remains stable after an agent image rebuild. Refuse an incomplete identity.
RUN set -eux; \
if [ -n "${GIT_USER_NAME}" ] || [ -n "${GIT_USER_EMAIL}" ]; then \
test -n "${GIT_USER_NAME}"; \
test -n "${GIT_USER_EMAIL}"; \
git config --system user.name "${GIT_USER_NAME}"; \
git config --system user.email "${GIT_USER_EMAIL}"; \
fi
# GitHub CLI and Azure CLI (+ the azure-devops extension) with the same system
# git credential helpers as docker/server.Dockerfile, so an agent in a Docker
# case can clone and push to private GitHub / Azure DevOps repositories. The
@@ -268,6 +254,21 @@ RUN useradd -g 0 -m -d /home/agent -s /bin/bash agent \
&& chgrp -R 0 /home/agent \
&& chmod -R g=u /home/agent
# Docker cases have a fresh, container-owned home directory. Declare the
# optional identity here so changing it invalidates only this final layer, then
# configure Git's system defaults. A user-level config still takes precedence.
ARG GIT_USER_EMAIL=
ARG GIT_USER_NAME=
RUN set -eux; \
if [ -n "${GIT_USER_NAME}" ] || [ -n "${GIT_USER_EMAIL}" ]; then \
if [ -z "${GIT_USER_NAME}" ] || [ -z "${GIT_USER_EMAIL}" ]; then \
echo 'Git user name and email must both be set when configuring Git identity' >&2; \
exit 1; \
fi; \
git config --system user.name "${GIT_USER_NAME}"; \
git config --system user.email "${GIT_USER_EMAIL}"; \
fi
USER agent
WORKDIR /home/agent
+2 -2
View File
@@ -36,8 +36,8 @@ services:
CODEMAN_CASES_PATH: ${CODEMAN_CASES_PATH}
# Passed through only so Codeman can use the same identity when it builds
# the Docker-case agent image.
GIT_USER_EMAIL: ${GIT_USER_EMAIL:-}
GIT_USER_NAME: ${GIT_USER_NAME:-}
CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL: ${GIT_USER_EMAIL:-}
CODEMAN_AGENT_IMAGE_GIT_USER_NAME: ${GIT_USER_NAME:-}
# Extra Host-header allowlist entries for a reverse-proxied deployment
# (docker/README.md, "Reverse-proxy host allowlist"). Optional, so it
# defaults to empty rather than requiring a line in every .env.
+15 -14
View File
@@ -25,8 +25,6 @@ RUN npm ci \
FROM node:22-bookworm-slim
ARG CODEMAN_RUNTIME_USER=codeman
ARG GIT_USER_EMAIL=
ARG GIT_USER_NAME=
ARG PUID=1000
ARG PGID=1000
@@ -50,18 +48,6 @@ RUN apt-get update \
tmux \
&& rm -rf /var/lib/apt/lists/*
# A runtime home is normally a bind mount, so user-level Git configuration is
# not durable across a fresh deployment. Keep the operator-supplied identity in
# the image's system config instead. Both values are required together to avoid
# producing commits with a misleading partial identity.
RUN set -eux; \
if [ -n "${GIT_USER_NAME}" ] || [ -n "${GIT_USER_EMAIL}" ]; then \
test -n "${GIT_USER_NAME}"; \
test -n "${GIT_USER_EMAIL}"; \
git config --system user.name "${GIT_USER_NAME}"; \
git config --system user.email "${GIT_USER_EMAIL}"; \
fi
# The Docker CLI, taken from the official image rather than Debian's `docker.io`.
# That package is the full ENGINE: with --no-install-recommends it still pulls 15
# packages including containerd, runc, dmsetup and iptables, none of which a
@@ -313,6 +299,21 @@ EXPOSE 3000
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 0755 /usr/local/bin/entrypoint.sh
# Declare the optional identity immediately before configuring it so a change
# invalidates only this final layer. This is declarative setup: a persisted
# ~/.gitconfig in CODEMAN_APPDATA_PATH still overrides the system-level values.
ARG GIT_USER_EMAIL=
ARG GIT_USER_NAME=
RUN set -eux; \
if [ -n "${GIT_USER_NAME}" ] || [ -n "${GIT_USER_EMAIL}" ]; then \
if [ -z "${GIT_USER_NAME}" ] || [ -z "${GIT_USER_EMAIL}" ]; then \
echo 'Git user name and email must both be set when configuring Git identity' >&2; \
exit 1; \
fi; \
git config --system user.name "${GIT_USER_NAME}"; \
git config --system user.email "${GIT_USER_EMAIL}"; \
fi
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["node", "dist/index.js", "web"]
+2
View File
@@ -83,6 +83,8 @@ Pi's credentials are seeded per-FILE rather than as a whole directory (`auth.jso
The image can also carry the GitHub CLI (`gh`) and the Azure CLI (`az` + the `azure-devops` extension, in `AZURE_EXTENSION_DIR=/opt/az-extensions` so it stays out of the seeded HOME), wired into the system git config as credential helpers for github.com and dev.azure.com / *.visualstudio.com, exactly as in `docker/server.Dockerfile`. Their sign-ins are seeded per-FILE like pi's: `~/.config/gh/{hosts.yml,config.yml}` and `~/.azure/{azureProfile.json,msal_token_cache.json,service_principal_entries.json,clouds.config,config}`, never `~/.azure`'s logs, command index or extensions. A token kept in a desktop keyring, or in the encrypted MSAL cache az uses on Windows/macOS, is not in those files and does not carry. None of the three is version-pinned; the `--no-cache` rebuild recommended above is also what refreshes them. Both CLIs are opt-in and OFF by default: `CODEMAN_AGENT_IMAGE_INSTALL_GH=1` / `CODEMAN_AGENT_IMAGE_INSTALL_AZ=1` in the environment of `scripts/build-agent-image.mjs`, or of the Codeman server for its own auto-build (in the Compose deployment, `environment:` in `docker-compose.override.yml`), become the `CODEMAN_INSTALL_GH` / `CODEMAN_INSTALL_AZ` build args and put that CLI, its extension and its helper entry into the image. Unset passes nothing, so a default build's argv is unchanged and the image has neither. The sign-in seeds follow the same switches, read when a case container is created: `.config/gh` only with `CODEMAN_AGENT_IMAGE_INSTALL_GH=1`, `.azure` only with `CODEMAN_AGENT_IMAGE_INSTALL_AZ=1` (`enabledByEnv` in `CRED_STORES`), never merely because the files exist. Seeds are create-time mounts and deliberately not part of the config hash (hashing them would trip the drift gate for every case), so an existing case container picks them up only when it is recreated.
Set `CODEMAN_AGENT_IMAGE_GIT_USER_NAME` and `CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL` together to configure the agent image's Git identity. Rebuild an existing `codeman/agent:base` with `node scripts/build-agent-image.mjs --no-cache`, then recreate Docker-case containers so they use the rebuilt image.
## Quickest path: one-click "Run in Docker"
On the **New case → Create New** tab there's a **🐳 Run in an isolated Docker container** checkbox. Checking it alone is enough: Codeman creates the case folder in `~/codeman-cases/<name>`, spins up a hardened container with sensible defaults (auto-provisioning a shared `default` host), and starts the session inside it. No host/image/network fields to fill in.
+3 -3
View File
@@ -66,8 +66,8 @@ export const GIT_HOST_CLI_BUILD_ARGS = [
/** Environment variables passed through to the agent image's system Git configuration. */
export const GIT_IDENTITY_BUILD_ARGS = [
['GIT_USER_NAME', 'GIT_USER_NAME'],
['GIT_USER_EMAIL', 'GIT_USER_EMAIL'],
['CODEMAN_AGENT_IMAGE_GIT_USER_NAME', 'GIT_USER_NAME'],
['CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL', 'GIT_USER_EMAIL'],
];
/**
@@ -94,7 +94,7 @@ export function gitIdentityBuildArgPairs(env) {
const configured = pairs.filter(([, value]) => value !== '');
if (configured.length === 0) return [];
if (configured.length !== pairs.length) {
throw new Error('GIT_USER_NAME and GIT_USER_EMAIL must both be set when configuring Git identity');
throw new Error('Git user name and email must both be set when configuring Git identity');
}
return pairs;
}
+4 -4
View File
@@ -617,8 +617,8 @@ export const GIT_HOST_CLI_BUILD_ARGS: ReadonlyArray<readonly [string, string]> =
/** Environment variables passed through to the agent image's system Git configuration. */
export const GIT_IDENTITY_BUILD_ARGS: ReadonlyArray<readonly [string, string]> = [
['GIT_USER_NAME', 'GIT_USER_NAME'],
['GIT_USER_EMAIL', 'GIT_USER_EMAIL'],
['CODEMAN_AGENT_IMAGE_GIT_USER_NAME', 'GIT_USER_NAME'],
['CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL', 'GIT_USER_EMAIL'],
];
/**
@@ -648,7 +648,7 @@ export function gitIdentityBuildArgPairs(env: NodeJS.ProcessEnv): Array<[string,
const configured = pairs.filter(([, value]) => value !== '');
if (configured.length === 0) return [];
if (configured.length !== pairs.length) {
throw new Error('GIT_USER_NAME and GIT_USER_EMAIL must both be set when configuring Git identity');
throw new Error('Git user name and email must both be set when configuring Git identity');
}
return pairs;
}
@@ -1228,7 +1228,7 @@ function buildAgentImage(
try {
buildArgPairs = agentImageBuildArgPairs();
} catch (err) {
// A malformed CODEMAN_AGENT_IMAGE_INSTALL_* value: report it like any other build failure.
// A malformed CODEMAN_AGENT_IMAGE_* value: report it like any other build failure.
return Promise.resolve({ ok: false, built: false, alreadyPresent: false, error: String((err as Error).message) });
}
const argv = dockerEngineArgv(docker);
+12 -6
View File
@@ -154,13 +154,16 @@ describe('Git identity in the agent image: both producers pass the same settings
it('maps the Git environment variables to matching Dockerfile ARGs', () => {
expect(tsGitIdentityArgs).toEqual(mjsGitIdentityArgs);
expect(tsGitIdentityArgs).toEqual([
['GIT_USER_NAME', 'GIT_USER_NAME'],
['GIT_USER_EMAIL', 'GIT_USER_EMAIL'],
['CODEMAN_AGENT_IMAGE_GIT_USER_NAME', 'GIT_USER_NAME'],
['CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL', 'GIT_USER_EMAIL'],
]);
});
it('passes a complete identity and omits an absent identity', () => {
const identity = { GIT_USER_NAME: 'Ada Lovelace', GIT_USER_EMAIL: 'ada@example.com' };
const identity = {
CODEMAN_AGENT_IMAGE_GIT_USER_NAME: 'Ada Lovelace',
CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL: 'ada@example.com',
};
const expected: Array<[string, string]> = [
['GIT_USER_NAME', 'Ada Lovelace'],
['GIT_USER_EMAIL', 'ada@example.com'],
@@ -172,9 +175,12 @@ describe('Git identity in the agent image: both producers pass the same settings
});
it('refuses a partial identity in both build paths', () => {
for (const identity of [{ GIT_USER_NAME: 'Ada Lovelace' }, { GIT_USER_EMAIL: 'ada@example.com' }]) {
expect(() => tsGitIdentityPairs(identity)).toThrow(/GIT_USER_NAME and GIT_USER_EMAIL/);
expect(() => mjsGitIdentityPairs(identity)).toThrow(/GIT_USER_NAME and GIT_USER_EMAIL/);
for (const identity of [
{ CODEMAN_AGENT_IMAGE_GIT_USER_NAME: 'Ada Lovelace' },
{ CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL: 'ada@example.com' },
]) {
expect(() => tsGitIdentityPairs(identity)).toThrow(/Git user name and email/);
expect(() => mjsGitIdentityPairs(identity)).toThrow(/Git user name and email/);
}
});