mirror of https://github.com/garrytan/gstack.git
Merge 8825d8eddb into a3259400a3
This commit is contained in:
commit
71d872b269
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
# Output (one line, or nothing):
|
||||
# JUST_UPGRADED <old> <new> — marker found from recent upgrade
|
||||
# UPGRADED <old> <new> — --apply fast-forwarded the install
|
||||
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
|
||||
# (nothing) — up to date, snoozed, disabled, or check skipped
|
||||
#
|
||||
|
|
@ -22,8 +23,22 @@ VERSION_FILE="$GSTACK_DIR/VERSION"
|
|||
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
|
||||
REMOTE_REPO="${GSTACK_REMOTE_REPO:-https://github.com/garrytan/gstack.git}"
|
||||
|
||||
FORCE=0
|
||||
APPLY=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--force) FORCE=1 ;;
|
||||
--apply) APPLY=1 ;;
|
||||
*)
|
||||
echo "gstack-update-check: unknown argument: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# ─── Force flag (busts cache + snooze for standalone /gstack-upgrade) ──
|
||||
if [ "${1:-}" = "--force" ]; then
|
||||
if [ "$FORCE" -eq 1 ]; then
|
||||
rm -f "$CACHE_FILE"
|
||||
rm -f "$SNOOZE_FILE"
|
||||
fi
|
||||
|
|
@ -101,6 +116,56 @@ check_snooze() {
|
|||
return 1 # snooze expired
|
||||
}
|
||||
|
||||
fail_apply() {
|
||||
echo "gstack-update-check: --apply failed: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
apply_upgrade() {
|
||||
local old="$1"
|
||||
local target="$2"
|
||||
local branch upstream after
|
||||
|
||||
git -C "$GSTACK_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
||||
|| fail_apply "$GSTACK_DIR is not a git checkout"
|
||||
|
||||
branch="$(git -C "$GSTACK_DIR" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
|
||||
[ -n "$branch" ] || fail_apply "$GSTACK_DIR is on a detached HEAD"
|
||||
|
||||
git -C "$GSTACK_DIR" diff --quiet --ignore-submodules -- \
|
||||
|| fail_apply "$GSTACK_DIR has uncommitted tracked changes"
|
||||
git -C "$GSTACK_DIR" diff --cached --quiet --ignore-submodules -- \
|
||||
|| fail_apply "$GSTACK_DIR has staged changes"
|
||||
|
||||
if ! GIT_TERMINAL_PROMPT=0 git -C "$GSTACK_DIR" fetch origin "$branch" >/dev/null 2>&1; then
|
||||
GIT_TERMINAL_PROMPT=0 git -C "$GSTACK_DIR" fetch origin main >/dev/null 2>&1 \
|
||||
|| fail_apply "could not fetch origin/$branch or origin/main"
|
||||
fi
|
||||
|
||||
upstream="origin/$branch"
|
||||
if ! git -C "$GSTACK_DIR" rev-parse --verify "$upstream" >/dev/null 2>&1; then
|
||||
upstream="origin/main"
|
||||
git -C "$GSTACK_DIR" rev-parse --verify "$upstream" >/dev/null 2>&1 \
|
||||
|| fail_apply "could not resolve origin/$branch or origin/main"
|
||||
fi
|
||||
|
||||
git -C "$GSTACK_DIR" merge --ff-only "$upstream" >/dev/null 2>&1 \
|
||||
|| fail_apply "could not fast-forward to $upstream"
|
||||
|
||||
after=""
|
||||
if [ -f "$VERSION_FILE" ]; then
|
||||
after="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')"
|
||||
fi
|
||||
[ -n "$after" ] || fail_apply "VERSION is missing after fast-forward"
|
||||
[ "$after" != "$old" ] || fail_apply "VERSION stayed at $old after fast-forward"
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
echo "$old" > "$MARKER_FILE"
|
||||
rm -f "$CACHE_FILE" "$SNOOZE_FILE"
|
||||
echo "UPGRADED $old $after"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ─── Step 1: Read local version ──────────────────────────────
|
||||
LOCAL=""
|
||||
if [ -f "$VERSION_FILE" ]; then
|
||||
|
|
@ -146,6 +211,9 @@ if [ -f "$CACHE_FILE" ]; then
|
|||
CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
|
||||
if [ "$CACHED_OLD" = "$LOCAL" ]; then
|
||||
CACHED_NEW="$(echo "$CACHED" | awk '{print $3}')"
|
||||
if [ "$APPLY" -eq 1 ]; then
|
||||
apply_upgrade "$LOCAL" "$CACHED_NEW"
|
||||
fi
|
||||
if check_snooze "$CACHED_NEW"; then
|
||||
exit 0 # snoozed — stay quiet
|
||||
fi
|
||||
|
|
@ -234,6 +302,9 @@ fi
|
|||
|
||||
# REMOTE is strictly newer — upgrade available
|
||||
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
|
||||
if [ "$APPLY" -eq 1 ]; then
|
||||
apply_upgrade "$LOCAL" "$REMOTE"
|
||||
fi
|
||||
if check_snooze "$REMOTE"; then
|
||||
exit 0 # snoozed — stay quiet
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
const ROOT = join(import.meta.dir, "..");
|
||||
const UPDATE_CHECK = join(ROOT, "bin", "gstack-update-check");
|
||||
|
||||
const cleanupDirs: string[] = [];
|
||||
|
||||
function sh(args: string[], cwd: string, env: NodeJS.ProcessEnv = process.env) {
|
||||
const result = spawnSync(args[0], args.slice(1), {
|
||||
cwd,
|
||||
env: {
|
||||
...env,
|
||||
GIT_AUTHOR_NAME: "Test",
|
||||
GIT_AUTHOR_EMAIL: "test@example.com",
|
||||
GIT_COMMITTER_NAME: "Test",
|
||||
GIT_COMMITTER_EMAIL: "test@example.com",
|
||||
},
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${args.join(" ")} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeInstallPair() {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "gstack-update-check-"));
|
||||
cleanupDirs.push(tmp);
|
||||
|
||||
const origin = join(tmp, "origin");
|
||||
const install = join(tmp, "install");
|
||||
const state = join(tmp, "state");
|
||||
mkdirSync(origin, { recursive: true });
|
||||
mkdirSync(join(origin, "bin"), { recursive: true });
|
||||
mkdirSync(state, { recursive: true });
|
||||
|
||||
writeFileSync(join(origin, "VERSION"), "1.0.0\n");
|
||||
writeFileSync(join(origin, "bin", "gstack-config"), "#!/bin/sh\nexit 0\n");
|
||||
chmodSync(join(origin, "bin", "gstack-config"), 0o755);
|
||||
|
||||
sh(["git", "init", "-q", "-b", "main"], origin);
|
||||
sh(["git", "add", "VERSION", "bin/gstack-config"], origin);
|
||||
sh(["git", "commit", "-q", "-m", "initial"], origin);
|
||||
sh(["git", "clone", "-q", origin, install], tmp);
|
||||
|
||||
writeFileSync(join(origin, "VERSION"), "1.0.1\n");
|
||||
sh(["git", "add", "VERSION"], origin);
|
||||
sh(["git", "commit", "-q", "-m", "release 1.0.1"], origin);
|
||||
|
||||
return { tmp, origin, install, state };
|
||||
}
|
||||
|
||||
function runUpdateCheck(repo: ReturnType<typeof makeInstallPair>, args: string[]) {
|
||||
return spawnSync("bash", [UPDATE_CHECK, ...args], {
|
||||
cwd: repo.install,
|
||||
encoding: "utf-8",
|
||||
timeout: 10_000,
|
||||
env: {
|
||||
...process.env,
|
||||
GSTACK_DIR: repo.install,
|
||||
GSTACK_STATE_DIR: repo.state,
|
||||
GSTACK_REMOTE_URL: `file://${join(repo.origin, "VERSION")}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of cleanupDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("gstack-update-check --apply", () => {
|
||||
test("report-only check leaves the install unchanged", () => {
|
||||
const repo = makeInstallPair();
|
||||
const result = runUpdateCheck(repo, ["--force"]);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout.trim()).toBe("UPGRADE_AVAILABLE 1.0.0 1.0.1");
|
||||
expect(readFileSync(join(repo.install, "VERSION"), "utf-8").trim()).toBe("1.0.0");
|
||||
});
|
||||
|
||||
test("fast-forwards the install and writes the just-upgraded marker", () => {
|
||||
const repo = makeInstallPair();
|
||||
const result = runUpdateCheck(repo, ["--force", "--apply"]);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
expect(result.stdout.trim()).toBe("UPGRADED 1.0.0 1.0.1");
|
||||
expect(readFileSync(join(repo.install, "VERSION"), "utf-8").trim()).toBe("1.0.1");
|
||||
expect(readFileSync(join(repo.state, "just-upgraded-from"), "utf-8").trim()).toBe("1.0.0");
|
||||
expect(existsSync(join(repo.state, "last-update-check"))).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue