Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# aicodeman

## 20260925-1305

- Added static Git name and email configuration to the server and Docker-case agent image builds.

## 1.33.0

### Minor Changes
Expand Down
6 changes: 6 additions & 0 deletions docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ 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
# 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=

# Required. Persistent Codeman application data, CLI credentials, and session
# state are stored here on the host and mounted at the runtime account's home
# directory in the container.
Expand Down
19 changes: 19 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ two volumes are removed, by name within this Compose project; any volume a
`docker-compose.override.yml` adds is left alone, and application data and
case workspaces are host bind mounts, never touched either way.

## Git commit identity

Set `GIT_USER_NAME` and `GIT_USER_EMAIL` in `docker/.env` before rebuilding:

```sh
GIT_USER_NAME='Your Name'
GIT_USER_EMAIL='you@example.com'
```

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.

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
server container, then recreate any Docker cases that should use it.

## Private repositories (GitHub and Azure DevOps)

The images can include the GitHub CLI (`gh`) and the Azure CLI (`az`, with the `azure-devops` extension), wired into the system Git configuration as credential helpers, so Codeman can clone private repositories. Both are **opt-in and off by default**, and are turned on per host in `docker-compose.override.yml`.
Expand Down
14 changes: 14 additions & 0 deletions docker/agent.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
# 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 \
Expand All @@ -26,6 +29,17 @@ 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
Expand Down
6 changes: 6 additions & 0 deletions docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ services:
dockerfile: docker/server.Dockerfile
args:
CODEMAN_RUNTIME_USER: ${CODEMAN_RUNTIME_USER}
GIT_USER_EMAIL: ${GIT_USER_EMAIL:-}
GIT_USER_NAME: ${GIT_USER_NAME:-}
PGID: ${PGID:-1000}
PUID: ${PUID:-1000}
image: ${CODEMAN_IMAGE}
Expand All @@ -32,6 +34,10 @@ services:
CODEMAN_DOCKER_HOST_HOME: ${CODEMAN_APPDATA_PATH}
CODEMAN_DOCKER_DISABLE_SWAP_LIMIT: ${CODEMAN_DOCKER_DISABLE_SWAP_LIMIT}
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:-}
# 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.
Expand Down
14 changes: 14 additions & 0 deletions docker/server.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ 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

Expand All @@ -47,6 +49,18 @@ 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
Expand Down
23 changes: 22 additions & 1 deletion scripts/lib/cli-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export const GIT_HOST_CLI_BUILD_ARGS = [
['CODEMAN_AGENT_IMAGE_INSTALL_AZ', 'CODEMAN_INSTALL_AZ'],
];

/** 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'],
];

/**
* The `--build-arg` pairs for the optional git-host CLIs. PURE. An unset or empty variable
* contributes NOTHING, so the Dockerfile's own default (off) applies and the argv is the same
Expand All @@ -82,9 +88,24 @@ export function gitHostCliBuildArgPairs(env) {
return pairs;
}

/** The `--build-arg` pairs for Git identity, requiring either both values or neither. */
export function gitIdentityBuildArgPairs(env) {
const pairs = GIT_IDENTITY_BUILD_ARGS.map(([envName, argName]) => [argName, env[envName] ?? '']);
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');
}
return pairs;
}

/** The `--build-arg` pairs the agent image takes. PURE given `env`. */
export function agentImageBuildArgPairs(catalog, env = process.env) {
return [['CLI_NPM_PACKAGES', agentImageNpmPackages(catalog).join(' ')], ...gitHostCliBuildArgPairs(env)];
return [
['CLI_NPM_PACKAGES', agentImageNpmPackages(catalog).join(' ')],
...gitHostCliBuildArgPairs(env),
...gitIdentityBuildArgPairs(env),
];
}

/** Read the committed catalogue. IO. */
Expand Down
26 changes: 25 additions & 1 deletion src/docker-hosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,12 @@ export const GIT_HOST_CLI_BUILD_ARGS: ReadonlyArray<readonly [string, string]> =
['CODEMAN_AGENT_IMAGE_INSTALL_AZ', 'CODEMAN_INSTALL_AZ'],
];

