Files
shopdb-flask/tools/promote-to-main.sh
cproudlock 6d9954c6f9
Some checks failed
CI / naming (push) Successful in 3s
CI / frontend (push) Successful in 13s
CI / migrations-mysql (push) Successful in 42s
CI / backend (push) Has been cancelled
Add push and promote scripts for the wj branch
wj holds this site's work; main is the product every other site sees and the
publication mirror exports. Two scripts, one for each direction.

tools/git-push.sh pushes a branch without leaving the token behind. The way this
was being done by hand - rewrite remote.origin.url to embed the token, push,
rewrite it back - writes the token into .git/config in plaintext on every push,
and leaves it there if the push fails or the terminal closes mid-way, where
`git remote -v` will happily print it. Putting it in the URL on the command line
is no better: argv is visible in `ps` to every user on the box. The token now
goes in through GIT_ASKPASS, so it never reaches the config or the process list
and there is nothing to clean up when the script dies.

tools/promote-to-main.sh cherry-picks NAMED commits onto main, rather than
merging the branch. Merging takes the site-specific work along with the product
work, which is how a site branch quietly becomes the product; naming the commits
keeps that decision explicit and per change.

It runs main's gates after the pick and before any push, and does not push at
all - it prints what landed and leaves that to a human. The ADR-015 site-literal
check is the one that earns its place: it catches a hostname or share path
riding along in a commit that looked generic, which is the failure that makes a
site branch dangerous rather than merely awkward. It caught this very commit,
which named the site in a comment.

Both refuse a dirty tree. Pushing one pushes the last commit rather than what is
in front of you, and cherry-picking from one mixes uncommitted work into the
promotion. On failure the promote script aborts the pick and returns you to the
branch you started on, with main untouched.
2026-08-30 08:46:14 -04:00

146 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Take specific commits from wj to main.
#
# tools/promote-to-main.sh <commit> [<commit> ...]
# tools/promote-to-main.sh --dry-run <commit> ...
#
# 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
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] <commit> [<commit> ...]" >&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"