From 01ff7e51ee908a5b1016c2d2d7262c3765c73327 Mon Sep 17 00:00:00 2001 From: Chrys Bader Date: Tue, 14 Jul 2026 17:33:37 -0700 Subject: [PATCH 1/3] Fix GPT-5.5 onboarding authentication --- .../components/onboarding/welcome-config.js | 8 +++- .../js/components/welcome/use-welcome.js | 23 ++++++---- lib/public/js/lib/model-config.js | 38 +++++++++++++---- lib/server/onboarding/validation.js | 17 ++++++-- tests/frontend/model-config.test.js | 42 ++++++++++++++++--- tests/frontend/welcome-config.test.js | 2 +- tests/server/onboarding-validation.test.js | 24 +++++++++++ tests/server/routes-onboarding.test.js | 31 +++++++++++++- 8 files changed, 155 insertions(+), 30 deletions(-) diff --git a/lib/public/js/components/onboarding/welcome-config.js b/lib/public/js/components/onboarding/welcome-config.js index 5b2236cb..5514d6bc 100644 --- a/lib/public/js/components/onboarding/welcome-config.js +++ b/lib/public/js/components/onboarding/welcome-config.js @@ -53,12 +53,16 @@ const getAiGroupError = (vals, ctx = {}) => { if (!hasValue(vals.MODEL_KEY) || !String(vals.MODEL_KEY).includes("/")) { return "Choose a model to continue."; } - if (ctx.selectedProvider === "openai-codex" && ctx.codexLoading) { + if ( + ctx.selectedProvider === "openai-codex" && + ctx.codexLoading && + !ctx.hasAi + ) { return "Checking Codex OAuth status. Try Next again in a moment."; } if (!ctx.hasAi) { return ctx.selectedProvider === "openai-codex" - ? "Connect Codex OAuth to continue." + ? "Connect Codex OAuth or enter an OpenAI API key to continue." : "Add credentials for the selected model provider to continue."; } return ""; diff --git a/lib/public/js/components/welcome/use-welcome.js b/lib/public/js/components/welcome/use-welcome.js index f6254047..af9fe912 100644 --- a/lib/public/js/components/welcome/use-welcome.js +++ b/lib/public/js/components/welcome/use-welcome.js @@ -9,9 +9,11 @@ import { import { useCachedFetch } from "../../hooks/use-cached-fetch.js"; import { usePolling } from "../../hooks/usePolling.js"; import { - getModelProvider, getAuthProviderFromModelProvider, getFeaturedModels, + getOnboardingModelProvider, + getOnboardingModels, + isDeprecatedOnboardingModelKey, getVisibleAiFieldKeys, kProviderAuthFields, } from "../../lib/model-config.js"; @@ -167,7 +169,7 @@ export const useWelcome = ({ onComplete }) => { }; const applyModelCatalog = useCallback((payload) => { - const list = getModelCatalogModels(payload); + const list = getOnboardingModels(getModelCatalogModels(payload)); if (!payload) return; const isRefreshing = isModelCatalogRefreshing(payload); const isFallbackRefresh = @@ -181,11 +183,14 @@ export const useWelcome = ({ onComplete }) => { : null : "No models found", ); + const currentModelIsDeprecated = isDeprecatedOnboardingModelKey( + vals.MODEL_KEY, + ); const defaultModelKey = getInitialOnboardingModelKey({ catalog: list, - currentModelKey: vals.MODEL_KEY, + currentModelKey: currentModelIsDeprecated ? "" : vals.MODEL_KEY, }); - if (!vals.MODEL_KEY && defaultModelKey) { + if ((!vals.MODEL_KEY || currentModelIsDeprecated) && defaultModelKey) { setVals((prev) => ({ ...prev, MODEL_KEY: defaultModelKey })); } }, [setVals, vals.MODEL_KEY]); @@ -212,16 +217,18 @@ export const useWelcome = ({ onComplete }) => { }, [modelsFetchState.error]); const getValidationContext = (currentVals = {}) => { - const currentSelectedProvider = getModelProvider( - String(currentVals.MODEL_KEY || "").trim(), - ); + const currentSelectedProvider = getOnboardingModelProvider({ + modelKey: currentVals.MODEL_KEY, + models, + }); const currentSelectedAuthProvider = getAuthProviderFromModelProvider(currentSelectedProvider); const currentProviderAuthFields = kProviderAuthFields[currentSelectedAuthProvider] || []; const currentHasAi = currentSelectedProvider === "openai-codex" - ? !!codexStatus.connected + ? !!codexStatus.connected || + !!String(currentVals.OPENAI_API_KEY || "").trim() : currentProviderAuthFields.some((field) => !!String(currentVals[field.key] || "").trim(), ); diff --git a/lib/public/js/lib/model-config.js b/lib/public/js/lib/model-config.js index 1272b42b..02c43b1b 100644 --- a/lib/public/js/lib/model-config.js +++ b/lib/public/js/lib/model-config.js @@ -25,13 +25,6 @@ export const kFeaturedModelDefs = [ label: "Sonnet 4.6", preferredKeys: ["anthropic/claude-sonnet-4-6"], }, - { - label: "Codex 5.3", - preferredKeys: [ - "openai/gpt-5.3-codex", - "openai-codex/gpt-5.3-codex", - ], - }, { label: "GPT-5.5", preferredKeys: ["openai/gpt-5.5", "openai-codex/gpt-5.5"], @@ -42,6 +35,36 @@ export const kFeaturedModelDefs = [ }, ]; +const kDeprecatedOnboardingModelKeys = new Set([ + "openai/gpt-5.3-codex", + "openai-codex/gpt-5.3-codex", +]); +const kCanonicalCodexOauthModelKeys = new Set([ + "openai/gpt-5.4-mini", + "openai/gpt-5.5", +]); + +export const isDeprecatedOnboardingModelKey = (modelKey) => + kDeprecatedOnboardingModelKeys.has(String(modelKey || "").trim()); + +export const getOnboardingModels = (models = []) => + models.filter( + (model) => !isDeprecatedOnboardingModelKey(model?.key), + ); + +export const getOnboardingModelProvider = ({ modelKey, models = [] } = {}) => { + const normalizedKey = String(modelKey || "").trim(); + const model = models.find((candidate) => candidate?.key === normalizedKey); + if ( + getModelProvider(normalizedKey) === "openai-codex" || + kCanonicalCodexOauthModelKeys.has(normalizedKey) || + model?.agentRuntime?.id === "codex" + ) { + return "openai-codex"; + } + return getModelProvider(normalizedKey); +}; + export const kAlwaysAvailableModelDefs = [ { key: "openai/gpt-5.4-mini", @@ -345,7 +368,6 @@ export const kFeatureDefs = [ ]; export const getVisibleAiFieldKeys = (provider) => { - if (provider === "openai-codex") return new Set(); const authProvider = getAuthProviderFromModelProvider(provider); const fields = kProviderAuthFields[authProvider] || []; return new Set(fields.map((field) => field.key)); diff --git a/lib/server/onboarding/validation.js b/lib/server/onboarding/validation.js index 4cd77921..e1d318bb 100644 --- a/lib/server/onboarding/validation.js +++ b/lib/server/onboarding/validation.js @@ -2,6 +2,14 @@ const { getEnvVarForApiKeyProvider } = require("../auth-profiles"); const kAnthropicSetupTokenPrefix = "sk-ant-oat01-"; const kAnthropicApiKeyPrefix = "sk-ant-api"; +const kCanonicalCodexOauthModelKeys = new Set([ + "openai/gpt-5.4-mini", + "openai/gpt-5.5", +]); + +const usesCodexOauth = (modelKey, provider) => + provider === "openai-codex" || + kCanonicalCodexOauthModelKeys.has(String(modelKey || "").trim()); const validateAnthropicCredentialShape = (varMap) => { const anthropicToken = String(varMap.ANTHROPIC_TOKEN || "").trim(); @@ -71,7 +79,10 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode if (!anthropicValidation.ok) return anthropicValidation; const githubToken = String(varMap.GITHUB_TOKEN || ""); const githubRepoInput = String(varMap.GITHUB_WORKSPACE_REPO || "").trim(); - const selectedProvider = resolveModelProvider(modelKey); + const resolvedProvider = resolveModelProvider(modelKey); + const selectedProvider = usesCodexOauth(modelKey, resolvedProvider) + ? "openai-codex" + : resolvedProvider; const hasCodexOauth = hasCodexOauthProfile(); const hasAnyAi = !!( varMap.ANTHROPIC_API_KEY || @@ -82,7 +93,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode ); const hasAi = (() => { if (selectedProvider === "openai-codex") { - return hasCodexOauth; + return hasCodexOauth || !!String(varMap.OPENAI_API_KEY || "").trim(); } if (selectedProvider === "anthropic") { return !!(varMap.ANTHROPIC_API_KEY || varMap.ANTHROPIC_TOKEN); @@ -101,7 +112,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode return { ok: false, status: 400, - error: "Connect OpenAI Codex OAuth before continuing", + error: "Connect OpenAI Codex OAuth or add an OpenAI API key before continuing", }; } return { diff --git a/tests/frontend/model-config.test.js b/tests/frontend/model-config.test.js index bd969ee3..685b556c 100644 --- a/tests/frontend/model-config.test.js +++ b/tests/frontend/model-config.test.js @@ -17,7 +17,7 @@ describe("frontend/model-config", () => { it("returns visible AI field keys for provider", async () => { const modelConfig = await loadModelConfig(); const keys = modelConfig.getVisibleAiFieldKeys("openai-codex"); - expect(keys.has("OPENAI_API_KEY")).toBe(false); + expect(keys.has("OPENAI_API_KEY")).toBe(true); expect(keys.has("ANTHROPIC_API_KEY")).toBe(false); const zaiKeys = modelConfig.getVisibleAiFieldKeys("zai"); expect(zaiKeys.has("ZAI_API_KEY")).toBe(true); @@ -32,9 +32,7 @@ describe("frontend/model-config", () => { { key: "anthropic/claude-opus-4-8", label: "Opus 4.8" }, { key: "anthropic/claude-opus-4-7", label: "Opus 4.7" }, { key: "anthropic/claude-opus-4-6", label: "Opus 4.6" }, - { key: "openai/gpt-5.3-codex", label: "Codex 5.3" }, { key: "openai/gpt-5.5", label: "GPT-5.5" }, - { key: "openai-codex/gpt-5.3-codex", label: "Codex 5.3" }, { key: "openai-codex/gpt-5.4", label: "GPT-5.4" }, { key: "openai-codex/gpt-5.5", label: "GPT-5.5" }, ]); @@ -43,14 +41,46 @@ describe("frontend/model-config", () => { "anthropic/claude-opus-4-8", "anthropic/claude-opus-4-7", "anthropic/claude-opus-4-6", - "openai/gpt-5.3-codex", "openai/gpt-5.5", "google/gemini-3.1-pro-preview", ]); expect(featured[0]?.featuredLabel).toBe("Opus 4.8"); expect(featured[1]?.featuredLabel).toBe("Opus 4.7"); - expect(featured[4]?.featuredLabel).toBe("GPT-5.5"); - expect(featured[5]?.featuredLabel).toBe("Gemini 3.1 Pro"); + expect(featured[3]?.featuredLabel).toBe("GPT-5.5"); + expect(featured[4]?.featuredLabel).toBe("Gemini 3.1 Pro"); + }); + + it("removes deprecated Codex 5.3 models from onboarding", async () => { + const modelConfig = await loadModelConfig(); + const models = modelConfig.getOnboardingModels([ + { key: "openai/gpt-5.3-codex" }, + { key: "openai-codex/gpt-5.3-codex" }, + { key: "openai/gpt-5.5", agentRuntime: { id: "codex" } }, + ]); + + expect(models.map((model) => model.key)).toEqual(["openai/gpt-5.5"]); + expect( + modelConfig.isDeprecatedOnboardingModelKey("openai/gpt-5.3-codex"), + ).toBe(true); + expect( + modelConfig.isDeprecatedOnboardingModelKey("openai/gpt-5.5"), + ).toBe(false); + }); + + it("uses Codex OAuth for canonical models with the Codex runtime", async () => { + const modelConfig = await loadModelConfig(); + expect( + modelConfig.getOnboardingModelProvider({ + modelKey: "openai/gpt-5.5", + models: [{ key: "openai/gpt-5.5" }], + }), + ).toBe("openai-codex"); + expect( + modelConfig.getOnboardingModelProvider({ + modelKey: "openai/gpt-4.1", + models: [{ key: "openai/gpt-4.1" }], + }), + ).toBe("openai"); }); it("keeps canonical GPT-5.5 selectable when live discovery omits it", async () => { diff --git a/tests/frontend/welcome-config.test.js b/tests/frontend/welcome-config.test.js index 8ce7b002..e7d78707 100644 --- a/tests/frontend/welcome-config.test.js +++ b/tests/frontend/welcome-config.test.js @@ -39,7 +39,7 @@ describe("frontend/welcome-config", () => { codexLoading: false, }, ), - ).toBe("Connect Codex OAuth to continue."); + ).toBe("Connect Codex OAuth or enter an OpenAI API key to continue."); }); it("requires both Slack tokens before the channels step can pass", async () => { diff --git a/tests/server/onboarding-validation.test.js b/tests/server/onboarding-validation.test.js index b179738c..c38fde3e 100644 --- a/tests/server/onboarding-validation.test.js +++ b/tests/server/onboarding-validation.test.js @@ -54,4 +54,28 @@ describe("onboarding/validation", () => { }); expect(res.ok).toBe(true); }); + + it("accepts canonical GPT-5.5 with Codex OAuth and no OpenAI API key", () => { + const res = validateOnboardingInput({ + vars: kBaseVars(), + modelKey: "openai/gpt-5.5", + resolveModelProvider: kResolveProvider, + hasCodexOauthProfile: () => true, + }); + + expect(res.ok).toBe(true); + expect(res.data.selectedProvider).toBe("openai-codex"); + }); + + it("accepts canonical GPT-5.5 with an OpenAI API key and no Codex OAuth", () => { + const res = validateOnboardingInput({ + vars: [...kBaseVars(), { key: "OPENAI_API_KEY", value: "sk-test-123" }], + modelKey: "openai/gpt-5.5", + resolveModelProvider: kResolveProvider, + hasCodexOauthProfile: () => false, + }); + + expect(res.ok).toBe(true); + expect(res.data.selectedProvider).toBe("openai-codex"); + }); }); diff --git a/tests/server/routes-onboarding.test.js b/tests/server/routes-onboarding.test.js index 780bea32..c6a05d72 100644 --- a/tests/server/routes-onboarding.test.js +++ b/tests/server/routes-onboarding.test.js @@ -190,7 +190,7 @@ describe("server/routes/onboarding", () => { expect(deps.shellCmd).not.toHaveBeenCalled(); }); - it("requires codex oauth for openai-codex provider", async () => { + it("requires codex oauth or an OpenAI API key for openai-codex provider", async () => { const deps = createBaseDeps({ hasCodexOauth: false }); const app = createApp(deps); @@ -208,7 +208,27 @@ describe("server/routes/onboarding", () => { expect(res.status).toBe(400); expect(res.body).toEqual({ ok: false, - error: "Connect OpenAI Codex OAuth before continuing", + error: "Connect OpenAI Codex OAuth or add an OpenAI API key before continuing", + }); + }); + + it("requires codex oauth or an OpenAI API key for canonical GPT-5.5", async () => { + const deps = createBaseDeps({ hasCodexOauth: false }); + const app = createApp(deps); + + const res = await request(app).post("/api/onboard").send({ + modelKey: "openai/gpt-5.5", + vars: [ + { key: "GITHUB_TOKEN", value: "ghp_test_123456789" }, + { key: "GITHUB_WORKSPACE_REPO", value: "owner/repo" }, + { key: "TELEGRAM_BOT_TOKEN", value: "telegram_123456789" }, + ], + }); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ + ok: false, + error: "Connect OpenAI Codex OAuth or add an OpenAI API key before continuing", }); }); @@ -412,6 +432,13 @@ describe("server/routes/onboarding", () => { expect(res.status).toBe(200); expect(res.body).toEqual({ ok: true }); + const progressRes = await request(app).get("/api/onboard/progress"); + expect(progressRes.status).toBe(200); + expect(progressRes.body).toMatchObject({ + active: false, + stage: "starting_gateway", + message: "Starting gateway...", + }); expect(deps.runOnboardedBootSequence).toHaveBeenCalledTimes(1); expect(deps.authProfiles.upsertApiKeyProfileForEnvVar).toHaveBeenCalledWith( "openai", From 3f80d65fc5b6883b1b1073c2aaa186f2cb1382ba Mon Sep 17 00:00:00 2001 From: Chrys Bader Date: Tue, 14 Jul 2026 17:34:01 -0700 Subject: [PATCH 2/3] Show live onboarding progress --- .../onboarding/welcome-setup-step.js | 62 +++++++++++++++++-- lib/public/js/lib/api.js | 5 ++ lib/server/onboarding/index.js | 11 ++++ lib/server/routes/onboarding.js | 33 ++++++++++ tests/frontend/api.test.js | 18 ++++++ 5 files changed, 125 insertions(+), 4 deletions(-) diff --git a/lib/public/js/components/onboarding/welcome-setup-step.js b/lib/public/js/components/onboarding/welcome-setup-step.js index 2d4c41f8..177bff92 100644 --- a/lib/public/js/components/onboarding/welcome-setup-step.js +++ b/lib/public/js/components/onboarding/welcome-setup-step.js @@ -2,6 +2,7 @@ import { h } from "preact"; import { useEffect, useState } from "preact/hooks"; import htm from "htm"; import { LoadingSpinner } from "../loading-spinner.js"; +import { fetchOnboardProgress } from "../../lib/api.js"; const html = htm.bind(h); const kSetupTips = [ @@ -30,9 +31,24 @@ const kSetupTips = [ text: "Be incredibly careful installing skills from the internet - they may contain malicious code.", }, ]; +const kDefaultProgress = { + stage: "creating_repo", + message: "Creating repo...", +}; +const kProgressStepNumbers = { + creating_repo: 1, + running_openclaw_onboard: 2, + initial_git_push: 3, + starting_gateway: 4, +}; +const kProgressPollIntervalMs = 500; +const kProgressDotsIntervalMs = 450; +const kProgressDotSlots = [1, 2, 3]; export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => { const [tipIndex, setTipIndex] = useState(0); + const [progress, setProgress] = useState(kDefaultProgress); + const [progressDotCount, setProgressDotCount] = useState(1); useEffect(() => { if (error || !loading) return; @@ -42,6 +58,35 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => { return () => clearInterval(timer); }, [error, loading]); + useEffect(() => { + if (error || !loading) return; + let cancelled = false; + const refreshProgress = async () => { + try { + const progress = await fetchOnboardProgress(); + if (!cancelled && progress?.message) { + setProgress(progress); + } + } catch {} + }; + setProgress(kDefaultProgress); + refreshProgress(); + const timer = setInterval(refreshProgress, kProgressPollIntervalMs); + return () => { + cancelled = true; + clearInterval(timer); + }; + }, [error, loading]); + + useEffect(() => { + if (error || !loading) return; + setProgressDotCount(1); + const timer = setInterval(() => { + setProgressDotCount((count) => (count % 3) + 1); + }, kProgressDotsIntervalMs); + return () => clearInterval(timer); + }, [error, loading]); + if (error) { return html`
@@ -77,6 +122,8 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => { } const currentTip = kSetupTips[tipIndex]; + const progressStep = kProgressStepNumbers[progress.stage] || 1; + const progressLabel = (progress.message || kDefaultProgress.message).replace(/\.{1,3}$/, ""); return html`
@@ -84,10 +131,17 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => { class="flex-1 flex flex-col items-center justify-center text-center gap-4" > <${LoadingSpinner} className="h-8 w-8 text-body" /> -

- Initializing OpenClaw... -

-

This could take 10-15 seconds

+

Initializing AlphaClaw...

+

+ ${progressStep} / 4: ${progressLabel} +

+

This could take up to 30 seconds

{ + const reportProgress = (stage) => { + if (typeof onProgress !== "function") return; + try { + onProgress(stage); + } catch {} + }; const validation = validateOnboardingInput({ vars, modelKey, @@ -425,6 +432,7 @@ const createOnboardingService = ({ syncApiKeyAuthProfilesFromEnvVars(authProfiles, varsToSave); const [, repoName] = repoUrl.split("/"); + reportProgress("creating_repo"); const repoCheck = await ensureGithubRepoAccessible({ repoUrl, repoName, @@ -481,6 +489,7 @@ const createOnboardingService = ({ ); } + reportProgress("running_openclaw_onboard"); if (!existingConfigPresent) { const onboardArgs = buildOnboardArgs({ varMap, @@ -551,6 +560,7 @@ const createOnboardingService = ({ ensureGatewayProxyConfig(getBaseUrl(req)); + reportProgress("initial_git_push"); try { const commitMsg = importMode ? "imported existing setup via AlphaClaw" @@ -567,6 +577,7 @@ const createOnboardingService = ({ console.error("[onboard] Git push error:", e.message); } + reportProgress("starting_gateway"); runOnboardedBootSequence(); return { status: 200, body: { ok: true } }; }; diff --git a/lib/server/routes/onboarding.js b/lib/server/routes/onboarding.js index 23696661..ab92803b 100644 --- a/lib/server/routes/onboarding.js +++ b/lib/server/routes/onboarding.js @@ -95,6 +95,28 @@ const registerOnboardingRoutes = ({ getBaseUrl, runOnboardedBootSequence, }) => { + const kOnboardingProgressMessages = { + creating_repo: "Creating repo...", + running_openclaw_onboard: "Running openclaw onboard...", + initial_git_push: "Initial git commit/push...", + starting_gateway: "Starting gateway...", + }; + let onboardingProgress = { + active: false, + stage: "idle", + message: "", + updatedAt: null, + }; + const setOnboardingProgress = (stage, { active = true } = {}) => { + const normalizedStage = String(stage || "").trim(); + onboardingProgress = { + active, + stage: normalizedStage, + message: kOnboardingProgressMessages[normalizedStage] || "", + updatedAt: new Date().toISOString(), + }; + }; + // Keep mutating onboarding routes marker-gated so in-progress imports // can promote files before the final completion marker is written. const hasExplicitOnboardingMarker = () => @@ -157,21 +179,32 @@ const registerOnboardingRoutes = ({ res.json({ onboarded: hasExplicitOnboardingMarker() }); }); + app.get("/api/onboard/progress", (req, res) => { + res.json(onboardingProgress); + }); + app.post("/api/onboard", async (req, res) => { if (hasExplicitOnboardingMarker()) return res.json({ ok: false, error: "Already onboarded" }); try { + setOnboardingProgress("creating_repo"); const { vars, modelKey, importMode } = req.body; const result = await onboardingService.completeOnboarding({ req, vars, modelKey, importMode: !!importMode, + onProgress: (stage) => setOnboardingProgress(stage), }); + setOnboardingProgress( + result.body?.ok ? "starting_gateway" : "failed", + { active: false }, + ); res.status(result.status).json(result.body); } catch (err) { console.error("[onboard] Error:", err); + setOnboardingProgress("failed", { active: false }); res.status(500).json({ ok: false, error: sanitizeOnboardingError(err) }); } }); diff --git a/tests/frontend/api.test.js b/tests/frontend/api.test.js index 58c60a4e..5f982b06 100644 --- a/tests/frontend/api.test.js +++ b/tests/frontend/api.test.js @@ -67,6 +67,24 @@ describe("frontend/api", () => { expect(result).toEqual({ ok: true }); }); + it("fetchOnboardProgress returns the current onboarding milestone", async () => { + const payload = { + active: true, + stage: "running_openclaw_onboard", + message: "Running openclaw onboard...", + }; + global.fetch.mockResolvedValue(mockJsonResponse(200, payload)); + const api = await loadApiModule(); + + const result = await api.fetchOnboardProgress(); + + expect(global.fetch).toHaveBeenCalledWith( + "/api/onboard/progress", + expect.objectContaining({ headers: expect.any(Headers) }), + ); + expect(result).toEqual(payload); + }); + it("verifyGithubOnboardingRepo posts repo, token, and mode", async () => { global.fetch.mockResolvedValue(mockJsonResponse(200, { ok: true, repoExists: true })); const api = await loadApiModule(); From cd66563575bcd1afe6ce8fe2d6bbbb86598c2980 Mon Sep 17 00:00:00 2001 From: Chrys Bader Date: Tue, 14 Jul 2026 17:35:35 -0700 Subject: [PATCH 3/3] Harden deployment template release preflight --- .cursor/skills/release-publish/SKILL.md | 41 ++++++++++++++++++++----- AGENTS.md | 2 +- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.cursor/skills/release-publish/SKILL.md b/.cursor/skills/release-publish/SKILL.md index 0b8fa9d0..ceefe703 100644 --- a/.cursor/skills/release-publish/SKILL.md +++ b/.cursor/skills/release-publish/SKILL.md @@ -101,8 +101,12 @@ Once approved, execute in order: 1. **Ensure clean state**: `git status` should show no uncommitted changes. Switch to `main` if not already there. -2. **Run tests**: `npm test` — abort if any fail. -3. **Verify the OpenClaw dependency**: if AlphaClaw depends on a pinned +2. **Preflight stable template access**: for a stable release, run the Render + repository permission and remote checks in Phase 4.5 before changing the + AlphaClaw version. Abort if the authenticated user cannot push to Render's + template; all deployment templates must remain in sync with the release. +3. **Run tests**: `npm test` — abort if any fail. +4. **Verify the OpenClaw dependency**: if AlphaClaw depends on a pinned `openclaw` version, confirm `package-lock.json` and the local install resolve to the same version before publishing. ``` @@ -115,10 +119,10 @@ Once approved, execute in order: ``` grep -R -n "allowConversationAccess" node_modules/openclaw/dist/zod-schema-* node_modules/openclaw/dist/runtime-schema-* ``` -4. **Bump version**: `npm version ` (creates commit + tag). -5. **Push**: `git push && git push --tags`. -6. **Publish to npm**: `npm publish` (publishes to `latest` tag). -7. **Create GitHub release**: +5. **Bump version**: `npm version ` (creates commit + tag). +6. **Push**: `git push && git push --tags`. +7. **Publish to npm**: `npm publish` (publishes to `latest` tag). +8. **Create GitHub release**: ``` gh release create v --title "AlphaClaw " --notes "" ``` @@ -136,7 +140,8 @@ Repos: - `~/Projects/openclaw-railway-template` (typically `main` for production; `beta` only when cutting a beta — see release-beta skill; merge `main` into `beta` after stable pins when you want `beta` to match production pins) -- `~/Projects/openclaw-render-template` (typically `main`) +- `~/Projects/openclaw-render-template` (typically `main`; this checkout must + track `https://github.com/render-examples/openclaw-render-template.git`) - `~/Projects/openclaw-apex-template` (typically `main`) For **each** repo: @@ -159,6 +164,28 @@ For **each** repo: 5. Commit and push (include both `package.json` and `package-lock.json` when the lockfile exists or was added). +Before updating the Render template, verify that the authenticated GitHub user +can push to Render's repository and that the local checkout targets it: + +``` +gh api repos/render-examples/openclaw-render-template --jq '.permissions.push' +git -C ~/Projects/openclaw-render-template remote get-url origin +``` + +The permission check must return `true`, and `origin` must be +`https://github.com/render-examples/openclaw-render-template.git` (or the SSH +equivalent). If the checkout still targets the former `chrysb` repository, +update it before syncing: + +``` +git -C ~/Projects/openclaw-render-template remote set-url origin https://github.com/render-examples/openclaw-render-template.git +git -C ~/Projects/openclaw-render-template fetch origin +``` + +Stop before changing or publishing any template if push access is missing; ask +an administrator of `render-examples/openclaw-render-template` to grant the +authenticated user write access, then repeat the preflight. + Do not skip Render or Apex: pinning only one template while others stay on `latest` causes drift and non-reproducible installs between platforms. diff --git a/AGENTS.md b/AGENTS.md index 666413b6..f53fc05e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,7 +87,7 @@ Use this release flow when promoting tested beta builds to production: - `npm version 0.3.2` - `git push && git push --tags` - `npm publish` (publishes to `latest`) - - Pin all deployment templates on `main` to that release: set `@chrysb/alphaclaw` in `~/Projects/openclaw-railway-template`, `~/Projects/openclaw-render-template`, and `~/Projects/openclaw-apex-template` to the released version (templates rely on AlphaClaw’s declared `openclaw` dependency — do not add `package.json` `overrides` for `openclaw` unless you have a one-off debug reason). Run `npm install` in each repo, confirm `npm ls openclaw` matches AlphaClaw’s `package.json` pin, commit `package.json` and `package-lock.json`, and push. Skipping a template leaves it stale relative to the others. + - Pin all deployment templates on `main` to that release: set `@chrysb/alphaclaw` in `~/Projects/openclaw-railway-template`, `~/Projects/openclaw-render-template`, and `~/Projects/openclaw-apex-template` to the released version. The Render checkout must track `render-examples/openclaw-render-template`; verify `gh api repos/render-examples/openclaw-render-template --jq '.permissions.push'` returns `true` before publishing, and stop if write access is missing. Templates rely on AlphaClaw’s declared `openclaw` dependency — do not add `package.json` `overrides` for `openclaw` unless you have a one-off debug reason. Run `npm install` in each repo, confirm `npm ls openclaw` matches AlphaClaw’s `package.json` pin, commit `package.json` and `package-lock.json`, and push. Skipping a template leaves it stale relative to the others. 5. Return templates to production channel: - `@chrysb/alphaclaw: "latest"` 6. Optionally keep beta branch/tag flows active for next release cycle.