Refine checkout definition hashing and architecture docs

This commit is contained in:
2026-05-23 23:39:22 +00:00
parent 957ffc0932
commit 6de5060a51
2 changed files with 15 additions and 12 deletions

View File

@@ -74,13 +74,15 @@ async function compose(compose_config, args) {
}) })
} }
// Hash (owner, repo_url, revision) to get an unambiguous volume name. // Hash the entire checkout definition to get an unambiguous volume name.
// Human-readable metadata is stored as Docker volume labels instead. // Any change to any field (repo, revision, key, submodules, etc.) produces
function worktree_volume_name(owner, repo_url, revision) { // a different volume. Human-readable metadata is stored as Docker volume labels.
function worktree_volume_name(owner, repo_def) {
const sorted_keys = Object.keys(repo_def).sort()
const canonical = JSON.stringify(Object.fromEntries(sorted_keys.map(k => [k, repo_def[k]])))
const hash = createHash('sha256') const hash = createHash('sha256')
.update(owner).update('\0') .update(owner).update('\0')
.update(repo_url).update('\0') .update(canonical)
.update(revision)
.digest('hex') .digest('hex')
.slice(0, 24) .slice(0, 24)
return `ci-worktree-${hash}` return `ci-worktree-${hash}`
@@ -90,8 +92,9 @@ function worktree_volume_name(owner, repo_url, revision) {
// Step 1: Prepare base worktree volume // Step 1: Prepare base worktree volume
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function prepare_worktree(owner, repo_url, revision, mutable, private_key_path) { async function prepare_worktree(owner, repo_def, private_key_path) {
const volume_name = worktree_volume_name(owner, repo_url, revision) const { repo_url, revision, mutable } = repo_def
const volume_name = worktree_volume_name(owner, repo_def)
// Check if the volume already exists // Check if the volume already exists
let exists = false let exists = false
@@ -103,7 +106,7 @@ async function prepare_worktree(owner, repo_url, revision, mutable, private_key_
} }
if (exists && !mutable) { if (exists && !mutable) {
// Immutable revision — volume is permanent once created // Immutable — volume is permanent once created
console.log(`Base worktree volume ${volume_name} already exists, reusing.`) console.log(`Base worktree volume ${volume_name} already exists, reusing.`)
return volume_name return volume_name
} }
@@ -189,7 +192,7 @@ async function main() {
const repo = checkout.repositories[0] const repo = checkout.repositories[0]
const private_key_path = path.join(secrets_base, 'git-ssh', user, 'private', repo.key) const private_key_path = path.join(secrets_base, 'git-ssh', user, 'private', repo.key)
const worktree_volume = await prepare_worktree(user, repo.repo_url, repo.revision, repo.mutable, private_key_path) const worktree_volume = await prepare_worktree(user, repo, private_key_path)
console.log(`\nWorktree volume: ${worktree_volume}\n`) console.log(`\nWorktree volume: ${worktree_volume}\n`)
const container_name = `ci-rsync-${worktree_volume.slice('ci-worktree-'.length)}` const container_name = `ci-rsync-${worktree_volume.slice('ci-worktree-'.length)}`

View File

@@ -16,13 +16,13 @@ Images and tasks are declared separately. A task references an image by name. Mu
### Worktree layers (overlayfs) ### Worktree layers (overlayfs)
Each run mounts a single worktree into the container, but this is composed from two separately managed Docker volumes: A run may check out one or more repositories. Each repository produces two separately managed Docker volumes:
**Base worktree** — a named Docker volume containing a plain `git checkout` of a specific `(owner, repo, revision)`. Read-only from the task container's perspective. Whether it is reused or re-checked-out on each run depends on whether the revision is mutable (see below). **Base worktree** — a named Docker volume containing a plain `git checkout` of a specific repository at a specific revision. The volume name is derived from a hash of the entire checkout definition (owner, repo URL, revision, key, submodules flag, etc.) so any change to any field produces a distinct volume. Keys are sorted before hashing so field order in the config file does not affect the result. Read-only from the task container's perspective. Whether it is reused or re-checked-out on each run depends on whether the revision is mutable (see below).
**Worktree mutations** — a named Docker volume holding the overlayfs upper layer for a specific run. Unique per run. Contains only what the container wrote or modified — the base worktree is never touched. **Worktree mutations** — a named Docker volume holding the overlayfs upper layer for a specific run. Unique per run. Contains only what the container wrote or modified — the base worktree is never touched.
From inside the container these appear as a single mount point (the overlayfs merged view). From outside, the CI system names and manages them separately. Each repository is mounted at a distinct path inside the container. From the outside, the CI system names and manages the volumes per repository independently.
Using named Docker volumes means the checkout is a straightforward `git clone` or `git checkout` into a volume — no need for the `--git-dir` / `--work-tree` / `GIT_INDEX_FILE` technique. That approach remains useful for direct host-based deployments but is out of scope here. Using named Docker volumes means the checkout is a straightforward `git clone` or `git checkout` into a volume — no need for the `--git-dir` / `--work-tree` / `GIT_INDEX_FILE` technique. That approach remains useful for direct host-based deployments but is out of scope here.