Repository: TobikoData/sqlmesh
Pull request #1885 (2023-12-21 23:43:00)

Feat: cicd bot add include_unmodified


Patch:
diff --git a/docs/integrations/github.md b/docs/integrations/github.md
index 1ab43dbb..95e338ec 100644
--- a/docs/integrations/github.md
+++ b/docs/integrations/github.md
@@ -292,7 +292,7 @@ Below is an example of how to define the default config for the bot in either YA
 | `auto_categorize_changes`             | Auto categorization behavior to use for the bot. If not provided then the project-wide categorization behavior is used. See [Auto-categorize model changes](https://sqlmesh.readthedocs.io/en/stable/guides/configuration/#auto-categorize-model-changes) for details. |  dict  |    N     |
 | `default_pr_start`                    | Default start when creating PR environment plans. If running in a mode where the bot automatically backfills models (based on `auto_categorize_changes` behavior) then this can be used to limit the amount of data backfilled. Defaults to `1 day ago`                |  str   |    N     |
 | `skip_pr_backfill`                    | Indicates if the bot should skip backfilling models in the PR environment. Default: `True`                                                                                                                                                                             |  bool  |    N     |
-
+| `pr_include_unmodified`               | Indicates whether to include unmodified models in the PR environment. Default to the project's config value (which defaults to `False`)                                                                                                                                |  bool  |    N     |
 
 Example with all properties defined:
 
diff --git a/sqlmesh/integrations/github/cicd/config.py b/sqlmesh/integrations/github/cicd/config.py
index b6383022..daeb6532 100644
--- a/sqlmesh/integrations/github/cicd/config.py
+++ b/sqlmesh/integrations/github/cicd/config.py
@@ -31,6 +31,7 @@ class GithubCICDBotConfig(BaseConfig):
     auto_categorize_changes: CategorizerConfig = CategorizerConfig.all_off()
     default_pr_start: t.Optional[TimeLike] = "1 day ago"
     skip_pr_backfill: bool = True
+    pr_include_unmodified: t.Optional[bool] = None
 
     @model_validator(mode="before")
     @model_validator_v1_args
diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py
index 2cfec1e2..9d90eca1 100644
--- a/sqlmesh/integrations/github/cicd/controller.py
+++ b/sqlmesh/integrations/github/cicd/controller.py
@@ -391,6 +391,7 @@ class GithubController:
                 categorizer_config=self.bot_config.auto_categorize_changes,
                 start=self.bot_config.default_pr_start,
                 skip_backfill=self.bot_config.skip_pr_backfill,
+                include_unmodified=self.bot_config.pr_include_unmodified,
             )
         return self._pr_plan
 
