From 71c39aa18e7c95e7f2781780e3ed69f736ca8ce2 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 9 Aug 2026 20:11:13 +0200 Subject: [PATCH] fix: restore Workshop runtime URLs --- apps/docs/docs/management/workshop/index.mdx | 5 +++-- apps/docs/docusaurus.config.ts | 3 +++ .../src/components/workshop/DetailPage.tsx | 3 ++- apps/docs/src/lib/runtime-config.spec.ts | 19 +++++++++++++++++++ apps/docs/src/lib/runtime-config.ts | 16 ++++++++++++++++ apps/docs/src/pages/workshop/admin/index.tsx | 4 +++- apps/docs/src/pages/workshop/index.tsx | 3 ++- apps/docs/static/workshop-runtime-config.js | 1 + apps/workshop/Dockerfile | 8 ++++++++ apps/workshop/README.md | 3 ++- apps/workshop/docker-compose.yml | 3 +++ apps/workshop/entrypoint.sh | 18 ++++++++++++++++++ apps/workshop/tests/workshop-contracts.mjs | 11 +++++++++++ 13 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 apps/docs/src/lib/runtime-config.spec.ts create mode 100644 apps/docs/src/lib/runtime-config.ts create mode 100644 apps/docs/static/workshop-runtime-config.js diff --git a/apps/docs/docs/management/workshop/index.mdx b/apps/docs/docs/management/workshop/index.mdx index 7caffcd42..472111f22 100644 --- a/apps/docs/docs/management/workshop/index.mdx +++ b/apps/docs/docs/management/workshop/index.mdx @@ -108,8 +108,9 @@ The public URL contract is: | `WORKSHOP_PUBLIC_ORIGIN` | PocketBase-generated links and email | Required in production | | `PB_ALLOWED_ORIGINS` | PocketBase browser origins | `*` for the central community | -The deprecated docs-only `WORKSHOP_URL` alias remains for one release; `WORKSHOP_API_URL` takes precedence. Restart -services after changing these values. +The deprecated docs-only `WORKSHOP_URL` alias remains for one release; `WORKSHOP_API_URL` takes precedence. The +combined Workshop image writes these public URLs into its browser runtime configuration whenever the container starts; +the Docker build arguments remain fallbacks. Restart the container after changing these values, without rebuilding it. Back up application and PocketBase data before deployment. Publish the combined image by immutable digest, test the exact digest in staging, rehearse application rollback and PocketBase restore, then promote that same digest. diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index 13040b826..895dab602 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -12,6 +12,7 @@ if (deprecatedWorkshopUrl && !process.env.WORKSHOP_API_URL) { const publicUrls = resolveHomarrUrlConfig({ homarrWebsiteUrl: process.env.HOMARR_WEBSITE_URL, workshopApiUrl: process.env.WORKSHOP_API_URL ?? deprecatedWorkshopUrl, + workshopWebUrl: process.env.WORKSHOP_WEB_URL, }); const config: Config = { title: "Homarr documentation", @@ -24,7 +25,9 @@ const config: Config = { projectName: "homarr", customFields: { workshopUrl: publicUrls.workshopApiUrl, + workshopWebUrl: publicUrls.workshopWebUrl, }, + scripts: [{ src: "/workshop-runtime-config.js" }], i18n: { defaultLocale: "en", locales: ["en"], diff --git a/apps/docs/src/components/workshop/DetailPage.tsx b/apps/docs/src/components/workshop/DetailPage.tsx index 9c04274b1..79fb42408 100644 --- a/apps/docs/src/components/workshop/DetailPage.tsx +++ b/apps/docs/src/components/workshop/DetailPage.tsx @@ -29,6 +29,7 @@ import { toast } from "sonner"; import type { WorkshopSubmission, WorkshopVote } from "@site/src/lib/pocketbase"; import { getWorkshopBackend } from "@site/src/lib/pocketbase"; +import { getRuntimeWorkshopApiUrl } from "@site/src/lib/runtime-config"; import type { SubmissionType } from "@site/src/lib/workshop-schema"; import type { HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core"; import { @@ -1028,7 +1029,7 @@ export default function MarketplaceDetailPage() { }> {() => ( - + )} diff --git a/apps/docs/src/lib/runtime-config.spec.ts b/apps/docs/src/lib/runtime-config.spec.ts new file mode 100644 index 000000000..90d3a9ed6 --- /dev/null +++ b/apps/docs/src/lib/runtime-config.spec.ts @@ -0,0 +1,19 @@ +import { afterEach, describe, expect, test } from "vitest"; + +import { getRuntimeWorkshopApiUrl } from "./runtime-config"; + +describe("Workshop runtime configuration", () => { + afterEach(() => { + delete window.homarrRuntimeConfig; + }); + + test("prefers the URL supplied when the container starts", () => { + window.homarrRuntimeConfig = { workshopApiUrl: "https://v2.preview.homarr.dev" }; + + expect(getRuntimeWorkshopApiUrl("https://homarr.dev")).toBe("https://v2.preview.homarr.dev"); + }); + + test("keeps the build-time URL as a fallback", () => { + expect(getRuntimeWorkshopApiUrl("https://homarr.dev")).toBe("https://homarr.dev"); + }); +}); diff --git a/apps/docs/src/lib/runtime-config.ts b/apps/docs/src/lib/runtime-config.ts new file mode 100644 index 000000000..cf8cb5e3e --- /dev/null +++ b/apps/docs/src/lib/runtime-config.ts @@ -0,0 +1,16 @@ +interface HomarrRuntimeConfig { + homarrWebsiteUrl?: string; + workshopApiUrl?: string; + workshopWebUrl?: string; +} + +declare global { + interface Window { + homarrRuntimeConfig?: Readonly; + } +} + +export const getRuntimeWorkshopApiUrl = (configuredUrl: string): string => { + if (typeof window === "undefined") return configuredUrl; + return window.homarrRuntimeConfig?.workshopApiUrl || configuredUrl || window.location.origin; +}; diff --git a/apps/docs/src/pages/workshop/admin/index.tsx b/apps/docs/src/pages/workshop/admin/index.tsx index a8baf5bcf..10244048a 100644 --- a/apps/docs/src/pages/workshop/admin/index.tsx +++ b/apps/docs/src/pages/workshop/admin/index.tsx @@ -3,12 +3,14 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; import { WorkshopAdmin } from "@site/src/components/workshop/WorkshopAdmin"; +import { getRuntimeWorkshopApiUrl } from "@site/src/lib/runtime-config"; + export default function WorkshopAdminRoute() { const { siteConfig } = useDocusaurusContext(); const workshopUrl = (siteConfig.customFields?.workshopUrl as string | undefined) ?? ""; return ( - + ); } diff --git a/apps/docs/src/pages/workshop/index.tsx b/apps/docs/src/pages/workshop/index.tsx index f7fadabf8..e7b8e3a82 100644 --- a/apps/docs/src/pages/workshop/index.tsx +++ b/apps/docs/src/pages/workshop/index.tsx @@ -5,6 +5,7 @@ import Layout from "@theme/Layout"; import { WorkshopApp, WorkshopListingFallback } from "@site/src/components/workshop/WorkshopApp"; import { WorkshopErrorBoundary } from "@site/src/components/workshop/WorkshopErrorBoundary"; +import { getRuntimeWorkshopApiUrl } from "@site/src/lib/runtime-config"; export default function WorkshopPage() { const { siteConfig } = useDocusaurusContext(); @@ -16,7 +17,7 @@ export default function WorkshopPage() { }> {() => ( - + )} diff --git a/apps/docs/static/workshop-runtime-config.js b/apps/docs/static/workshop-runtime-config.js new file mode 100644 index 000000000..6fd1814b0 --- /dev/null +++ b/apps/docs/static/workshop-runtime-config.js @@ -0,0 +1 @@ +globalThis.homarrRuntimeConfig ??= Object.freeze({}); diff --git a/apps/workshop/Dockerfile b/apps/workshop/Dockerfile index cbae362db..b48eb2509 100644 --- a/apps/workshop/Dockerfile +++ b/apps/workshop/Dockerfile @@ -18,8 +18,10 @@ RUN --mount=type=cache,id=workshop-pnpm-store,target=/pnpm/store pnpm config set FROM dependencies AS website ARG HOMARR_WEBSITE_URL=https://homarr.dev ARG WORKSHOP_API_URL=https://homarr.dev +ARG WORKSHOP_WEB_URL=https://homarr.dev/workshop ENV HOMARR_WEBSITE_URL=$HOMARR_WEBSITE_URL ENV WORKSHOP_API_URL=$WORKSHOP_API_URL +ENV WORKSHOP_WEB_URL=$WORKSHOP_WEB_URL RUN CI=true SKIP_ENV_VALIDATION=true pnpm turbo build --filter=@homarr/docs FROM ${POCKETBASE_IMAGE} AS pocketbase-runtime @@ -34,6 +36,12 @@ EXPOSE 8090 HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=6 CMD wget -q --spider "http://127.0.0.1:${PORT}/api/health" || exit 1 FROM pocketbase-runtime AS production +ARG HOMARR_WEBSITE_URL=https://homarr.dev +ARG WORKSHOP_API_URL=https://homarr.dev +ARG WORKSHOP_WEB_URL=https://homarr.dev/workshop +ENV HOMARR_WEBSITE_URL=$HOMARR_WEBSITE_URL +ENV WORKSHOP_API_URL=$WORKSHOP_API_URL +ENV WORKSHOP_WEB_URL=$WORKSHOP_WEB_URL COPY --from=website /app/apps/docs/build /pb_public COPY apps/workshop/pb_migrations /pb_migrations COPY apps/workshop/pb_hooks /pb_hooks diff --git a/apps/workshop/README.md b/apps/workshop/README.md index a66208514..09807d893 100644 --- a/apps/workshop/README.md +++ b/apps/workshop/README.md @@ -70,7 +70,8 @@ pnpm dev `WORKSHOP_API_URL` controls API calls. `WORKSHOP_WEB_URL` controls links from Homarr. `HOMARR_WEBSITE_URL` is the documentation/site base. All three must be absolute HTTP(S) URLs. The old Docusaurus-only `WORKSHOP_URL` variable -remains a deprecated alias for one release. +remains a deprecated alias for one release. The production image writes these values into the browser runtime +configuration at container startup, so changing them requires a restart but not an image rebuild. ## Optional GitHub OAuth diff --git a/apps/workshop/docker-compose.yml b/apps/workshop/docker-compose.yml index c6eac0034..350538104 100644 --- a/apps/workshop/docker-compose.yml +++ b/apps/workshop/docker-compose.yml @@ -33,9 +33,12 @@ services: args: HOMARR_WEBSITE_URL: ${HOMARR_WEBSITE_URL:-http://localhost:3003} WORKSHOP_API_URL: ${WORKSHOP_API_URL:-http://localhost:3003} + WORKSHOP_WEB_URL: ${WORKSHOP_WEB_URL:-http://localhost:3003/workshop} environment: + HOMARR_WEBSITE_URL: ${HOMARR_WEBSITE_URL:-http://localhost:3003} PB_ALLOWED_ORIGINS: ${PB_ALLOWED_ORIGINS:-*} PORT: ${DOCS_EXPOSE_PORT:-3003} + WORKSHOP_API_URL: ${WORKSHOP_API_URL:-http://localhost:3003} WORKSHOP_WEB_URL: ${WORKSHOP_WEB_URL:-http://localhost:3003/workshop} GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-} diff --git a/apps/workshop/entrypoint.sh b/apps/workshop/entrypoint.sh index 84a04ade4..31d204ed7 100644 --- a/apps/workshop/entrypoint.sh +++ b/apps/workshop/entrypoint.sh @@ -6,6 +6,24 @@ if [ "$(id -u)" -eq 0 ]; then exec su -p -s /bin/sh pocketbase -c 'exec /usr/local/bin/workshop-entrypoint' fi +HOMARR_WEBSITE_URL=${HOMARR_WEBSITE_URL:-https://homarr.dev} +WORKSHOP_API_URL=${WORKSHOP_API_URL:-$HOMARR_WEBSITE_URL} +WORKSHOP_WEB_URL=${WORKSHOP_WEB_URL:-$HOMARR_WEBSITE_URL/workshop} + +encode_runtime_value() { + printf '%s' "$1" | base64 | tr -d '\n' +} + +cat >/pb_public/workshop-runtime-config.js < + new TextDecoder().decode(Uint8Array.from(atob(value), (character) => character.charCodeAt(0))); +globalThis.homarrRuntimeConfig = Object.freeze({ + homarrWebsiteUrl: decodeRuntimeValue("$(encode_runtime_value "$HOMARR_WEBSITE_URL")"), + workshopApiUrl: decodeRuntimeValue("$(encode_runtime_value "$WORKSHOP_API_URL")"), + workshopWebUrl: decodeRuntimeValue("$(encode_runtime_value "$WORKSHOP_WEB_URL")") +}); +EOF + exec pocketbase serve \ --http="0.0.0.0:${PORT:-8090}" \ --origins="${PB_ALLOWED_ORIGINS:-*}" \ diff --git a/apps/workshop/tests/workshop-contracts.mjs b/apps/workshop/tests/workshop-contracts.mjs index 7219fe970..ed4d59333 100644 --- a/apps/workshop/tests/workshop-contracts.mjs +++ b/apps/workshop/tests/workshop-contracts.mjs @@ -54,4 +54,15 @@ for (const required of [ const workflow = await read(".github/workflows/workshop.yml"); if (/\n\s+paths:/u.test(workflow)) throw new Error("Workshop workflow must not filter out Docker build inputs"); +const entrypoint = await read("apps/workshop/entrypoint.sh"); +if (!entrypoint.includes("/pb_public/workshop-runtime-config.js") || !entrypoint.includes("WORKSHOP_API_URL")) { + throw new Error("Workshop must publish its API URL when the container starts"); +} + +const docsConfig = await read("apps/docs/docusaurus.config.ts"); +if (!docsConfig.includes('scripts: [{ src: "/workshop-runtime-config.js" }]')) { + throw new Error("Workshop must load the container runtime configuration before the documentation bundle"); +} +await access(resolve("apps/docs/static/workshop-runtime-config.js")); + console.log("Workshop static contracts passed");