forked from efforting.tech/gitea.efforting.tech
Support multiple repo checkouts per run, one volume per repo
This commit is contained in:
@@ -14,9 +14,10 @@ checkout:
|
||||
repositories:
|
||||
- repo_url: git@git.efforting.tech:mikael-lovqvist/websperiments.git
|
||||
revision: main
|
||||
mutable: true # true = resolve via ls-remote on each run; false = treat revision as a fixed sha
|
||||
mutable: true # true = re-checkout every run; false = checkout once and reuse
|
||||
key: websperiments-deploy # references git-ssh/<user>/private/<key>
|
||||
submodules: false
|
||||
# name: custom-name # optional — overrides the default mount name (repo basename)
|
||||
|
||||
# Task container — runs the actual job
|
||||
task:
|
||||
|
||||
@@ -74,6 +74,12 @@ async function compose(compose_config, args) {
|
||||
})
|
||||
}
|
||||
|
||||
// Derive the mount name for a repo inside the container.
|
||||
// Mimics git clone behaviour (uses repo basename) unless overridden by repo_def.name.
|
||||
function repo_mount_name(repo_def) {
|
||||
return repo_def.name ?? path.basename(repo_def.repo_url, '.git')
|
||||
}
|
||||
|
||||
// Hash the entire checkout definition to get an unambiguous volume name.
|
||||
// Any change to any field (repo, revision, key, submodules, etc.) produces
|
||||
// a different volume. Human-readable metadata is stored as Docker volume labels.
|
||||
@@ -141,7 +147,8 @@ async function prepare_worktree(owner, repo_def, private_key_path) {
|
||||
// Compose config builders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function build_rsync_compose_config({ container_name, worktree_volume, deploy_target }) {
|
||||
// worktree_volumes: array of { volume_name, mount_name } objects
|
||||
function build_rsync_compose_config({ container_name, worktree_volumes, deploy_target }) {
|
||||
return {
|
||||
name: container_name,
|
||||
services: {
|
||||
@@ -153,7 +160,9 @@ function build_rsync_compose_config({ container_name, worktree_volume, deploy_ta
|
||||
},
|
||||
image: 'ci-rsync',
|
||||
volumes: [
|
||||
`${worktree_volume}:/src:ro`,
|
||||
...worktree_volumes.map(({ volume_name, mount_name }) =>
|
||||
`${volume_name}:/src/${mount_name}:ro`
|
||||
),
|
||||
`${deploy_target}:/deploy`,
|
||||
],
|
||||
restart: 'no',
|
||||
@@ -165,9 +174,9 @@ function build_rsync_compose_config({ container_name, worktree_volume, deploy_ta
|
||||
},
|
||||
},
|
||||
},
|
||||
volumes: {
|
||||
[worktree_volume]: { external: true },
|
||||
},
|
||||
volumes: Object.fromEntries(
|
||||
worktree_volumes.map(({ volume_name }) => [volume_name, { external: true }])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,17 +198,28 @@ async function main() {
|
||||
console.log('=== Experiment 01: rsync publish ===\n')
|
||||
|
||||
const { secrets_base, user, checkout, task } = CONFIG
|
||||
const repo = checkout.repositories[0]
|
||||
const private_key_path = path.join(secrets_base, 'git-ssh', user, 'private', repo.key)
|
||||
|
||||
const worktree_volume = await prepare_worktree(user, repo, private_key_path)
|
||||
console.log(`\nWorktree volume: ${worktree_volume}\n`)
|
||||
// Prepare one worktree volume per repository entry
|
||||
const worktree_volumes = await Promise.all(
|
||||
checkout.repositories.map(async repo_def => {
|
||||
const private_key_path = path.join(secrets_base, 'git-ssh', user, 'private', repo_def.key)
|
||||
const volume_name = await prepare_worktree(user, repo_def, private_key_path)
|
||||
const mount_name = repo_mount_name(repo_def)
|
||||
console.log(`Worktree: ${volume_name} → /src/${mount_name}`)
|
||||
return { volume_name, mount_name }
|
||||
})
|
||||
)
|
||||
|
||||
const container_name = `ci-rsync-${worktree_volume.slice('ci-worktree-'.length)}`
|
||||
// Container name is a hash of the full checkout set so it is stable across runs
|
||||
const run_hash = createHash('sha256')
|
||||
.update(JSON.stringify(checkout.repositories.map(r => worktree_volume_name(user, r)).sort()))
|
||||
.digest('hex')
|
||||
.slice(0, 24)
|
||||
const container_name = `ci-rsync-${run_hash}`
|
||||
|
||||
const rsync_config = build_rsync_compose_config({
|
||||
container_name,
|
||||
worktree_volume,
|
||||
worktree_volumes,
|
||||
deploy_target: task.deploy_target,
|
||||
})
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ Images and tasks are declared separately. A task references an image by name. Mu
|
||||
|
||||
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 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).
|
||||
**Base worktree** — one named Docker volume per repository entry in the checkout definition, each containing a plain `git checkout` of that repository at its configured revision. The volume name is derived from a hash of the full checkout entry (owner, repo URL, revision, key, submodules flag, etc.) with keys sorted before hashing so field order in the config does not affect the result. Any change to any field produces a distinct volume. Base worktree volumes are read-only from the task container's perspective. Whether each 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.
|
||||
|
||||
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.
|
||||
Each repository is mounted at a distinct path inside the container under `/src/<name>`. The mount name defaults to the repository basename (mimicking `git clone` behaviour) but can be overridden per entry in the task declaration. From the outside, the CI system names and manages volumes per repository entry 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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user