build: refuse to package from a branch that is behind main
Everyone branches off main and builds from their own branch. A branch that is behind main still builds and installs perfectly — it just packages a binary missing whatever landed on main meanwhile, and `rpm -U --force` overwrites the good one with it. Nothing reports an error; the release succeeds and the feature simply vanishes from the installed CLI. That is what happened today: a build from a stale checkout replaced /usr/bin/mcpctl with one that has no `statusline` command at all, months after the status line landed on main (`mcpctl statusline` -> "unknown command"). `check-main-sync.sh` fetches main, compares, and fails before any work happens, listing the commits the branch is missing and the merge that fixes it. Sourced by build-rpm.sh and build-deb.sh — both are run standalone, so neither can rely on the other having checked. A hard failure rather than a warning: a warning scrolls past in a build log, and the whole point is to stop before the artifact exists. MCPCTL_ALLOW_BEHIND_MAIN=1 is the escape hatch for a deliberate old-tree build; MCPCTL_BASE_BRANCH retargets the comparison. Offline it degrades to the last fetched origin/main, then a local main, saying which it used; outside a git checkout it skips. Verified: passes on this branch (up to date with main); against a synthetic ref one commit ahead, build-rpm.sh aborts with exit 1 before ensure_build_deps and before any compilation; the escape hatch bypasses it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019wUmrfkVQR6CKcYKxENq7k
This commit is contained in:
@@ -884,6 +884,24 @@ All pushed to `mysources.co.uk/michal/` registry.
|
|||||||
source .env && bash scripts/release.sh
|
source .env && bash scripts/release.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**The build refuses to run from a branch that is behind `main`.** Everyone
|
||||||
|
branches off main, so a stale branch still builds and installs cleanly — it just
|
||||||
|
ships a binary missing whatever landed on main meanwhile, and `rpm -U --force`
|
||||||
|
overwrites the good one with it. That happened on 2026-08-10: a build from a
|
||||||
|
stale checkout replaced `/usr/bin/mcpctl` with one that had no `statusline`
|
||||||
|
command, months after the status line landed. `scripts/check-main-sync.sh`
|
||||||
|
(sourced by `build-rpm.sh` and `build-deb.sh`) fetches `main`, compares, and
|
||||||
|
fails before any work happens, listing the commits you are missing.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git merge main # the fix
|
||||||
|
MCPCTL_ALLOW_BEHIND_MAIN=1 bash scripts/release.sh # deliberate old-tree build
|
||||||
|
MCPCTL_BASE_BRANCH=release-2.x bash scripts/build-rpm.sh # compare to another branch
|
||||||
|
```
|
||||||
|
|
||||||
|
Offline it falls back to the last fetched `origin/main`, then to a local `main`,
|
||||||
|
and says which it used; outside a git checkout it skips entirely.
|
||||||
|
|
||||||
Installs via nfpm:
|
Installs via nfpm:
|
||||||
- `/usr/bin/mcpctl` — CLI binary (bun compiled)
|
- `/usr/bin/mcpctl` — CLI binary (bun compiled)
|
||||||
- `/usr/bin/mcpctl-local` — Local proxy binary (bun compiled)
|
- `/usr/bin/mcpctl-local` — Local proxy binary (bun compiled)
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ source "$SCRIPT_DIR/arch-helper.sh"
|
|||||||
resolve_arch "${MCPCTL_TARGET_ARCH:-}"
|
resolve_arch "${MCPCTL_TARGET_ARCH:-}"
|
||||||
# Sets: NFPM_ARCH, BUN_TARGET, ARCH_SUFFIX
|
# Sets: NFPM_ARCH, BUN_TARGET, ARCH_SUFFIX
|
||||||
|
|
||||||
|
# Same guard as build-rpm.sh: this script is also run on its own, so it cannot
|
||||||
|
# rely on that one having checked.
|
||||||
|
source "$SCRIPT_DIR/check-main-sync.sh"
|
||||||
|
check_main_sync
|
||||||
|
|
||||||
# Check and install missing build dependencies
|
# Check and install missing build dependencies
|
||||||
source "$SCRIPT_DIR/ensure-deps.sh"
|
source "$SCRIPT_DIR/ensure-deps.sh"
|
||||||
ensure_build_deps
|
ensure_build_deps
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ source "$SCRIPT_DIR/arch-helper.sh"
|
|||||||
resolve_arch "${MCPCTL_TARGET_ARCH:-}"
|
resolve_arch "${MCPCTL_TARGET_ARCH:-}"
|
||||||
# Sets: NFPM_ARCH, BUN_TARGET, ARCH_SUFFIX
|
# Sets: NFPM_ARCH, BUN_TARGET, ARCH_SUFFIX
|
||||||
|
|
||||||
|
# Before anything expensive: a branch behind main packages a binary missing
|
||||||
|
# whatever landed there, and installing it silently downgrades the machine.
|
||||||
|
source "$SCRIPT_DIR/check-main-sync.sh"
|
||||||
|
check_main_sync
|
||||||
|
|
||||||
# Check and install missing build dependencies
|
# Check and install missing build dependencies
|
||||||
source "$SCRIPT_DIR/ensure-deps.sh"
|
source "$SCRIPT_DIR/ensure-deps.sh"
|
||||||
ensure_build_deps
|
ensure_build_deps
|
||||||
|
|||||||
99
scripts/check-main-sync.sh
Executable file
99
scripts/check-main-sync.sh
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/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=<name> 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/<remote>/<base>: 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user