Files
Behavision/release.sh
Suriyakumarvijayanayagam 8f07dee048 The engine stopped updating, and said "installed" every time
The SSL fix shipped and did not reach the machine it was written for. That
log said so, one line above the tick:

  behavision is already installed with the same version as the provided
  wheel. Use --force-reinstall to force an installation of the wheel.
   [ok] Engine and dependencies installed

and the traceback below it still pointed at model_assets.py line 59,
urllib.request.urlretrieve - code the fix had deleted.

Two frozen literals caused it: version = "1.1.0" in pyproject.toml and
__version__ = "1.0.0" in behavision/__init__.py. They disagreed with each
other and neither tracked a release, so every release built
behavision-1.1.0-py3-none-any.whl and pip install --upgrade on a machine that
already had 1.1.0 is a no-op. The comment beside that call claimed the
opposite.

The shape of the damage is what makes it bad. The Go binaries - app, agent,
setup tool - are rebuilt every release and updated normally. So a shop PC ran
a current app supervising an engine several releases old, and nothing said
which: /api/health reported the model, the paths, the cameras and the gallery,
and no version at all.

- One version, in the package, read by pyproject through
  [tool.setuptools.dynamic]. In a checkout it reads 0.0.0+dev: a
  plausible-looking number on a developer's /api/health is worse than none.
- pip install --force-reinstall --no-deps <wheel>, after the ordinary
  --upgrade. --upgrade settles the dependencies; the second call guarantees our
  own code is the code in the folder. --no-deps keeps it cheap - forcing the
  dependencies too would re-download ~300 MB every run. A rebuild at an
  unchanged version is the ordinary case while developing, so this must not
  rely on the version moving.
- release.sh stamps the tag: v0.5.6-demo -> 0.5.6+demo, valid PEP 440. Into a
  copy of the line, reverted in a trap, so the tree is never left dirty.

/api/health reports version now. Without it there is no way to tell a shop PC
three releases behind from a current one, which is how this survived several
releases.

Reproduced end to end before fixing, against real wheels on Python 3.12: two
builds of the same version, --upgrade leaves the old code in place and prints
the same sentence the colleague's Mac printed, --force-reinstall --no-deps
replaces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-30 17:30:24 +05:30

