mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 06:24:41 +01:00
A8: rename the artifacts repo with OWNER/REPO, rewrite the remote only after it
The v1.27.0.0 migration passed a bare repo name to gh repo rename / gh repo edit, which always fails (gh wants [HOST/]OWNER/REPO), then step 2 rewrote ~/.gstack-artifacts-remote.txt to the never-renamed repository. - Qualify both names with the owner from the recorded remote URL (else the signed-in gh user) and surface gh's error. - Step 2 moves the remote file but rewrites the URL only once the rename is confirmed; a later successful retry rewrites it then. - A pending journal now counts as state to migrate, so the #1383 retry actually re-runs step 1 (it used to see no legacy file and declare the migration done). - The v1.91.17.0 migration (placeholder name) repairs installs already hit: when the remote names gstack-artifacts-<user> that does not exist but gstack-brain-<user> does, it restores the old URL in the remote file and ~/.gstack's origin and says so. It only calls gh for such a remote and never fails the upgrade. Co-authored-by: Brandon Pugsley <aibpaem@gmail.com>
This commit is contained in:
1 parent
a05965fb0a
commit
c612f28339
5 files changed
+177
-14
No files matched your search
@@ -490,6 +490,27 @@ gstack-gbrain-install # upgrade gbrain
|
||||
|
||||
---
|
||||
|
||||
## `artifacts remote: restored <url> because <owner>/gstack-artifacts-<user> does not exist and <owner>/gstack-brain-<user> does`
|
||||
|
||||
**Problem.** Printed once during upgrade. Artifact pushes had been failing
|
||||
because `~/.gstack-artifacts-remote.txt` named a repository that was never
|
||||
created.
|
||||
|
||||
**Cause.** The v1.27.0.0 upgrade passed a bare repo name to
|
||||
`gh repo rename`, which always fails, then rewrote the remote to the new name
|
||||
anyway. This upgrade points the remote (and `~/.gstack`'s `origin`) back at
|
||||
the repository that exists.
|
||||
|
||||
**Fix.** Nothing is required; pushes work again. To finish the rename:
|
||||
```bash
|
||||
gh repo rename gstack-artifacts-<user> --repo <owner>/gstack-brain-<user> --yes
|
||||
GSTACK_MIGRATE_ASSUME_YES=1 bash ~/.claude/skills/gstack/gstack-upgrade/migrations/v1.27.0.0.sh
|
||||
```
|
||||
If it instead says it `could not confirm ... exists (gh is not installed or not
|
||||
signed in)`, run `gh auth login` and check the repository name on GitHub.
|
||||
|
||||
---
|
||||
|
||||
## Nothing is syncing but I expect it to
|
||||
|
||||
**Not an error, but a common gotcha.** Check in order:
|
||||
|
||||
@@ -95,6 +95,34 @@ mark_done() {
|
||||
echo "$step" >> "$JOURNAL"
|
||||
}
|
||||
|
||||
# The GitHub owner of the brain repo, from the recorded remote URL
|
||||
# (https://github.com/<owner>/<repo> or git@github.com:<owner>/<repo>), else
|
||||
# the authenticated gh user. `gh repo rename --repo` and `gh repo edit` need
|
||||
# the "[HOST/]OWNER/REPO" form; a bare name always fails (#1437).
|
||||
repo_owner() {
|
||||
local url="" owner=""
|
||||
for f in "$OLD_REMOTE_TXT" "$NEW_REMOTE_TXT"; do
|
||||
[ -f "$f" ] && url=$(head -1 "$f" 2>/dev/null) && break
|
||||
done
|
||||
owner=$(printf '%s\n' "$url" | sed -n 's#^.*github\.com[:/]\([^/]*\)/.*$#\1#p')
|
||||
[ -z "$owner" ] && owner=$(gh api user --jq .login 2>/dev/null || true)
|
||||
printf '%s' "$owner"
|
||||
}
|
||||
|
||||
# Point ~/.gstack-artifacts-remote.txt at the renamed repo. Only called once
|
||||
# the rename is confirmed: rewriting it earlier pointed the artifacts remote
|
||||
# at a repository that did not exist (#1437).
|
||||
rewrite_remote_to_new() {
|
||||
[ -f "$NEW_REMOTE_TXT" ] || return 0
|
||||
local url new
|
||||
url=$(head -1 "$NEW_REMOTE_TXT" 2>/dev/null)
|
||||
new=$(echo "$url" | sed "s|/${OLD_REPO_NAME}|/${NEW_REPO_NAME}|; s|:${OLD_REPO_NAME}|:${NEW_REPO_NAME}|")
|
||||
[ "$new" = "$url" ] && return 0
|
||||
echo "$new" > "$NEW_REMOTE_TXT"
|
||||
chmod 600 "$NEW_REMOTE_TXT"
|
||||
echo " remote URL rewritten: $url → $new" >&2
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detect environment + ask once if there's anything to migrate
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -105,6 +133,9 @@ mark_done() {
|
||||
HAS_LEGACY_STATE=0
|
||||
[ -f "$OLD_REMOTE_TXT" ] && HAS_LEGACY_STATE=1
|
||||
[ -d "$GSTACK_HOME/.git" ] && HAS_LEGACY_STATE=1
|
||||
# A journal means an earlier run left a step pending (step 2 already moved the
|
||||
# remote file), so the retry must run instead of declaring nothing to do.
|
||||
[ -f "$JOURNAL" ] && HAS_LEGACY_STATE=1
|
||||
|
||||
# If nothing to migrate, finalize silently.
|
||||
if [ "$HAS_LEGACY_STATE" = "0" ]; then
|
||||
@@ -217,19 +248,27 @@ if ! journal_done "gh_repo_renamed"; then
|
||||
case "$HOST" in
|
||||
github)
|
||||
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
||||
GH_OWNER=$(repo_owner)
|
||||
OLD_QUALIFIED="${GH_OWNER:+$GH_OWNER/}${OLD_REPO_NAME}"
|
||||
NEW_QUALIFIED="${GH_OWNER:+$GH_OWNER/}${NEW_REPO_NAME}"
|
||||
# Idempotent: if new name already exists, treat as success.
|
||||
if gh repo view "$NEW_REPO_NAME" >/dev/null 2>&1; then
|
||||
if gh repo view "$NEW_QUALIFIED" >/dev/null 2>&1; then
|
||||
echo " repo already named $NEW_REPO_NAME on GitHub — no-op" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
mark_done "gh_repo_rename_verified"
|
||||
rewrite_remote_to_new
|
||||
else
|
||||
if gh repo rename "$NEW_REPO_NAME" --repo "$OLD_REPO_NAME" --yes 2>/dev/null \
|
||||
|| gh repo edit "$OLD_REPO_NAME" --name "$NEW_REPO_NAME" 2>/dev/null; then
|
||||
echo " renamed on GitHub" >&2
|
||||
RENAME_ERR=""
|
||||
if RENAME_ERR=$(gh repo rename "$NEW_REPO_NAME" --repo "$OLD_QUALIFIED" --yes 2>&1) \
|
||||
|| RENAME_ERR=$(gh repo edit "$OLD_QUALIFIED" --name "$NEW_REPO_NAME" 2>&1); then
|
||||
echo " renamed $OLD_QUALIFIED → $NEW_REPO_NAME on GitHub" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
mark_done "gh_repo_rename_verified"
|
||||
rewrite_remote_to_new
|
||||
else
|
||||
echo " WARNING: gh rename failed (repo may not exist or permission denied)" >&2
|
||||
echo " WARNING: gh rename failed for $OLD_QUALIFIED: ${RENAME_ERR:-no output}" >&2
|
||||
echo " step 1 stays PENDING and will retry on re-run; later steps still run (#1383)" >&2
|
||||
echo " manual: gh repo rename $NEW_REPO_NAME --repo $OLD_REPO_NAME --yes" >&2
|
||||
echo " manual: gh repo rename $NEW_REPO_NAME --repo $OLD_QUALIFIED --yes" >&2
|
||||
fi
|
||||
fi
|
||||
else
|
||||
@@ -267,14 +306,18 @@ fi
|
||||
if ! journal_done "remote_txt_renamed"; then
|
||||
echo " [v1.27.0.0] step 2: rename ~/.gstack-brain-remote.txt → ~/.gstack-artifacts-remote.txt" >&2
|
||||
if [ -f "$OLD_REMOTE_TXT" ] && [ ! -f "$NEW_REMOTE_TXT" ]; then
|
||||
# Update the URL inside if the rename happened on the host: replace
|
||||
# gstack-brain-$USER with gstack-artifacts-$USER in the URL.
|
||||
# Move the file; rewrite the URL inside only when step 1 confirmed the
|
||||
# repository was renamed. Otherwise the old URL is the one that exists.
|
||||
OLD_URL=$(head -1 "$OLD_REMOTE_TXT" 2>/dev/null)
|
||||
NEW_URL=$(echo "$OLD_URL" | sed "s|/${OLD_REPO_NAME}|/${NEW_REPO_NAME}|; s|:${OLD_REPO_NAME}|:${NEW_REPO_NAME}|")
|
||||
echo "$NEW_URL" > "$NEW_REMOTE_TXT"
|
||||
echo "$OLD_URL" > "$NEW_REMOTE_TXT"
|
||||
chmod 600 "$NEW_REMOTE_TXT"
|
||||
rm -f "$OLD_REMOTE_TXT"
|
||||
echo " moved + URL rewritten: $OLD_URL → $NEW_URL" >&2
|
||||
if journal_done "gh_repo_rename_verified"; then
|
||||
echo " moved remote file" >&2
|
||||
rewrite_remote_to_new
|
||||
else
|
||||
echo " moved remote file; kept $OLD_URL because the repository was not renamed" >&2
|
||||
fi
|
||||
elif [ -f "$NEW_REMOTE_TXT" ]; then
|
||||
echo " new file already exists — no-op" >&2
|
||||
rm -f "$OLD_REMOTE_TXT" 2>/dev/null || true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v1.91.17.0 — severe fix wave (memory ingest).
|
||||
# Migration: v1.91.17.0 — severe fix wave (memory ingest, artifacts remote).
|
||||
# Placeholder name: the release queue may rename this file at /ship.
|
||||
#
|
||||
# Each step is independent and guarded. Idempotent and non-fatal: every path
|
||||
@@ -25,4 +25,29 @@ if [ -f "$GH/.transcript-ingest-state.json" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2 (A8, #1437): the v1.27.0.0 migration passed a bare repo name to
|
||||
# `gh repo rename`, which always failed, then pointed the artifacts remote at
|
||||
# gstack-artifacts-<user> anyway. When that repository does not exist but the
|
||||
# old gstack-brain-<user> does, point the remote back at the old one. Only a
|
||||
# remote that names a gstack-artifacts-* GitHub repo is checked.
|
||||
REMOTE_TXT="${HOME:-}/.gstack-artifacts-remote.txt"
|
||||
if [ -n "${HOME:-}" ] && [ -f "$REMOTE_TXT" ]; then
|
||||
URL=$(head -1 "$REMOTE_TXT" 2>/dev/null | tr -d '[:space:]')
|
||||
OWNER=$(printf '%s\n' "$URL" | sed -n 's#^.*github\.com[:/]\([^/]*\)/gstack-artifacts-[^/]*$#\1#p')
|
||||
NEW_NAME=$(printf '%s\n' "$URL" | sed -n 's#^.*/\(gstack-artifacts-[^/]*\)$#\1#p' | sed 's/\.git$//')
|
||||
if [ -n "$OWNER" ] && [ -n "$NEW_NAME" ]; then
|
||||
OLD_NAME="gstack-brain-${NEW_NAME#gstack-artifacts-}"
|
||||
if ! command -v gh >/dev/null 2>&1 || ! gh auth status >/dev/null 2>&1; then
|
||||
echo "artifacts remote: could not confirm $OWNER/$NEW_NAME exists (gh is not installed or not signed in). If artifact pushes fail, run: gh repo rename $NEW_NAME --repo $OWNER/$OLD_NAME --yes"
|
||||
elif ! gh repo view "$OWNER/$NEW_NAME" >/dev/null 2>&1 && gh repo view "$OWNER/$OLD_NAME" >/dev/null 2>&1; then
|
||||
OLD_URL=$(printf '%s\n' "$URL" | sed "s#/${NEW_NAME}#/${OLD_NAME}#")
|
||||
echo "$OLD_URL" > "$REMOTE_TXT" && chmod 600 "$REMOTE_TXT"
|
||||
if [ "$(git -C "$GH" remote get-url origin 2>/dev/null)" = "$URL" ]; then
|
||||
git -C "$GH" remote set-url origin "$OLD_URL" 2>/dev/null || true
|
||||
fi
|
||||
echo "artifacts remote: restored $OLD_URL because $OWNER/$NEW_NAME does not exist and $OWNER/$OLD_NAME does (an earlier upgrade rewrote it before the rename ran). To finish the rename later: gh repo rename $NEW_NAME --repo $OWNER/$OLD_NAME --yes"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -24,9 +24,13 @@ beforeEach(() => {
|
||||
});
|
||||
afterEach(() => fs.rmSync(home, { recursive: true, force: true }));
|
||||
|
||||
function gitIsolation() {
|
||||
return { GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: path.join(home, ".gitconfig") };
|
||||
}
|
||||
|
||||
function run() {
|
||||
const r = spawnSync("bash", [MIGRATION], {
|
||||
env: { PATH: `${bin}:${process.env.PATH}`, HOME: home, GSTACK_HOME: gstackHome },
|
||||
env: { PATH: `${bin}:${process.env.PATH}`, HOME: home, GSTACK_HOME: gstackHome, ...gitIsolation() },
|
||||
encoding: "utf-8",
|
||||
cwd: home,
|
||||
timeout: 30_000,
|
||||
@@ -58,3 +62,59 @@ describe("v1.91.17.0 migration: memory reconcile pending (A1)", () => {
|
||||
expect(fs.existsSync(path.join(gstackHome, ".transcript-ingest-state.json"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("v1.91.17.0 migration: artifacts remote repair (A8, #1437)", () => {
|
||||
const NEW = "https://github.com/acme/gstack-artifacts-dev";
|
||||
const OLD = "https://github.com/acme/gstack-brain-dev";
|
||||
function gh(views: string[]) {
|
||||
fs.writeFileSync(path.join(bin, "gh"), `#!/bin/sh
|
||||
echo "$@" >> "${home}/gh-calls.log"
|
||||
case "$1 $2" in
|
||||
"auth status") exit 0 ;;
|
||||
"repo view") for r in ${views.join(" ")}; do [ "$3" = "$r" ] && exit 0; done; exit 1 ;;
|
||||
esac
|
||||
exit 1
|
||||
`, { mode: 0o755 });
|
||||
}
|
||||
const remote = () => fs.readFileSync(path.join(home, ".gstack-artifacts-remote.txt"), "utf-8").trim();
|
||||
const ghCalls = () => (fs.existsSync(path.join(home, "gh-calls.log")) ? fs.readFileSync(path.join(home, "gh-calls.log"), "utf-8") : "");
|
||||
|
||||
test("restores the old URL (file and state-root origin) when only the old repo exists", () => {
|
||||
fs.writeFileSync(path.join(home, ".gstack-artifacts-remote.txt"), NEW + "\n");
|
||||
fs.mkdirSync(gstackHome, { recursive: true });
|
||||
for (const a of [["init", "-q", gstackHome], ["-C", gstackHome, "remote", "add", "origin", NEW]]) {
|
||||
expect(spawnSync("git", a, { timeout: 10_000, env: { ...process.env, ...gitIsolation() } }).status).toBe(0);
|
||||
}
|
||||
gh(["acme/gstack-brain-dev"]);
|
||||
const r = run();
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.stdout).toContain(`artifacts remote: restored ${OLD}`);
|
||||
expect(remote()).toBe(OLD);
|
||||
expect(spawnSync("git", ["-C", gstackHome, "remote", "get-url", "origin"], { encoding: "utf-8", timeout: 10_000, env: { ...process.env, ...gitIsolation() } }).stdout.trim()).toBe(OLD);
|
||||
expect(run().stdout).not.toContain("restored");
|
||||
});
|
||||
|
||||
test("leaves a remote alone when the renamed repo exists", () => {
|
||||
fs.writeFileSync(path.join(home, ".gstack-artifacts-remote.txt"), NEW + "\n");
|
||||
gh(["acme/gstack-artifacts-dev", "acme/gstack-brain-dev"]);
|
||||
expect(run().code).toBe(0);
|
||||
expect(remote()).toBe(NEW);
|
||||
});
|
||||
|
||||
test("without a signed-in gh, says so and changes nothing", () => {
|
||||
fs.writeFileSync(path.join(home, ".gstack-artifacts-remote.txt"), NEW + "\n");
|
||||
fs.writeFileSync(path.join(bin, "gh"), "#!/bin/sh\nexit 1\n", { mode: 0o755 });
|
||||
const r = run();
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.stdout).toContain("could not confirm acme/gstack-artifacts-dev exists");
|
||||
expect(remote()).toBe(NEW);
|
||||
});
|
||||
|
||||
test("never calls gh when the remote does not name a gstack-artifacts repo", () => {
|
||||
fs.writeFileSync(path.join(home, ".gstack-artifacts-remote.txt"), OLD + "\n");
|
||||
gh([]);
|
||||
expect(run().code).toBe(0);
|
||||
expect(ghCalls()).toBe("");
|
||||
expect(remote()).toBe(OLD);
|
||||
});
|
||||
});
|
||||
@@ -226,11 +226,25 @@ describe('v1.27.0.0 migration — #1383 consent + failure-stays-pending contract
|
||||
expect(journal).not.toContain('gh_repo_renamed');
|
||||
// Later steps DID run and are journaled (independent of step 1)...
|
||||
expect(journal).toContain('remote_txt_renamed');
|
||||
// ...so a retry with working gh only redoes step 1 and completes.
|
||||
// ...but the remote keeps naming the repository that exists (#1437):
|
||||
// rewriting it here pointed the artifacts remote at a repo never renamed.
|
||||
const remote = () => fs.readFileSync(path.join(tmpHome, '.gstack-artifacts-remote.txt'), 'utf-8').trim();
|
||||
expect(remote()).toBe('https://github.com/testuser/gstack-brain-testuser');
|
||||
// A retry with working gh only redoes step 1, completes, and only then
|
||||
// points the remote at the renamed repository.
|
||||
makeFakeGh({});
|
||||
const r2 = run();
|
||||
expect(r2.code).toBe(0);
|
||||
expect(fs.existsSync(path.join(tmpHome, '.gstack/.migrations/v1.27.0.0.done'))).toBe(true);
|
||||
expect(remote()).toBe('https://github.com/testuser/gstack-artifacts-testuser');
|
||||
});
|
||||
|
||||
test('A8 (#1437): gh rename and edit get the OWNER/REPO form gh requires', () => {
|
||||
run();
|
||||
const ghLog = fs.readFileSync(path.join(fakeBinDir, 'gh-calls.log'), 'utf-8');
|
||||
expect(ghLog).toContain('gh repo view testuser/gstack-artifacts-testuser');
|
||||
expect(ghLog).toMatch(/gh repo rename gstack-artifacts-testuser --repo testuser\/gstack-brain-testuser --yes/);
|
||||
expect(ghLog).not.toMatch(/--repo gstack-brain-testuser\b/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user