Files
8d401929b6 The repo scripts make repos on an operator's own account ("owner": "user") (#81)
* The repo scripts make repos on an operator's own account ("owner": "user")

Any operator can now make private or public repos on their own GitHub account with the same setup as
ouroboric-dev's. A config with "owner": "user" gets it, with the differences GitHub forces:

- create-repo.sh creates through /user/repos, after checking that the token belongs to the config's
  user, so a repo never lands on the wrong account.
- setup-repo.sh writes each user repo's own Actions allowlist (lint.yml's pinned actions, nothing
  else), since a user has no org to hold it. On a user's private repo it skips secret scanning, push
  protection and private vulnerability reporting, which GitHub doesn't offer there, and says so.
- If GitHub refuses the ruleset (a user's private repo without GitHub Pro), setup-repo.sh fails and
  says main is not protected, after every other setting is in place; new-repo.sh then reports the repo
  as unprotected. The ruleset itself is unchanged.

An operator's config is theirs, not the company's: it lives in scripts/orgs/local/, which git ignores
except for its README, and --org reads it from there (a name in both folders is refused). Only
generic examples are committed, in scripts/orgs/examples/: a user config and a proprietary licence
text that names no one. Invariant 9 in AGENTS.md says never to commit an operator's config, and a test
fails if any committed config is neither an org's nor an example. The fakes learn GET /user,
POST /user/repos and refused writes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* check-repos.sh --check operator: no tracked file names the operator (invariant 9)

The check reads each repo's local clone and fails when a tracked file names the operator who runs it: their
login, their home folder's name, or a user account in scripts/orgs/local/, as whole words in any case. The
names come from the machine, so the check serves any operator and the script names no one;
CHECK_REPOS_OPERATOR replaces them. It prints the files, never the names, and fails closed on a missing
clone, an empty name list or a git error. The records of past work keep their paths as written: RECORDS
lists those files one by one, so a new plan beside them is still checked. It is a default check.

The local-qwen design, plan and how-to now give paths relative to the clones, and the design no longer
names the founder's folders. Invariant 9 also covers docs, plans and fixtures.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Bruno Guilhermo de Barros Valério <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-24 18:35:25 +02:00

103 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# shellcheck source-path=SCRIPTDIR
# Create new repos in the org that a config names, or in a user's own account for a config with
# "owner": "user", each with a first commit and the required setup, so agents only ever see a protected
# repo. Every check runs before the first repo is created: if any repo
# already exists, a name repeats, a --ci piece is unknown or the config is bad, nothing is created.
# Then, for each repo in turn: it builds the first commit in a temporary folder, with README.md (the
# name as the title, the description as the first paragraph), the licence files (add-licences.sh) and
# the base CI (add-ci.sh); create-repo.sh creates the repo; the commit is pushed to main; setup-repo.sh
# protects it. If building the commit fails, such as a failed licence download, that repo is not
# created. If the push or the setup fails, the repo exists but is not protected, and the script says
# so. Stops at the first error: in a batch, the repos before it are complete. It never prompts.
# Founder-run: GH_TOKEN must hold the admin token: fine-grained, short-lived, owned by the org,
# repository access "All repositories", with Administration, Contents and Workflows read and write
# (the pr-bot can't create repos). A real run confirmed this token on 2026-10-18; a token limited to
# selected repositories is untested. The org's Actions allowlist must hold lint.yml's two actions.
# For a user config, the token is owned by that user instead, with the same access, and each repo holds
# its own allowlist (setup-repo.sh writes it). GitHub refuses rulesets on a user's private repo without
# GitHub Pro: then setup-repo.sh fails, and the repo is reported as not protected.
# GH_TOKEN=... new-repo.sh (--org <name>|--config <path>) [--private] [--ci <piece>,...] <repo> "<description>" [<repo> "<description>"]...
# --private makes private repos, such as huldra, the hidden benchmark suite. --ci replaces the
# config's base CI pieces. After each repo, it prints the config's after-create message, such as how
# to add a public repo to the pr-bot.
set -euo pipefail
exec </dev/null
export GIT_TERMINAL_PROMPT=0
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GIT="${CREATE_REPO_GIT:-https://github.com}"
SETUP="${NEW_REPO_SETUP:-$HERE/setup-repo.sh}"
die() { echo "new-repo: $*" >&2; exit 2; }
# shellcheck source=repo-config.sh
. "$HERE/repo-config.sh"
[ -n "${GH_TOKEN:-}" ] || die "GH_TOKEN must hold the short-lived admin token"
repo_config_args "$@"
repo_config_load
visibility=public private=() ci=()
set -- "${RC_REST[@]}"
while [ $# -gt 0 ]; do
case "$1" in
--private) visibility=private; private=(--private) ;;
--ci) [ $# -ge 2 ] || die "--ci needs a value"; ci=(--ci "$2"); shift ;;
-*) die "unknown option: $1" ;;
*) break ;;
esac
shift
done
[ $# -ge 2 ] && [ $(($# % 2)) = 0 ] || die 'usage: new-repo.sh (--org <name>|--config <path>) [--private] [--ci <piece>,...] <repo> "<description>" [<repo> "<description>"]...'
args=("$@")
"$HERE/create-repo.sh" "${RC_FROM[@]}" ${private[@]+"${private[@]}"} --check "${args[@]}"
"$HERE/add-licences.sh" "${RC_FROM[@]}" --check
"$HERE/add-ci.sh" "${RC_FROM[@]}" ${ci[@]+"${ci[@]}"} --check
T="$(mktemp -d)"
trap 'rm -rf -- "${T:?}"' EXIT
# build NAME CMD...: runs CMD, its output on stderr. If it fails, says that repo $r is not on GitHub
# and exits with CMD's code.
build() {
local name="$1" rc=0
shift
"$@" >&2 || rc=$?
[ "$rc" = 0 ] && return
echo "new-repo: $r: $name failed before the repo was made, so it is not on GitHub. Fix the cause and run new-repo.sh again for $r." >&2
exit "$rc"
}
# step NAME CMD...: runs CMD, its output on stderr. If it fails, says that repo $r is not protected
# and exits with CMD's code.
step() {
local name="$1" rc=0
shift
"$@" >&2 || rc=$?
[ "$rc" = 0 ] && return
echo "new-repo: $r was created, but $name failed. It is not protected and agents must not get it yet. Delete it, or push a first commit and run setup-repo.sh." >&2
exit "$rc"
}
readme() { printf '# %s\n\n%s\n' "$2" "$3" > "$1/README.md"; }
commit() { git -C "$1" add -A && git -C "$1" commit -q -m "Licences and base CI"; }
for ((i = 0; i < ${#args[@]}; i += 2)); do
r="${args[i]}" d="${args[i + 1]}"
w="$T/$r"
build "git init" git init -q -b main "$w"
build "writing the README" readme "$w" "$r" "$d"
build "add-licences.sh" "$HERE/add-licences.sh" "${RC_FROM[@]}" "$w"
build "add-ci.sh" "$HERE/add-ci.sh" "${RC_FROM[@]}" ${ci[@]+"${ci[@]}"} "$w"
build "the first commit" commit "$w"
"$HERE/create-repo.sh" "${RC_FROM[@]}" ${private[@]+"${private[@]}"} "$r" "$d" >/dev/null
echo "new-repo: $r: created"
# The helper reads GH_TOKEN when git asks, so the token never appears in a command line.
# shellcheck disable=SC2016 # $1 and $GH_TOKEN expand in the helper, not here
step "the push" git -C "$w" -c credential.helper= \
-c 'credential.helper=!f() { test "$1" = get && printf "username=x-access-token\npassword=%s\n" "$GH_TOKEN"; }; f' \
push -q "$GIT/$RC_ORG/$r.git" main
echo "new-repo: $r: first commit pushed"
step "setup-repo.sh" "$SETUP" "${RC_FROM[@]}" "$r"
echo "new-repo: $r: protected"
repo_after_create "$visibility" "$r"
done