Compare commits
4 Commits
main
...
cursor/ci-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dd4ff4ba1 | ||
|
|
1c3d8c40fe | ||
|
|
80c56eef36 | ||
|
|
1704d1cec9 |
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@ -246,6 +246,7 @@ jobs:
|
|||||||
publish-mobile-update:
|
publish-mobile-update:
|
||||||
name: Publish compatible mobile update
|
name: Publish compatible mobile update
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
continue-on-error: true
|
||||||
needs: [lint, check, build, test, test-integration, test-e2e]
|
needs: [lint, check, build, test, test-integration, test-e2e]
|
||||||
runs-on: [self-hosted, evoo-ci, Linux]
|
runs-on: [self-hosted, evoo-ci, Linux]
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@ -344,23 +345,30 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "publish=$publish" >> "$GITHUB_OUTPUT"
|
echo "publish=$publish" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Require Expo automation token
|
- name: Check Expo automation token
|
||||||
|
id: expo_token
|
||||||
if: steps.ota.outputs.publish == 'true'
|
if: steps.ota.outputs.publish == 'true'
|
||||||
env:
|
env:
|
||||||
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
if [[ -z "$EXPO_TOKEN" ]]; then
|
if [[ -z "$EXPO_TOKEN" ]]; then
|
||||||
echo "Add an EXPO_TOKEN repository secret with access to the inbox-zero/rakazo Expo project." >&2
|
{
|
||||||
exit 1
|
echo "### Mobile OTA skipped"
|
||||||
|
echo
|
||||||
|
echo "No \`EXPO_TOKEN\` repository secret is configured, so CI will not publish a production update. Add a token with access to the inbox-zero/rakazo Expo project when OTA from main is desired."
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "ready=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
echo "ready=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
||||||
if: steps.ota.outputs.publish == 'true'
|
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
||||||
id: pnpm-setup
|
id: pnpm-setup
|
||||||
with:
|
with:
|
||||||
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }}
|
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }}
|
||||||
- name: Isolate pnpm store and PATH
|
- name: Isolate pnpm store and PATH
|
||||||
if: steps.ota.outputs.publish == 'true'
|
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@ -377,25 +385,25 @@ jobs:
|
|||||||
mkdir -p "$store"
|
mkdir -p "$store"
|
||||||
echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV"
|
echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV"
|
||||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||||
if: steps.ota.outputs.publish == 'true'
|
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
- name: Set up Expo
|
- name: Set up Expo
|
||||||
if: steps.ota.outputs.publish == 'true'
|
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
||||||
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # v9
|
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # v9
|
||||||
with:
|
with:
|
||||||
eas-version: latest
|
eas-version: latest
|
||||||
packager: pnpm
|
packager: pnpm
|
||||||
token: ${{ secrets.EXPO_TOKEN }}
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.ota.outputs.publish == 'true'
|
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
- name: Verify mobile project
|
- name: Verify mobile project
|
||||||
if: steps.ota.outputs.publish == 'true'
|
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
||||||
run: pnpm --filter @rakazo/mobile check
|
run: pnpm --filter @rakazo/mobile check
|
||||||
- name: Publish production update
|
- name: Publish production update
|
||||||
if: steps.ota.outputs.publish == 'true'
|
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
|
||||||
working-directory: apps/mobile
|
working-directory: apps/mobile
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|||||||
@ -75,7 +75,11 @@ test("local settings open and save integrations without an app session", async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("ordinary browsers cannot use local settings", async ({ page }) => {
|
test("ordinary browsers cannot use local settings", async ({ page }) => {
|
||||||
await page.goto("/desktop-settings");
|
await expect(async () => {
|
||||||
await expect(page.getByRole("alert")).toContainText("Could not open local settings");
|
await page.goto("/desktop-settings", { waitUntil: "domcontentloaded" });
|
||||||
|
await expect(page.getByRole("alert")).toContainText("Could not open local settings", {
|
||||||
|
timeout: 5_000,
|
||||||
|
});
|
||||||
|
}).toPass({ timeout: 45_000 });
|
||||||
await expect(page.getByRole("button", { name: "Models", exact: true })).toBeHidden();
|
await expect(page.getByRole("button", { name: "Models", exact: true })).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import {
|
|||||||
showSessionUnavailable,
|
showSessionUnavailable,
|
||||||
} from "./lib/session-gate";
|
} from "./lib/session-gate";
|
||||||
import { IntegrationSetupPage } from "./pages/IntegrationSetup";
|
import { IntegrationSetupPage } from "./pages/IntegrationSetup";
|
||||||
import { LocalSettingsPage } from "./pages/LocalSettings";
|
|
||||||
import { McpOAuthCallbackPage } from "./pages/McpOAuthCallback";
|
import { McpOAuthCallbackPage } from "./pages/McpOAuthCallback";
|
||||||
import { ShellPage } from "./pages/Shell";
|
import { ShellPage } from "./pages/Shell";
|
||||||
|
|
||||||
@ -30,9 +29,18 @@ const OnboardingPage = lazyWithRecovery("pages/Onboarding", () =>
|
|||||||
const WelcomePage = lazyWithRecovery("pages/Welcome", () =>
|
const WelcomePage = lazyWithRecovery("pages/Welcome", () =>
|
||||||
import("./pages/Welcome").then((module) => ({ default: module.WelcomePage })),
|
import("./pages/Welcome").then((module) => ({ default: module.WelcomePage })),
|
||||||
);
|
);
|
||||||
|
const LocalSettingsPage = lazyWithRecovery("pages/LocalSettings", () =>
|
||||||
|
import("./pages/LocalSettings").then((module) => ({ default: module.LocalSettingsPage })),
|
||||||
|
);
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
if (window.location.pathname === LOCAL_SETTINGS_PAGE) return <LocalSettingsPage />;
|
if (window.location.pathname === LOCAL_SETTINGS_PAGE) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="h-full bg-background" data-rakazo-app-state="loading" />}>
|
||||||
|
<LocalSettingsPage />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
return <SessionApp />;
|
return <SessionApp />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,31 @@
|
|||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { Button } from "@rakazo/ui-web";
|
import { Button } from "@rakazo/ui-web";
|
||||||
import { useEffect, useState } from "react";
|
import { Suspense, useEffect, useState } from "react";
|
||||||
import { IntegrationSetup } from "../components/integrations/IntegrationSetup";
|
import { desktopBridge } from "../lib/desktop";
|
||||||
|
import { lazyWithRecovery } from "../lib/lazy-recovery";
|
||||||
import { rpc } from "../lib/rpc";
|
import { rpc } from "../lib/rpc";
|
||||||
import { ModelSettingsOverlay } from "./ModelSettingsOverlay";
|
|
||||||
|
const IntegrationSetup = lazyWithRecovery("integrations/IntegrationSetup", () =>
|
||||||
|
import("../components/integrations/IntegrationSetup").then((module) => ({
|
||||||
|
default: module.IntegrationSetup,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const ModelSettingsOverlay = lazyWithRecovery("pages/ModelSettingsOverlay", () =>
|
||||||
|
import("./ModelSettingsOverlay").then((module) => ({ default: module.ModelSettingsOverlay })),
|
||||||
|
);
|
||||||
|
|
||||||
export function LocalSettingsPage() {
|
export function LocalSettingsPage() {
|
||||||
const [section, setSection] = useState<"models" | "integrations" | null>(null);
|
const [section, setSection] = useState<"models" | "integrations" | null>(null);
|
||||||
|
const hasLocalBridge = Boolean(desktopBridge()?.localSettings);
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(!hasLocalBridge);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!hasLocalBridge) return;
|
||||||
void rpc.integrationSetup
|
void rpc.integrationSetup
|
||||||
.get()
|
.get()
|
||||||
.then(() => setReady(true))
|
.then(() => setReady(true))
|
||||||
.catch(() => setError(true));
|
.catch(() => setError(true));
|
||||||
}, []);
|
}, [hasLocalBridge]);
|
||||||
return (
|
return (
|
||||||
<main className="h-full overflow-auto bg-background px-6 py-12">
|
<main className="h-full overflow-auto bg-background px-6 py-12">
|
||||||
<div className="mx-auto max-w-xl space-y-6">
|
<div className="mx-auto max-w-xl space-y-6">
|
||||||
@ -43,9 +54,15 @@ export function LocalSettingsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</nav>
|
</nav>
|
||||||
{section === "models" ? (
|
{section === "models" ? (
|
||||||
|
<Suspense fallback={null}>
|
||||||
<ModelSettingsOverlay onClose={() => setSection(null)} localOwner />
|
<ModelSettingsOverlay onClose={() => setSection(null)} localOwner />
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
|
{section === "integrations" ? (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<IntegrationSetup serverSetup managedOnly />
|
||||||
|
</Suspense>
|
||||||
) : null}
|
) : null}
|
||||||
{section === "integrations" ? <IntegrationSetup serverSetup managedOnly /> : null}
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
47
infra/updater/src/ci-workflow.test.ts
Normal file
47
infra/updater/src/ci-workflow.test.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
|
const workflowText = readFileSync(
|
||||||
|
path.resolve(import.meta.dirname, "../../../.github/workflows/ci.yml"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
interface WorkflowJob {
|
||||||
|
if?: string;
|
||||||
|
needs?: string[];
|
||||||
|
"continue-on-error"?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflow = parse(workflowText) as {
|
||||||
|
jobs: Record<string, WorkflowJob>;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("ci workflow merge gate", () => {
|
||||||
|
it("aggregates only product jobs in Product CI", () => {
|
||||||
|
const productGate = workflow.jobs["product-gate"];
|
||||||
|
expect(productGate?.needs).toEqual([
|
||||||
|
"lint",
|
||||||
|
"check",
|
||||||
|
"build",
|
||||||
|
"test",
|
||||||
|
"test-integration",
|
||||||
|
"test-e2e",
|
||||||
|
]);
|
||||||
|
expect(productGate?.if).toBe("always()");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("soft-fails mobile OTA so missing Expo credentials do not fail the workflow", () => {
|
||||||
|
const mobile = workflow.jobs["publish-mobile-update"];
|
||||||
|
expect(mobile?.["continue-on-error"]).toBe(true);
|
||||||
|
expect(workflowText).toContain('echo "ready=false" >> "$GITHUB_OUTPUT"');
|
||||||
|
expect(workflowText).not.toMatch(
|
||||||
|
/Check Expo automation token[\s\S]*?exit 1[\s\S]*?Require Expo/,
|
||||||
|
);
|
||||||
|
expect(workflowText).not.toContain(
|
||||||
|
"Add an EXPO_TOKEN repository secret with access to the inbox-zero/rakazo Expo project.",
|
||||||
|
);
|
||||||
|
expect(workflowText).toContain("steps.expo_token.outputs.ready == 'true'");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user