From af44520eecaf8b647dac8ef21c880c820b22e79b Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Wed, 8 Jul 2026 16:16:20 +0530 Subject: [PATCH 1/2] fix: unpublished project's env is not editable --- .../[project]/-/edit/+layout.svelte | 40 ++++++++++++------- .../actions/protected-files.ts | 2 +- .../entity-management/file-artifact.ts | 9 ++++- .../entity-management/file-artifacts.ts | 4 ++ 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte index cc67834718b4..815199de2bcf 100644 --- a/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte @@ -30,6 +30,7 @@ import { InfoIcon } from "lucide-svelte"; import { overlay } from "@rilldata/web-common/layout/overlay-store"; import BlockingOverlayContainer from "@rilldata/web-common/layout/BlockingOverlayContainer.svelte"; + import { fileArtifacts } from "@rilldata/web-common/features/entity-management/file-artifacts.ts"; $: organization = $page.params.organization; $: project = $page.params.project; @@ -83,8 +84,12 @@ // keeps the UI in loading state while the backend transitions STOPPED → PENDING → RUNNING. let starting = false; + // Wait for `primaryProjectQuery` too: the cloud readonly notice is gated on + // `hasPrimaryDeployment`, and it must be registered before `` renders + // any file editor (getReadonlyNotice reads the notice non-reactively). $: isLoading = $projectQuery.isPending || + $primaryProjectQuery.isPending || starting || deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING; @@ -119,7 +124,17 @@ }); }; - setCloudReadonlyNotice(envEditDisabled); + // Only surface the env notice once the project has a primary deployment. + // `isLoading` blocks `` until `primaryProjectQuery` resolves, so this + // has run before any file editor reads the notice. + $: if (!$primaryProjectQuery.isPending) { + setCloudReadonlyNotice( + $primaryProjectQuery.data?.project?.primaryDeploymentId + ? envEditDisabled + : undefined, + ); + fileArtifacts.recheckReadonlyStatus(); + } onDestroy(() => { $editorRoutePrefix = ""; @@ -161,7 +176,7 @@ bind:starting /> {:else if isReady && deployment?.id && instanceId && runtimeHost && jwt} - {#key `${runtimeHost}::${instanceId}`} + {#key `${runtimeHost}::${instanceId}::${hasPrimaryDeployment}`} {#if !inProjectWelcomePage} - {#if hasPrimaryDeployment} - Manage environment variables in - - Settings → - - {:else} - You can manage environment variables from settings page - after the project has been published. - {/if} + Manage environment variables in + + Settings → + {/snippet} diff --git a/web-common/src/features/entity-management/actions/protected-files.ts b/web-common/src/features/entity-management/actions/protected-files.ts index 598b64122a26..e6cead250362 100644 --- a/web-common/src/features/entity-management/actions/protected-files.ts +++ b/web-common/src/features/entity-management/actions/protected-files.ts @@ -40,7 +40,7 @@ export function setCloudReadonlyNotice(notice: Snippet | undefined) { export function isManaged(path: string): boolean { if (isCloudRuntimeEditEnvironment()) { - return CLOUD_READONLY.some((m) => m(path)); + return CLOUD_READONLY.some((m) => m(path)) && !!cloudReadonlyNotice; } return false; } diff --git a/web-common/src/features/entity-management/file-artifact.ts b/web-common/src/features/entity-management/file-artifact.ts index fba5e3c001d7..d4f6f0222c89 100644 --- a/web-common/src/features/entity-management/file-artifact.ts +++ b/web-common/src/features/entity-management/file-artifact.ts @@ -91,10 +91,10 @@ export class FileArtifact { readonly autoSave: Writable; // Path is locked: file can't be renamed or deleted, and other files can't // be renamed onto this path. - readonly pinned: boolean; + pinned: boolean; // Content is managed outside of editors. // Currently **/.*.env files are managed from project settings page on cloud editor - readonly managed: boolean; + managed: boolean; readonly snapshot: Writable<{ scroll?: ReturnType; selection?: EditorSelection; @@ -412,6 +412,11 @@ export class FileArtifact { ); } + recheckReadonlyStatus() { + this.pinned = isPinned(this.path); + this.managed = isManaged(this.path); + } + private updateResourceNameIfChanged(resource: V1Resource) { const isSubResource = !!resource.component?.spec?.definedInCanvas; if (isSubResource) return; diff --git a/web-common/src/features/entity-management/file-artifacts.ts b/web-common/src/features/entity-management/file-artifacts.ts index 7488bcdec863..7c1f97670bc2 100644 --- a/web-common/src/features/entity-management/file-artifacts.ts +++ b/web-common/src/features/entity-management/file-artifacts.ts @@ -187,6 +187,10 @@ export class FileArtifacts { const fileParseErrors = fileArtifact.fetchParserErrors(queryClient); return fileParseErrors[0]?.message ?? null; } + + recheckReadonlyStatus() { + this.artifacts.forEach((artifact) => artifact.recheckReadonlyStatus()); + } } export const fileArtifacts = new FileArtifacts(); From 60ab798ad22a7e8682486ab8c613037ea81a31ee Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Thu, 9 Jul 2026 11:48:57 +0530 Subject: [PATCH 2/2] PR comments --- .../[project]/-/edit/+layout.svelte | 11 +++--- .../actions/protected-files.spec.ts | 34 ++++++++++++++++++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte index 815199de2bcf..8a6a5d7c521a 100644 --- a/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte @@ -125,14 +125,15 @@ }; // Only surface the env notice once the project has a primary deployment. + // Fail closed: env becomes editable only once we've positively confirmed the + // project has no primary deployment. A failed or otherwise inconclusive lookup + // keeps the notice set, so a published project never exposes editable `.env` + // files while the deployment state is unknown. // `isLoading` blocks `` until `primaryProjectQuery` resolves, so this // has run before any file editor reads the notice. $: if (!$primaryProjectQuery.isPending) { - setCloudReadonlyNotice( - $primaryProjectQuery.data?.project?.primaryDeploymentId - ? envEditDisabled - : undefined, - ); + const envEditable = $primaryProjectQuery.isSuccess && !hasPrimaryDeployment; + setCloudReadonlyNotice(envEditable ? undefined : envEditDisabled); fileArtifacts.recheckReadonlyStatus(); } diff --git a/web-common/src/features/entity-management/actions/protected-files.spec.ts b/web-common/src/features/entity-management/actions/protected-files.spec.ts index c4ea75d5a767..3101748f435e 100644 --- a/web-common/src/features/entity-management/actions/protected-files.spec.ts +++ b/web-common/src/features/entity-management/actions/protected-files.spec.ts @@ -1,6 +1,16 @@ import { beforeAll, afterAll, describe, expect, it } from "vitest"; +import type { Snippet } from "svelte"; import { setRuntimeEditEnvironment } from "../edit-environment.ts"; -import { isPinned, isProtectedDirectory, isManaged } from "./protected-files"; +import { + isPinned, + isProtectedDirectory, + isManaged, + setCloudReadonlyNotice, +} from "./protected-files"; + +// A published project registers a readonly notice; the content is irrelevant to +// these predicates, so a placeholder snippet stands in for it. +const notice = (() => {}) as unknown as Snippet; describe("isPinned", () => { it("matches /rill.yaml exactly", () => { @@ -35,10 +45,13 @@ describe("isReadonly on local", () => { describe("isReadonly on cloud", () => { beforeAll(() => { setRuntimeEditEnvironment("cloud"); + // Simulate a published project, where env editing is managed from settings. + setCloudReadonlyNotice(notice); }); afterAll(() => { setRuntimeEditEnvironment("local"); + setCloudReadonlyNotice(undefined); }); it("locks /.env at the project root", () => { @@ -70,6 +83,25 @@ describe("isReadonly on cloud", () => { }); }); +describe("isReadonly on cloud without a readonly notice", () => { + beforeAll(() => { + setRuntimeEditEnvironment("cloud"); + // No notice registered: the project has no primary deployment (unpublished), + // or the deployment lookup has not yet resolved. + setCloudReadonlyNotice(undefined); + }); + + afterAll(() => { + setRuntimeEditEnvironment("local"); + }); + + it("does not lock .env files", () => { + expect(isManaged("/.env")).toBe(false); + expect(isManaged("/foo/.env")).toBe(false); + expect(isManaged("/.dev.env")).toBe(false); + }); +}); + describe("isProtectedDirectory", () => { it("matches /tmp exactly", () => { expect(isProtectedDirectory("/tmp")).toBe(true);