From 6d9954c6f9932d0d137f9d52d827b6b35f304abf Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 30 Aug 2026 08:46:14 -0400 Subject: [PATCH] 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. --- tools/git-push.sh | 109 +++++++++++++++++++++++++++++ tools/promote-to-main.sh | 145 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100755 tools/git-push.sh create mode 100755 tools/promote-to-main.sh diff --git a/tools/git-push.sh b/tools/git-push.sh new file mode 100755 index 00000000..778840be --- /dev/null +++ b/tools/git-push.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# +# Push a branch to the internal git server, without leaving the token behind. +# +# tools/git-push.sh push the current branch +# tools/git-push.sh wj push a named branch +# +# WHY THIS EXISTS +# +# The remote is HTTPS and needs a token. The obvious way to do that - rewrite +# remote.origin.url to include the token, push, rewrite it back - was how this +# was done by hand, and it is worse than it looks: +# +# - the token is written into .git/config, in plaintext, on every push +# - if the push fails, or the terminal is closed mid-push, it STAYS there +# - `git remote -v` then prints it, and so does anything that logs the remote +# +# Passing it in the URL on the command line is no better: the whole argv is +# visible in `ps` to every user on the box for the length of the push. +# +# So the token goes in through GIT_ASKPASS instead. Git asks a helper program +# for the password; the helper prints it on stdout and exits. It never touches +# the config, never appears in argv, and there is nothing to clean up if this +# script dies halfway. +set -euo pipefail + +SECRETS="${SHOPDB_SECRETS:-/home/camp/projects/windows/shopdb/secrets.md}" +REMOTE_HOST="gitea.proudtech.net" +REMOTE_PATH="ge-aerospace/shopdb-flask.git" +USERNAME="cproudlock" + +BRANCH="${1:-$(git rev-parse --abbrev-ref HEAD)}" + +if [ "$BRANCH" = "HEAD" ]; then + echo "ERROR: detached HEAD. Check out a branch, or name one." >&2 + exit 1 +fi + +if ! git show-ref --verify --quiet "refs/heads/$BRANCH"; then + echo "ERROR: no such local branch: $BRANCH" >&2 + exit 1 +fi + +# Refuse to push a branch that is not what is in front of you. Pushing a stale +# branch while your work sits uncommitted looks like a successful push and is +# the kind of thing found out much later. +if [ -n "$(git status --porcelain)" ]; then + echo "Working tree is not clean:" + git status --short + echo + echo "Commit or stash first - a push of a dirty tree pushes the LAST commit," + echo "not what you are looking at." + exit 1 +fi + +if [ ! -r "$SECRETS" ]; then + echo "ERROR: cannot read $SECRETS" >&2 + echo "Set SHOPDB_SECRETS to point at it." >&2 + exit 1 +fi + +# The proudtech.net section, not the localhost one above it. +TOKEN="$(sed -n '/Gitea API (proudtech.net)/,/^## /p' "$SECRETS" \ + | grep -i 'token' \ + | sed 's/.*`\(.*\)`.*/\1/' \ + | head -1)" + +if [ -z "$TOKEN" ]; then + echo "ERROR: no token found in the proudtech.net section of $SECRETS" >&2 + exit 1 +fi + +# The askpass helper. Written to a file only this user can read, in a directory +# that is removed on exit however this script ends. +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +ASKPASS="$WORKDIR/askpass.sh" +umask 077 +cat > "$ASKPASS" < itself. +git update-ref "refs/remotes/origin/$BRANCH" "refs/heads/$BRANCH" + +echo "Pushed $BRANCH." diff --git a/tools/promote-to-main.sh b/tools/promote-to-main.sh new file mode 100755 index 00000000..aef79a66 --- /dev/null +++ b/tools/promote-to-main.sh @@ -0,0 +1,145 @@ +#!/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 + +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"