From: Plom Heller Date: Wed, 23 Sep 2026 01:16:31 +0000 (+0200) Subject: Split (and extend) setup_git_mirror.sh into adopt_repo.sh, setup_git_server.sh. X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/all?a=commitdiff_plain;h=665060fc919da6787f0ae56cd3898e27718f64c9;p=confplom Split (and extend) setup_git_mirror.sh into adopt_repo.sh, setup_git_server.sh. --- diff --git a/CLAUDE.md b/CLAUDE.md index 192a9b0..6c18632 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,9 @@ Both tracks converge on the same first-login family — `start_root_t490s.sh` - admin workstation: `install_server.sh` - inside a target system: `start_root_t490s.sh`, `start_root_server.sh`, `start_user.sh`, `update_efi.sh` (local systems only, see below) - - optional, by hand on an already-set-up target: `setup_git_mirror.sh` + - optional, by hand on an already-set-up target: `setup_git_server.sh` + - optional, by hand from any machine holding a checkout, targeting a + repos server over ssh: `adopt_repo.sh` - `to_install/` — per-target package lists (`t490s`, `server`), read in place by `start_root` from `${PATH_REPO}/to_install/${TARGET}`. - `to_copy/` — plain files installed verbatim by a target-side script. @@ -48,7 +50,7 @@ Both tracks converge on the same first-login family — `start_root_t490s.sh` - `templates/` — files rendered by `render_template` with `${VAR}`/`$(cmd)` expansion, instead of inline heredocs: `fstab`, `wifi.nmconnection` (rendered by `install_debian.sh`), `git-daemon.service` - (`setup_git_mirror.sh`), `update-efi-hook` (`start_root_t490s.sh`). + (`setup_git_server.sh`), `update-efi-hook` (`start_root_t490s.sh`). - `home/` — dotfile skeletons `home/any/`, `home/root/`, `home/user/`, symlinked file-by-file into an account's home by `link_home` (see below). `link_home ` always links `home/any/` (what every account wants) @@ -79,15 +81,15 @@ root, so all of it is present on every target. `start_root_t490s.sh` already does it): `sh scripts/update_efi.sh` - Push repo to a remote server and start its first-login setup (from the admin's workstation): `sh scripts/install_server.sh ` +- Make an already-set-up server a git host: `sh scripts/setup_git_server.sh` +- Make it the authoritative home of a repo (from any machine holding a + checkout of that repo): `sh scripts/adopt_repo.sh ` - First login on a new local system: nothing to type — root's first console login runs `start_root_t490s.sh` via a hook left by `install_debian.sh`. - First login on a new remote server: triggered by `install_server.sh`. - Both first-login scripts can be rerun by hand later (e.g. `/data/confplom/scripts/start_root_t490s.sh`, or `~/confplom/scripts/start_root_server.sh`); so can `start_user.sh`. -- Make an already-set-up target a distribution point for fresh systems: - `sh scripts/setup_git_mirror.sh`; a fresh system then runs - `git clone git:///.git`. There is no test runner. For logic changes prefer a dry read-through plus `sh -n`/`shellcheck -s dash`; never execute the scripts against real block @@ -126,7 +128,7 @@ devices or real remote servers to "test" them. parsed at runtime out of the rescue environment's `/etc/network/interfaces` and re-emitted into a NetworkManager keyfile in the target. The repo therefore carries nothing secret, which is what makes - serving it anonymously via `setup_git_mirror.sh` acceptable. + serving it anonymously via `setup_git_server.sh` acceptable. ## Shared library @@ -183,8 +185,11 @@ another script; core utilities like `mkdir`/`sed`/`mktemp` aren't listed). script**, because each script's `$HOME` differs: `install_server.sh` reads the *admin's* `~/.ssh/known_hosts`; `start_root_server.sh` reads *root's* `~/.ssh` on the freshly provisioned target. -- `FNAME_INITRD`/`FNAME_VMLINUZ`, `FNAME_PROFILE`, `DIRNAME_SSH`, `TO_RBIND`: - the remaining constants shared by two or more scripts. +- `FNAME_INITRD`/`FNAME_VMLINUZ`, `FNAME_PROFILE`, `DIRNAME_SSH`, `TO_RBIND`, + `PATH_GIT_BASE` (`/srv/git`), `OPTS_SSH_NEW_HOST` (the `ssh`/`scp` option + accepting an unseen host key on first contact, so a first-ever connection + doesn't block on a prompt): the remaining constants shared by two or more + scripts. Anything derived from values only known after argument parsing is *not* a shared constant: build it from `path_luks_mapper`/`path_vg` (each taking the @@ -273,11 +278,11 @@ time — e.g. no `chrooted_command.sh` while `install_debian.sh` is mid- debootstrap; the second's `close_luksvg` would tear the VG down under the first. Run them serially. -Exempt: `start_root_*.sh`, `start_user.sh`, `setup_git_mirror.sh` (already -booted into a target, never touch the LUKS+VG), `install_server.sh` (only -talks to a remote server over SSH; can run concurrently with the others or -with itself against another server), and `update_efi.sh` (only mounts the -plain EFI partition). +Exempt: `start_root_*.sh`, `start_user.sh`, `setup_git_server.sh` (already +booted into a target, never touch the LUKS+VG), `install_server.sh` and +`adopt_repo.sh` (only talk to a remote server over SSH; either can run +concurrently with the others or with itself against another server), and +`update_efi.sh` (only mounts the plain EFI partition). ## `setup_luksvg.sh` @@ -439,8 +444,8 @@ tar`. Constants: `FNAME_REPO_TAR=repo.tar`; 4. `ssh` in to `tar xf --no-same-owner` (lands at `~/`). The flag matters: GNU `tar` as root defaults to `--same-owner`, which would restore the *workstation's* numeric UID onto files under `/root`; that - silent mismatch later makes root-run `git` (e.g. `setup_git_mirror.sh`'s - `git clone --bare`) refuse the tree as "dubious ownership". Then `ssh -t` + silent mismatch would otherwise make any later root-run `git` operation + against that tree refuse it as "dubious ownership". Then `ssh -t` (pseudo-tty, since the script ends prompting for a password) to run `./${RELPATH_SETUP_SCRIPT}`. @@ -536,34 +541,83 @@ No arguments; just `include link_home; link_home user`. Safe to run again by hand as any account: as root it additionally picks up `home/user/` into root's home; as the user it picks up entries added after provisioning. -## `setup_git_mirror.sh` +## `setup_git_server.sh` -Optional, run by hand on an already-set-up target to make it a distribution -point. Motivation: a fresh system with no SSH keys exchanged can't use -`install_server.sh`; it can instead anonymously -`git clone git:///.git`. No arguments; refuses to run -if `${PATH_GIT_MIRROR}` (`/srv/git/${DIRNAME_REPO}.git`) exists — one-time -setup, not a sync step. +Optional, run by hand on an already-set-up target to turn it into a git +host — infrastructure only, no specific repo. Motivation: a fresh system +with no SSH keys exchanged can't use `install_server.sh`; a git host lets it +instead anonymously `git clone git:///.git`. Repos themselves +are added afterwards, one at a time, by `adopt_repo.sh` (below). No +arguments; safe to rerun — every step here is idempotent, since the one +thing worth protecting against a rerun (an already-adopted repo) is state +`adopt_repo.sh` owns, not this script. 1. `apt-get -y update` + install `git` (a bare target may lack it). -2. `git clone --bare "${PATH_REPO}"` into the mirror path — a snapshot, not - a live link. -3. `touch git-daemon-export-ok` in it, rather than `--export-all`, so other - repos later placed under `/srv/git` aren't served automatically. -4. `chown -R "${USERNAME}:${USERNAME}"`: `git://` is read-only, so keeping - the mirror current means the admin `git push`ing over `ssh` as - `${USERNAME}` (root SSH is already disabled), so that account must own it. - Consequence, not a bug: root running `git` against the mirror trips - "dubious ownership"; inspect via - `sudo -u "${USERNAME}" git -C "${PATH_GIT_MIRROR}" log`, or for a - one-off `git config --global --add safe.directory "${PATH_GIT_MIRROR}"`. -5. Render `templates/git-daemon.service` (`User=${USERNAME}`; never run a +2. `mkdir -p "${PATH_GIT_BASE}"` (`/srv/git`) and `chown` it (not `-R`) to + `${USERNAME}`, so that account can later `git init --bare` repos under + it (over `ssh`, via `adopt_repo.sh`) without sudo — root SSH is already + disabled by `start_root_server.sh`, and `git://` itself is read-only, so + this is the only way a repo ever gets created or updated there. + Deliberately not recursive: repos under `${PATH_GIT_BASE}` are + `adopt_repo.sh`'s to own, not this script's to reassert on a rerun — e.g. + a future repo owned by some other account shouldn't get silently + reclaimed by a rerun of this one. +3. Render `templates/git-daemon.service` (`User=${USERNAME}`; never run a network-facing daemon as root needlessly) running - `git daemon --reuseaddr --base-path=/srv/git /srv/git`; + `git daemon --reuseaddr --base-path=/srv/git /srv/git` — one daemon, + started once, ends up serving every repo `adopt_repo.sh` later adds + under `${PATH_GIT_BASE}`, so it's never touched again per repo; `systemctl daemon-reload` + `enable --now git-daemon` (systemd is already present; no inetd needed). -6. Print the anonymous clone URL, the `ssh` push URL, and a reminder that - git daemon listens on 9418/tcp in case a firewall is added later. +4. Print the clone/push URL patterns and a reminder that git daemon listens + on 9418/tcp in case a firewall is added later. + +## `adopt_repo.sh` + +Optional, run by hand from any machine holding a checkout of the repo to +adopt — this repo's own or another project's; only this repo's own +`scripts/lib/` is needed, so per "Hardcoded machine assumptions" the running +machine just needs *some* checkout of this repo present, same as any other +script here — against a `setup_git_server.sh`-provisioned server. Two +arguments: the local repo's path and the server. Makes that server the +repo's authoritative, single copy of history, so it also repoints the local +checkout's own `origin` there. Unlike every other server-facing script here +it never runs *on* the server via a console/first-login hook — it drives the +server purely over `ssh`, closer to `install_server.sh` than to a +target-side script. + +1. Sanity checks: `${PATH_SRC_REPO}` looks like a git checkout; its current + branch (`NAME_BRANCH`, via `symbolic-ref`) resolves — refuses a detached + `HEAD`; the target `${PATH_GIT_BASE}/.git` doesn't already exist on + the server (checked over `ssh`) — one-time adoption per repo, not a sync + step. +2. Over `ssh`, as `${USERNAME}` (root SSH is disabled by + `start_root_server.sh`): `git init --bare + --initial-branch="${NAME_BRANCH}"` — genuinely empty, not a clone, since + there's nothing local to the server to clone from; content arrives in + the next step. `--initial-branch` matters because an empty bare repo has + nothing to derive a default branch from otherwise, and its `HEAD` + (pointing at a ref that doesn't exist yet) only resolves once that + same-named branch is pushed. Then `touch git-daemon-export-ok` (rather + than `--export-all`, so other repos later adopted onto the same server + aren't served automatically) and sets `receive.denyNonFastForwards` + + `receive.denyDeletes`: once this is the repo's only copy, a force-push or + branch deletion has nothing to recover from, so pushes may only add + commits. Deliberate friction; lift either by hand on the server for a + rare, genuine history rewrite. +3. `git push --mirror` from the local checkout carries over every branch + and tag in one shot — the actual content transfer, analogous to a + same-host `git clone --bare`. +4. Only once that push has succeeded: point the local checkout's own + `origin` at the new `ssh://` URL (`remote set-url`, or `remote add` if it + had none). `set-url` alone never touches a separately configured + `remote.origin.pushurl` — which git prefers over `url` for pushes — so if + one was already set (and now differs), print how to also repoint it by + hand rather than silently dropping what may be a deliberate split. +5. Print the new anonymous clone URL. + +Both scripts share `PATH_GIT_BASE` and (with `install_server.sh`) +`OPTS_SSH_NEW_HOST` — see "Shared library". ## `home/` skeletons diff --git a/scripts/adopt_repo.sh b/scripts/adopt_repo.sh new file mode 100755 index 0000000..c432f0a --- /dev/null +++ b/scripts/adopt_repo.sh @@ -0,0 +1,64 @@ +#!/bin/sh +. "$(dirname "$0")/_lib.sh" +include OPTS_SSH_NEW_HOST +include PATH_GIT_BASE +include USERNAME +include check_tools +include error +include msg +include try_quiet +include usage + +# inputs to confirm +usage $# "path-repo" "repos-server" +PATH_SRC_REPO=$1 +SERVER=$2 +LOGIN="${USERNAME}@${SERVER}" + +# early sanity check, with further-down constant declarations relying on this … +[ -d "${PATH_SRC_REPO}/.git" ]\ + || error "${PATH_SRC_REPO}: not a git checkout" + +# constants derived from changeables +NAME_REPO=$(basename "$(cd "${PATH_SRC_REPO}" && pwd)") +PATH_REMOTE_REPO="${PATH_GIT_BASE}/${NAME_REPO}.git" +URL_REMOTE_REPO="ssh://${LOGIN}${PATH_REMOTE_REPO}" + +# sanity checks and determination of current active repo branch as NAME_BRANCH +# (needed for git init's --initial-branch, so the history-free remote repo's +# HEAD resolves once our later git push --mirror creates that branch) +check_tools git ssh +NAME_BRANCH=$(git -C "${PATH_SRC_REPO}" symbolic-ref --quiet --short HEAD)\ + || error "${PATH_SRC_REPO}: not on a branch (detached HEAD?)" +msg 'Checking %s does not already host %s …' "${SERVER}" "${NAME_REPO}" +ssh ${OPTS_SSH_NEW_HOST} "${LOGIN}" "[ ! -e '${PATH_REMOTE_REPO}' ]"\ + || error "${PATH_REMOTE_REPO} already exists on ${SERVER}" + +msg 'Creating bare repo on %s, hardened against history loss …' "${SERVER}" +ssh ${OPTS_SSH_NEW_HOST} "${LOGIN}" "git init --quiet --bare \ + --initial-branch='${NAME_BRANCH}' '${PATH_REMOTE_REPO}' \ + && touch '${PATH_REMOTE_REPO}/git-daemon-export-ok' \ + && git -C '${PATH_REMOTE_REPO}' config receive.denyNonFastForwards true \ + && git -C '${PATH_REMOTE_REPO}' config receive.denyDeletes true" + +msg 'Pushing all branches and tags to %s …' "${SERVER}" +GIT_SSH_COMMAND="ssh ${OPTS_SSH_NEW_HOST}" \ + git -C "${PATH_SRC_REPO}" push --quiet --mirror "${URL_REMOTE_REPO}" + +msg "Pointing this checkout's origin at %s …" "${SERVER}" +if try_quiet git -C "${PATH_SRC_REPO}" remote get-url origin; then + PUSHURL=$(git -C "${PATH_SRC_REPO}" config --get remote.origin.pushurl)\ + || PUSHURL="" + git -C "${PATH_SRC_REPO}" remote set-url origin "${URL_REMOTE_REPO}" + if [ -n "${PUSHURL}" ] && [ "${PUSHURL}" != "${URL_REMOTE_REPO}" ]; then + msg 'Note: origin also has a separate pushurl (%s)' "${PUSHURL}" + msg 'left untouched; a bare git push will still use it. To also' + msg 'repoint pushes here:' + msg ' git remote set-url --push origin %s' "${URL_REMOTE_REPO}" + fi +else + git -C "${PATH_SRC_REPO}" remote add origin "${URL_REMOTE_REPO}" +fi + +msg '%s is now authoritative for %s.' "${SERVER}" "${NAME_REPO}" +msg 'Anonymous clone URL: git://%s/%s.git' "${SERVER}" "${NAME_REPO}" diff --git a/scripts/install_server.sh b/scripts/install_server.sh index 94e16c8..ac52aa7 100755 --- a/scripts/install_server.sh +++ b/scripts/install_server.sh @@ -2,6 +2,7 @@ . "$(dirname "$0")/_lib.sh" include DIRNAME_REPO include DIRNAME_SCRIPTS +include OPTS_SSH_NEW_HOST include PATH_MY_SSH include PATH_REPO include check_tools @@ -10,7 +11,6 @@ include usage # constants we might want to change at some point FNAME_REPO_TAR=repo.tar -OPTS_SSH_NEW_HOST="-o StrictHostKeyChecking=accept-new" # constants derived from changeables PATH_REPO_PARENT=$(dirname "${PATH_REPO}") @@ -37,7 +37,7 @@ scp ${OPTS_SSH_NEW_HOST} "${PATH_REPO_TAR}" "${LOGIN}:~" rm -rf "${PATH_TEMP}" msg 'Unpacking repo and running setup script …' -# --no-same-owner owns the files to root, prevening "dubious ownership" when -# running setup_git_mirror.sh on it later! +# --no-same-owner owns the files to root, preventing "dubious ownership" for +# any later git operation against this tree (by hand, or a future script)! ssh ${OPTS_SSH_NEW_HOST} "${LOGIN}" tar xf "${FNAME_REPO_TAR}" --no-same-owner ssh -t ${OPTS_SSH_NEW_HOST} "${LOGIN}" "./${RELPATH_SETUP_SCRIPT}" diff --git a/scripts/lib/OPTS_SSH_NEW_HOST.sh b/scripts/lib/OPTS_SSH_NEW_HOST.sh new file mode 100644 index 0000000..ae71709 --- /dev/null +++ b/scripts/lib/OPTS_SSH_NEW_HOST.sh @@ -0,0 +1,2 @@ +# accept-new avoids blocking on a host-key prompt on a first-ever connection +OPTS_SSH_NEW_HOST="-o StrictHostKeyChecking=accept-new" diff --git a/scripts/lib/PATH_GIT_BASE.sh b/scripts/lib/PATH_GIT_BASE.sh new file mode 100644 index 0000000..8dcceda --- /dev/null +++ b/scripts/lib/PATH_GIT_BASE.sh @@ -0,0 +1 @@ +PATH_GIT_BASE=/srv/git diff --git a/scripts/setup_git_mirror.sh b/scripts/setup_git_mirror.sh deleted file mode 100755 index 250e608..0000000 --- a/scripts/setup_git_mirror.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_lib.sh" -include DIRNAME_REPO -include PATH_REPO -include USERNAME -include error -include msg -include render_template - -# constants we might want to change at some point -PATH_GIT_BASE=/srv/git - -# constants derived from changeables -FNAME_GIT_DAEMON_UNIT=git-daemon.service -PATH_GIT_DAEMON_UNIT="/etc/systemd/system/${FNAME_GIT_DAEMON_UNIT}" -PATH_GIT_MIRROR="${PATH_GIT_BASE}/${DIRNAME_REPO}.git" -PATH_GIT_MIRROR_EXPORT_OK="${PATH_GIT_MIRROR}/git-daemon-export-ok" - -[ -e "${PATH_GIT_MIRROR}" ]\ - && error "${PATH_GIT_MIRROR} already exists" - -msg 'Installing git …' -apt-get -y update -apt-get -y install git - -msg 'Bare-cloning repo to %s for anonymous serving …' "${PATH_GIT_MIRROR}" -mkdir -p "${PATH_GIT_BASE}" -git clone --quiet --bare "${PATH_REPO}" "${PATH_GIT_MIRROR}" -touch "${PATH_GIT_MIRROR_EXPORT_OK}" - -msg 'Own repo to %s so they can update it via ssh …' "${USERNAME}" -chown -R "${USERNAME}:${USERNAME}" "${PATH_GIT_MIRROR}" - -msg 'Writing and enabling git-daemon systemd unit …' -render_template "${FNAME_GIT_DAEMON_UNIT}" >| "${PATH_GIT_DAEMON_UNIT}" -systemctl daemon-reload -systemctl enable --now git-daemon - -msg 'Anonymous clone URL: git:///%s.git' "${DIRNAME_REPO}" -msg 'Push updates later with: git push ssh://%s@%s' \ - "${USERNAME}" "${PATH_GIT_MIRROR}" -msg 'Firewall note: git daemon listens on port 9418/tcp.' diff --git a/scripts/setup_git_server.sh b/scripts/setup_git_server.sh new file mode 100755 index 0000000..768c3e3 --- /dev/null +++ b/scripts/setup_git_server.sh @@ -0,0 +1,33 @@ +#!/bin/sh +. "$(dirname "$0")/_lib.sh" +include PATH_GIT_BASE +include USERNAME +include msg +include render_template + +# constants derived from changeables +FNAME_GIT_DAEMON_UNIT=git-daemon.service +PATH_GIT_DAEMON_UNIT="/etc/systemd/system/${FNAME_GIT_DAEMON_UNIT}" + +msg 'Ensuring installation of git …' +apt-get -y update +apt-get -y install git + +msg 'Creating %s to hold hosted repos, owned by %s …' \ + "${PATH_GIT_BASE}" "${USERNAME}" +mkdir -p "${PATH_GIT_BASE}" +chown "${USERNAME}:${USERNAME}" "${PATH_GIT_BASE}" + +msg 'Writing and enabling git-daemon systemd unit …' +render_template "${FNAME_GIT_DAEMON_UNIT}" >| "${PATH_GIT_DAEMON_UNIT}" +systemctl daemon-reload +systemctl enable --now git-daemon + +msg 'Ready. Adopt a repo onto this server from wherever it lives with:' +msg ' sh scripts/adopt_repo.sh ' +msg 'Anonymous clone URL pattern (read-only): git:///.git' +msg 'Push URL pattern (read-write): ssh://%s@%s/.git' \ + "${USERNAME}" "${PATH_GIT_BASE}" +msg 'Clone via the git:// form only for a checkout that never needs to' +msg 'push; it has no push access at all, by design.' +msg 'Firewall note: git daemon listens on port 9418/tcp.'