mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-06 18:39:47 +00:00
feat(deploy): install to the fixed path again (one TCC grant, verified rollback)
Versioned filenames fixed the per-path code-signature cache kill (exit 137) but made macOS TCC treat every build as a new app: each deploy re-prompted for Documents/media-library access, and while the prompt was unanswered every process touching those folders (model probes, session startup) blocked - visible as slow/timeout requests in the app. Go back to the fixed path ~/.hapi/bin/hapi (a real file) so TCC asks once and remembers it across deploys. Guard the signature-cache hazard with explicit verification: back up the installed binary to ~/.hapi/bin/backups/, install with a fresh mtime, prove it execs repeatedly (--version x3) plus codesign --verify, restart the hub / kickstart the runner, and restore the backup on any failure. - deploy-local.sh / deploy-remote.sh: fixed-path install, verify, rollback - prune-backups.sh replaces prune-versions.sh (keeps newest N backups, drops legacy versioned binaries) - AGENTS.md, docs/local-deployment.md and the hapi-deploy skill updated
This commit is contained in:
@@ -76,26 +76,35 @@ cd android && ./gradlew :core:protocol:test # Android protocol conformance
|
||||
|
||||
## Local binary deployment
|
||||
|
||||
Deploy with `scripts/deploy-local.sh [tag]` after `bun run build:single-exe`.
|
||||
The script signs with a pinned codesigning identity, copies to a new versioned
|
||||
filename, repoints the `~/.hapi/bin/hapi` symlink, restarts the hub, refreshes
|
||||
a running runner, and rolls back if `/health` fails.
|
||||
Deploy with `scripts/deploy-local.sh [tag]` after `bun run build:single-exe`;
|
||||
remote Macs: `scripts/deploy-remote.sh <ssh-target> [tag]`.
|
||||
|
||||
Two macOS rules the script enforces:
|
||||
The binary always installs to the **fixed path** `~/.hapi/bin/hapi` (a real
|
||||
file, no versioned filenames). Fixed path is deliberate: macOS TCC keys
|
||||
permission grants (Documents, media library, ...) to the executable path, so
|
||||
one stable path means the user grants access once and macOS remembers it
|
||||
across deploys. Versioned filenames re-prompted for every build - do not
|
||||
reintroduce them.
|
||||
|
||||
1. **Never overwrite `~/.hapi/bin/hapi` in place**, even with an atomic
|
||||
rename. macOS caches the Mach-O code signature by executable path/mtime;
|
||||
replacing that path can make the embedded signature disagree with the
|
||||
cached signature and kill every new process with `OS_REASON_CODESIGNING` /
|
||||
`embedded signature doesn't match attached signature` (often exit 137). A
|
||||
plain `cp` also loses the signed mtime. Always copy to a new versioned
|
||||
filename, keep the stable path as a symlink, and keep the previous
|
||||
versioned file for rollback (do not delete it while sessions are running).
|
||||
2. **Never ship an ad-hoc signature** (`--sign -`). Ad-hoc signatures have no
|
||||
stable identity, so macOS TCC treats every build as a new app and re-asks
|
||||
each protected permission (Documents, Downloads, media library, ...). Pin
|
||||
one Apple Development identity instead: SHA-1 in `~/.hapi/signing-identity`,
|
||||
override via `HAPI_SIGN_IDENTITY`, signed identifier `run.hapi.cli`.
|
||||
The fixed path has one hazard: the kernel caches the Mach-O signature per
|
||||
path, so overwriting it can kill new processes with
|
||||
`OS_REASON_CODESIGNING` / `embedded signature doesn't match attached
|
||||
signature` (often exit 137). The deploy scripts handle it:
|
||||
|
||||
1. back up the installed binary to `~/.hapi/bin/backups/` (newest
|
||||
`HAPI_KEEP_BACKUPS` kept, default 2; never exec from the backup dir),
|
||||
2. install the new file with a fresh mtime so the path-keyed signature cache
|
||||
re-reads it,
|
||||
3. prove it execs repeatedly (`hapi --version` x3) and `codesign --verify`,
|
||||
4. restart the hub (local) / kickstart the launchd job (remote) and verify
|
||||
(`/health` locally; runner state + exec path on the remote),
|
||||
5. on any failure restore the backup onto the fixed path and restart.
|
||||
|
||||
Never leave an unverified binary installed. Never ship an ad-hoc signature
|
||||
(`--sign -`): ad-hoc signatures have no stable identity, so macOS TCC treats
|
||||
every build as a new app and re-asks each protected permission. Pin one Apple
|
||||
Development identity instead: SHA-1 in `~/.hapi/signing-identity`, override
|
||||
via `HAPI_SIGN_IDENTITY`, signed identifier `run.hapi.cli`.
|
||||
|
||||
The runner must be refreshed on every deploy (`hapi runner start` replaces the
|
||||
stale one; running sessions survive). Compiled binaries never self-update: the
|
||||
@@ -104,9 +113,9 @@ fixed for the life of the process. A stale runner keeps old machine RPCs and
|
||||
capability flags, so hub features can fail with "restart the runner" errors.
|
||||
|
||||
Remote Macs with the same layout: `scripts/deploy-remote.sh <ssh-target> [tag]`
|
||||
(signs locally, copies to a new versioned file, swaps the symlink, restarts the
|
||||
launchd job; label defaults to `com.hapi.runner`, override with
|
||||
`HAPI_REMOTE_LAUNCHD_LABEL`).
|
||||
(signs locally, uploads next to the fixed path, installs by move, verifies,
|
||||
then kickstarts the launchd job; label defaults to `com.hapi.runner`, override
|
||||
with `HAPI_REMOTE_LAUNCHD_LABEL`).
|
||||
|
||||
Agent sessions must not run recursive `$HOME` sweeps (`find ~`, `du -sh ~`)
|
||||
without pruning TCC-protected folders (`~/Music`, `~/Pictures`, `~/Movies`,
|
||||
@@ -114,8 +123,8 @@ without pruning TCC-protected folders (`~/Music`, `~/Pictures`, `~/Movies`,
|
||||
Apple Music prompt attributed to the hapi binary.
|
||||
|
||||
After deploy: `curl -fsS http://127.0.0.1:3006/health`, `~/.hapi/bin/hapi
|
||||
--version`, `hapi runner status`. If health fails, restore the previous
|
||||
symlink before doing anything else.
|
||||
--version`, `hapi runner status`. If health fails, restore the backup from
|
||||
`~/.hapi/bin/backups/` before doing anything else.
|
||||
|
||||
`docs/local-deployment.md` contains the same rationale and rollback checklist.
|
||||
|
||||
|
||||
+72
-67
@@ -46,39 +46,60 @@ bun run build:single-exe
|
||||
scripts/deploy-local.sh [tag]
|
||||
```
|
||||
|
||||
The script implements the sequence below, including the runner refresh; read
|
||||
on to understand the macOS constraints it works around.
|
||||
Remote Macs with the same layout:
|
||||
|
||||
### Code-signature cache (never overwrite the stable path)
|
||||
```bash
|
||||
scripts/deploy-remote.sh <ssh-target> [tag] # e.g. scripts/deploy-remote.sh k2lab card-dedupe
|
||||
```
|
||||
|
||||
macOS caches the Mach-O signature against the executable pathname and
|
||||
modification time. Replacing `~/.hapi/bin/hapi` in place (including a
|
||||
temp-file + rename) or using a plain `cp` can leave a stale code-signature
|
||||
cache. The next launch then fails with:
|
||||
### Fixed install path (why)
|
||||
|
||||
The binary always installs to the **fixed path** `~/.hapi/bin/hapi` (a real
|
||||
file). macOS TCC keys permission grants (Documents, Downloads, Apple
|
||||
Music/media library, ...) to the executable path, so one stable path means the
|
||||
user grants access **once** and macOS remembers it across deploys. The earlier
|
||||
"new versioned filename per build + symlink" scheme re-prompted for every
|
||||
build: each deploy looked like a brand-new app, and while the prompt was
|
||||
unanswered every process touching a protected folder (model probes, session
|
||||
startup) blocked - which showed up as slow/timeout requests in the app. Do not
|
||||
reintroduce versioned filenames.
|
||||
|
||||
### Code-signature cache (why deploys verify, and roll back)
|
||||
|
||||
The fixed path has one hazard: the kernel caches the Mach-O signature per
|
||||
path, so overwriting it can kill new processes with:
|
||||
|
||||
```text
|
||||
OS_REASON_CODESIGNING
|
||||
embedded signature doesn't match attached signature
|
||||
```
|
||||
|
||||
This is a deployment/install issue, not a HAPI application error. Use a fresh
|
||||
versioned path for every build and keep the stable command path as a symlink.
|
||||
Do not deploy by copying over the stable path.
|
||||
This is a deployment/install issue, not a HAPI application error. The deploy
|
||||
scripts handle it end to end:
|
||||
|
||||
### Sign with a stable identity (TCC)
|
||||
1. back up the installed binary to `~/.hapi/bin/backups/` (newest
|
||||
`HAPI_KEEP_BACKUPS` kept, default 2; never exec from the backup dir)
|
||||
2. install the new file with a fresh mtime so the path-keyed signature cache
|
||||
re-reads it
|
||||
3. prove it execs repeatedly (`hapi --version` x3) and `codesign --verify`
|
||||
4. restart the hub (local) / kickstart the launchd job (remote) and verify
|
||||
(`/health` locally; runner state + exec path on the remote)
|
||||
5. on any failure restore the backup onto the fixed path and restart
|
||||
|
||||
macOS TCC records permission grants against the code-signing identity. An
|
||||
ad-hoc signature (`codesign --sign -`) has no stable identity, so each rebuild
|
||||
looks like a brand-new app and every protected permission (Documents,
|
||||
Downloads, Apple Music/media library, ...) is asked again. Sign every build
|
||||
with one pinned identity:
|
||||
Never leave an unverified binary installed.
|
||||
|
||||
- `scripts/deploy-local.sh` stores the chosen Apple Development SHA-1 in
|
||||
`~/.hapi/signing-identity` and reuses it. Override with
|
||||
`HAPI_SIGN_IDENTITY` when rotating certificates.
|
||||
- All builds use the signed identifier `run.hapi.cli`.
|
||||
- With no Apple Development identity the script falls back to ad-hoc; that
|
||||
still works, but expect TCC prompts to reappear after every deploy.
|
||||
### Signing
|
||||
|
||||
Sign every build with one pinned identity; never ship an ad-hoc signature
|
||||
(`codesign --sign -`), which has no stable identity and makes TCC re-ask every
|
||||
permission:
|
||||
|
||||
- `scripts/sign-build.sh` (called by both deploy scripts) stores the chosen
|
||||
Apple Development SHA-1 in `~/.hapi/signing-identity` and reuses it; override
|
||||
with `HAPI_SIGN_IDENTITY` when rotating certificates
|
||||
- all builds use the signed identifier `run.hapi.cli`
|
||||
- with no Apple Development identity the scripts fall back to ad-hoc; that
|
||||
still runs, but expect TCC prompts to reappear after every deploy
|
||||
|
||||
Agent sessions should also avoid recursive `$HOME` sweeps (`find ~`,
|
||||
`du -sh ~`, ...) unless they prune TCC-protected folders (`~/Music`,
|
||||
@@ -99,74 +120,58 @@ runner's own code stays old until the process restarts:
|
||||
|
||||
`scripts/deploy-local.sh` runs `hapi runner start` when a runner is already
|
||||
running; the CLI stops the stale runner and starts a fresh one with
|
||||
`HAPI_CLI_EXECUTABLE` pinned to the stable symlink. Running sessions are
|
||||
detached and survive the restart.
|
||||
`HAPI_CLI_EXECUTABLE` pinned to the fixed path. Running sessions are detached
|
||||
and survive the restart.
|
||||
|
||||
### Manual sequence
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
build=cli/dist-exe/bun-darwin-arm64/hapi
|
||||
bin_dir="$HOME/.hapi/bin"
|
||||
stable="$bin_dir/hapi"
|
||||
stamp=$(date +%Y%m%d-%H%M%S)
|
||||
release="$HOME/.hapi/bin/hapi.$stamp"
|
||||
|
||||
identity=$(cat "$HOME/.hapi/signing-identity") # SHA-1, or a unique cert name
|
||||
# Sign with the pinned identity (see scripts/sign-build.sh).
|
||||
bash scripts/sign-build.sh "$build"
|
||||
|
||||
# Bun's linker signature is not suitable after installation; re-sign once.
|
||||
codesign --remove-signature "$build" 2>/dev/null || true
|
||||
codesign --force --sign "$identity" --identifier run.hapi.cli "$build"
|
||||
codesign --verify --deep --strict "$build"
|
||||
# Back up the installed binary for rollback.
|
||||
mkdir -p "$bin_dir/backups"
|
||||
backup="$bin_dir/backups/hapi.$stamp"
|
||||
[ -f "$stable" ] && cp -p "$stable" "$backup"
|
||||
|
||||
# -p preserves the mtime covered by the code-signature cache.
|
||||
cp -p "$build" "$release"
|
||||
codesign --verify --deep --strict "$release"
|
||||
"$release" --version
|
||||
|
||||
# Keep the old target as a rollback point. If hapi is already a symlink,
|
||||
# replace only the link; otherwise move the legacy regular file aside first.
|
||||
stable="$HOME/.hapi/bin/hapi"
|
||||
if [ -L "$stable" ]; then
|
||||
old_target=$(readlink "$stable")
|
||||
else
|
||||
old_target="hapi.bak.$stamp"
|
||||
mv "$stable" "$HOME/.hapi/bin/$old_target"
|
||||
fi
|
||||
ln -sfn "$(basename "$release")" "$stable"
|
||||
# Install to the fixed path with a fresh mtime, then prove it execs.
|
||||
rm -f "$stable"
|
||||
cp "$build" "$stable"
|
||||
chmod 755 "$stable"
|
||||
"$stable" --version && "$stable" --version && "$stable" --version
|
||||
codesign --verify --deep --strict "$stable"
|
||||
|
||||
launchctl kickstart -k "gui/$(id -u)/com.hapi.hub"
|
||||
sleep 2
|
||||
curl -fsS http://127.0.0.1:3006/health >/dev/null
|
||||
"$stable" --version
|
||||
|
||||
# Refresh a running runner; new sessions already use the new binary via the
|
||||
# symlink, but the runner's own machine RPCs/capabilities stay stale.
|
||||
# Refresh a running runner so its machine RPCs/capabilities match.
|
||||
HAPI_CLI_EXECUTABLE="$stable" "$stable" runner start
|
||||
```
|
||||
|
||||
If the health check fails, immediately restore the prior link and restart the
|
||||
agent:
|
||||
If the health check or a cold start fails, restore the backup and restart:
|
||||
|
||||
```bash
|
||||
ln -sfn "$old_target" "$HOME/.hapi/bin/hapi"
|
||||
rm -f "$stable"
|
||||
cp "$backup" "$stable"
|
||||
chmod 755 "$stable"
|
||||
"$stable" --version
|
||||
launchctl kickstart -k "gui/$(id -u)/com.hapi.hub"
|
||||
```
|
||||
|
||||
Never use `cp`, `mv`, or `codesign` on the stable symlink target after the
|
||||
launch agent has been started. Keep versioned binaries until the replacement
|
||||
has been running and verified.
|
||||
|
||||
### Remote machines
|
||||
|
||||
Macs that run the same layout (versioned binary + `~/.hapi/bin/hapi` symlink +
|
||||
a supervised runner) are updated from here over SSH:
|
||||
|
||||
```bash
|
||||
scripts/deploy-remote.sh <ssh-target> [tag] # e.g. scripts/deploy-remote.sh k2lab stable-signing
|
||||
```
|
||||
|
||||
The script signs the build with the same pinned identity, copies it to a new
|
||||
versioned file, swaps the symlink, restarts the launchd job (`com.hapi.runner`
|
||||
by default; override with `HAPI_REMOTE_LAUNCHD_LABEL`), and verifies the runner
|
||||
executes the new file. Running sessions survive; roll back by restoring the
|
||||
previous symlink and kicking the job again. The remote host needs no build
|
||||
toolchain — it only receives the signed binary.
|
||||
`scripts/deploy-remote.sh <ssh-target> [tag]` performs the same flow over
|
||||
SSH: signs locally, checks the remote arch matches, uploads next to the fixed
|
||||
path, installs by move (fresh inode + mtime), proves the binary execs, then
|
||||
kickstarts the launchd job (`com.hapi.runner` by default; override with
|
||||
`HAPI_REMOTE_LAUNCHD_LABEL`) and verifies the runner executes the fixed path.
|
||||
On any failure it restores the backup and kicks the job again. Running
|
||||
sessions survive; the remote host needs no build toolchain.
|
||||
|
||||
+50
-30
@@ -1,6 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Deploy the freshly built all-in-one binary following docs/local-deployment.md.
|
||||
# Deploy the freshly built all-in-one binary to the fixed path ~/.hapi/bin/hapi.
|
||||
# Usage: scripts/deploy-local.sh [tag]
|
||||
#
|
||||
# Fixed path is deliberate: macOS TCC keys permission grants to the executable
|
||||
# path, so a stable path means Documents / media-library access is granted once
|
||||
# and remembered across deploys (versioned filenames re-prompted every build).
|
||||
#
|
||||
# At the same time macOS caches the Mach-O signature per path, so overwriting
|
||||
# the path can kill new processes (exit 137). This script therefore installs
|
||||
# with a fresh mtime, proves the new binary execs repeatedly, and rolls back
|
||||
# from ~/.hapi/bin/backups on any failure.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
@@ -8,55 +17,64 @@ cd "$(dirname "$0")/.."
|
||||
build=cli/dist-exe/bun-darwin-arm64/hapi
|
||||
bin_dir="$HOME/.hapi/bin"
|
||||
stable="$bin_dir/hapi"
|
||||
backup_dir="$bin_dir/backups"
|
||||
stamp=$(date +%Y%m%d-%H%M%S)
|
||||
tag=${1:-}
|
||||
release="$bin_dir/hapi.$stamp${tag:+-$tag}"
|
||||
|
||||
if [ ! -x "$build" ]; then
|
||||
echo "error: build missing at $build; run 'bun run build:single-exe' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$bin_dir"
|
||||
mkdir -p "$bin_dir" "$backup_dir"
|
||||
|
||||
bash scripts/sign-build.sh "$build"
|
||||
|
||||
# -p preserves the mtime covered by the code-signature cache.
|
||||
cp -p "$build" "$release"
|
||||
codesign --verify --deep --strict "$release"
|
||||
"$release" --version
|
||||
|
||||
if [ -L "$stable" ]; then
|
||||
old_target=$(readlink "$stable")
|
||||
elif [ -e "$stable" ]; then
|
||||
old_target="hapi.bak.$stamp"
|
||||
mv "$stable" "$bin_dir/$old_target"
|
||||
else
|
||||
old_target=""
|
||||
# Rollback copy of the currently installed binary. Never exec from here;
|
||||
# rollback restores it onto the fixed path instead.
|
||||
backup=""
|
||||
if [ -f "$stable" ]; then
|
||||
backup="$backup_dir/hapi.$stamp${tag:+-$tag}"
|
||||
cp -p "$stable" "$backup"
|
||||
echo "backup: $backup"
|
||||
fi
|
||||
ln -sfn "$(basename "$release")" "$stable"
|
||||
echo "stable link: $stable -> $(readlink "$stable")"
|
||||
echo "rollback target: ${old_target:-none}"
|
||||
|
||||
restore_backup() {
|
||||
if [ -z "$backup" ]; then
|
||||
echo "no backup to restore" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "restoring $backup" >&2
|
||||
rm -f "$stable"
|
||||
cp "$backup" "$stable"
|
||||
chmod 755 "$stable"
|
||||
"$stable" --version
|
||||
}
|
||||
|
||||
# Install to the fixed path with a fresh mtime (invalidates the path-keyed
|
||||
# signature cache), then prove it execs repeatedly.
|
||||
rm -f "$stable"
|
||||
cp "$build" "$stable"
|
||||
chmod 755 "$stable"
|
||||
"$stable" --version
|
||||
"$stable" --version
|
||||
"$stable" --version
|
||||
codesign --verify --deep --strict "$stable"
|
||||
echo "installed: $stable"
|
||||
|
||||
launchctl kickstart -k "gui/$(id -u)/com.hapi.hub"
|
||||
sleep 2
|
||||
if ! curl -fsS http://127.0.0.1:3006/health >/dev/null; then
|
||||
echo "health check failed; restoring ${old_target:-none}" >&2
|
||||
if [ -n "$old_target" ]; then
|
||||
ln -sfn "$old_target" "$stable"
|
||||
else
|
||||
rm -f "$stable"
|
||||
fi
|
||||
echo "health check failed" >&2
|
||||
restore_backup || true
|
||||
launchctl kickstart -k "gui/$(id -u)/com.hapi.hub"
|
||||
exit 1
|
||||
fi
|
||||
echo "health ok"
|
||||
"$stable" --version
|
||||
|
||||
# Refresh the runner so its machine RPCs/capabilities match the new binary.
|
||||
# Compiled binaries never self-update after a deploy: the heartbeat mtime check
|
||||
# compares against the runner's own resolved exec path, which never changes.
|
||||
# `runner start` stops the stale runner first; running sessions are unaffected.
|
||||
# Compiled binaries never self-update: the heartbeat mtime check compares the
|
||||
# runner's own resolved exec path, which is fixed for the life of the process.
|
||||
runner_state="$HOME/.hapi/runner.state.json"
|
||||
runner_pid=$(sed -n 's/.*"pid": *\([0-9][0-9]*\).*/\1/p' "$runner_state" 2>/dev/null | head -n 1 || true)
|
||||
if [ -n "${runner_pid:-}" ] && kill -0 "$runner_pid" 2>/dev/null; then
|
||||
@@ -70,5 +88,7 @@ else
|
||||
echo "runner not running; skipped refresh"
|
||||
fi
|
||||
|
||||
# Keep disk usage bounded: current + previous version (override HAPI_KEEP_VERSIONS).
|
||||
bash scripts/prune-versions.sh "${HAPI_KEEP_VERSIONS:-2}" "$bin_dir"
|
||||
# Keep disk usage bounded: newest N backups (default 2), drop legacy versioned files.
|
||||
bash scripts/prune-backups.sh "${HAPI_KEEP_BACKUPS:-2}" "$bin_dir"
|
||||
|
||||
echo "rollback: rm -f '$stable' && cp '${backup:-<backup>}' '$stable' && chmod 755 '$stable' && launchctl kickstart -k gui/$(id -u)/com.hapi.hub"
|
||||
|
||||
+56
-34
@@ -1,12 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Deploy the built-and-signed all-in-one binary to a remote Mac over SSH.
|
||||
# Usage: scripts/deploy-remote.sh <ssh-target> [tag]
|
||||
# e.g. scripts/deploy-remote.sh k2lab stable-signing
|
||||
# e.g. scripts/deploy-remote.sh k2lab card-dedupe
|
||||
#
|
||||
# The remote host must use this repo's deployment layout: a versioned binary
|
||||
# under ~/.hapi/bin/ with the stable `hapi` symlink, supervised by a launchd
|
||||
# job (default com.hapi.runner; override with HAPI_REMOTE_LAUNCHD_LABEL).
|
||||
# Running sessions survive the restart; new sessions use the new binary.
|
||||
# The remote host uses the same fixed-path layout as this repo:
|
||||
# ~/.hapi/bin/hapi (real file) supervised by a launchd job
|
||||
# (default com.hapi.runner; override with HAPI_REMOTE_LAUNCHD_LABEL).
|
||||
#
|
||||
# Fixed path keeps macOS TCC grants stable (granted once, remembered across
|
||||
# deploys). To stay safe against the per-path code-signature cache the script
|
||||
# backs up the installed binary, installs with a fresh mtime, proves the new
|
||||
# binary execs repeatedly, and rolls back on any failure.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
@@ -21,7 +25,6 @@ fi
|
||||
label=${HAPI_REMOTE_LAUNCHD_LABEL:-com.hapi.runner}
|
||||
build=cli/dist-exe/bun-darwin-arm64/hapi
|
||||
stamp=$(date +%Y%m%d-%H%M%S)
|
||||
release="hapi.$stamp${tag:+-$tag}"
|
||||
ssh_opts=(-o BatchMode=yes -o ConnectTimeout=10)
|
||||
|
||||
if [ ! -x "$build" ]; then
|
||||
@@ -29,9 +32,7 @@ if [ ! -x "$build" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sign locally with the pinned identity: the remote TCC database keys grants
|
||||
# to the signing identity, so shipping an ad-hoc build there re-triggers
|
||||
# permission prompts after every deploy.
|
||||
# Sign locally with the pinned identity so the remote keeps a stable signer.
|
||||
bash scripts/sign-build.sh "$build"
|
||||
|
||||
local_arch=$(uname -m)
|
||||
@@ -43,40 +44,59 @@ fi
|
||||
remote_home=$(ssh "${ssh_opts[@]}" "$target" 'printf %s "$HOME"')
|
||||
|
||||
echo "target: $target ($remote_arch)"
|
||||
echo "release: $release"
|
||||
echo "release: $stamp${tag:+-$tag}"
|
||||
|
||||
# Copy to a NEW versioned filename: macOS caches Mach-O signatures by path,
|
||||
# so never overwrite an existing executable path.
|
||||
ssh "${ssh_opts[@]}" "$target" "mkdir -p '$remote_home/.hapi/bin'"
|
||||
scp -q -C "${ssh_opts[@]}" "$build" "$target:$remote_home/.hapi/bin/$release"
|
||||
# Upload next to the fixed path, then install by move (fresh inode + mtime).
|
||||
ssh "${ssh_opts[@]}" "$target" "mkdir -p '$remote_home/.hapi/bin/backups'"
|
||||
scp -q -C "${ssh_opts[@]}" "$build" "$target:$remote_home/.hapi/bin/.hapi.incoming"
|
||||
|
||||
ssh "${ssh_opts[@]}" "$target" "STAMP='$stamp' TAG='$tag' LABEL='$label' bash -s" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
|
||||
bin_dir="$HOME/.hapi/bin"
|
||||
stable="$bin_dir/hapi"
|
||||
new_name="hapi.$STAMP${TAG:+-$TAG}"
|
||||
new="$bin_dir/$new_name"
|
||||
backup_dir="$bin_dir/backups"
|
||||
incoming="$bin_dir/.hapi.incoming"
|
||||
|
||||
if [ ! -x "$new" ]; then
|
||||
echo "error: uploaded binary missing at $new" >&2
|
||||
if [ ! -x "$incoming" ]; then
|
||||
echo "error: uploaded binary missing at $incoming" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
codesign --verify --deep --strict "$new"
|
||||
"$new" --version
|
||||
backup=""
|
||||
if [ -f "$stable" ]; then
|
||||
backup="$backup_dir/hapi.$STAMP${TAG:+-$TAG}"
|
||||
cp -p "$stable" "$backup"
|
||||
echo "backup: $backup"
|
||||
fi
|
||||
|
||||
restore_backup() {
|
||||
if [ -z "$backup" ]; then
|
||||
echo "no backup to restore" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "restoring $backup" >&2
|
||||
rm -f "$stable"
|
||||
cp "$backup" "$stable"
|
||||
chmod 755 "$stable"
|
||||
"$stable" --version
|
||||
}
|
||||
|
||||
rm -f "$stable"
|
||||
mv "$incoming" "$stable"
|
||||
chmod 755 "$stable"
|
||||
"$stable" --version
|
||||
"$stable" --version
|
||||
"$stable" --version
|
||||
codesign --verify --deep --strict "$stable"
|
||||
echo "installed: $stable"
|
||||
|
||||
if ! launchctl print "gui/$(id -u)/$LABEL" >/dev/null 2>&1; then
|
||||
echo "error: launchd job $LABEL is not loaded on this host" >&2
|
||||
restore_backup || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
old_target=""
|
||||
if [ -L "$stable" ]; then
|
||||
old_target=$(readlink "$stable")
|
||||
fi
|
||||
|
||||
ln -sfn "$new_name" "$stable"
|
||||
launchctl kickstart -k "gui/$(id -u)/$LABEL"
|
||||
|
||||
runner_pid=""
|
||||
@@ -93,21 +113,23 @@ for _ in $(seq 1 15); do
|
||||
runner_pid=""
|
||||
done
|
||||
|
||||
echo "link: $(readlink "$stable")"
|
||||
echo "rollback target: ${old_target:-none}"
|
||||
if [ -z "$runner_pid" ]; then
|
||||
echo "warning: runner not up yet; check 'launchctl print gui/$(id -u)/$LABEL'" >&2
|
||||
echo "warning: runner did not come up" >&2
|
||||
if restore_backup; then
|
||||
launchctl kickstart -k "gui/$(id -u)/$LABEL"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "runner pid: $runner_pid"
|
||||
echo "runner exe: $runner_exe"
|
||||
case "$runner_exe" in
|
||||
*"$new_name"*) echo "remote deploy ok" ;;
|
||||
*) echo "warning: runner executable is not $new_name; it will switch on the next restart" >&2 ;;
|
||||
"$stable") echo "remote deploy ok" ;;
|
||||
*) echo "warning: runner executable is $runner_exe, expected $stable" >&2 ;;
|
||||
esac
|
||||
REMOTE
|
||||
|
||||
# Keep disk usage bounded on the remote host: current + previous version.
|
||||
ssh "${ssh_opts[@]}" "$target" "HAPI_KEEP_VERSIONS='${HAPI_KEEP_VERSIONS:-2}' bash -s" < scripts/prune-versions.sh
|
||||
# Keep remote disk usage bounded: newest N backups, drop legacy versioned files.
|
||||
ssh "${ssh_opts[@]}" "$target" "HAPI_KEEP_BACKUPS='${HAPI_KEEP_BACKUPS:-2}' bash -s" < scripts/prune-backups.sh
|
||||
|
||||
echo "rollback: ssh $target \"ln -sfn <old target> ~/.hapi/bin/hapi && launchctl kickstart -k gui/\\\$(id -u)/$label\""
|
||||
echo "rollback: ssh $target \"rm -f ~/.hapi/bin/hapi && cp <backup> ~/.hapi/bin/hapi && chmod 755 ~/.hapi/bin/hapi && launchctl kickstart -k gui/\\\$(id -u)/$label\""
|
||||
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# Keep disk usage bounded for the fixed-path deployment:
|
||||
# - keep the newest N backup copies in <bin-dir>/backups (default 2)
|
||||
# - remove legacy versioned binaries (hapi.YYYYMMDD-HHMMSS*) from the
|
||||
# pre-fixed-path era; the fixed path ~/.hapi/bin/hapi is never touched
|
||||
# Usage: scripts/prune-backups.sh [keep] [bin-dir]
|
||||
set -euo pipefail
|
||||
|
||||
keep=${1:-${HAPI_KEEP_BACKUPS:-2}}
|
||||
bin_dir=${2:-$HOME/.hapi/bin}
|
||||
backup_dir="$bin_dir/backups"
|
||||
|
||||
case "$keep" in
|
||||
''|*[!0-9]*)
|
||||
echo "error: keep must be a number >= 1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if [ "$keep" -lt 1 ]; then
|
||||
echo "error: keep must be >= 1" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$bin_dir" ]; then
|
||||
echo "prune: no $bin_dir, nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
freed=0
|
||||
|
||||
if [ -d "$backup_dir" ]; then
|
||||
count=0
|
||||
for backup in $(ls -1 "$backup_dir" 2>/dev/null | grep -E '^hapi\.' | sort -r || true); do
|
||||
count=$((count + 1))
|
||||
if [ "$count" -le "$keep" ]; then
|
||||
continue
|
||||
fi
|
||||
size=$(stat -f "%z" "$backup_dir/$backup" 2>/dev/null || echo 0)
|
||||
rm -f "$backup_dir/$backup"
|
||||
freed=$((freed + size))
|
||||
echo "pruned backup: $backup ($((size / 1048576))MB)"
|
||||
done
|
||||
echo "kept backups: $((count < keep ? count : keep))"
|
||||
fi
|
||||
|
||||
for version in $(ls -1 "$bin_dir" 2>/dev/null | grep -E '^hapi\.[0-9]{8}-[0-9]{6}' | sort -r || true); do
|
||||
size=$(stat -f "%z" "$bin_dir/$version" 2>/dev/null || echo 0)
|
||||
rm -f "$bin_dir/$version"
|
||||
freed=$((freed + size))
|
||||
echo "removed legacy: $version ($((size / 1048576))MB)"
|
||||
done
|
||||
|
||||
echo "freed: $((freed / 1048576))MB"
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Prune old versioned hapi binaries in a deployment bin dir, keeping the current
|
||||
# symlink target plus the N most recent previous versions (default 2 total).
|
||||
# Usage: scripts/prune-versions.sh [keep-count] [bin-dir]
|
||||
# keep-count defaults to $HAPI_KEEP_VERSIONS, then 2 (current + previous).
|
||||
# bin-dir defaults to $HOME/.hapi/bin.
|
||||
# Safe to run while sessions are live: unlinking a binary does not affect
|
||||
# running processes (they keep the open file), only future rollbacks.
|
||||
set -euo pipefail
|
||||
|
||||
keep=${1:-${HAPI_KEEP_VERSIONS:-2}}
|
||||
bin_dir=${2:-$HOME/.hapi/bin}
|
||||
|
||||
case "$keep" in
|
||||
''|*[!0-9]*)
|
||||
echo "error: keep-count must be a number >= 2" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if [ "$keep" -lt 2 ]; then
|
||||
echo "error: keep-count must be >= 2 (current + previous)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$bin_dir" ]; then
|
||||
echo "prune: no $bin_dir, nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
stable="$bin_dir/hapi"
|
||||
current=""
|
||||
if [ -L "$stable" ]; then
|
||||
current=$(readlink "$stable")
|
||||
fi
|
||||
|
||||
# Newest-first by filename: hapi.YYYYMMDD-HHMMSS[-tag] sorts chronologically.
|
||||
versions=$(ls -1 "$bin_dir" 2>/dev/null | grep -E '^hapi\.[0-9]{8}-[0-9]{6}' | sort -r || true)
|
||||
|
||||
kept=""
|
||||
count=0
|
||||
freed=0
|
||||
if [ -n "$current" ] && [ -f "$bin_dir/$current" ]; then
|
||||
kept="$current"
|
||||
count=1
|
||||
fi
|
||||
|
||||
for version in $versions; do
|
||||
[ "$version" = "$current" ] && continue
|
||||
if [ "$count" -lt "$keep" ]; then
|
||||
kept="$kept $version"
|
||||
count=$((count + 1))
|
||||
continue
|
||||
fi
|
||||
size=$(stat -f "%z" "$bin_dir/$version" 2>/dev/null || echo 0)
|
||||
rm -f "$bin_dir/$version"
|
||||
freed=$((freed + size))
|
||||
echo "pruned: $version ($((size / 1048576))MB)"
|
||||
done
|
||||
|
||||
echo "kept:${kept:- none} (count=$count)"
|
||||
echo "freed: $((freed / 1048576))MB"
|
||||
Reference in New Issue
Block a user