123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 | #!/bin/bash
# Usage: tools/create-repo [--dry-run] [--yes] [-d DESCRIPTION] [--github [--private]]
# [--section TITLE] [--config FILE] [--key FILE]
# [--git-host HOST] [--remote NAME] SERVER [NAME]
#
# Run in a working copy: makes the repository on SERVER, for a server set
# up as in docs/openbsd.md, adds it here as a remote, and pushes to it.
#
# - with --github, creates the repository on GitHub too (public unless
# --private), if it is not there, as SERVER's mirror
# - creates NAME.git on SERVER with new-repo, wired to the renderer, with
# the current branch as its default
# - with --section, lists it in that section of the renderer's config, so
# it is on the index; the config is written only if repo-render check
# passes, and the old one kept as .bak. Without, it is private.
# - adds git@HOST:NAME.git as the remote (origin unless --remote), and
# pushes the branch and the tags, which the hook renders and mirrors
#
# Everything that needs root runs in one doas on SERVER, after asking
# (--yes doesn't ask); --dry-run prints what it would do instead.
#
# SERVER what ssh logs you in to SERVER as, with doas
# NAME the repository's name (default: this directory's)
# --config FILE the renderer's config on SERVER
# (default /etc/repo-render/repo-render.yaml)
# --key FILE an ssh key on SERVER for pushing to GitHub, which the
# repository's core.sshCommand is set to use
# --git-host HOST where to push as git (default: SERVER's host name)
#
# Needs git, ssh and mikefarah's yq here, gh (logged in) for --github, and
# repo-render-new-repo on SERVER.
# shellcheck disable=SC2029 # the commands are meant to expand here, then run there
set -euo pipefail
usage() {
sed -n '2,5s/^# //p' "$0" >&2
exit 2
}
die() { echo "create-repo: $*" >&2; exit 1; }
config=/etc/repo-render/repo-render.yaml
dry="" yes="" description="" github="" private="" section="" key="" git_host="" remote=origin
server="" name=""
while (($#)); do
case $1 in
--dry-run) dry=1 ;;
--yes) yes=1 ;;
-d) description=${2?}; shift ;;
--github) github=1 ;;
--private) private=1 ;;
--section) section=${2?}; shift ;;
--config) config=${2?}; shift ;;
--key) key=${2?}; shift ;;
--git-host) git_host=${2?}; shift ;;
--remote) remote=${2?}; shift ;;
-*) usage ;;
*)
if [[ ! $server ]]; then server=$1
elif [[ ! $name ]]; then name=$1
else usage
fi
;;
esac
shift
done
[[ $server ]] || usage
[[ $git_host ]] || git_host=${server#*@}
[[ $private && ! $github ]] && die "--private is for --github"
top=$(git rev-parse --show-toplevel 2>/dev/null) || die "not in a git working copy"
[[ $name ]] || name=$(basename "$top")
[[ $name =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ && $name != *.git ]] || die "$name: a name has only letters, digits and . _ - (and no .git)"
branch=$(git branch --show-current)
[[ $branch ]] || die "not on a branch"
[[ $branch =~ ^[A-Za-z0-9_][A-Za-z0-9._/-]*$ ]] || die "branch $branch: letters, digits and . _ - / only"
git remote get-url "$remote" >/dev/null 2>&1 && die "this working copy has a remote $remote already: give --remote NAME"
description=${description//$'\n'/ }
# These go into commands run as root on SERVER.
for p in "$config" "$key"; do
[[ -z $p || $p =~ ^/[A-Za-z0-9._/-]+$ ]] || die "$p: an absolute path of letters, digits and . _ - / only"
done
need=(git ssh yq)
[[ $github ]] && need+=(gh)
for p in "${need[@]}"; do
command -v "$p" >/dev/null || die "needs $p"
done
# Runs a command, or with --dry-run prints it, quoted to paste into a shell
run() { if [[ $dry ]]; then printf '%q ' "$@"; echo; else "$@"; fi; }
# Quotes a string for sh, in single quotes
sq() { printf "'%s'" "${1//\'/\'\\\'\'}"; }
# A command line for sh, each word quoted
sqline() { local out="" w; for w in "$@"; do out+="${out:+ }$(sq "$w")"; done; printf '%s' "$out"; }
ask() {
[[ $yes || $dry ]] && return 0
read -rp "$1 [y/N] " answer
[[ $answer == [yY]* ]]
}
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
ssh "$server" cat "$config" >"$tmp/old.yaml"
git_dir=$(yq '.git_dir // ""' "$tmp/old.yaml")
[[ $git_dir =~ ^/[A-Za-z0-9._/-]+$ ]] || die "$config on $server has no usable git_dir (got '$git_dir')"
dir=$git_dir/$name.git
ssh "$server" "test ! -e $(sq "$dir")" || die "$dir is on $server already"
# GitHub's repository is made once SERVER's is, before the push, which
# the hook mirrors to it.
mirror="" make_github=()
if [[ $github ]]; then
if full=$(gh repo view "$name" --json nameWithOwner --jq .nameWithOwner 2>/dev/null); then
echo "GitHub has $full already: it becomes the mirror"
else
full=$(gh api user --jq .login)/$name
visibility=--public
[[ $private ]] && visibility=--private
make_github=(gh repo create "$name" "$visibility")
[[ $description ]] && make_github+=(--description "$description")
echo "GitHub has no $full: it will be made, as the mirror"
fi
mirror=git@github.com:$full.git
fi
as_git=()
args=(-c "$config" -b "$branch")
[[ $description ]] && args+=(-d "$description")
[[ $mirror ]] && args+=(-m "$mirror")
as_git+=("$(sqline repo-render-new-repo "${args[@]}" "$dir")")
if [[ $key && $mirror ]]; then
as_git+=("$(sqline git -C "$dir" config core.sshCommand "ssh -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -i $key")")
fi
# With --section, the new config is checked before the repository is
# made, and put in place after: a config that fails the check changes
# nothing, and one that passes is only written once the repository is
# there.
create=""
for c in "${as_git[@]}"; do
create+="su -s /bin/sh git -c $(sq "$c")"
[[ $section ]] && create+=" || { rm -f $(sq "$config.new"); exit 1; }"
create+="; "
done
root=$create
if [[ $section ]]; then
n=$(T=$section yq '[.sections[] | select(.title == strenv(T))] | length' "$tmp/old.yaml")
((n == 1)) || die "$config on $server has no section titled '$section'"
N=$name T=$section yq '(.sections[] | select(.title == strenv(T)) | .repos) += [strenv(N)]' "$tmp/old.yaml" >"$tmp/new.yaml"
diff -u --label "$config" --label "$config (new)" "$tmp/old.yaml" "$tmp/new.yaml" || true
home=$(ssh "$server" pwd)
upload=$home/.repo-render.yaml.new
[[ $upload =~ ^/[A-Za-z0-9._/-]+$ ]] || die "$server: can't use the home directory $home"
check=$(sqline repo-render --config "$config.new" check)
root="install -m 644 $(sq "$upload") $(sq "$config.new"); rm $(sq "$upload"); "
root+="if ! out=\$(su -s /bin/sh git -c $(sq "$check") 2>&1); then echo \"\$out\" >&2; rm $(sq "$config.new"); echo 'the new config fails repo-render check: nothing changed' >&2; exit 1; fi; "
root+=$create
root+="cp -p $(sq "$config") $(sq "$config.bak"); mv $(sq "$config.new") $(sq "$config"); echo $(sq "wrote $config (the old one is $config.bak)"); "
fi
echo "To run as root on $server:"
printf ' %s\n' "$root"
if [[ $dry ]]; then
[[ $section ]] && echo "(dry run: the new config is not uploaded to $server:$upload)"
echo "(dry run: not run)"
elif ask "Run it?"; then
if [[ $section ]]; then
ssh "$server" "umask 077 && cat > $(sq "$upload")" <"$tmp/new.yaml"
fi
ssh -t "$server" "doas sh -ec $(sq "$root")"
else
echo "nothing changed on $server"
exit 1
fi
((${#make_github[@]})) && run "${make_github[@]}"
url=git@$git_host:$name.git
run git remote add "$remote" "$url"
if git rev-parse --verify --quiet HEAD >/dev/null; then
run git push -u "$remote" "$branch"
run git push "$remote" --tags
else
echo "no commits yet: push with git push -u $remote $branch"
fi
[[ $section ]] || echo "$name is in no section of the index, so it is private: --section TITLE lists it"
|