fix: restore Workshop runtime URLs

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:51:25 +02:00
1 parent 000c51a131
commit 71c39aa18e
13 files changed
+91 -6

No files matched your search

+3 -2
View File
@@ -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.
+3
View File
@@ -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"],
@@ -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() {
<BrowserOnly fallback={<DetailSkeleton />}>
{() => (
<WorkshopErrorBoundary>
<MarketplaceDetail workshopUrl={configuredWorkshopUrl || window.location.origin} />
<MarketplaceDetail workshopUrl={getRuntimeWorkshopApiUrl(configuredWorkshopUrl)} />
</WorkshopErrorBoundary>
)}
</BrowserOnly>
+19
View File
@@ -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");
});
});
+16
View File
@@ -0,0 +1,16 @@
interface HomarrRuntimeConfig {
homarrWebsiteUrl?: string;
workshopApiUrl?: string;
workshopWebUrl?: string;
}
declare global {
interface Window {
homarrRuntimeConfig?: Readonly<HomarrRuntimeConfig>;
}
}
export const getRuntimeWorkshopApiUrl = (configuredUrl: string): string => {
if (typeof window === "undefined") return configuredUrl;
return window.homarrRuntimeConfig?.workshopApiUrl || configuredUrl || window.location.origin;
};
+3 -1
View File
@@ -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 (
<Layout title="Workshop moderation" noFooter>
<WorkshopAdmin workshopUrl={workshopUrl} />
<WorkshopAdmin workshopUrl={getRuntimeWorkshopApiUrl(workshopUrl)} />
</Layout>
);
}
+2 -1
View File
@@ -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() {
<BrowserOnly fallback={<WorkshopListingFallback />}>
{() => (
<WorkshopErrorBoundary>
<WorkshopApp workshopUrl={configuredWorkshopUrl || window.location.origin} />
<WorkshopApp workshopUrl={getRuntimeWorkshopApiUrl(configuredWorkshopUrl)} />
</WorkshopErrorBoundary>
)}
</BrowserOnly>
+1
View File
@@ -0,0 +1 @@
globalThis.homarrRuntimeConfig ??= Object.freeze({});
+8
View File
@@ -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
+2 -1
View File
@@ -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
+3
View File
@@ -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:-}
+18
View File
@@ -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 <<EOF
const decodeRuntimeValue = (value) =>
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:-*}" \
@@ -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");