#!/bin/bash # Refuse to build a package from a branch that main has already moved past. # # WHY # # Everyone branches off main and builds from their own branch. A branch that is # behind main still builds and installs perfectly — it just quietly ships a # binary missing whatever landed on main in the meantime, and `rpm -U --force` # overwrites the good one with it. # # That is not hypothetical: on 2026-08-10 a build from a stale checkout replaced # /usr/bin/mcpctl with one that had no `statusline` command at all, months after # the status line landed on main. Nothing reported an error — the release # succeeded, the feature just vanished from the installed CLI. # # So this is a hard failure rather than a warning. A warning scrolls past in a # build log; the whole point is to stop before the artifact exists. # # ESCAPE HATCH # # MCPCTL_ALLOW_BEHIND_MAIN=1 build anyway (deliberate build of an old tree) # MCPCTL_BASE_BRANCH= compare against something other than main # # Skips itself entirely outside a git checkout, so tarball builds still work. # Resolve the ref to compare against, echoing it on stdout. Prefers a fresh # fetch; falls back to whatever is already on disk so an offline build is # degraded rather than blocked. Returns 1 when there is nothing to compare to. _main_sync_ref() { local base="$1" remote="$2" if [ -n "$remote" ] && git fetch --quiet "$remote" "$base" 2>/dev/null; then # FETCH_HEAD rather than refs/remotes//: it is what this fetch # just wrote, so it cannot be a stale opportunistic update. echo "FETCH_HEAD" return 0 fi if [ -n "$remote" ] && git rev-parse --verify --quiet "refs/remotes/$remote/$base" >/dev/null; then echo " (could not reach $remote — comparing against the last fetched $remote/$base)" >&2 echo "refs/remotes/$remote/$base" return 0 fi if git rev-parse --verify --quiet "refs/heads/$base" >/dev/null; then echo " (no reachable remote — comparing against local $base)" >&2 echo "refs/heads/$base" return 0 fi return 1 } check_main_sync() { local base="${MCPCTL_BASE_BRANCH:-main}" if ! git rev-parse --git-dir >/dev/null 2>&1; then return 0 # not a checkout; nothing to be behind fi if [ "${MCPCTL_ALLOW_BEHIND_MAIN:-}" = "1" ]; then echo "==> Skipping the '$base' sync check (MCPCTL_ALLOW_BEHIND_MAIN=1)" return 0 fi echo "==> Checking this branch is not behind '$base'..." local remote ref remote="$(git remote | head -1)" if ! ref="$(_main_sync_ref "$base" "$remote")"; then echo " (no '$base' branch found anywhere — skipping)" return 0 fi local behind behind="$(git rev-list --count "HEAD..$ref" 2>/dev/null || echo 0)" if [ "$behind" -eq 0 ]; then echo " up to date with $base" return 0 fi local branch branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)" echo "" >&2 echo "ERROR: '$branch' is $behind commit(s) behind $base — refusing to build." >&2 echo "" >&2 echo " Building now would package a binary without these, and installing it" >&2 echo " would overwrite a good one with a version missing them:" >&2 echo "" >&2 git log --oneline --no-decorate "HEAD..$ref" | head -15 | sed 's/^/ /' >&2 if [ "$behind" -gt 15 ]; then echo " … and $((behind - 15)) more" >&2 fi echo "" >&2 echo " Fix it: git merge $base # or: git rebase $base" >&2 echo " Anyway: MCPCTL_ALLOW_BEHIND_MAIN=1 $0" >&2 echo "" >&2 return 1 }