diff --git a/web/client/src/context/context.ts b/web/client/src/context/context.ts
index 746aa668..02c042b3 100644
--- a/web/client/src/context/context.ts
+++ b/web/client/src/context/context.ts
@@ -15,8 +15,6 @@ interface ContextStore {
   confirmations: Confirmation[]
   environment: ModelEnvironment
   environments: Set<ModelEnvironment>
-  defaultEnvironment?: ModelEnvironment
-  pinnedEnvironments: ModelEnvironment[]
   models: Map<string, ModelSQLMeshModel>
   setVersion: (version?: string) => void
   setShowConfirmation: (showConfirmation: boolean) => void
@@ -33,16 +31,17 @@ interface ContextStore {
     created_from: EnvironmentName,
   ) => void
   removeLocalEnvironment: (environments: ModelEnvironment) => void
-  addSynchronizedEnvironments: (
+  addRemoteEnvironments: (
     environments: Environment[],
     defaultEnvironment?: string,
     pinnedEnvironments?: string[],
   ) => void
-  hasSynchronizedEnvironments: () => boolean
+  hasRemoteEnvironments: () => boolean
 }
 
 const environments = new Set(ModelEnvironment.getEnvironments())
-const environment = environments.values().next().value
+const environment =
+  ModelEnvironment.getEnvironment() ?? environments.values().next().value
 
 export const useStoreContext = create<ContextStore>((set, get) => ({
   version: undefined,
@@ -50,8 +49,6 @@ export const useStoreContext = create<ContextStore>((set, get) => ({
   confirmations: [],
   environment,
   environments,
-  defaultEnvironment: undefined,
-  pinnedEnvironments: [],
   initialStartDate: undefined,
   initialEndDate: undefined,
   models: new Map(),
@@ -171,25 +168,26 @@ export const useStoreContext = create<ContextStore>((set, get) => ({
       }
     })
   },
-  addSynchronizedEnvironments(envs = [], defaultEnvironment, pinnedEnvs = []) {
+  addRemoteEnvironments(
+    remoteEnvironments = [],
+    defaultEnvironment,
+    pinnedEnvs = [],
+  ) {
     set(s => {
       const environments = Array.from(s.environments)
 
-      envs.forEach(env => {
+      remoteEnvironments.forEach(env => {
         let environment = environments.find(
           ({ name: envNameLocal }) => env.name === envNameLocal,
         )
 
         if (isNil(environment)) {
-          environment = new ModelEnvironment(
-            env,
-            EnumRelativeLocation.Synchronized,
-          )
+          environment = new ModelEnvironment(env, EnumRelativeLocation.Remote)
 
           environments.push(environment)
         } else {
           environment.update(env)
-          environment.setType(EnumRelativeLocation.Synchronized)
+          environment.setType(EnumRelativeLocation.Remote)
         }
 
         if (environment.isInitial && environment.isDefault) {
@@ -218,15 +216,10 @@ export const useStoreContext = create<ContextStore>((set, get) => ({
         }
       })
 
-      ModelEnvironment.save({ environments })
-      ModelEnvironment.sort(environments)
-
       const profileEnv = ModelEnvironment.getEnvironment()
-      let prodEnv = environments.find(({ name }) => name === 'prod')
-      let storedEnv = environments.find(({ name }) => name === profileEnv?.name)
-      let defaultEnv = environments.find(
-        ({ name }) => name === defaultEnvironment,
-      )
+      let prodEnv: Optional<ModelEnvironment>
+      let storedEnv: Optional<ModelEnvironment>
+      let defaultEnv: Optional<ModelEnvironment>
 
       environments.forEach(env => {
         switch (env.name) {
@@ -237,27 +230,28 @@ export const useStoreContext = create<ContextStore>((set, get) => ({
             storedEnv = env
             break
           case defaultEnvironment:
+            env.isDefault = true
             defaultEnv = env
         }
       })
 
       const currentEnv = storedEnv ?? defaultEnv ?? s.environment
+      const environment = isStringEmptyOrNil(prodEnv?.id) ? prodEnv : currentEnv
+
+      ModelEnvironment.save({ environments, environment })
+      ModelEnvironment.sort(environments)
 
       return {
-        environment: isStringEmptyOrNil(prodEnv?.id) ? prodEnv : currentEnv,
-        defaultEnvironment: defaultEnv,
-        pinnedEnvironments: environments.filter(env =>
-          pinnedEnvs.includes(env.name),
-        ),
+        environment,
         environments: new Set(environments),
       }
     })
   },
-  hasSynchronizedEnvironments() {
+  hasRemoteEnvironments() {
     const s = get()
 
     return Array.from(s.environments).some(
-      ({ type }) => type === EnumRelativeLocation.Synchronized,
+      ({ type }) => type === EnumRelativeLocation.Remote,
     )
   },
 }))
diff --git a/web/client/src/library/components/editor/EditorInspector.tsx b/web/client/src/library/components/editor/EditorInspector.tsx
index a08a46d4..16612b2d 100644
--- a/web/client/src/library/components/editor/EditorInspector.tsx
+++ b/web/client/src/library/components/editor/EditorInspector.tsx
@@ -71,7 +71,7 @@ function InspectorModel({
   const environment = useStoreContext(s => s.environment)
   const environments = useStoreContext(s => s.environments)
   const list = Array.from(environments)
-    .filter(({ isSynchronized }) => isSynchronized)
+    .filter(({ isRemote }) => isRemote)
     .map(({ name }) => ({ text: name, value: name }))
 
   const modelExtensions = useSQLMeshModelExtensions(model.path, model => {
@@ -85,7 +85,7 @@ function InspectorModel({
             'Actions',
             'Columns',
             tab.file.isSQLMeshModelSQL && 'Query',
-            list.length > 1 && environment.isSynchronized && 'Diff',
+            list.length > 1 && environment.isRemote && 'Diff',
           ].filter(Boolean) as string[]
         }
       />
@@ -130,7 +130,7 @@ function InspectorModel({
             className="text-xs"
           />
         </Tab.Panel>
-        {list.length > 1 && environment.isSynchronized && (
+        {list.length > 1 && environment.isRemote && (
           <Tab.Panel
             unmount={false}
             className={clsx(
diff --git a/web/client/src/library/components/plan/PlanHeader.tsx b/web/client/src/library/components/plan/PlanHeader.tsx
index ce1f03a5..475e9274 100644
--- a/web/client/src/library/components/plan/PlanHeader.tsx
+++ b/web/client/src/library/components/plan/PlanHeader.tsx
@@ -12,7 +12,7 @@ export default function PlanHeader(): JSX.Element {
 
   return (
     <div className="flex flex-col pb-2 w-full">
-      <div className="flex justify-between items-center">
+      <div className="flex justify-between items-center mt-2">
         <h4 className="flex items-center text-xl px-6 font-bold whitespace-nowrap">
           <span>Target Environment is</span>
           <span className="block ml-2 px-2 py-1 font-sm rounded-md bg-primary-10 text-primary-500">
diff --git a/web/client/src/library/components/plan/PlanOptions.tsx b/web/client/src/library/components/plan/PlanOptions.tsx
index 0b93663a..cfd6bfa7 100644
--- a/web/client/src/library/components/plan/PlanOptions.tsx
+++ b/web/client/src/library/components/plan/PlanOptions.tsx
@@ -27,7 +27,7 @@ export default function PlanOptions(): JSX.Element {
   const environment = useStoreContext(s => s.environment)
   const environments = useStoreContext(s => s.environments)
 
-  const synchronizedEnvironments = ModelEnvironment.getOnlySynchronized(
+  const remoteEnvironments = ModelEnvironment.getOnlyRemote(
     Array.from(environments),
   )
 
@@ -58,17 +58,17 @@ export default function PlanOptions(): JSX.Element {
               <Disclosure.Panel className="px-4 pb-2 text-sm text-neutral-500">
                 <div className="mt-3">
                   <div className="flex flex-wrap md:flex-nowrap">
-                    {isFalse(environment.isDefault) && (
+                    {isFalse(environment.isProd) && (
                       <Input
                         className="w-full"
                         label="Create From Environment"
                         info="The environment to base the plan on rather than local files"
-                        disabled={synchronizedEnvironments.length < 2}
+                        disabled={remoteEnvironments.length < 2}
                       >
                         {({ className, disabled }) => (
                           <Input.Selector
                             className={clsx(className, 'w-full')}
-                            list={ModelEnvironment.getOnlySynchronized(
+                            list={ModelEnvironment.getOnlyRemote(
                               Array.from(environments),
                             ).map(env => ({
                               value: env.name,
@@ -140,13 +140,13 @@ export default function PlanOptions(): JSX.Element {
               models in the target environment"
                         enabled={
                           no_gaps ||
-                          (skip_backfill && environment.isDefaultInitial) ||
-                          environment.isDefaultInitial
+                          (skip_backfill && environment.isInitialProd) ||
+                          environment.isInitialProd
                         }
                         disabled={
                           isInitialPlanRun ||
-                          (skip_backfill && environment.isDefaultInitial) ||
-                          environment.isDefaultInitial
+                          (skip_backfill && environment.isInitialProd) ||
+                          environment.isInitialProd
                         }
                         setEnabled={(value: boolean) => {
                           dispatch({
@@ -161,9 +161,7 @@ export default function PlanOptions(): JSX.Element {
                         label="Skip Backfill"
                         info="Skip the backfill step"
                         enabled={skip_backfill}
-                        disabled={
-                          isInitialPlanRun || environment.isDefaultInitial
-                        }
+                        disabled={isInitialPlanRun || environment.isInitialProd}
                         setEnabled={(value: boolean) => {
                           dispatch({
                             type: EnumPlanActions.PlanOptions,
@@ -179,9 +177,7 @@ export default function PlanOptions(): JSX.Element {
                         label="Include Unmodified"
                         info="Indicates whether to create views for all models in the target development environment or only for modified ones"
                         enabled={include_unmodified}
-                        disabled={
-                          isInitialPlanRun || environment.isDefaultInitial
-                        }
+                        disabled={isInitialPlanRun || environment.isInitialProd}
                         setEnabled={(value: boolean) => {
                           dispatch({
                             type: EnumPlanActions.PlanOptions,
@@ -193,9 +189,7 @@ export default function PlanOptions(): JSX.Element {
                         label="Forward Only"
                         info="Create a plan for forward-only changes"
                         enabled={forward_only}
-                        disabled={
-                          isInitialPlanRun || environment.isDefaultInitial
-                        }
+                        disabled={isInitialPlanRun || environment.isInitialProd}
                         setEnabled={(value: boolean) => {
                           dispatch({
                             type: EnumPlanActions.PlanOptions,
@@ -222,9 +216,7 @@ export default function PlanOptions(): JSX.Element {
                         label="No Auto Categorization"
                         info="Set category manually"
                         enabled={no_auto_categorization}
-                        disabled={
-                          isInitialPlanRun || environment.isDefaultInitial
-                        }
+                        disabled={isInitialPlanRun || environment.isInitialProd}
                         setEnabled={(value: boolean) => {
                           dispatch({
                             type: EnumPlanActions.PlanOptions,
diff --git a/web/client/src/library/components/plan/hooks.ts b/web/client/src/library/components/plan/hooks.ts
index 7ebe0dea..aed6f314 100644
--- a/web/client/src/library/components/plan/hooks.ts
+++ b/web/client/src/library/components/plan/hooks.ts
@@ -31,7 +31,7 @@ export function usePlanPayload({
   } = usePlan()
 
   const planDates = useMemo(() => {
-    if (environment.isDefault) return
+    if (environment.isProd) return
 
     return {
       start,
@@ -43,7 +43,7 @@ export function usePlanPayload({
   }, [environment, start, end, isInitialPlanRun, restate_models])
 
   const planOptions = useMemo(() => {
-    return environment.isDefaultInitial
+    return environment.isInitialProd
       ? {
           include_unmodified: true,
           no_gaps: true,
diff --git a/web/client/src/library/pages/ide/IDE.tsx b/web/client/src/library/pages/ide/IDE.tsx
index fa4c0564..389e76f0 100644
--- a/web/client/src/library/pages/ide/IDE.tsx
+++ b/web/client/src/library/pages/ide/IDE.tsx
@@ -70,9 +70,7 @@ export default function PageIDE(): JSX.Element {
   const removeConfirmation = useStoreContext(s => s.removeConfirmation)
   const environment = useStoreContext(s => s.environment)
   const setModels = useStoreContext(s => s.setModels)
-  const addSynchronizedEnvironments = useStoreContext(
-    s => s.addSynchronizedEnvironments,
-  )
+  const addRemoteEnvironments = useStoreContext(s => s.addRemoteEnvironments)
 
   const planOverview = useStorePlan(s => s.planOverview)
   const planApply = useStorePlan(s => s.planApply)
@@ -355,7 +353,7 @@ export default function PageIDE(): JSX.Element {
       data ?? {}
 
     if (isObjectNotEmpty<EnvironmentsEnvironments>(environments)) {
-      addSynchronizedEnvironments(
+      addRemoteEnvironments(
         Object.values(environments),
         default_target_environment,
         pinned_environments,
diff --git a/web/client/src/library/pages/ide/RunPlan.tsx b/web/client/src/library/pages/ide/RunPlan.tsx
index 184ff423..c2e981fc 100644
--- a/web/client/src/library/pages/ide/RunPlan.tsx
+++ b/web/client/src/library/pages/ide/RunPlan.tsx
@@ -7,7 +7,7 @@ import {
 } from '@heroicons/react/24/solid'
 import clsx from 'clsx'
 import { useState, useEffect, Fragment, type MouseEvent } from 'react'
-import { apiDeleteEnvironment, useApiPlanRun } from '~/api'
+import { apiDeleteEnvironment, useApiEnvironments } from '~/api'
 import { useStoreContext } from '~/context/context'
 import { type ModelEnvironment } from '~/models/environment'
 import { EnumSide, EnumSize, EnumVariant, type Side } from '~/types/enum'
@@ -24,7 +24,7 @@ import PlanChangePreview from '@components/plan/PlanChangePreview'
 import { EnumErrorKey, useIDE } from './context'
 import { EnumPlanAction, ModelPlanAction } from '@models/plan-action'
 import { useStorePlan } from '@context/plan'
-import { type SnapshotId } from '@api/client'
+import { initiatePlanApiPlanPost, type SnapshotId } from '@api/client'
 
 export default function RunPlan(): JSX.Element {
   const { setIsPlanOpen } = useIDE()
@@ -38,6 +38,8 @@ export default function RunPlan(): JSX.Element {
   const environment = useStoreContext(s => s.environment)
   const environments = useStoreContext(s => s.environments)
 
+  const { isFetching: isFetchingEnvironments } = useApiEnvironments()
+
   const [shouldStartPlanAutomatically, setShouldStartPlanAutomatically] =
     useState(false)
 
@@ -68,10 +70,7 @@ export default function RunPlan(): JSX.Element {
 
             if (planAction.isProcessing) {
               setIsPlanOpen(true)
-            } else if (
-              environment.isDefault &&
-              isFalse(environment.isInitial)
-            ) {
+            } else if (environment.isProd && environment.isSyncronized) {
               addConfirmation({
                 headline: 'Running Plan Directly On Prod Environment!',
                 description: `Are you sure you want to run your changes directly on prod? Safer choice will be to select or add new environment first.`,
@@ -90,7 +89,6 @@ export default function RunPlan(): JSX.Element {
                         <SelectEnvironemnt
                           className="mr-2"
                           side="left"
-                          environment={environment}
                           showAddEnvironment={false}
                           onSelect={() => {
                             setShouldStartPlanAutomatically(true)
@@ -124,8 +122,11 @@ export default function RunPlan(): JSX.Element {
         </Button>
         <SelectEnvironemnt
           className="rounded-none rounded-r-lg border-l mx-0"
-          environment={environment}
-          disabled={planAction.isProcessing || environment.isDefaultInitial}
+          disabled={
+            isFetchingEnvironments ||
+            planAction.isProcessing ||
+            environment.isInitialProd
+          }
         />
       </div>
       <EnvironmentStatus />
@@ -140,19 +141,24 @@ function EnvironmentStatus(): JSX.Element {
 
   return (
     <span className="flex align-center h-full w-full">
-      {environment.isLocal && (
-        <span
-          title="New"
-          className="block ml-1 px-2 first-child:ml-0 rounded-full bg-success-10 text-success-500 text-xs text-center font-bold"
-        >
+      {environment.isProd ? (
+        <span className="block ml-1 px-2 first-child:ml-0 rounded-full bg-warning-10 text-warning-700  dark:text-warning-400 text-xs text-center">
+          Production
+        </span>
+      ) : environment.isDefault ? (
+        <span className="block ml-1 px-2 first-child:ml-0 rounded-full bg-neutral-10 text-xs text-center">
+          Default
+        </span>
+      ) : (
+        <></>
+      )}
+      {environment.isInitial && (
+        <span className="block ml-1 px-2 first-child:ml-0 rounded-full bg-success-10 text-success-500 text-xs text-center font-bold">
           New
         </span>
       )}
-      {environment.isSynchronized && planOverview.isLatest && (
-        <span
-          title="Latest"
-          className="block ml-1 px-2 first-child:ml-0 rounded-full bg-neutral-10 text-xs text-center"
-        >
+      {environment.isSyncronized && planOverview.isLatest && (
+        <span className="block ml-1 px-2 first-child:ml-0 rounded-full bg-neutral-10 text-xs text-center">
           Latest
         </span>
       )}
@@ -165,19 +171,6 @@ function PlanChanges(): JSX.Element {
 
   return (
     <span className="flex align-center h-full w-full">
-      {isArrayNotEmpty(planOverview.backfills?.models) && (
-        <ChangesPreview
-          headline="Backfills"
-          type={EnumPlanChangeType.Default}
-          changes={
-            (planOverview.backfills?.models.map(
-              ({ model_name, view_name }) => ({
-                name: model_name ?? view_name,
-              }),
-            ) as SnapshotId[]) ?? []
-          }
-        />
-      )}
       {isArrayNotEmpty(planOverview.added) && (
         <ChangesPreview
           headline="Added Models"
@@ -221,20 +214,31 @@ function PlanChanges(): JSX.Element {
           changes={planOverview.removed ?? []}
         />
       )}
+      {isArrayNotEmpty(planOverview.backfills?.models) && (
+        <ChangesPreview
+          headline="Backfills"
+          type={EnumPlanChangeType.Default}
+          changes={
+            (planOverview.backfills?.models.map(
+              ({ model_name, view_name }) => ({
+                name: model_name ?? view_name,
+              }),
+            ) as SnapshotId[]) ?? []
+          }
+        />
+      )}
     </span>
   )
 }
 
 function SelectEnvironemnt({
   onSelect,
-  environment,
   disabled,
   side = EnumSide.Right,
   className,
   showAddEnvironment = true,
   size = EnumSize.sm,
 }: {
-  environment: ModelEnvironment
   disabled: boolean
   className?: string
   size?: ButtonSize
@@ -244,16 +248,11 @@ function SelectEnvironemnt({
 }): JSX.Element {
   const { addError } = useIDE()
 
+  const environment = useStoreContext(s => s.environment)
   const environments = useStoreContext(s => s.environments)
-  const pinnedEnvironments = useStoreContext(s => s.pinnedEnvironments)
-  const defaultEnvironment = useStoreContext(s => s.defaultEnvironment)
   const setEnvironment = useStoreContext(s => s.setEnvironment)
   const removeLocalEnvironment = useStoreContext(s => s.removeLocalEnvironment)
 
-  const { refetch: planRun } = useApiPlanRun(environment.name, {
-    planOptions: { skip_tests: true, include_unmodified: true },
-  })
-
   const ButtonMenu = makeButton<HTMLDivElement>(Menu.Button)
 
   function deleteEnvironment(env: ModelEnvironment): void {
@@ -266,6 +265,16 @@ function SelectEnvironemnt({
       })
   }
 
+  function runPlan(environment: ModelEnvironment): void {
+    void initiatePlanApiPlanPost({
+      environment: environment.name,
+      plan_options: {
+        skip_tests: true,
+        include_unmodified: true,
+      },
+    })
+  }
+
   return (
     <Menu>
       {({ close }) => (
@@ -280,7 +289,7 @@ function SelectEnvironemnt({
               className={clsx(
                 'block overflow-hidden truncate',
                 (environment.isLocal || disabled) && 'text-neutral-500',
-                environment.isSynchronized && 'text-primary-500',
+                environment.isRemote && 'text-primary-500',
               )}
             >
               {environment.name}
@@ -313,12 +322,10 @@ function SelectEnvironemnt({
                         onClick={(e: MouseEvent) => {
                           e.stopPropagation()
 
-                          if (env.isSynchronized) {
-                            void planRun()
-                          }
-
                           setEnvironment(env)
 
+                          runPlan(env)
+
                           onSelect?.()
                         }}
                         className={clsx(
@@ -341,21 +348,21 @@ function SelectEnvironemnt({
                               <span
                                 className={clsx(
                                   'block truncate ml-2',
-                                  env.isSynchronized && 'text-primary-500',
+                                  env.isRemote && 'text-primary-500',
                                 )}
                               >
                                 {env.name}
                               </span>
                               <small className="block ml-2">({env.type})</small>
                             </span>
-                            {env.isDefault && (
+                            {env.isProd && (
                               <span className="flex ml-2">
                                 <small className="text-xs text-neutral-500">
-                                  Main Environment
+                                  Production Environment
                                 </small>
                               </span>
                             )}
-                            {defaultEnvironment === env && (
+                            {env.isDefault && (
                               <span className="flex ml-2">
                                 <small className="text-xs text-neutral-500">
                                   Default Environment
@@ -371,8 +378,8 @@ function SelectEnvironemnt({
                               className="w-4 text-primary-500 dark:text-primary-100 mx-1"
                             />
                           )}
-                          {env.isLocal &&
-                            isFalse(env.isPinned) &&
+                          {isFalse(env.isPinned) &&
+                            env.isLocal &&
                             env !== environment && (
                               <Button
                                 className="!m-0 !px-2 bg-transparent hover:bg-transparent border-none"
@@ -387,10 +394,9 @@ function SelectEnvironemnt({
                                 <MinusCircleIcon className="w-4 text-neutral-500 dark:text-neutral-100" />
                               </Button>
                             )}
-                          {isFalse(env.isDefault) &&
-                            env !== environment &&
-                            env.isSynchronized &&
-                            isFalse(pinnedEnvironments.includes(env)) && (
+                          {isFalse(env.isPinned) &&
+                            env.isRemote &&
+                            env !== environment && (
                               <Button
                                 className="!px-2 !my-0"
                                 size={EnumSize.xs}
@@ -545,7 +551,7 @@ function ChangesPreview({
               <PlanChangePreview
                 headline={headline}
                 type={type}
-                className="w-full h-full max-w-[50vw] max-h-[40vh] overflow-hidden overflow-y-auto "
+                className="w-full h-full max-w-[50vw] max-h-[40vh] overflow-hidden overflow-y-auto hover:scrollbar scrollbar--vertical"
               >
                 <PlanChangePreview.Default
                   type={type}
diff --git a/web/client/src/models/environment.ts b/web/client/src/models/environment.ts
index 6162b4fc..706b71a6 100644
--- a/web/client/src/models/environment.ts
+++ b/web/client/src/models/environment.ts
@@ -15,7 +15,7 @@ export const EnumDefaultEnvironment = {
 
 export const EnumRelativeLocation = {
   Local: 'local',
-  Synchronized: 'synchronized',
+  Remote: 'remote',
 } as const
 
 export type EnvironmentName = DefaultEnvironment | string
@@ -28,9 +28,11 @@ interface InitialEnvironmemt extends Partial<Environment> {
 
 interface ProfileEnvironment {
   name: EnvironmentName
+  plan_id: string
   type: RelativeLocation
   createFrom: EnvironmentName
   isPinned: boolean
+  isDefault: boolean
 }
 
 interface Profile {
@@ -45,7 +47,8 @@ export class ModelEnvironment {
   private _type: RelativeLocation
   private _createFrom: EnvironmentName
 
-  isPinned = false
+  _isPinned = false
+  _isDefault = false
   isModel = true
 
   constructor(
@@ -53,13 +56,15 @@ export class ModelEnvironment {
     type: RelativeLocation,
     createFrom: EnvironmentName = EnumDefaultEnvironment.Prod,
     isPinned = false,
+    isDefault = false,
   ) {
     this._initial = initial
     this._type = type ?? EnumRelativeLocation.Local
-    this._createFrom = this.isDefault
+    this._createFrom = this._isDefault
       ? EnumDefaultEnvironment.Empty
       : createFrom
-    this.isPinned = isPinned
+    this._isPinned = isPinned
+    this._isDefault = isDefault
   }
 
   get id(): string {
@@ -78,8 +83,24 @@ export class ModelEnvironment {
     return this._createFrom
   }
 
+  get isProd(): boolean {
+    return this.name === EnumDefaultEnvironment.Prod
+  }
+
   get isDefault(): boolean {
-    return isNil(this.name) || this.name === EnumDefaultEnvironment.Prod
+    return this._isDefault
+  }
+
+  set isDefault(isDefault: boolean) {
+    this._isDefault = isDefault
+  }
+
+  get isPinned(): boolean {
+    return this._isPinned || this.isDefault || this.isProd
+  }
+
+  set isPinned(isPinned: boolean) {
+    this._isPinned = isPinned
   }
 
   get isInitial(): boolean {
@@ -87,18 +108,19 @@ export class ModelEnvironment {
   }
 
   get isLocal(): boolean {
-    return this._type === EnumRelativeLocation.Local && this.isInitial
+    return this._type === EnumRelativeLocation.Local
   }
 
-  get isSynchronized(): boolean {
-    return (
-      this._type === EnumRelativeLocation.Synchronized &&
-      isFalse(this.isInitial)
-    )
+  get isRemote(): boolean {
+    return this._type === EnumRelativeLocation.Remote
+  }
+
+  get isSyncronized(): boolean {
+    return this.isRemote && isFalse(this.isInitial)
   }
 
-  get isDefaultInitial(): boolean {
-    return this.isDefault && this.isInitial
+  get isInitialProd(): boolean {
+    return this.isInitial && this.isProd
   }
 
   setType(type: RelativeLocation): void {
@@ -125,9 +147,11 @@ export class ModelEnvironment {
     if (isNotNil(environment)) {
       output.environment = {
         name: environment.name,
+        plan_id: environment.id,
         type: environment.type,
         createFrom: environment.createFrom,
         isPinned: environment.isPinned,
+        isDefault: environment.isDefault,
       }
     }
 
@@ -135,9 +159,11 @@ export class ModelEnvironment {
       output.environments = ModelEnvironment.getOnlyLocal(environments).map(
         env => ({
           name: env.name,
+          plan_id: env.id,
           type: env.type,
           createFrom: env.createFrom,
           isPinned: env.isPinned,
+          isDefault: env.isDefault,
         }),
       )
     }
@@ -151,18 +177,26 @@ export class ModelEnvironment {
     )
   }
 
-  static getOnlySynchronized(
-    envs: ModelEnvironment[] = [],
-  ): ModelEnvironment[] {
+  static getOnlyRemote(envs: ModelEnvironment[] = []): ModelEnvironment[] {
     return envs.filter(
-      env => isFalse(isStringEmptyOrNil(env.name)) && env.isSynchronized,
+      env => isFalse(isStringEmptyOrNil(env.name)) && env.isRemote,
     )
   }
 
-  static getEnvironment(): Optional<ProfileEnvironment> {
-    const profile = getProfile()
+  static getEnvironment(): Optional<ModelEnvironment> {
+    const { environment } = getProfile() ?? {}
 
-    return profile?.environment
+    if (isNil(environment)) return
+
+    const { name, plan_id, type, createFrom, isPinned, isDefault } = environment
+
+    return new ModelEnvironment(
+      { name, plan_id },
+      type,
+      createFrom,
+      isPinned,
+      isDefault,
+    )
   }
 
   static getEnvironments(): ModelEnvironment[] {
@@ -184,10 +218,11 @@ export class ModelEnvironment {
     Array.from(environments.entries()).forEach(([name, environment]) => {
       output.push(
         new ModelEnvironment(
-          { name },
+          { name, plan_id: environment.plan_id },
           environment.type,
           environment.createFrom,
           environment.isPinned,
+          environment.isDefault,
         ),
       )
     })
@@ -205,7 +240,7 @@ export class ModelEnvironment {
   }
 
   static sort(environments: ModelEnvironment[]): ModelEnvironment[] {
-    environments.sort(env => (env.isSynchronized ? -1 : 1))
+    environments.sort(env => (env.isRemote ? -1 : 1))
 
     return environments
   }


Test patch:
diff --git a/tests/integrations/github/cicd/test_config.py b/tests/integrations/github/cicd/test_config.py
index c35fc5e5..41fd6d34 100644
--- a/tests/integrations/github/cicd/test_config.py
+++ b/tests/integrations/github/cicd/test_config.py
@@ -33,6 +33,7 @@ model_defaults:
     assert config.cicd_bot.default_pr_start == "1 day ago"
     assert not config.cicd_bot.enable_deploy_command
     assert config.cicd_bot.skip_pr_backfill
+    assert config.cicd_bot.pr_include_unmodified is None
 
 
 def test_load_yaml_config(tmp_path):
@@ -53,6 +54,7 @@ cicd_bot:
     default_pr_start: 
     enable_deploy_command: true
     skip_pr_backfill: false
+    pr_include_unmodified: true
 model_defaults:
     dialect: duckdb
 """,
@@ -73,6 +75,7 @@ model_defaults:
     assert config.cicd_bot.default_pr_start is None
     assert config.cicd_bot.enable_deploy_command
     assert not config.cicd_bot.skip_pr_backfill
+    assert config.cicd_bot.pr_include_unmodified
 
 
 def test_load_python_config_defaults(tmp_path):
@@ -100,6 +103,7 @@ config = Config(
     assert config.cicd_bot.default_pr_start == "1 day ago"
     assert not config.cicd_bot.enable_deploy_command
     assert config.cicd_bot.skip_pr_backfill
+    assert config.cicd_bot.pr_include_unmodified is None
 
 
 def test_load_python_config(tmp_path):
@@ -124,6 +128,7 @@ config = Config(
         default_pr_start="1 week ago",
         enable_deploy_command=True,
         skip_pr_backfill=False,
+        pr_include_unmodified=True,
     ),
     model_defaults=ModelDefaultsConfig(dialect="duckdb"),
 )
@@ -146,6 +151,7 @@ config = Config(
     assert config.cicd_bot.default_pr_start == "1 week ago"
     assert config.cicd_bot.enable_deploy_command
     assert not config.cicd_bot.skip_pr_backfill
+    assert config.cicd_bot.pr_include_unmodified
 
 
 def test_validation(tmp_path):