136 lines
8.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build the Windows shop-PC package and publish it as a Gitea release.
#
# ./release.sh v0.4.2 build the Windows zip and publish
# PUBLISH=0 ./release.sh v0.4.2 build only
# MAC=1 ./release.sh v0.4.2 also build and attach the macOS zip
# DEMO_PACK=demo-cameras.enc ./release.sh v0.4.4-demo
# a demo build: the sealed bundle from
# behavision-demo-pack ships in engine-src,
# and setup asks for its unlock code
#
# The package is a SOURCE install: the Go binaries are cross-compiled here, the
# engine ships as a pure-Python wheel and behavision-setup.exe builds a venv on
# the shop PC. PyInstaller cannot cross-compile, so a frozen engine needs a
# Windows build machine we do not have; this is what lets a release happen
# from this Mac at all. Layout matches what behavision-setup expects and what
# INSTALL.txt describes.
set -euo pipefail
cd "$(dirname "$0")"
export PATH="$PATH:$HOME/go/bin:/opt/homebrew/bin"
TAG=${1:?usage: release.sh vX.Y.Z}
REPO_API=https://gitapp.workolik.com/api/v1/repos/Loyaly/Behavision
STAGE=dist/Behavision
ZIP="dist/Behavision-$TAG-windows-x64.zip"
MACZIP="dist/Behavision-$TAG-macos-arm64.zip"
MACSTAGE=dist/Behavision-mac
step() { printf '\n\033[1m%s\033[0m\n' "$*"; }
case "$(git describe --tags --always --dirty)" in *-dirty) echo "refusing to release uncommitted changes" >&2; exit 1;; esac
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
[ "$(git rev-parse "$TAG^{}")" = "$(git rev-parse HEAD)" ] || { echo "$TAG exists and is not HEAD" >&2; exit 1; }
fi
step "1. Desktop app (Wails, pure-Go Windows target)"
(cd desktop/frontend && npm run build >/dev/null)
rm -rf "$STAGE" && mkdir -p "$STAGE/engine-src"
# -tags desktop,production is what `wails build` passes; without them the
# binary starts, shows "Wails applications will not build without the correct
# build tags" and exits. Measured on the first Windows install of v0.4.4-demo.
(cd desktop && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -tags desktop,production \
-ldflags "-H windowsgui -s -w -X main.version=$TAG" -o "../$STAGE/Behavision.exe" .)
step "2. Agent and setup tool"
(cd agent && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath \
-ldflags "-s -w -X main.version=$TAG" -o "../$STAGE/behavision-agent.exe" . \
&& CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath \
-ldflags "-s -w -X main.version=$TAG" -o "../$STAGE/behavision-setup.exe" ./cmd/behavision-setup)
step "3. Engine source and wheel"
# The wheel is built with the checkout's own interpreter; requires-python is a
# statement about the SHOP PC, which setup enforces when it finds Python there.
# Stamp the tag into the wheel version. Without this every release built
# behavision-1.1.0-py3-none-any.whl, and `pip install --upgrade` on a machine
# that already had 1.1.0 is a no-op - so an engine fix reached nobody who had
# ever run setup, while the Go binaries beside it updated normally. Nothing
# reported a version either, so there was no way to tell a shop PC three
# releases behind from a current one.
#
# v0.5.6-demo -> 0.5.6+demo, which is valid PEP 440: a local segment takes
# alphanumerics and dots, never hyphens.
TMPVER=$(mktemp)
PEP440=$(printf '%s' "${TAG#v}" | sed 's/-/+/; s/[^0-9A-Za-z.+]/./g')
trap 'git checkout -- behavision/__init__.py 2>/dev/null || true; rm -f "$TMPVER"' EXIT
# A temp file rather than `sed -i`, whose argument handling differs between
# BSD and GNU - this script is run from a Mac today and that is not a reason
# to plant a portability trap in a release path.
sed "s/^__version__ = .*/__version__ = \"$PEP440\"/" behavision/__init__.py > "$TMPVER"
cat "$TMPVER" > behavision/__init__.py
.venv/bin/python -m pip wheel --no-deps --ignore-requires-python -q -w "$STAGE/engine-src" . 2>&1 | grep -v "DEPRECATION\|WARNING: Ignoring" || true
git checkout -- behavision/__init__.py
rm -f "$TMPVER"
trap - EXIT
echo " engine wheel version $PEP440"
ls "$STAGE"/engine-src/behavision-*.whl >/dev/null || { echo "wheel was not built" >&2; exit 1; }
cp pyproject.toml requirements.txt "$STAGE/engine-src/"
mkdir -p "$STAGE/engine-src/config" && cp config/default.yaml "$STAGE/engine-src/config/"
rsync -a --exclude '__pycache__' behavision/ "$STAGE/engine-src/behavision/"
cp installer/INSTALL.txt installer/run-with-lan-head-office.cmd "$STAGE/"
if [ -n "${DEMO_PACK:-}" ]; then
case "$TAG" in *-demo*) ;; *) echo "a DEMO_PACK build must be tagged -demo" >&2; exit 1;; esac
cp "$DEMO_PACK" "$STAGE/engine-src/demo-cameras.enc" && echo " demo bundle: $(basename "$DEMO_PACK")"
fi
step "4. Package"
rm -f "$ZIP" && (cd dist && zip -qr "$(basename "$ZIP")" Behavision) && ls -la "$ZIP" | awk '{print " " $5 " bytes " $9}'
unzip -l "$ZIP" | grep -E "Behavision\.exe|agent\.exe|setup\.exe|\.whl|INSTALL" | awk '{print " " $4}'
# --- macOS, same SOURCE-install shape as Windows -------------------------
#
# Worth stating because it is the reason this is cheap: the Windows package
# already ships a pure-Python WHEEL and builds a venv on the target machine,
# because PyInstaller cannot cross-compile. macOS needs nothing different -
# the same wheel, the same setup tool, a natively built .app instead of an
# .exe. No frozen engine, no 200 MB, no second packaging story.
#
# Two things it is NOT, and the release notes should say so:
# * not notarised. macOS blocks an unsigned download outright rather than
# warning like SmartScreen, so the first launch needs right-click > Open.
# Notarising needs an Apple Developer account.
# * arm64 only. Every Mac worth demoing on since 2020, and building a
# universal binary doubles the size for machines nobody here has.
if [ "${MAC:-0}" = "1" ]; then
step "5. macOS package"
command -v wails >/dev/null || { echo "wails CLI not found; go install github.com/wailsapp/wails/v2/cmd/wails@latest" >&2; exit 1; }
rm -rf "$MACSTAGE" && mkdir -p "$MACSTAGE/engine-src"
(cd desktop && wails build -platform darwin/arm64 -tags "desktop,production" -skipbindings >/dev/null)
cp -R desktop/build/bin/Behavision.app "$MACSTAGE/"
(cd agent && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath \
-ldflags "-s -w -X main.version=$TAG" -o "../$MACSTAGE/behavision-agent" . \
&& CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath \
-ldflags "-s -w -X main.version=$TAG" -o "../$MACSTAGE/behavision-setup" ./cmd/behavision-setup)
# The identical engine payload the Windows package carries.
cp -R "$STAGE/engine-src/." "$MACSTAGE/engine-src/"
rm -f "$MACZIP" && (cd dist && zip -qry "$(basename "$MACZIP")" Behavision-mac)
ls -la "$MACZIP" | awk '{print " " $5 " bytes " $9}'
fi
[ "${PUBLISH:-1}" = "1" ] || { echo "built, not published"; exit 0; }
step "6. Tag and publish"
git rev-parse -q --verify "refs/tags/$TAG" >/dev/null || git tag -a "$TAG" -m "$TAG"
git push -q origin "$TAG"
# The notes come from a file so they are reviewed, not typed into a shell.
NOTES=${NOTES:-dist/RELEASE-NOTES-$TAG.md}
[ -f "$NOTES" ] || { echo "write the release notes to $NOTES first" >&2; exit 1; }
# Same credential git pushes with; Gitea accepts it as Basic auth for the API.
CRED=$(printf 'protocol=https\nhost=gitapp.workolik.com\n' | git credential fill)
USER=$(printf '%s' "$CRED" | sed -n 's/^username=//p'); PASS=$(printf '%s' "$CRED" | sed -n 's/^password=//p')
BODY=$(python3 -c 'import json,sys;print(json.dumps({"tag_name":sys.argv[1],"name":sys.argv[2],"body":open(sys.argv[3]).read(),"prerelease":True}))' "$TAG" "$TAG — $(head -1 "$NOTES" | sed 's/^#* *//')" "$NOTES")
REL=$(curl -sS -u "$USER:$PASS" -H 'content-type: application/json' -d "$BODY" "$REPO_API/releases")
ID=$(printf '%s' "$REL" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
curl -sS -u "$USER:$PASS" -F "attachment=@$ZIP" "$REPO_API/releases/$ID/assets?name=$(basename "$ZIP")" >/dev/null
[ "${MAC:-0}" = "1" ] && curl -sS -u "$USER:$PASS" -F "attachment=@$MACZIP" "$REPO_API/releases/$ID/assets?name=$(basename "$MACZIP")" >/dev/null
echo " published: https://gitapp.workolik.com/Loyaly/Behavision/releases/tag/$TAG"