Files
shopdb-flask/tools/git-push.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

110 lines
3.5 KiB
Bash
Executable File

#!/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" <<HELPER
#!/usr/bin/env bash
# Git calls this for the password. The token is in the environment, not in the
# script text, so it is not readable from the file itself.
echo "\$SHOPDB_GIT_TOKEN"
HELPER
chmod 700 "$ASKPASS"
echo "Pushing $BRANCH to $REMOTE_HOST ..."
set +e
SHOPDB_GIT_TOKEN="$TOKEN" \
GIT_ASKPASS="$ASKPASS" \
GIT_TERMINAL_PROMPT=0 \
git push "https://${USERNAME}@${REMOTE_HOST}/${REMOTE_PATH}" "$BRANCH"
STATUS=$?
set -e
if [ "$STATUS" -ne 0 ]; then
echo
echo "Push failed (exit $STATUS). Nothing was written to .git/config, so"
echo "there is no token to clean up."
exit "$STATUS"
fi
# Keep the local tracking ref honest, since we pushed by URL rather than by
# remote name and git will not have updated origin/<branch> itself.
git update-ref "refs/remotes/origin/$BRANCH" "refs/heads/$BRANCH"
echo "Pushed $BRANCH."