mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(docker): configure static git identity
This commit is contained in:
@@ -15,6 +15,12 @@ TZ=Australia/Perth
|
|||||||
# this value rebuilds the image with a matching account.
|
# this value rebuilds the image with a matching account.
|
||||||
CODEMAN_RUNTIME_USER=codeman
|
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
|
# Required. Persistent Codeman application data, CLI credentials, and session
|
||||||
# state are stored here on the host and mounted at the runtime account's home
|
# state are stored here on the host and mounted at the runtime account's home
|
||||||
# directory in the container.
|
# directory in the container.
|
||||||
|
|||||||
@@ -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
|
`docker-compose.override.yml` adds is left alone, and application data and
|
||||||
case workspaces are host bind mounts, never touched either way.
|
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)
|
## 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`.
|
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`.
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
# writable even though the uid is not the baked 1000.
|
# writable even though the uid is not the baked 1000.
|
||||||
FROM node:22-bookworm-slim
|
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`),
|
# Base toolchain. `curl` is needed for the hook callbacks (`curl -sk $CODEMAN_API_URL`),
|
||||||
# `procps` for `ps`, `tmux` for the durable in-container session.
|
# `procps` for `ps`, `tmux` for the durable in-container session.
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
@@ -27,6 +30,17 @@ RUN apt-get update \
|
|||||||
openssh-client \
|
openssh-client \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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
|
# 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
|
# 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
|
# case can clone and push to private GitHub / Azure DevOps repositories. The
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
dockerfile: docker/server.Dockerfile
|
dockerfile: docker/server.Dockerfile
|
||||||
args:
|
args:
|
||||||
CODEMAN_RUNTIME_USER: ${CODEMAN_RUNTIME_USER}
|
CODEMAN_RUNTIME_USER: ${CODEMAN_RUNTIME_USER}
|
||||||
|
GIT_USER_EMAIL: ${GIT_USER_EMAIL:-}
|
||||||
|
GIT_USER_NAME: ${GIT_USER_NAME:-}
|
||||||
PGID: ${PGID:-1000}
|
PGID: ${PGID:-1000}
|
||||||
PUID: ${PUID:-1000}
|
PUID: ${PUID:-1000}
|
||||||
image: ${CODEMAN_IMAGE}
|
image: ${CODEMAN_IMAGE}
|
||||||
@@ -32,6 +34,10 @@ services:
|
|||||||
CODEMAN_DOCKER_HOST_HOME: ${CODEMAN_APPDATA_PATH}
|
CODEMAN_DOCKER_HOST_HOME: ${CODEMAN_APPDATA_PATH}
|
||||||
CODEMAN_DOCKER_DISABLE_SWAP_LIMIT: ${CODEMAN_DOCKER_DISABLE_SWAP_LIMIT}
|
CODEMAN_DOCKER_DISABLE_SWAP_LIMIT: ${CODEMAN_DOCKER_DISABLE_SWAP_LIMIT}
|
||||||
CODEMAN_CASES_PATH: ${CODEMAN_CASES_PATH}
|
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
|
# Extra Host-header allowlist entries for a reverse-proxied deployment
|
||||||
# (docker/README.md, "Reverse-proxy host allowlist"). Optional, so it
|
# (docker/README.md, "Reverse-proxy host allowlist"). Optional, so it
|
||||||
# defaults to empty rather than requiring a line in every .env.
|
# defaults to empty rather than requiring a line in every .env.
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ RUN npm ci \
|
|||||||
FROM node:22-bookworm-slim
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
ARG CODEMAN_RUNTIME_USER=codeman
|
ARG CODEMAN_RUNTIME_USER=codeman
|
||||||
|
ARG GIT_USER_EMAIL=
|
||||||
|
ARG GIT_USER_NAME=
|
||||||
ARG PUID=1000
|
ARG PUID=1000
|
||||||
ARG PGID=1000
|
ARG PGID=1000
|
||||||
|
|
||||||
@@ -48,6 +50,18 @@ RUN apt-get update \
|
|||||||
tmux \
|
tmux \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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`.
|
# 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
|
# 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
|
# packages including containerd, runc, dmsetup and iptables, none of which a
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ export const GIT_HOST_CLI_BUILD_ARGS = [
|
|||||||
['CODEMAN_AGENT_IMAGE_INSTALL_AZ', 'CODEMAN_INSTALL_AZ'],
|
['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
|
* 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
|
* contributes NOTHING, so the Dockerfile's own default (off) applies and the argv is the same
|
||||||
@@ -82,9 +88,24 @@ export function gitHostCliBuildArgPairs(env) {
|
|||||||
return pairs;
|
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`. */
|
/** The `--build-arg` pairs the agent image takes. PURE given `env`. */
|
||||||
export function agentImageBuildArgPairs(catalog, env = process.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. */
|
/** Read the committed catalogue. IO. */
|
||||||
|
|||||||
+25
-1
@@ -615,6 +615,12 @@ export const GIT_HOST_CLI_BUILD_ARGS: ReadonlyArray<readonly [string, string]> =
|
|||||||
['CODEMAN_AGENT_IMAGE_INSTALL_AZ', 'CODEMAN_INSTALL_AZ'],
|
['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
|
* 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
|
* contributes NOTHING, so the Dockerfile's own default (off) applies and the argv is the same
|
||||||
@@ -633,9 +639,27 @@ export function gitHostCliBuildArgPairs(env: NodeJS.ProcessEnv): Array<[string,
|
|||||||
return pairs;
|
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`. */
|
/** The `--build-arg` pairs the agent image takes. PURE given `env`. */
|
||||||
export function agentImageBuildArgPairs(env: NodeJS.ProcessEnv = process.env): Array<[string, string]> {
|
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) ==========
|
// ========== Credential mount resolution (IO) ==========
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import {
|
|||||||
agentImageNpmPackages as mjsPackages,
|
agentImageNpmPackages as mjsPackages,
|
||||||
GIT_HOST_CLI_BUILD_ARGS as mjsGitHostArgs,
|
GIT_HOST_CLI_BUILD_ARGS as mjsGitHostArgs,
|
||||||
gitHostCliBuildArgPairs as mjsGitHostPairs,
|
gitHostCliBuildArgPairs as mjsGitHostPairs,
|
||||||
|
GIT_IDENTITY_BUILD_ARGS as mjsGitIdentityArgs,
|
||||||
|
gitIdentityBuildArgPairs as mjsGitIdentityPairs,
|
||||||
} from '../scripts/lib/cli-catalog.mjs';
|
} from '../scripts/lib/cli-catalog.mjs';
|
||||||
import {
|
import {
|
||||||
agentImageBuildArgPairs as tsPairs,
|
agentImageBuildArgPairs as tsPairs,
|
||||||
@@ -29,6 +31,8 @@ import {
|
|||||||
agentImageNpmPackages as tsPackages,
|
agentImageNpmPackages as tsPackages,
|
||||||
GIT_HOST_CLI_BUILD_ARGS as tsGitHostArgs,
|
GIT_HOST_CLI_BUILD_ARGS as tsGitHostArgs,
|
||||||
gitHostCliBuildArgPairs as tsGitHostPairs,
|
gitHostCliBuildArgPairs as tsGitHostPairs,
|
||||||
|
GIT_IDENTITY_BUILD_ARGS as tsGitIdentityArgs,
|
||||||
|
gitIdentityBuildArgPairs as tsGitIdentityPairs,
|
||||||
} from '../src/docker-hosts.js';
|
} from '../src/docker-hosts.js';
|
||||||
|
|
||||||
const CATALOG = JSON.parse(readFileSync(fileURLToPath(new URL('../config/clis.stock.json', import.meta.url)), 'utf-8'));
|
const CATALOG = JSON.parse(readFileSync(fileURLToPath(new URL('../config/clis.stock.json', import.meta.url)), 'utf-8'));
|
||||||
@@ -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}"');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user