mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
ci: deploy demo automatically
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
name: Deploy Demo to Cloudflare Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "apps/demo/**"
|
||||
- "apps/web/**"
|
||||
- "packages/shared/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
- "tsconfig.base.json"
|
||||
- ".github/actions/setup/**"
|
||||
- ".github/workflows/deploy-demo.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-demo
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Demo
|
||||
run: pnpm --filter @snapotter/demo build
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
run: npx wrangler pages deploy apps/demo/dist --project-name snapotter-demo --branch main --commit-dirty=true
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
@@ -1,3 +1,7 @@
|
||||
import packageJson from "../../../package.json";
|
||||
|
||||
const DEMO_APP_VERSION = packageJson.version;
|
||||
|
||||
const PERMISSIONS = [
|
||||
"tools:use",
|
||||
"files:own",
|
||||
@@ -108,11 +112,18 @@ function json(data: unknown, status = 200): Response {
|
||||
});
|
||||
}
|
||||
|
||||
function matchRoute(url: string, method: string): Response | null {
|
||||
export function matchDemoRoute(url: string, method: string): Response | null {
|
||||
const path = new URL(url, "http://localhost").pathname;
|
||||
|
||||
if (path === "/api/v1/config/auth" && method === "GET") {
|
||||
return json({ authEnabled: true, oidcEnabled: false });
|
||||
return json({
|
||||
authEnabled: true,
|
||||
oidcEnabled: false,
|
||||
oidcProviderName: null,
|
||||
samlEnabled: false,
|
||||
samlProviderName: null,
|
||||
ssoEnforced: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (path === "/api/auth/login" && method === "POST") {
|
||||
@@ -145,7 +156,7 @@ function matchRoute(url: string, method: string): Response | null {
|
||||
}
|
||||
|
||||
if (path === "/api/v1/health") {
|
||||
return json({ status: "ok", version: "1.17.0" });
|
||||
return json({ status: "ok", version: DEMO_APP_VERSION });
|
||||
}
|
||||
|
||||
if (path === "/api/v1/settings" && method === "GET") {
|
||||
@@ -261,14 +272,14 @@ function matchRoute(url: string, method: string): Response | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
|
||||
export function installMocks() {
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
|
||||
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
const method = init?.method?.toUpperCase() || "GET";
|
||||
|
||||
const mock = matchRoute(url, method);
|
||||
const mock = matchDemoRoute(url, method);
|
||||
if (mock) return mock;
|
||||
|
||||
return originalFetch(input, init);
|
||||
@@ -288,7 +299,7 @@ export function installMocks() {
|
||||
|
||||
send(body?: Document | XMLHttpRequestBodyInit | null) {
|
||||
if (this._url.startsWith("/api/")) {
|
||||
const mock = matchRoute(this._url, this._method);
|
||||
const mock = matchDemoRoute(this._url, this._method);
|
||||
if (mock) {
|
||||
setTimeout(async () => {
|
||||
const responseText = await mock.text();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { matchDemoRoute } from "../../../apps/demo/src/mock-api.js";
|
||||
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(path.resolve(process.cwd(), "package.json"), "utf8"),
|
||||
) as { version: string };
|
||||
|
||||
async function readJson(response: Response): Promise<unknown> {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
describe("demo mock API", () => {
|
||||
it("reports the same app version as the current package build", async () => {
|
||||
const response = matchDemoRoute("/api/v1/health", "GET");
|
||||
|
||||
expect(response?.status).toBe(200);
|
||||
await expect(readJson(response as Response)).resolves.toMatchObject({
|
||||
status: "ok",
|
||||
version: packageJson.version,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the full auth configuration shape consumed by the real web app", async () => {
|
||||
const response = matchDemoRoute("/api/v1/config/auth", "GET");
|
||||
|
||||
expect(response?.status).toBe(200);
|
||||
await expect(readJson(response as Response)).resolves.toEqual({
|
||||
authEnabled: true,
|
||||
oidcEnabled: false,
|
||||
oidcProviderName: null,
|
||||
samlEnabled: false,
|
||||
samlProviderName: null,
|
||||
ssoEnforced: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const workflowPath = path.resolve(process.cwd(), ".github/workflows/deploy-demo.yml");
|
||||
|
||||
function workflowText(): string {
|
||||
expect(existsSync(workflowPath)).toBe(true);
|
||||
return readFileSync(workflowPath, "utf8");
|
||||
}
|
||||
|
||||
describe("demo deployment workflow", () => {
|
||||
it("deploys the demo app to the Cloudflare Pages demo project", () => {
|
||||
const workflow = workflowText();
|
||||
|
||||
expect(workflow).toContain("name: Deploy Demo to Cloudflare Pages");
|
||||
expect(workflow).toContain("pnpm --filter @snapotter/demo build");
|
||||
expect(workflow).toContain(
|
||||
"npx wrangler pages deploy apps/demo/dist --project-name snapotter-demo --branch main --commit-dirty=true",
|
||||
);
|
||||
expect(workflow).toContain("CLOUDFLARE_API_TOKEN: $" + "{{ secrets.CLOUDFLARE_API_TOKEN }}");
|
||||
expect(workflow).toContain("CLOUDFLARE_ACCOUNT_ID: $" + "{{ secrets.CLOUDFLARE_ACCOUNT_ID }}");
|
||||
});
|
||||
|
||||
it("rebuilds when the demo shell, real app UI, shared code, or build inputs change", () => {
|
||||
const workflow = workflowText();
|
||||
|
||||
for (const pathFilter of [
|
||||
'"apps/demo/**"',
|
||||
'"apps/web/**"',
|
||||
'"packages/shared/**"',
|
||||
'"package.json"',
|
||||
'"pnpm-lock.yaml"',
|
||||
'"pnpm-workspace.yaml"',
|
||||
'".github/actions/setup/**"',
|
||||
'".github/workflows/deploy-demo.yml"',
|
||||
]) {
|
||||
expect(workflow).toContain(pathFilter);
|
||||
}
|
||||
|
||||
expect(workflow).toContain("branches: [main]");
|
||||
expect(workflow).toContain("workflow_dispatch:");
|
||||
expect(workflow).toContain("concurrency:");
|
||||
expect(workflow).toContain("group: deploy-demo");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user