Skip to content

Commit c33f0c3

Browse files
committed
Update: support fork gitRepo stable/dev update flow
1 parent 5432c45 commit c33f0c3

File tree

8 files changed

+195
-24
lines changed

8 files changed

+195
-24
lines changed

scripts/install-openclaw-fork.sh

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ set -euo pipefail
2020
# OPENCLAW_SPEC=<npm-install-spec> # optional direct spec (e.g. file:/path, git+https://...)
2121
# OPENCLAW_RELEASE_REPO=<github-url> # repo URL for release tarballs (default: daydreamsai/openclaw-x402-router)
2222
# OPENCLAW_REPO=https://github.com/<org>/<repo>.git # for git+https:// fallback
23-
# OPENCLAW_REF=<tag-or-commit> # preferred (immutable)
23+
# OPENCLAW_REF=<tag-or-commit> # preferred (immutable); if omitted, resolves latest stable release tag
2424
# OPENCLAW_BRANCH=<branch> # fallback for development
2525
# OPENCLAW_INSTALLER=npm|pnpm|auto
2626
# OPENCLAW_BIN=openclaw|moltbot
@@ -51,6 +51,28 @@ set -euo pipefail
5151

5252
OPENCLAW_SPEC="${OPENCLAW_SPEC:-}"
5353
OPENCLAW_REPO="${OPENCLAW_REPO:-https://github.com/daydreamsai/openclaw-x402-router.git}"
54+
OPENCLAW_RELEASE_REPO="${OPENCLAW_RELEASE_REPO:-https://github.com/daydreamsai/openclaw-x402-router}"
55+
normalize_openclaw_release_slug() {
56+
local slug="${OPENCLAW_RELEASE_REPO%.git}"
57+
slug="${slug#https://github.com/}"
58+
slug="${slug#http://github.com/}"
59+
slug="${slug#git@github.com:}"
60+
slug="${slug#github.com/}"
61+
slug="${slug#/}"
62+
printf '%s\n' "$slug"
63+
}
64+
65+
resolve_latest_openclaw_release_tag() {
66+
local slug
67+
slug="$(normalize_openclaw_release_slug)"
68+
if [[ -z "$slug" || "$slug" != */* ]]; then
69+
return 1
70+
fi
71+
curl -fsSL -H "Accept: application/vnd.github+json" \
72+
"https://api.github.com/repos/${slug}/releases/latest" \
73+
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
74+
| head -n 1
75+
}
5476
OPENCLAW_REF="${OPENCLAW_REF:-}"
5577
OPENCLAW_BRANCH="${OPENCLAW_BRANCH:-}"
5678
OPENCLAW_INSTALLER="${OPENCLAW_INSTALLER:-npm}"
@@ -63,10 +85,6 @@ OPENCLAW_SKILLS_DIR="${OPENCLAW_SKILLS_DIR:-$HOME/.openclaw/skills}"
6385
OPENCLAW_LUCID_SDK_SKILL_URL="${OPENCLAW_LUCID_SDK_SKILL_URL:-https://raw.githubusercontent.com/daydreamsai/skills-market/main/plugins/lucid-agents-sdk/skills/SKILL.md}"
6486
OPENCLAW_XGATE_ROUTER_SKILL_URL="${OPENCLAW_XGATE_ROUTER_SKILL_URL:-https://ai.xgate.run/SKILL.md}"
6587

66-
if [[ -z "$OPENCLAW_SPEC" && -z "$OPENCLAW_REF" && -z "$OPENCLAW_BRANCH" ]]; then
67-
OPENCLAW_REF="v2026.2.25.daydreams.1"
68-
fi
69-
7088
if [[ -n "$OPENCLAW_SPEC" && ( -n "$OPENCLAW_REF" || -n "$OPENCLAW_BRANCH" ) ]]; then
7189
echo "ERROR: set OPENCLAW_SPEC or OPENCLAW_REF/OPENCLAW_BRANCH, not both" >&2
7290
exit 1
@@ -586,8 +604,6 @@ echo " Phase 2: OpenClaw Gateway Install"
586604
echo "============================================"
587605
echo ""
588606

589-
OPENCLAW_RELEASE_REPO="${OPENCLAW_RELEASE_REPO:-https://github.com/daydreamsai/openclaw-x402-router}"
590-
591607
resolve_release_tarball_url() {
592608
local tag="$1"
593609
local repo_url="${OPENCLAW_RELEASE_REPO%.git}"
@@ -599,6 +615,16 @@ resolve_release_tarball_url() {
599615
return 1
600616
}
601617

618+
if [[ -z "$OPENCLAW_SPEC" && -z "$OPENCLAW_REF" && -z "$OPENCLAW_BRANCH" ]]; then
619+
OPENCLAW_REF="$(resolve_latest_openclaw_release_tag || true)"
620+
if [[ -z "$OPENCLAW_REF" ]]; then
621+
echo "ERROR: could not determine latest stable release tag from ${OPENCLAW_RELEASE_REPO}" >&2
622+
echo "Set OPENCLAW_REF=<tag> explicitly and retry." >&2
623+
exit 1
624+
fi
625+
echo "==> Resolved latest stable release tag: ${OPENCLAW_REF}"
626+
fi
627+
602628
if [[ -n "$OPENCLAW_SPEC" ]]; then
603629
SPEC="$OPENCLAW_SPEC"
604630
REF_KIND="spec"
@@ -766,6 +792,21 @@ echo "==> Installed CLI: ${CLI_BIN_NAME} (${CLI_BIN_PATH})"
766792
INSTALLED_VERSION="$("$CLI_BIN_PATH" --version 2>/dev/null || true)"
767793
echo "==> Installed version: ${INSTALLED_VERSION:-unknown}"
768794

795+
echo "==> Configuring fork-aware update defaults..."
796+
if "$CLI_BIN_PATH" config set update.channel stable >/dev/null 2>&1; then
797+
echo "==> Set update.channel=stable"
798+
else
799+
echo "WARNING: failed to set update.channel=stable; run manually:" >&2
800+
echo " $CLI_BIN_PATH config set update.channel stable" >&2
801+
fi
802+
803+
if "$CLI_BIN_PATH" config set update.gitRepo "$OPENCLAW_REPO" >/dev/null 2>&1; then
804+
echo "==> Set update.gitRepo=${OPENCLAW_REPO}"
805+
else
806+
echo "WARNING: failed to set update.gitRepo; run manually:" >&2
807+
echo " $CLI_BIN_PATH config set update.gitRepo \"$OPENCLAW_REPO\"" >&2
808+
fi
809+
769810
install_remote_skill() {
770811
local skill_name="$1"
771812
local skill_url="$2"

src/cli/update-cli.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,52 @@ describe("update-cli", () => {
476476
expect(call?.tag).toBe("latest");
477477
});
478478

479+
it("uses configured update.gitRepo when switching package installs to dev", async () => {
480+
const tempDir = createCaseDir("openclaw-update-git-dir");
481+
await withEnvAsync({ OPENCLAW_GIT_DIR: tempDir }, async () => {
482+
mockPackageInstallStatus(createCaseDir("openclaw-update"));
483+
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
484+
...baseSnapshot,
485+
config: {
486+
update: {
487+
gitRepo: "https://github.com/daydreamsai/openclaw-x402-router.git",
488+
},
489+
} as OpenClawConfig,
490+
});
491+
492+
await updateCommand({ channel: "dev", yes: true });
493+
494+
expect(runCommandWithTimeout).toHaveBeenCalledWith(
495+
["git", "clone", "https://github.com/daydreamsai/openclaw-x402-router.git", tempDir],
496+
expect.objectContaining({ timeoutMs: expect.any(Number) }),
497+
);
498+
});
499+
});
500+
501+
it("uses git stable-tag flow when update.gitRepo and update.channel=stable are configured", async () => {
502+
const tempDir = createCaseDir("openclaw-update-git-stable-dir");
503+
await withEnvAsync({ OPENCLAW_GIT_DIR: tempDir }, async () => {
504+
mockPackageInstallStatus(createCaseDir("openclaw-update"));
505+
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
506+
...baseSnapshot,
507+
config: {
508+
update: {
509+
channel: "stable",
510+
gitRepo: "https://github.com/daydreamsai/openclaw-x402-router.git",
511+
},
512+
} as OpenClawConfig,
513+
});
514+
515+
await updateCommand({ yes: true });
516+
517+
expect(runCommandWithTimeout).toHaveBeenCalledWith(
518+
["git", "clone", "https://github.com/daydreamsai/openclaw-x402-router.git", tempDir],
519+
expect.objectContaining({ timeoutMs: expect.any(Number) }),
520+
);
521+
expectUpdateCallChannel("stable");
522+
});
523+
});
524+
479525
it("honors --tag override", async () => {
480526
const tempDir = createCaseDir("openclaw-update");
481527

src/cli/update-cli/shared.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,37 @@ const MAX_LOG_CHARS = 8000;
5757
export const DEFAULT_PACKAGE_NAME = "openclaw";
5858
const CORE_PACKAGE_NAMES = new Set([DEFAULT_PACKAGE_NAME]);
5959

60+
export type UpdateGitRepoSource = "default" | "config" | "env";
61+
62+
export type ResolvedUpdateGitRepo = {
63+
url: string;
64+
source: UpdateGitRepoSource;
65+
};
66+
67+
function normalizeGitRepoUrl(value?: string | null): string | null {
68+
if (!value) {
69+
return null;
70+
}
71+
const trimmed = value.trim();
72+
return trimmed.length > 0 ? trimmed : null;
73+
}
74+
75+
export function resolveUpdateGitRepo(configuredRepo?: string | null): ResolvedUpdateGitRepo {
76+
const envOverride =
77+
normalizeGitRepoUrl(process.env.OPENCLAW_UPDATE_GIT_REPO) ??
78+
normalizeGitRepoUrl(process.env.OPENCLAW_REPO);
79+
if (envOverride) {
80+
return { url: envOverride, source: "env" };
81+
}
82+
83+
const configRepo = normalizeGitRepoUrl(configuredRepo);
84+
if (configRepo) {
85+
return { url: configRepo, source: "config" };
86+
}
87+
88+
return { url: OPENCLAW_REPO_URL, source: "default" };
89+
}
90+
6091
export function normalizeTag(value?: string | null): string | null {
6192
if (!value) {
6293
return null;
@@ -198,12 +229,15 @@ export async function ensureGitCheckout(params: {
198229
dir: string;
199230
timeoutMs: number;
200231
progress?: UpdateStepProgress;
232+
repoUrl?: string;
233+
enforceRepo?: boolean;
201234
}): Promise<UpdateStepResult | null> {
235+
const repoUrl = normalizeGitRepoUrl(params.repoUrl) ?? OPENCLAW_REPO_URL;
202236
const dirExists = await pathExists(params.dir);
203237
if (!dirExists) {
204238
return await runUpdateStep({
205239
name: "git clone",
206-
argv: ["git", "clone", OPENCLAW_REPO_URL, params.dir],
240+
argv: ["git", "clone", repoUrl, params.dir],
207241
timeoutMs: params.timeoutMs,
208242
progress: params.progress,
209243
});
@@ -219,7 +253,7 @@ export async function ensureGitCheckout(params: {
219253

220254
return await runUpdateStep({
221255
name: "git clone",
222-
argv: ["git", "clone", OPENCLAW_REPO_URL, params.dir],
256+
argv: ["git", "clone", repoUrl, params.dir],
223257
cwd: params.dir,
224258
timeoutMs: params.timeoutMs,
225259
progress: params.progress,
@@ -230,6 +264,29 @@ export async function ensureGitCheckout(params: {
230264
throw new Error(`OPENCLAW_GIT_DIR does not look like a core checkout: ${params.dir}.`);
231265
}
232266

267+
if (params.enforceRepo) {
268+
const originRes = await runCommandWithTimeout(
269+
["git", "-C", params.dir, "remote", "get-url", "origin"],
270+
{
271+
cwd: params.dir,
272+
timeoutMs: params.timeoutMs,
273+
},
274+
).catch(() => null);
275+
276+
const currentOrigin = originRes?.code === 0 ? originRes.stdout.trim() : null;
277+
if (currentOrigin !== repoUrl) {
278+
return await runUpdateStep({
279+
name: currentOrigin ? "git remote set-url origin" : "git remote add origin",
280+
argv: currentOrigin
281+
? ["git", "-C", params.dir, "remote", "set-url", "origin", repoUrl]
282+
: ["git", "-C", params.dir, "remote", "add", "origin", repoUrl],
283+
cwd: params.dir,
284+
timeoutMs: params.timeoutMs,
285+
progress: params.progress,
286+
});
287+
}
288+
}
289+
233290
return null;
234291
}
235292

src/cli/update-cli/update-command.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
resolveGlobalManager,
5858
resolveNodeRunner,
5959
resolveTargetVersion,
60+
resolveUpdateGitRepo,
6061
resolveUpdateRoot,
6162
runUpdateStep,
6263
tryWriteCompletionCache,
@@ -323,6 +324,8 @@ async function runPackageInstallUpdate(params: {
323324
async function runGitUpdate(params: {
324325
root: string;
325326
switchToGit: boolean;
327+
gitRepoUrl: string;
328+
enforceGitRepo: boolean;
326329
installKind: "git" | "package" | "unknown";
327330
timeoutMs: number | undefined;
328331
startedAt: number;
@@ -336,21 +339,24 @@ async function runGitUpdate(params: {
336339
const updateRoot = params.switchToGit ? resolveGitInstallDir() : params.root;
337340
const effectiveTimeout = params.timeoutMs ?? 20 * 60_000;
338341

339-
const cloneStep = params.switchToGit
340-
? await ensureGitCheckout({
341-
dir: updateRoot,
342-
timeoutMs: effectiveTimeout,
343-
progress: params.progress,
344-
})
345-
: null;
346-
347-
if (cloneStep && cloneStep.exitCode !== 0) {
342+
const checkoutStep =
343+
params.switchToGit || params.enforceGitRepo
344+
? await ensureGitCheckout({
345+
dir: updateRoot,
346+
timeoutMs: effectiveTimeout,
347+
progress: params.progress,
348+
repoUrl: params.gitRepoUrl,
349+
enforceRepo: params.enforceGitRepo,
350+
})
351+
: null;
352+
353+
if (checkoutStep && checkoutStep.exitCode !== 0) {
348354
const result: UpdateRunResult = {
349355
status: "error",
350356
mode: "git",
351357
root: updateRoot,
352-
reason: cloneStep.name,
353-
steps: [cloneStep],
358+
reason: checkoutStep.name,
359+
steps: [checkoutStep],
354360
durationMs: Date.now() - params.startedAt,
355361
};
356362
params.stop();
@@ -367,7 +373,7 @@ async function runGitUpdate(params: {
367373
channel: params.channel,
368374
tag: params.tag,
369375
});
370-
const steps = [...(cloneStep ? [cloneStep] : []), ...updateResult.steps];
376+
const steps = [...(checkoutStep ? [checkoutStep] : []), ...updateResult.steps];
371377

372378
if (params.switchToGit && updateResult.status === "ok") {
373379
const manager = await resolveGlobalManager({
@@ -661,10 +667,18 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
661667
return;
662668
}
663669

670+
const gitRepo = resolveUpdateGitRepo(
671+
configSnapshot.valid ? configSnapshot.config.update?.gitRepo : undefined,
672+
);
673+
const preferGitRepo = gitRepo.source !== "default";
674+
664675
const installKind = updateStatus.installKind;
665-
const switchToGit = requestedChannel === "dev" && installKind !== "git";
676+
const switchToGit = installKind !== "git" && (requestedChannel === "dev" || preferGitRepo);
666677
const switchToPackage =
667-
requestedChannel !== null && requestedChannel !== "dev" && installKind === "git";
678+
requestedChannel !== null &&
679+
requestedChannel !== "dev" &&
680+
installKind === "git" &&
681+
!preferGitRepo;
668682
const updateInstallKind = switchToGit ? "git" : switchToPackage ? "package" : installKind;
669683
const defaultChannel =
670684
updateInstallKind === "git" ? DEFAULT_GIT_CHANNEL : DEFAULT_PACKAGE_CHANNEL;
@@ -711,11 +725,16 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
711725
actions.push(`Persist update.channel=${requestedChannel} in config`);
712726
}
713727
if (switchToGit) {
714-
actions.push("Switch install mode from package to git checkout (dev channel)");
728+
actions.push(
729+
`Switch install mode from package to git checkout (channel ${channel}, repo ${gitRepo.url})`,
730+
);
715731
} else if (switchToPackage) {
716732
actions.push(`Switch install mode from git to package manager (${mode})`);
717733
} else if (updateInstallKind === "git") {
718734
actions.push(`Run git update flow on channel ${channel} (fetch/rebase/build/doctor)`);
735+
if (gitRepo.source !== "default") {
736+
actions.push(`Ensure git origin matches configured repo (${gitRepo.url})`);
737+
}
719738
} else {
720739
actions.push(`Run global package manager update with spec openclaw@${tag}`);
721740
}
@@ -842,6 +861,8 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
842861
: await runGitUpdate({
843862
root,
844863
switchToGit,
864+
gitRepoUrl: gitRepo.url,
865+
enforceGitRepo: gitRepo.source !== "default",
845866
installKind,
846867
timeoutMs,
847868
startedAt,

src/config/schema.help.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export const FIELD_HELP: Record<string, string> = {
4848
update:
4949
"Update-channel and startup-check behavior for keeping OpenClaw runtime versions current. Use conservative channels in production and more experimental channels only in controlled environments.",
5050
"update.channel": 'Update channel for git + npm installs ("stable", "beta", or "dev").',
51+
"update.gitRepo":
52+
"Optional git repository URL to use for update checkouts. Set this for forks so update does not clone upstream OpenClaw by default.",
5153
"update.checkOnStart": "Check for npm updates when the gateway starts (default: true).",
5254
"update.auto.enabled": "Enable background auto-update for package installs (default: false).",
5355
"update.auto.stableDelayHours":

src/config/schema.labels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const FIELD_LABELS: Record<string, string> = {
2727
"logging.redactPatterns": "Custom Redaction Patterns",
2828
update: "Updates",
2929
"update.channel": "Update Channel",
30+
"update.gitRepo": "Update Git Repository",
3031
"update.checkOnStart": "Update Check on Start",
3132
"update.auto.enabled": "Auto Update Enabled",
3233
"update.auto.stableDelayHours": "Auto Update Stable Delay (hours)",

src/config/types.openclaw.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export type OpenClawConfig = {
6464
update?: {
6565
/** Update channel for git + npm installs ("stable", "beta", or "dev"). */
6666
channel?: "stable" | "beta" | "dev";
67+
/** Optional git repository URL used for update checkouts. */
68+
gitRepo?: string;
6769
/** Check for updates on gateway start (npm installs only). */
6870
checkOnStart?: boolean;
6971
/** Core auto-update policy for package installs. */

src/config/zod-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export const OpenClawSchema = z
225225
update: z
226226
.object({
227227
channel: z.union([z.literal("stable"), z.literal("beta"), z.literal("dev")]).optional(),
228+
gitRepo: z.string().optional(),
228229
checkOnStart: z.boolean().optional(),
229230
auto: z
230231
.object({

0 commit comments

Comments
 (0)