#!/usr/bin/env bash # # Take specific commits from wj to main. # # tools/promote-to-main.sh [ ...] # tools/promote-to-main.sh --dry-run ... # # Commits are applied in the order given, so give them oldest first. # # WHY CHERRY-PICK AND NOT A MERGE # # wj holds this site's work. Some of it is product - the rest of the world # should have it - and some belongs to this site alone. Merging the branch takes # both; that is how a site branch quietly becomes the product. Naming the # commits keeps the decision explicit and per change. # # WHAT THIS REFUSES TO DO # # main is what every other site sees, and what the publication mirror exports. # So this runs main's gates AFTER the pick and BEFORE the push, and leaves the # pick unpushed if any of them fail. The ADR-015 check matters most: it is what # catches a hostname, share path or subnet riding along in a commit that looked # generic. That check is the whole reason a site branch is survivable. # # It does NOT push automatically. Read what it says, then run tools/git-push.sh # main yourself. set -euo pipefail # RUN FROM A COPY OF OURSELF. # # This script checks out main. The tooling may live only on the site branch, in # which case that checkout DELETES this file while bash is still reading it - # and bash reads a script lazily, by offset, so it would carry on executing # whatever bytes happen to be at that position afterwards. While manipulating # branches. This is not theoretical: main did not have tools/ when this was # written, so the very first real use would have hit it. # # Copy to a temp file, re-exec from there, and delete the copy immediately. On # Unix the open file handle keeps working after the unlink, so the running # script survives whatever git does to the tree. if [ "${PROMOTE_SELFCOPY:-}" != "1" ]; then SELF="$(mktemp)" cat "$0" > "$SELF" chmod 700 "$SELF" export PROMOTE_SELFCOPY=1 exec bash "$SELF" "$@" fi rm -f "$0" 2>/dev/null || true cd "$(git rev-parse --show-toplevel)" DRYRUN=0 if [ "${1:-}" = "--dry-run" ]; then DRYRUN=1 shift fi if [ "$#" -eq 0 ]; then echo "usage: tools/promote-to-main.sh [--dry-run] [ ...]" >&2 echo >&2 echo "Give commits oldest first. Find them with:" >&2 echo " git log --oneline main..wj" >&2 exit 1 fi STARTING_BRANCH="$(git rev-parse --abbrev-ref HEAD)" if [ -n "$(git status --porcelain)" ]; then echo "Working tree is not clean:" git status --short echo echo "Cherry-picking onto main from here would mix uncommitted work into the" echo "promotion, or fail halfway and leave you on main with a dirty tree." exit 1 fi # Resolve and describe first, so a typo is caught before anything moves. echo "About to promote onto main, in this order:" for ref in "$@"; do if ! git rev-parse --verify --quiet "$ref^{commit}" >/dev/null; then echo "ERROR: not a commit: $ref" >&2 exit 1 fi echo " $(git log --oneline -1 "$ref")" done echo if [ "$DRYRUN" -eq 1 ]; then echo "--dry-run: files each commit touches, so you can see what reaches main." for ref in "$@"; do echo echo "--- $(git log --oneline -1 "$ref") ---" git show --stat --oneline "$ref" | tail -n +2 done echo echo "Nothing was changed." exit 0 fi # Put us back where we started, whatever happens next. restore() { local code=$? if [ "$code" -ne 0 ]; then echo echo "Promotion did NOT complete." if git rev-parse --verify --quiet CHERRY_PICK_HEAD >/dev/null 2>&1; then echo "Aborting the in-progress cherry-pick ..." git cherry-pick --abort || true fi local now now="$(git rev-parse --abbrev-ref HEAD)" if [ "$now" != "$STARTING_BRANCH" ]; then git checkout "$STARTING_BRANCH" >/dev/null 2>&1 || true fi echo "Back on $STARTING_BRANCH. main is unchanged and nothing was pushed." fi } trap restore EXIT git checkout main MAIN_BEFORE="$(git rev-parse HEAD)" for ref in "$@"; do echo echo "==> picking $(git log --oneline -1 "$ref")" if ! git cherry-pick -x "$ref"; then echo echo "That commit does not apply cleanly to main." echo "Resolve it by hand if it is worth having:" echo " git status # see the conflict" echo " git cherry-pick --abort # or walk away" exit 1 fi done echo echo "==> main gates" echo "--- naming, style, site literals (ADR-015) ---" bash scripts/check-naming-and-style.sh echo "--- generated project map ---" if ! venv/bin/python scripts/gen_project_map.py --check >/dev/null 2>&1; then echo "Project map is stale; regenerating." venv/bin/python scripts/gen_project_map.py git add docs/PROJECT-MAP.md git commit -q -m "Regenerate the project map after promotion" fi echo "--- python suite ---" venv/bin/python -m pytest -q echo "--- frontend ---" ( cd frontend && npm test --silent ) trap - EXIT echo echo "main is now:" git log --oneline "$MAIN_BEFORE"..HEAD echo echo "Gates passed. NOT pushed - review the above, then:" echo " tools/git-push.sh main" echo echo "You are on main. Return to your branch with:" echo " git checkout $STARTING_BRANCH"