From 36c7b078b9a8ca2e6f24f8f1af85f053befb2bc9 Mon Sep 17 00:00:00 2001
From: Salah-Eddine Saakoun <salah-eddine.saakoun@consensys.net>
Date: Tue, 11 Mar 2025 16:04:40 +0100
Subject: [PATCH 1/6] fix: include unchanged dependents in release validation

---
 src/release-specification.test.ts | 230 +++++++++++++++++++++++++++++-
 src/release-specification.ts      |  11 +-
 2 files changed, 228 insertions(+), 13 deletions(-)

diff --git a/src/release-specification.test.ts b/src/release-specification.test.ts
index 8274216..2677d1d 100644
--- a/src/release-specification.test.ts
+++ b/src/release-specification.test.ts
@@ -602,7 +602,7 @@ Your release spec could not be processed due to the following issues:
       });
     });
 
-    it('throws if there are any packages in the release with a major version bump using the word "major", but any of their dependents defined as "peerDependencies" are not listed in the release', async () => {
+    it('throws if there are any packages in the release with a major version bump using the word "major", but any of their dependents defined as "peerDependencies" have changes since their latest release and are not listed in the release', async () => {
       await withSandbox(async (sandbox) => {
         const project = buildMockProject({
           workspacePackages: {
@@ -657,7 +657,62 @@ ${releaseSpecificationPath}
       });
     });
 
-    it('throws if there are any packages in the release with a major version bump using a literal version, but any of their dependents defined as "peerDependencies" are not listed in the release', async () => {
+    it('throws if there are any packages in the release with a major version bump using the word "major", but any of their dependents defined as "peerDependencies" have no changes since their latest release and are not listed in the release', async () => {
+      await withSandbox(async (sandbox) => {
+        const project = buildMockProject({
+          workspacePackages: {
+            a: buildMockPackage('a', {
+              hasChangesSinceLatestRelease: true,
+            }),
+            b: buildMockPackage('b', {
+              hasChangesSinceLatestRelease: false,
+              validatedManifest: {
+                peerDependencies: {
+                  a: '1.0.0',
+                },
+              },
+            }),
+          },
+        });
+        const releaseSpecificationPath = path.join(
+          sandbox.directoryPath,
+          'release-spec',
+        );
+        await fs.promises.writeFile(
+          releaseSpecificationPath,
+          YAML.stringify({
+            packages: {
+              a: 'major',
+            },
+          }),
+        );
+
+        await expect(
+          validateReleaseSpecification(project, releaseSpecificationPath),
+        ).rejects.toThrow(
+          `
+Your release spec could not be processed due to the following issues:
+
+* The following dependents of package 'a', which is being released with a major version bump, are missing from the release spec.
+
+  - b
+
+ Consider including them in the release spec so that they are compatible with the new 'a' version.
+
+  If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example:
+
+    packages:
+      b: intentionally-skip
+
+The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool.
+
+${releaseSpecificationPath}
+`.trim(),
+        );
+      });
+    });
+
+    it('throws if there are any packages in the release with a major version bump using a literal version, but any of their dependents defined as "peerDependencies" have changes since their latest release and are not listed in the release', async () => {
       await withSandbox(async (sandbox) => {
         const project = buildMockProject({
           workspacePackages: {
@@ -712,7 +767,62 @@ ${releaseSpecificationPath}
       });
     });
 
-    it('throws if there are any packages in the release with a major version bump using the word "major", but their dependents via "peerDependencies" have their version specified as null in the release spec', async () => {
+    it('throws if there are any packages in the release with a major version bump using a literal version, but any of their dependents defined as "peerDependencies" have no changes since their latest release and are not listed in the release', async () => {
+      await withSandbox(async (sandbox) => {
+        const project = buildMockProject({
+          workspacePackages: {
+            a: buildMockPackage('a', '2.1.4', {
+              hasChangesSinceLatestRelease: true,
+            }),
+            b: buildMockPackage('b', {
+              hasChangesSinceLatestRelease: true,
+              validatedManifest: {
+                peerDependencies: {
+                  a: '2.1.4',
+                },
+              },
+            }),
+          },
+        });
+        const releaseSpecificationPath = path.join(
+          sandbox.directoryPath,
+          'release-spec',
+        );
+        await fs.promises.writeFile(
+          releaseSpecificationPath,
+          YAML.stringify({
+            packages: {
+              a: '3.0.0',
+            },
+          }),
+        );
+
+        await expect(
+          validateReleaseSpecification(project, releaseSpecificationPath),
+        ).rejects.toThrow(
+          `
+Your release spec could not be processed due to the following issues:
+
+* The following dependents of package 'a', which is being released with a major version bump, are missing from the release spec.
+
+  - b
+
+ Consider including them in the release spec so that they are compatible with the new 'a' version.
+
+  If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example:
+
+    packages:
+      b: intentionally-skip
+
+The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool.
+
+${releaseSpecificationPath}
+`.trim(),
+        );
+      });
+    });
+
+    it('throws if there are any packages in the release with a major version bump using the word "major", but their dependents via "peerDependencies" have changes since their latest release and have their version specified as null in the release spec', async () => {
       await withSandbox(async (sandbox) => {
         const project = buildMockProject({
           workspacePackages: {
@@ -768,7 +878,63 @@ ${releaseSpecificationPath}
       });
     });
 
-    it('throws if there are any packages in the release with a major version bump using a literal version, but their dependents via "peerDependencies" have their version specified as null in the release spec', async () => {
+    it('throws if there are any packages in the release with a major version bump using the word "major", but their dependents via "peerDependencies" have no changes since their latest release and have their version specified as null in the release spec', async () => {
+      await withSandbox(async (sandbox) => {
+        const project = buildMockProject({
+          workspacePackages: {
+            a: buildMockPackage('a', {
+              hasChangesSinceLatestRelease: true,
+            }),
+            b: buildMockPackage('b', {
+              hasChangesSinceLatestRelease: false,
+              validatedManifest: {
+                peerDependencies: {
+                  a: '1.0.0',
+                },
+              },
+            }),
+          },
+        });
+        const releaseSpecificationPath = path.join(
+          sandbox.directoryPath,
+          'release-spec',
+        );
+        await fs.promises.writeFile(
+          releaseSpecificationPath,
+          YAML.stringify({
+            packages: {
+              a: 'major',
+              b: null,
+            },
+          }),
+        );
+
+        await expect(
+          validateReleaseSpecification(project, releaseSpecificationPath),
+        ).rejects.toThrow(
+          `
+Your release spec could not be processed due to the following issues:
+
+* The following dependents of package 'a', which is being released with a major version bump, are missing from the release spec.
+
+  - b
+
+ Consider including them in the release spec so that they are compatible with the new 'a' version.
+
+  If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example:
+
+    packages:
+      b: intentionally-skip
+
+The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool.
+
+${releaseSpecificationPath}
+`.trim(),
+        );
+      });
+    });
+
+    it('throws if there are any packages in the release with a major version bump using a literal version, but their dependents via "peerDependencies" have changes since their latest release and have their version specified as null in the release spec', async () => {
       await withSandbox(async (sandbox) => {
         const project = buildMockProject({
           workspacePackages: {
@@ -818,6 +984,62 @@ Your release spec could not be processed due to the following issues:
 
 The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool.
 
+${releaseSpecificationPath}
+`.trim(),
+        );
+      });
+    });
+
+    it('throws if there are any packages in the release with a major version bump using a literal version, but their dependents via "peerDependencies" have no changes since their latest release and have their version specified as null in the release spec', async () => {
+      await withSandbox(async (sandbox) => {
+        const project = buildMockProject({
+          workspacePackages: {
+            a: buildMockPackage('a', '2.1.4', {
+              hasChangesSinceLatestRelease: true,
+            }),
+            b: buildMockPackage('b', {
+              hasChangesSinceLatestRelease: false,
+              validatedManifest: {
+                peerDependencies: {
+                  a: '2.1.4',
+                },
+              },
+            }),
+          },
+        });
+        const releaseSpecificationPath = path.join(
+          sandbox.directoryPath,
+          'release-spec',
+        );
+        await fs.promises.writeFile(
+          releaseSpecificationPath,
+          YAML.stringify({
+            packages: {
+              a: '3.0.0',
+              b: null,
+            },
+          }),
+        );
+
+        await expect(
+          validateReleaseSpecification(project, releaseSpecificationPath),
+        ).rejects.toThrow(
+          `
+Your release spec could not be processed due to the following issues:
+
+* The following dependents of package 'a', which is being released with a major version bump, are missing from the release spec.
+
+  - b
+
+ Consider including them in the release spec so that they are compatible with the new 'a' version.
+
+  If you are ABSOLUTELY SURE these packages are safe to omit, however, and want to postpone the release of a package, then list it with a directive of "intentionally-skip". For example:
+
+    packages:
+      b: intentionally-skip
+
+The release spec file has been retained for you to edit again and make the necessary fixes. Once you've done this, re-run this tool.
+
 ${releaseSpecificationPath}
 `.trim(),
         );
diff --git a/src/release-specification.ts b/src/release-specification.ts
index 151a9ad..aac0b85 100644
--- a/src/release-specification.ts
+++ b/src/release-specification.ts
@@ -168,7 +168,7 @@ export async function waitForUserToEditReleaseSpecification(
 }
 
 /**
- * Finds all workspace packages that depend on the given package and have changes since their latest release.
+ * Finds all workspace packages that depend on the given package.
  *
  * @param project - The project containing workspace packages.
  * @param packageName - The name of the package to find dependents for.
@@ -189,14 +189,7 @@ export function findMissingUnreleasedDependents(
     },
   );
 
-  const changedDependentNames = dependentNames.filter(
-    (possibleDependentName) => {
-      return project.workspacePackages[possibleDependentName]
-        .hasChangesSinceLatestRelease;
-    },
-  );
-
-  return changedDependentNames.filter((dependentName) => {
+  return dependentNames.filter((dependentName) => {
     return !unvalidatedReleaseSpecificationPackages[dependentName];
   });
 }

From 50306cc69786321a520f1a413802c70e450efb11 Mon Sep 17 00:00:00 2001
From: Salah-Eddine Saakoun <salah-eddine.saakoun@consensys.net>
Date: Thu, 13 Mar 2025 17:51:36 +0100
Subject: [PATCH 2/6] fix: apply peer dependency validation without changes to
 ui

---
 src/release-specification.ts | 27 +++++++++++++++++++++++----
 src/ui.ts                    | 23 ++++++++++++++++++++---
 src/ui/App.tsx               | 31 ++++++++++++++++++++++++++++---
 3 files changed, 71 insertions(+), 10 deletions(-)

diff --git a/src/release-specification.ts b/src/release-specification.ts
index aac0b85..e551a2e 100644
--- a/src/release-specification.ts
+++ b/src/release-specification.ts
@@ -172,13 +172,11 @@ export async function waitForUserToEditReleaseSpecification(
  *
  * @param project - The project containing workspace packages.
  * @param packageName - The name of the package to find dependents for.
- * @param unvalidatedReleaseSpecificationPackages - The packages in the release specification.
- * @returns An array of package names that depend on the given package and are missing from the release spec.
+ * @returns An array of package names that depend on the given package.
  */
-export function findMissingUnreleasedDependents(
+export function findAllWorkspacePackagesThatDependOnPackage(
   project: Project,
   packageName: string,
-  unvalidatedReleaseSpecificationPackages: Record<string, string | null>,
 ): string[] {
   const dependentNames = Object.keys(project.workspacePackages).filter(
     (possibleDependentName) => {
@@ -189,6 +187,27 @@ export function findMissingUnreleasedDependents(
     },
   );
 
+  return dependentNames;
+}
+
+/**
+ * Finds all workspace packages that depend on the given package.
+ *
+ * @param project - The project containing workspace packages.
+ * @param packageName - The name of the package to find dependents for.
+ * @param unvalidatedReleaseSpecificationPackages - The packages in the release specification.
+ * @returns An array of package names that depend on the given package and are missing from the release spec.
+ */
+export function findMissingUnreleasedDependents(
+  project: Project,
+  packageName: string,
+  unvalidatedReleaseSpecificationPackages: Record<string, string | null>,
+): string[] {
+  const dependentNames = findAllWorkspacePackagesThatDependOnPackage(
+    project,
+    packageName,
+  );
+
   return dependentNames.filter((dependentName) => {
     return !unvalidatedReleaseSpecificationPackages[dependentName];
   });
diff --git a/src/ui.ts b/src/ui.ts
index 46ff94e..0757bf3 100644
--- a/src/ui.ts
+++ b/src/ui.ts
@@ -10,6 +10,7 @@ import {
 } from './project.js';
 import { Package } from './package.js';
 import {
+  findAllWorkspacePackagesThatDependOnPackage,
   findMissingUnreleasedDependenciesForRelease,
   findMissingUnreleasedDependentsForBreakingChanges,
   IncrementableVersionParts,
@@ -130,15 +131,31 @@ function createApp({
   app.use(express.static(UI_BUILD_DIR));
   app.use(express.json());
 
-  app.get('/api/packages', (_req, res) => {
+  app.get('/api/packages', (req, res) => {
+    const { majorBumps } = req.query;
+
+    const majorBumpsArray =
+      typeof majorBumps === 'string'
+        ? majorBumps.split(',').filter(Boolean)
+        : (req.query.majorBumps as string[] | undefined) || [];
+
+    const requiredDependents = [
+      ...new Set(
+        majorBumpsArray.flatMap((majorBump) =>
+          findAllWorkspacePackagesThatDependOnPackage(project, majorBump),
+        ),
+      ),
+    ];
+
     const pkgs = Object.values(project.workspacePackages).filter(
-      (pkg) => pkg.hasChangesSinceLatestRelease,
+      (pkg) =>
+        pkg.hasChangesSinceLatestRelease ||
+        requiredDependents.includes(pkg.validatedManifest.name),
     );
 
     const packages = pkgs.map((pkg) => ({
       name: pkg.validatedManifest.name,
       version: pkg.validatedManifest.version.version,
-      location: pkg.directoryPath,
     }));
 
     res.json(packages);
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 2282a4b..f2b2d2c 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -1,11 +1,17 @@
 import './style.css';
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
 import { createRoot } from 'react-dom/client';
 import { SemVer } from 'semver';
 import { ErrorMessage } from './ErrorMessage.js';
 import { PackageItem } from './PackageItem.js';
 import { Package, RELEASE_TYPE_OPTIONS, ReleaseType } from './types.js';
 
+// Helper function to compare sets
+const setsAreEqual = (a: Set<string>, b: Set<string>) => {
+  if (a.size !== b.size) return false;
+  return [...a].every((value) => b.has(value));
+};
+
 type SubmitButtonProps = {
   selections: Record<string, string>;
   packageDependencyErrors: Record<
@@ -67,9 +73,14 @@ function App() {
     new Set(),
   );
   const [showCheckboxes, setShowCheckboxes] = useState(false);
+  const previousPackages = useRef<Set<string>>(new Set());
 
   useEffect(() => {
-    fetch('/api/packages')
+    const majorBumps = Object.entries(selections)
+      .filter(([_, type]) => type === 'major')
+      .map(([pkgName]) => pkgName);
+
+    fetch(`/api/packages?majorBumps=${majorBumps.join(',')}`)
       .then((res) => {
         if (!res.ok) {
           throw new Error(`Received ${res.status}`);
@@ -77,6 +88,20 @@ function App() {
         return res.json();
       })
       .then((data: Package[]) => {
+        const newPackageNames = new Set(data.map((pkg) => pkg.name));
+
+        // Only clean up selections if the package list actually changed
+        if (!setsAreEqual(previousPackages.current, newPackageNames)) {
+          setSelections((prev) =>
+            Object.fromEntries(
+              Object.entries(prev).filter(([pkgName]) =>
+                newPackageNames.has(pkgName),
+              ),
+            ),
+          );
+          previousPackages.current = newPackageNames;
+        }
+
         setPackages(data);
         setLoadingChangelogs(
           data.reduce((acc, pkg) => ({ ...acc, [pkg.name]: false }), {}),
@@ -86,7 +111,7 @@ function App() {
         setError(err.message);
         console.error('Error fetching packages:', err);
       });
-  }, []);
+  }, [selections]);
 
   const checkDependencies = async (selectionData: Record<string, string>) => {
     if (Object.keys(selectionData).length === 0) return;

From 7bc1221bb2c7e045901b8c4335e3f28e45caedce Mon Sep 17 00:00:00 2001
From: Salah-Eddine Saakoun <salah-eddine.saakoun@consensys.net>
Date: Thu, 13 Mar 2025 17:53:28 +0100
Subject: [PATCH 3/6] fix: use set for major bumps check

---
 src/ui.ts | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/ui.ts b/src/ui.ts
index 0757bf3..a646f9a 100644
--- a/src/ui.ts
+++ b/src/ui.ts
@@ -139,18 +139,16 @@ function createApp({
         ? majorBumps.split(',').filter(Boolean)
         : (req.query.majorBumps as string[] | undefined) || [];
 
-    const requiredDependents = [
-      ...new Set(
-        majorBumpsArray.flatMap((majorBump) =>
-          findAllWorkspacePackagesThatDependOnPackage(project, majorBump),
-        ),
+    const requiredDependents = new Set(
+      majorBumpsArray.flatMap((majorBump) =>
+        findAllWorkspacePackagesThatDependOnPackage(project, majorBump),
       ),
-    ];
+    );
 
     const pkgs = Object.values(project.workspacePackages).filter(
       (pkg) =>
         pkg.hasChangesSinceLatestRelease ||
-        requiredDependents.includes(pkg.validatedManifest.name),
+        requiredDependents.has(pkg.validatedManifest.name),
     );
 
     const packages = pkgs.map((pkg) => ({

From 7c90c8339e14424322ed33eea4c0c2fa3ca7099a Mon Sep 17 00:00:00 2001
From: Salah-Eddine Saakoun <salah-eddine.saakoun@consensys.net>
Date: Thu, 13 Mar 2025 21:35:05 +0100
Subject: [PATCH 4/6] Address PR feedback

---
 src/release-specification.test.ts |  8 ++---
 src/ui/App.tsx                    | 54 +++++++++++++++++--------------
 src/ui/PackageItem.tsx            | 33 +++++++++++--------
 3 files changed, 52 insertions(+), 43 deletions(-)

diff --git a/src/release-specification.test.ts b/src/release-specification.test.ts
index 2677d1d..9c4b2c6 100644
--- a/src/release-specification.test.ts
+++ b/src/release-specification.test.ts
@@ -657,7 +657,7 @@ ${releaseSpecificationPath}
       });
     });
 
-    it('throws if there are any packages in the release with a major version bump using the word "major", but any of their dependents defined as "peerDependencies" have no changes since their latest release and are not listed in the release', async () => {
+    it('throws if there are any packages in the release with a major version bump using the word "major", but any of their dependents defined as "peerDependencies" are not listed in the release, even if they have no changes', async () => {
       await withSandbox(async (sandbox) => {
         const project = buildMockProject({
           workspacePackages: {
@@ -767,7 +767,7 @@ ${releaseSpecificationPath}
       });
     });
 
-    it('throws if there are any packages in the release with a major version bump using a literal version, but any of their dependents defined as "peerDependencies" have no changes since their latest release and are not listed in the release', async () => {
+    it('throws if there are any packages in the release with a major version bump using a literal version, but any of their dependents defined as "peerDependencies" are not listed in the release, even if they have no changes', async () => {
       await withSandbox(async (sandbox) => {
         const project = buildMockProject({
           workspacePackages: {
@@ -878,7 +878,7 @@ ${releaseSpecificationPath}
       });
     });
 
-    it('throws if there are any packages in the release with a major version bump using the word "major", but their dependents via "peerDependencies" have no changes since their latest release and have their version specified as null in the release spec', async () => {
+    it('throws if there are any packages in the release with a major version bump using the word "major", but their dependents via "peerDependencies" have their version specified as null in the release spec, even if they have no changes', async () => {
       await withSandbox(async (sandbox) => {
         const project = buildMockProject({
           workspacePackages: {
@@ -990,7 +990,7 @@ ${releaseSpecificationPath}
       });
     });
 
-    it('throws if there are any packages in the release with a major version bump using a literal version, but their dependents via "peerDependencies" have no changes since their latest release and have their version specified as null in the release spec', async () => {
+    it('throws if there are any packages in the release with a major version bump using a literal version, but their dependents via "peerDependencies" have their version specified as null in the release spec, even if they have no changes', async () => {
       await withSandbox(async (sandbox) => {
         const project = buildMockProject({
           workspacePackages: {
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index f2b2d2c..e8f0826 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -13,7 +13,7 @@ const setsAreEqual = (a: Set<string>, b: Set<string>) => {
 };
 
 type SubmitButtonProps = {
-  selections: Record<string, string>;
+  releaseSelections: Record<string, string>;
   packageDependencyErrors: Record<
     string,
     { missingDependentNames: string[]; missingDependencies: string[] }
@@ -22,14 +22,16 @@ type SubmitButtonProps = {
 };
 
 function SubmitButton({
-  selections,
+  releaseSelections,
   packageDependencyErrors,
   onSubmit,
 }: SubmitButtonProps) {
   const isDisabled =
-    Object.keys(selections).length === 0 ||
+    Object.keys(releaseSelections).length === 0 ||
     Object.keys(packageDependencyErrors).length > 0 ||
-    Object.values(selections).every((value) => value === 'intentionally-skip');
+    Object.values(releaseSelections).every(
+      (value) => value === 'intentionally-skip',
+    );
 
   return (
     <button
@@ -48,7 +50,9 @@ function SubmitButton({
 
 function App() {
   const [packages, setPackages] = useState<Package[]>([]);
-  const [selections, setSelections] = useState<Record<string, string>>({});
+  const [releaseSelections, setReleaseSelections] = useState<
+    Record<string, string>
+  >({});
   const [isSubmitting, setIsSubmitting] = useState(false);
   const [error, setError] = useState<string | null>(null);
   const [changelogs, setChangelogs] = useState<Record<string, string>>({});
@@ -76,7 +80,7 @@ function App() {
   const previousPackages = useRef<Set<string>>(new Set());
 
   useEffect(() => {
-    const majorBumps = Object.entries(selections)
+    const majorBumps = Object.entries(releaseSelections)
       .filter(([_, type]) => type === 'major')
       .map(([pkgName]) => pkgName);
 
@@ -90,9 +94,9 @@ function App() {
       .then((data: Package[]) => {
         const newPackageNames = new Set(data.map((pkg) => pkg.name));
 
-        // Only clean up selections if the package list actually changed
+        // Only clean up releaseSelections if the package list actually changed
         if (!setsAreEqual(previousPackages.current, newPackageNames)) {
-          setSelections((prev) =>
+          setReleaseSelections((prev) =>
             Object.fromEntries(
               Object.entries(prev).filter(([pkgName]) =>
                 newPackageNames.has(pkgName),
@@ -111,7 +115,7 @@ function App() {
         setError(err.message);
         console.error('Error fetching packages:', err);
       });
-  }, [selections]);
+  }, [releaseSelections]);
 
   const checkDependencies = async (selectionData: Record<string, string>) => {
     if (Object.keys(selectionData).length === 0) return;
@@ -144,11 +148,11 @@ function App() {
 
   useEffect(() => {
     const timeoutId = setTimeout(() => {
-      void checkDependencies(selections);
+      void checkDependencies(releaseSelections);
     }, 500);
 
     return () => clearTimeout(timeoutId);
-  }, [selections]);
+  }, [releaseSelections]);
 
   const handleCustomVersionChange = (packageName: string, version: string) => {
     try {
@@ -178,7 +182,7 @@ function App() {
         return rest;
       });
 
-      setSelections((prev) => ({
+      setReleaseSelections((prev) => ({
         ...prev,
         [packageName]: version,
       }));
@@ -195,14 +199,14 @@ function App() {
     value: ReleaseType | '',
   ): void => {
     if (value === '') {
-      const { [packageName]: _, ...rest } = selections;
-      setSelections(rest);
+      const { [packageName]: _, ...rest } = releaseSelections;
+      setReleaseSelections(rest);
 
       const { [packageName]: __, ...remainingErrors } = packageDependencyErrors;
       setPackageDependencyErrors(remainingErrors);
     } else {
-      setSelections({
-        ...selections,
+      setReleaseSelections({
+        ...releaseSelections,
         [packageName]: value,
       });
     }
@@ -214,7 +218,7 @@ function App() {
       const response = await fetch('/api/release', {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify(selections),
+        body: JSON.stringify(releaseSelections),
       });
 
       const data: {
@@ -250,8 +254,8 @@ function App() {
       const errorMessage =
         err instanceof Error ? err.message : 'An error occurred';
       setError(errorMessage);
-      console.error('Error submitting selections:', err);
-      alert('Failed to submit selections. Please try again.');
+      console.error('Error submitting releaseSelections:', err);
+      alert('Failed to submit releaseSelections. Please try again.');
     } finally {
       setIsSubmitting(false);
     }
@@ -276,11 +280,11 @@ function App() {
   };
 
   const handleBulkAction = (action: ReleaseType) => {
-    const newSelections = { ...selections };
+    const newReleaseSelections = { ...releaseSelections };
     selectedPackages.forEach((packageName) => {
-      newSelections[packageName] = action;
+      newReleaseSelections[packageName] = action;
     });
-    setSelections(newSelections);
+    setReleaseSelections(newReleaseSelections);
     setSelectedPackages(new Set());
     setShowCheckboxes(true);
   };
@@ -389,7 +393,7 @@ function App() {
           <PackageItem
             key={pkg.name}
             pkg={pkg}
-            selections={selections}
+            releaseSelections={releaseSelections}
             versionErrors={versionErrors}
             packageDependencyErrors={packageDependencyErrors}
             loadingChangelogs={loadingChangelogs}
@@ -399,7 +403,7 @@ function App() {
             onSelectionChange={handleSelectionChange}
             onCustomVersionChange={handleCustomVersionChange}
             onFetchChangelog={fetchChangelog}
-            setSelections={setSelections}
+            setReleaseSelections={setReleaseSelections}
             setChangelogs={setChangelogs}
             onToggleSelect={() => togglePackageSelection(pkg.name)}
           />
@@ -408,7 +412,7 @@ function App() {
 
       {packages.length > 0 && (
         <SubmitButton
-          selections={selections}
+          releaseSelections={releaseSelections}
           packageDependencyErrors={packageDependencyErrors}
           onSubmit={handleSubmit}
         />
diff --git a/src/ui/PackageItem.tsx b/src/ui/PackageItem.tsx
index 7543fa2..662466b 100644
--- a/src/ui/PackageItem.tsx
+++ b/src/ui/PackageItem.tsx
@@ -6,7 +6,7 @@ import { Package, ReleaseType } from './types.js';
 
 type PackageItemProps = {
   pkg: Package;
-  selections: Record<string, string>;
+  releaseSelections: Record<string, string>;
   versionErrors: Record<string, string>;
   packageDependencyErrors: Record<
     string,
@@ -22,14 +22,16 @@ type PackageItemProps = {
   onSelectionChange: (packageName: string, value: ReleaseType | '') => void;
   onCustomVersionChange: (packageName: string, version: string) => void;
   onFetchChangelog: (packageName: string) => Promise<void>;
-  setSelections: React.Dispatch<React.SetStateAction<Record<string, string>>>;
+  setReleaseSelections: React.Dispatch<
+    React.SetStateAction<Record<string, string>>
+  >;
   setChangelogs: React.Dispatch<React.SetStateAction<Record<string, string>>>;
   onToggleSelect: () => void;
 };
 
 export function PackageItem({
   pkg,
-  selections,
+  releaseSelections,
   versionErrors,
   packageDependencyErrors,
   loadingChangelogs,
@@ -39,7 +41,7 @@ export function PackageItem({
   onSelectionChange,
   onCustomVersionChange,
   onFetchChangelog,
-  setSelections,
+  setReleaseSelections,
   setChangelogs,
   onToggleSelect,
 }: PackageItemProps) {
@@ -48,7 +50,8 @@ export function PackageItem({
       key={pkg.name}
       id={`package-${pkg.name}`}
       className={`border p-4 rounded-lg ${
-        selections[pkg.name] && selections[pkg.name] !== 'intentionally-skip'
+        releaseSelections[pkg.name] &&
+        releaseSelections[pkg.name] !== 'intentionally-skip'
           ? 'border-2'
           : 'border-gray-200'
       } ${
@@ -76,17 +79,19 @@ export function PackageItem({
           <div className="flex items-center justify-between">
             <div>
               <p className="text-gray-600">Current version: {pkg.version}</p>
-              {selections[pkg.name] &&
-                selections[pkg.name] !== 'intentionally-skip' &&
-                selections[pkg.name] !== 'custom' &&
+              {releaseSelections[pkg.name] &&
+                releaseSelections[pkg.name] !== 'intentionally-skip' &&
+                releaseSelections[pkg.name] !== 'custom' &&
                 !versionErrors[pkg.name] && (
                   <p className="text-yellow-700">
                     New version:{' '}
-                    {!['patch', 'minor', 'major'].includes(selections[pkg.name])
-                      ? selections[pkg.name]
+                    {!['patch', 'minor', 'major'].includes(
+                      releaseSelections[pkg.name],
+                    )
+                      ? releaseSelections[pkg.name]
                       : new SemVer(pkg.version)
                           .inc(
-                            selections[pkg.name] as Exclude<
+                            releaseSelections[pkg.name] as Exclude<
                               ReleaseType,
                               'intentionally-skip' | 'custom' | string
                             >,
@@ -102,7 +107,7 @@ export function PackageItem({
             </div>
             <VersionSelector
               packageName={pkg.name}
-              selection={selections[pkg.name]}
+              selection={releaseSelections[pkg.name]}
               onSelectionChange={onSelectionChange}
               onCustomVersionChange={onCustomVersionChange}
               onFetchChangelog={onFetchChangelog}
@@ -120,7 +125,7 @@ export function PackageItem({
               <DependencyErrorSection
                 title="Missing Dependencies"
                 items={packageDependencyErrors[pkg.name].missingDependencies}
-                setSelections={setSelections}
+                setSelections={setReleaseSelections}
                 description={`The following packages are dependencies or peer dependencies of ${pkg.name}. Because they may have introduced new changes that ${pkg.name} is now using, you need to verify whether to include them in the release.
 
 To do this, look at the change history for each package and compare it with the change history for ${pkg.name}. If ${pkg.name} uses any new changes from a package, then you need to include it by bumping its version. If you have confirmed that the changes to a package do not affect ${pkg.name}, you may omit it from the release by choosing "Skip" instead.`}
@@ -134,7 +139,7 @@ To do this, look at the change history for each package and compare it with the
                   items={
                     packageDependencyErrors[pkg.name].missingDependentNames
                   }
-                  setSelections={setSelections}
+                  setSelections={setReleaseSelections}
                   description={`Because ${pkg.name} is being released with a new major version, to prevent peer dependency warnings in consuming projects, all of the following packages which list ${pkg.name} as a peer dependency need to be included in the release. Please choose new versions for these packages. If for some reason you feel it is safe to omit a package you may choose "Skip".`}
                 />
               </div>

From 32361d790725af2668e8ed39b94b6788591423aa Mon Sep 17 00:00:00 2001
From: Salah-Eddine Saakoun <salah-eddine.saakoun@consensys.net>
Date: Thu, 13 Mar 2025 22:04:09 +0100
Subject: [PATCH 5/6] rollback releaseSelections renaming

---
 src/ui/App.tsx         | 52 +++++++++++++++++++-----------------------
 src/ui/PackageItem.tsx | 33 ++++++++++++---------------
 2 files changed, 38 insertions(+), 47 deletions(-)

diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index e8f0826..54ca12b 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -13,7 +13,7 @@ const setsAreEqual = (a: Set<string>, b: Set<string>) => {
 };
 
 type SubmitButtonProps = {
-  releaseSelections: Record<string, string>;
+  selections: Record<string, string>;
   packageDependencyErrors: Record<
     string,
     { missingDependentNames: string[]; missingDependencies: string[] }
@@ -22,16 +22,14 @@ type SubmitButtonProps = {
 };
 
 function SubmitButton({
-  releaseSelections,
+  selections,
   packageDependencyErrors,
   onSubmit,
 }: SubmitButtonProps) {
   const isDisabled =
-    Object.keys(releaseSelections).length === 0 ||
+    Object.keys(selections).length === 0 ||
     Object.keys(packageDependencyErrors).length > 0 ||
-    Object.values(releaseSelections).every(
-      (value) => value === 'intentionally-skip',
-    );
+    Object.values(selections).every((value) => value === 'intentionally-skip');
 
   return (
     <button
@@ -50,9 +48,7 @@ function SubmitButton({
 
 function App() {
   const [packages, setPackages] = useState<Package[]>([]);
-  const [releaseSelections, setReleaseSelections] = useState<
-    Record<string, string>
-  >({});
+  const [selections, setSelections] = useState<Record<string, string>>({});
   const [isSubmitting, setIsSubmitting] = useState(false);
   const [error, setError] = useState<string | null>(null);
   const [changelogs, setChangelogs] = useState<Record<string, string>>({});
@@ -80,7 +76,7 @@ function App() {
   const previousPackages = useRef<Set<string>>(new Set());
 
   useEffect(() => {
-    const majorBumps = Object.entries(releaseSelections)
+    const majorBumps = Object.entries(selections)
       .filter(([_, type]) => type === 'major')
       .map(([pkgName]) => pkgName);
 
@@ -94,9 +90,9 @@ function App() {
       .then((data: Package[]) => {
         const newPackageNames = new Set(data.map((pkg) => pkg.name));
 
-        // Only clean up releaseSelections if the package list actually changed
+        // Only clean up selections if the package list actually changed
         if (!setsAreEqual(previousPackages.current, newPackageNames)) {
-          setReleaseSelections((prev) =>
+          setSelections((prev) =>
             Object.fromEntries(
               Object.entries(prev).filter(([pkgName]) =>
                 newPackageNames.has(pkgName),
@@ -115,7 +111,7 @@ function App() {
         setError(err.message);
         console.error('Error fetching packages:', err);
       });
-  }, [releaseSelections]);
+  }, [selections]);
 
   const checkDependencies = async (selectionData: Record<string, string>) => {
     if (Object.keys(selectionData).length === 0) return;
@@ -148,11 +144,11 @@ function App() {
 
   useEffect(() => {
     const timeoutId = setTimeout(() => {
-      void checkDependencies(releaseSelections);
+      void checkDependencies(selections);
     }, 500);
 
     return () => clearTimeout(timeoutId);
-  }, [releaseSelections]);
+  }, [selections]);
 
   const handleCustomVersionChange = (packageName: string, version: string) => {
     try {
@@ -182,7 +178,7 @@ function App() {
         return rest;
       });
 
-      setReleaseSelections((prev) => ({
+      setSelections((prev) => ({
         ...prev,
         [packageName]: version,
       }));
@@ -199,14 +195,14 @@ function App() {
     value: ReleaseType | '',
   ): void => {
     if (value === '') {
-      const { [packageName]: _, ...rest } = releaseSelections;
-      setReleaseSelections(rest);
+      const { [packageName]: _, ...rest } = selections;
+      setSelections(rest);
 
       const { [packageName]: __, ...remainingErrors } = packageDependencyErrors;
       setPackageDependencyErrors(remainingErrors);
     } else {
-      setReleaseSelections({
-        ...releaseSelections,
+      setSelections({
+        ...selections,
         [packageName]: value,
       });
     }
@@ -218,7 +214,7 @@ function App() {
       const response = await fetch('/api/release', {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify(releaseSelections),
+        body: JSON.stringify(selections),
       });
 
       const data: {
@@ -254,8 +250,8 @@ function App() {
       const errorMessage =
         err instanceof Error ? err.message : 'An error occurred';
       setError(errorMessage);
-      console.error('Error submitting releaseSelections:', err);
-      alert('Failed to submit releaseSelections. Please try again.');
+      console.error('Error submitting selections:', err);
+      alert('Failed to submit selections. Please try again.');
     } finally {
       setIsSubmitting(false);
     }
@@ -280,11 +276,11 @@ function App() {
   };
 
   const handleBulkAction = (action: ReleaseType) => {
-    const newReleaseSelections = { ...releaseSelections };
+    const newReleaseSelections = { ...selections };
     selectedPackages.forEach((packageName) => {
       newReleaseSelections[packageName] = action;
     });
-    setReleaseSelections(newReleaseSelections);
+    setSelections(newReleaseSelections);
     setSelectedPackages(new Set());
     setShowCheckboxes(true);
   };
@@ -393,7 +389,7 @@ function App() {
           <PackageItem
             key={pkg.name}
             pkg={pkg}
-            releaseSelections={releaseSelections}
+            selections={selections}
             versionErrors={versionErrors}
             packageDependencyErrors={packageDependencyErrors}
             loadingChangelogs={loadingChangelogs}
@@ -403,7 +399,7 @@ function App() {
             onSelectionChange={handleSelectionChange}
             onCustomVersionChange={handleCustomVersionChange}
             onFetchChangelog={fetchChangelog}
-            setReleaseSelections={setReleaseSelections}
+            setSelections={setSelections}
             setChangelogs={setChangelogs}
             onToggleSelect={() => togglePackageSelection(pkg.name)}
           />
@@ -412,7 +408,7 @@ function App() {
 
       {packages.length > 0 && (
         <SubmitButton
-          releaseSelections={releaseSelections}
+          selections={selections}
           packageDependencyErrors={packageDependencyErrors}
           onSubmit={handleSubmit}
         />
diff --git a/src/ui/PackageItem.tsx b/src/ui/PackageItem.tsx
index 662466b..7543fa2 100644
--- a/src/ui/PackageItem.tsx
+++ b/src/ui/PackageItem.tsx
@@ -6,7 +6,7 @@ import { Package, ReleaseType } from './types.js';
 
 type PackageItemProps = {
   pkg: Package;
-  releaseSelections: Record<string, string>;
+  selections: Record<string, string>;
   versionErrors: Record<string, string>;
   packageDependencyErrors: Record<
     string,
@@ -22,16 +22,14 @@ type PackageItemProps = {
   onSelectionChange: (packageName: string, value: ReleaseType | '') => void;
   onCustomVersionChange: (packageName: string, version: string) => void;
   onFetchChangelog: (packageName: string) => Promise<void>;
-  setReleaseSelections: React.Dispatch<
-    React.SetStateAction<Record<string, string>>
-  >;
+  setSelections: React.Dispatch<React.SetStateAction<Record<string, string>>>;
   setChangelogs: React.Dispatch<React.SetStateAction<Record<string, string>>>;
   onToggleSelect: () => void;
 };
 
 export function PackageItem({
   pkg,
-  releaseSelections,
+  selections,
   versionErrors,
   packageDependencyErrors,
   loadingChangelogs,
@@ -41,7 +39,7 @@ export function PackageItem({
   onSelectionChange,
   onCustomVersionChange,
   onFetchChangelog,
-  setReleaseSelections,
+  setSelections,
   setChangelogs,
   onToggleSelect,
 }: PackageItemProps) {
@@ -50,8 +48,7 @@ export function PackageItem({
       key={pkg.name}
       id={`package-${pkg.name}`}
       className={`border p-4 rounded-lg ${
-        releaseSelections[pkg.name] &&
-        releaseSelections[pkg.name] !== 'intentionally-skip'
+        selections[pkg.name] && selections[pkg.name] !== 'intentionally-skip'
           ? 'border-2'
           : 'border-gray-200'
       } ${
@@ -79,19 +76,17 @@ export function PackageItem({
           <div className="flex items-center justify-between">
             <div>
               <p className="text-gray-600">Current version: {pkg.version}</p>
-              {releaseSelections[pkg.name] &&
-                releaseSelections[pkg.name] !== 'intentionally-skip' &&
-                releaseSelections[pkg.name] !== 'custom' &&
+              {selections[pkg.name] &&
+                selections[pkg.name] !== 'intentionally-skip' &&
+                selections[pkg.name] !== 'custom' &&
                 !versionErrors[pkg.name] && (
                   <p className="text-yellow-700">
                     New version:{' '}
-                    {!['patch', 'minor', 'major'].includes(
-                      releaseSelections[pkg.name],
-                    )
-                      ? releaseSelections[pkg.name]
+                    {!['patch', 'minor', 'major'].includes(selections[pkg.name])
+                      ? selections[pkg.name]
                       : new SemVer(pkg.version)
                           .inc(
-                            releaseSelections[pkg.name] as Exclude<
+                            selections[pkg.name] as Exclude<
                               ReleaseType,
                               'intentionally-skip' | 'custom' | string
                             >,
@@ -107,7 +102,7 @@ export function PackageItem({
             </div>
             <VersionSelector
               packageName={pkg.name}
-              selection={releaseSelections[pkg.name]}
+              selection={selections[pkg.name]}
               onSelectionChange={onSelectionChange}
               onCustomVersionChange={onCustomVersionChange}
               onFetchChangelog={onFetchChangelog}
@@ -125,7 +120,7 @@ export function PackageItem({
               <DependencyErrorSection
                 title="Missing Dependencies"
                 items={packageDependencyErrors[pkg.name].missingDependencies}
-                setSelections={setReleaseSelections}
+                setSelections={setSelections}
                 description={`The following packages are dependencies or peer dependencies of ${pkg.name}. Because they may have introduced new changes that ${pkg.name} is now using, you need to verify whether to include them in the release.
 
 To do this, look at the change history for each package and compare it with the change history for ${pkg.name}. If ${pkg.name} uses any new changes from a package, then you need to include it by bumping its version. If you have confirmed that the changes to a package do not affect ${pkg.name}, you may omit it from the release by choosing "Skip" instead.`}
@@ -139,7 +134,7 @@ To do this, look at the change history for each package and compare it with the
                   items={
                     packageDependencyErrors[pkg.name].missingDependentNames
                   }
-                  setSelections={setReleaseSelections}
+                  setSelections={setSelections}
                   description={`Because ${pkg.name} is being released with a new major version, to prevent peer dependency warnings in consuming projects, all of the following packages which list ${pkg.name} as a peer dependency need to be included in the release. Please choose new versions for these packages. If for some reason you feel it is safe to omit a package you may choose "Skip".`}
                 />
               </div>

From 2795046b12d145f0642b3f7ecb17fae5765eebe9 Mon Sep 17 00:00:00 2001
From: Salah-Eddine Saakoun <salah-eddine.saakoun@consensys.net>
Date: Fri, 14 Mar 2025 23:34:04 +0100
Subject: [PATCH 6/6] Revert newReleaseSelections renaming

---
 src/ui/App.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 54ca12b..f2b2d2c 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -276,11 +276,11 @@ function App() {
   };
 
   const handleBulkAction = (action: ReleaseType) => {
-    const newReleaseSelections = { ...selections };
+    const newSelections = { ...selections };
     selectedPackages.forEach((packageName) => {
-      newReleaseSelections[packageName] = action;
+      newSelections[packageName] = action;
     });
-    setSelections(newReleaseSelections);
+    setSelections(newSelections);
     setSelectedPackages(new Set());
     setShowCheckboxes(true);
   };