mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
chore(docker): add Update-Codeman.sh for scripted major-update rebuilds
docker/README.md and docs/docker-self-update.md both already point operators at "stop the stack, rebuild, restart" for anything the in-app updater refuses to apply (a changed server.Dockerfile, a changed docker-compose.yaml, or a new required .env key) — but that was a manual, hand-typed procedure with no script of its own, unlike every other start/update path this deployment has. docker/Update-Codeman.sh scripts it: `docker compose down`, then an unconditional `docker compose build --no-cache` (a major update should be certain of what actually ships, not reuse whatever layers happened to be cached), then hands off to the existing Start-Codeman.sh for the same careful PUID/PGID, override-file and fingerprint handling every other start already goes through — rather than reimplementing any of that by hand and risking it drifting out of step. An optional --volumes/-v flag also removes the codeman-node-modules/ codeman-dist named volumes, the scripted form of the "Resetting the build artefacts" procedure docs/docker-self-update.md already documents by hand. Safe: those two are the only named volumes this stack declares; application data and case workspaces are host bind mounts, never touched by `docker compose down` either way. Docs updated: a "Major updates" section in docker/README.md, and a pointer from docs/docker-self-update.md's existing "Resetting the build artefacts" troubleshooting entry. Tests: extended test/docker-entrypoint.test.ts (the existing home for Start-Codeman.sh's own static checks) with a bash -n parse check, the down-before-build-before-handoff ordering, the --volumes flag's effect, unrecognised-argument handling, and byte-for-byte agreement with Start-Codeman.sh's own override-file resolution logic (so `down` here and `up` there can never target different Compose files). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N6eadpRyqpA9PD3i139cSD
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9466acfc1a
commit
9ba90a674a
@@ -41,6 +41,26 @@ Releases that change `server.Dockerfile`, `docker-compose.yaml`, or add a key to
|
||||
changed, and asks you to run `Start-Codeman.sh` here on the host instead. Details:
|
||||
[`../docs/docker-self-update.md`](../docs/docker-self-update.md).
|
||||
|
||||
### Major updates
|
||||
|
||||
`Start-Codeman.sh` rebuilds the image and clears the build-artefact volumes on
|
||||
its own, but only when it detects the checkout's HEAD or `package-lock.json`
|
||||
moved — exactly right for an ordinary `git pull`, too conservative when a
|
||||
release note (or the updater's own blocker message) calls for starting over.
|
||||
For that case, `docker/Update-Codeman.sh` stops the stack, force-rebuilds the
|
||||
image with no layer cache, then hands off to `Start-Codeman.sh` for the usual
|
||||
start:
|
||||
|
||||
```sh
|
||||
bash docker/Update-Codeman.sh
|
||||
```
|
||||
|
||||
Add `--volumes` to also clear the `codeman-node-modules`/`codeman-dist`
|
||||
volumes — the scripted form of "Resetting the build artefacts" in
|
||||
[`../docs/docker-self-update.md`](../docs/docker-self-update.md). Those two
|
||||
are the only named volumes this stack declares; application data and case
|
||||
workspaces are host bind mounts and are never touched either way.
|
||||
|
||||
## Local customisation
|
||||
|
||||
Compose merges `docker-compose.override.yml` on top of `docker-compose.yaml`. Keep host-specific changes there rather than editing `docker-compose.yaml`, so this repository can be updated without losing them. Both `docker-compose.override.yml` and `docker-compose.override.yaml` are ignored by Git.
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# The scripted major-update path for the Docker Compose deployment.
|
||||
#
|
||||
# docker/README.md and docs/docker-self-update.md both point operators here for
|
||||
# anything the in-app updater itself refuses to apply: a changed
|
||||
# `server.Dockerfile`, a changed `docker-compose.yaml`, or a new required
|
||||
# `.env.example` key. None of those can be applied by a container restarting
|
||||
# itself — a restart reuses the existing image and configuration (see "The
|
||||
# environment gate" in docs/docker-self-update.md) — so this script does the
|
||||
# three things an in-place update cannot: stop the stack, force a real image
|
||||
# rebuild with no layer cache, then hand off to Start-Codeman.sh for the same
|
||||
# careful PUID/PGID, override-file and fingerprint handling every other start
|
||||
# goes through.
|
||||
#
|
||||
# This is the scripted form of "Resetting the build artefacts" in
|
||||
# docs/docker-self-update.md (`docker compose down -v`, then
|
||||
# `Start-Codeman.sh`), plus the unconditional `--no-cache` a major update
|
||||
# warrants: `Start-Codeman.sh` on its own only rebuilds without the cache flag,
|
||||
# and only clears the two build-artefact volumes when it detects the checkout's
|
||||
# HEAD or `package-lock.json` moved — exactly right for an ordinary `git pull`,
|
||||
# too conservative when the ask is "start over, certain of what ships".
|
||||
#
|
||||
# Usage: docker/Update-Codeman.sh [--volumes]
|
||||
# --volumes, -v Also remove the codeman-node-modules/codeman-dist named
|
||||
# volumes, so the fresh image's own node_modules/dist are
|
||||
# what actually run instead of sitting unused behind a
|
||||
# Docker-seeded volume's old content (Docker only seeds a
|
||||
# named volume from the image while that volume is EMPTY).
|
||||
# Safe: those two are the ONLY named volumes this stack
|
||||
# declares (`docker-compose.yaml`) — application data and
|
||||
# case workspaces are host bind mounts, never touched by
|
||||
# `docker compose down`, with or without this flag.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
env_file="$script_dir/.env"
|
||||
compose_file="$script_dir/docker-compose.yaml"
|
||||
|
||||
remove_volumes=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--volumes | -v)
|
||||
remove_volumes=1
|
||||
;;
|
||||
*)
|
||||
printf 'Error: unrecognised argument: %s\n' "$arg" >&2
|
||||
printf 'Usage: %s [--volumes]\n' "$0" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
printf 'Error: Docker environment file is missing: %s\n' "$env_file" >&2
|
||||
printf 'Create it from %s/.env.example before running this script.\n' "$script_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Same override-file discovery as Start-Codeman.sh, and deliberately kept in
|
||||
# step with it: a stack started through one script and updated through the
|
||||
# other must resolve to the exact same Compose files, or `down` here and `up`
|
||||
# there could target different configurations. Compose's own precedence
|
||||
# (measured on v5.5.0 with both present: it uses .yml and ignores .yaml).
|
||||
override_yml="$script_dir/docker-compose.override.yml"
|
||||
override_yaml="$script_dir/docker-compose.override.yaml"
|
||||
if [[ -f "$override_yml" && -f "$override_yaml" ]]; then
|
||||
printf 'Warning: both %s and %s exist; Compose uses .yml and ignores .yaml.\n' \
|
||||
"$override_yml" "$override_yaml" >&2
|
||||
fi
|
||||
compose_files=(-f "$compose_file")
|
||||
for override_file in "$override_yml" "$override_yaml"; do
|
||||
if [[ -f "$override_file" ]]; then
|
||||
compose_files+=(-f "$override_file")
|
||||
printf 'Using Compose override file: %s\n' "$override_file"
|
||||
break
|
||||
fi
|
||||
done
|
||||
compose_command=(docker compose --env-file "$env_file" "${compose_files[@]}")
|
||||
|
||||
printf 'Stopping the stack...\n'
|
||||
if [[ "$remove_volumes" == '1' ]]; then
|
||||
printf 'Also removing the codeman-node-modules/codeman-dist volumes (--volumes).\n'
|
||||
"${compose_command[@]}" down --volumes
|
||||
else
|
||||
"${compose_command[@]}" down
|
||||
fi
|
||||
|
||||
# --no-cache, always: a plain `build` reuses cached layers (npm install, apt
|
||||
# packages, the CLI installs baked into the image) and can silently keep them
|
||||
# frozen at whatever they were the day the cache was populated — exactly wrong
|
||||
# for a major update, whose whole point is being certain of what actually
|
||||
# ships. `scripts/build-agent-image.mjs` makes the same call for the same
|
||||
# reason (see its entry in CLAUDE.md's Additional Commands table).
|
||||
printf 'Building a fresh image (--no-cache)...\n'
|
||||
"${compose_command[@]}" build --no-cache
|
||||
|
||||
# Start-Codeman.sh does everything a plain `up -d` does not: resolves
|
||||
# PUID/PGID from CODEMAN_APPDATA_PATH's owner, pre-creates CODEMAN_CASES_PATH
|
||||
# with the right ownership, resolves DOCKER_SOCKET_GID, records the
|
||||
# server.Dockerfile/docker-compose.yaml fingerprint the in-app updater's gate
|
||||
# reads on every future update, and clears the build-artefact volumes itself
|
||||
# if it finds the checkout's source moved since the last start. Reimplementing
|
||||
# any of that here would only risk drifting out of step with it — hand off
|
||||
# instead, exactly as docs/docker-self-update.md's own reset procedure does.
|
||||
printf 'Handing off to Start-Codeman.sh...\n'
|
||||
exec "$script_dir/Start-Codeman.sh"
|
||||
@@ -224,7 +224,9 @@ the host and the in-app path works from then on.
|
||||
|
||||
**Resetting the build artefacts** — `docker compose down -v`, then
|
||||
`Start-Codeman.sh`. This discards the named volumes and re-seeds them from a fresh
|
||||
image.
|
||||
image. `docker/Update-Codeman.sh --volumes` scripts exactly this (plus an
|
||||
unconditional `--no-cache` rebuild, which a plain `Start-Codeman.sh` run does not
|
||||
force on its own) — see "Major updates" in `docker/README.md`.
|
||||
|
||||
## Disabling it
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { readFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { readFileSync, mkdtempSync, rmSync, writeFileSync, statSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
@@ -33,6 +33,7 @@ const compose = read('docker/docker-compose.yaml');
|
||||
const entrypoint = read('docker/entrypoint.sh');
|
||||
const dockerfile = read('docker/server.Dockerfile');
|
||||
const startScript = read('docker/Start-Codeman.sh');
|
||||
const updateScript = read('docker/Update-Codeman.sh');
|
||||
|
||||
/** The `- NAME` entries under `cap_add:` (the block ends at the next key at the same indent). */
|
||||
function composeCapAdd(text: string): string[] {
|
||||
@@ -174,6 +175,59 @@ describe('Start-Codeman.sh', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update-Codeman.sh (the scripted major-update path — docker/README.md "Major updates")', () => {
|
||||
it('parses under bash -n', () => {
|
||||
execFileSync('bash', ['-n', join(ROOT, 'docker/Update-Codeman.sh')]);
|
||||
});
|
||||
|
||||
it('is executable, like every other script this deployment runs directly', () => {
|
||||
// Windows checkouts (this repo is developed on both) do not carry a real
|
||||
// execute bit, so this only meaningfully asserts on POSIX — matching how
|
||||
// docker/README.md documents running it (`bash docker/Update-Codeman.sh`,
|
||||
// not `./docker/Update-Codeman.sh`) either way.
|
||||
if (process.platform === 'win32') return;
|
||||
const mode = statSync(join(ROOT, 'docker/Update-Codeman.sh')).mode;
|
||||
expect(mode & 0o111).not.toBe(0);
|
||||
});
|
||||
|
||||
it('stops the stack, THEN force-rebuilds with --no-cache, THEN hands off to Start-Codeman.sh', () => {
|
||||
const down = updateScript.indexOf('"${compose_command[@]}" down');
|
||||
const build = updateScript.indexOf('"${compose_command[@]}" build --no-cache');
|
||||
const handoff = updateScript.indexOf('exec "$script_dir/Start-Codeman.sh"');
|
||||
expect(down).toBeGreaterThan(-1);
|
||||
expect(build).toBeGreaterThan(down);
|
||||
expect(handoff).toBeGreaterThan(build);
|
||||
});
|
||||
|
||||
it('--volumes (or -v) removes the named volumes on the way down; the default path does not', () => {
|
||||
expect(updateScript).toMatch(/--volumes \| -v\)\s*\n\s*remove_volumes=1/);
|
||||
expect(updateScript).toMatch(/"\$\{compose_command\[@\]\}" down --volumes/);
|
||||
// The unconditional call further down (the else branch) must stay a plain
|
||||
// `down` — accidentally merging the two branches would silently start
|
||||
// wiping the build-artefact volumes on every major update, not just when
|
||||
// the flag is passed.
|
||||
expect(updateScript).toMatch(/else\s*\n\s*"\$\{compose_command\[@\]\}" down\s*\n\s*fi/);
|
||||
});
|
||||
|
||||
it('rejects an unrecognised argument rather than silently ignoring it', () => {
|
||||
expect(updateScript).toMatch(/Error: unrecognised argument/);
|
||||
expect(updateScript).toMatch(/exit 1/);
|
||||
});
|
||||
|
||||
it('resolves the override file exactly like Start-Codeman.sh, so `down` and `up` never target different Compose files', () => {
|
||||
// \r stripped before comparing: git's autocrlf normalises the COMMITTED blob to LF
|
||||
// either way, but a Windows checkout can have already converted one file's line
|
||||
// endings on disk and not the other's (e.g. Start-Codeman.sh checked out before this
|
||||
// script existed), which would fail a raw byte comparison for a reason that has
|
||||
// nothing to do with the two scripts actually agreeing.
|
||||
const overrideBlock = (script: string) =>
|
||||
script
|
||||
.slice(script.indexOf('override_yml='), script.indexOf('compose_command=(docker compose'))
|
||||
.replace(/\r\n/g, '\n');
|
||||
expect(overrideBlock(updateScript)).toBe(overrideBlock(startScript));
|
||||
});
|
||||
});
|
||||
|
||||
describe('git_head_commit resolves every ref layout a checkout can have', () => {
|
||||
let base: string;
|
||||
const git = (cwd: string, ...args: string[]) =>
|
||||
|
||||
Reference in New Issue
Block a user