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.
This commit is contained in:
109
tools/git-push.sh
Executable file
109
tools/git-push.sh
Executable file
@@ -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" <<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."
|
||||||
145
tools/promote-to-main.sh
Executable file
145
tools/promote-to-main.sh
Executable file
@@ -0,0 +1,145 @@
|
|||||||
|
#!/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"
|
||||||
Reference in New Issue
Block a user