promote-to-main.sh checks out main. The tooling lives only on the site branch, so that checkout removes tools/promote-to-main.sh while the script is still running - and bash reads a script lazily, by file offset, so it would carry on executing whatever bytes happen to land there. While manipulating branches. Not theoretical: main does not have tools/ right now, so the first real use would have hit it. Both versions now copy themselves to a temp file and re-exec from there. The bash copy unlinks itself immediately, since the open handle keeps working. The Windows copy has the same hazard for the same reason - cmd.exe re-seeks the batch file after every command - and two extra traps worth naming: the copy runs from %TEMP%, so the repository root has to travel with it rather than being derived from %~dp0, and %errorlevel% inside a parenthesised block is expanded when the block is PARSED, before the call it is meant to report on. Scope: product
167 lines
5.0 KiB
Bash
Executable File
167 lines
5.0 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
|
|
|
|
# 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] <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"
|