/** 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'],
];

/**
* The `--build-arg` pairs for the optional git-host CLIs. PURE. An unset or empty variable
* contributes NOTHING, so the Dockerfile's own default (off) applies and the argv is the same
Expand All @@ -633,9 +639,27 @@ export function gitHostCliBuildArgPairs(env: NodeJS.ProcessEnv): Array<[string,
return pairs;
}

/**
* The `--build-arg` pairs for a configured Git identity. An absent pair leaves
* Git unconfigured, preserving existing deployments; a partial pair is refused.
*/
export function gitIdentityBuildArgPairs(env: NodeJS.ProcessEnv): Array<[string, string]> {
const pairs = GIT_IDENTITY_BUILD_ARGS.map(([envName, argName]) => [argName, env[envName] ?? ''] as [string, 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');
}
return pairs;
}

/** The `--build-arg` pairs the agent image takes. PURE given `env`. */
export function agentImageBuildArgPairs(env: NodeJS.ProcessEnv = process.env): Array<[string, string]> {
return [['CLI_NPM_PACKAGES', agentImageNpmPackages().join(' ')], ...gitHostCliBuildArgPairs(env)];
return [
['CLI_NPM_PACKAGES', agentImageNpmPackages().join(' ')],
...gitHostCliBuildArgPairs(env),
...gitIdentityBuildArgPairs(env),
];
}

// ========== Credential mount resolution (IO) ==========
Expand Down
43 changes: 43 additions & 0 deletions test/agent-image-build-args-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ import {
agentImageNpmPackages as mjsPackages,
GIT_HOST_CLI_BUILD_ARGS as mjsGitHostArgs,
gitHostCliBuildArgPairs as mjsGitHostPairs,
GIT_IDENTITY_BUILD_ARGS as mjsGitIdentityArgs,
gitIdentityBuildArgPairs as mjsGitIdentityPairs,
} from '../scripts/lib/cli-catalog.mjs';
import {
agentImageBuildArgPairs as tsPairs,
agentImageBuildArgs,
agentImageNpmPackages as tsPackages,
GIT_HOST_CLI_BUILD_ARGS as tsGitHostArgs,
gitHostCliBuildArgPairs as tsGitHostPairs,
GIT_IDENTITY_BUILD_ARGS as tsGitIdentityArgs,
gitIdentityBuildArgPairs as tsGitIdentityPairs,
} from '../src/docker-hosts.js';

const CATALOG = JSON.parse(readFileSync(fileURLToPath(new URL('../config/clis.stock.json', import.meta.url)), 'utf-8'));
Expand Down Expand Up @@ -145,3 +149,42 @@ describe('optional gh / az in the agent image: both producers pass the same swit
}
});
});

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'],
]);
});

it('passes a complete identity and omits an absent identity', () => {
const identity = { GIT_USER_NAME: 'Ada Lovelace', GIT_USER_EMAIL: 'ada@example.com' };
const expected: Array<[string, string]> = [
['GIT_USER_NAME', 'Ada Lovelace'],
['GIT_USER_EMAIL', 'ada@example.com'],
];
expect(tsGitIdentityPairs(identity)).toEqual(expected);
expect(mjsGitIdentityPairs(identity)).toEqual(expected);
expect(tsGitIdentityPairs({})).toEqual([]);
expect(mjsGitIdentityPairs({})).toEqual([]);
});

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/);
}
});

it('both Dockerfiles configure system Git identity from the build arguments', () => {
for (const file of ['../docker/agent.Dockerfile', '../docker/server.Dockerfile']) {
const dockerfile = readFileSync(fileURLToPath(new URL(file, import.meta.url)), 'utf-8');
expect(dockerfile, file).toMatch(/^ARG GIT_USER_NAME=$/m);
expect(dockerfile, file).toMatch(/^ARG GIT_USER_EMAIL=$/m);
expect(dockerfile, file).toContain('git config --system user.name "${GIT_USER_NAME}"');
expect(dockerfile, file).toContain('git config --system user.email "${GIT_USER_EMAIL}"');
}
});
});
Loading