From 972fa944bbb58c1643214694c875191a0c2d8bde Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Wed, 12 Mar 2025 17:44:41 +0100
Subject: [PATCH] fix(postUpgradeCommands): support undeletion (#34766)

---
 .../execute-post-upgrade-commands.spec.ts     | 48 +++++++++++++++++++
 .../branch/execute-post-upgrade-commands.ts   | 23 ++++++++-
 2 files changed, 70 insertions(+), 1 deletion(-)

diff --git a/lib/workers/repository/update/branch/execute-post-upgrade-commands.spec.ts b/lib/workers/repository/update/branch/execute-post-upgrade-commands.spec.ts
index 0a4c335cc6..2ab57af976 100644
--- a/lib/workers/repository/update/branch/execute-post-upgrade-commands.spec.ts
+++ b/lib/workers/repository/update/branch/execute-post-upgrade-commands.spec.ts
@@ -167,5 +167,53 @@ describe('workers/repository/update/branch/execute-post-upgrade-commands', () =>
         'Post-upgrade file did not match any file filters',
       );
     });
+
+    it('handles previously-deleted files which are re-added', async () => {
+      const commands = partial<BranchUpgradeConfig>([
+        {
+          manager: 'some-manager',
+          branchName: 'main',
+          postUpgradeTasks: {
+            executionMode: 'branch',
+            commands: ['command'],
+            fileFilters: ['*.txt'],
+          },
+        },
+      ]);
+      const config: BranchConfig = {
+        manager: 'some-manager',
+        updatedPackageFiles: [
+          { type: 'addition', path: 'unchanged.txt', contents: 'changed' },
+          { type: 'deletion', path: 'was-deleted.txt' },
+        ],
+        upgrades: [],
+        branchName: 'main',
+        baseBranch: 'base',
+      };
+      git.getRepoStatus.mockResolvedValueOnce(
+        partial<StatusResult>({
+          modified: [],
+          not_added: [],
+          deleted: [],
+        }),
+      );
+      GlobalConfig.set({
+        localDir: __dirname,
+        allowedCommands: ['some-command'],
+      });
+      fs.localPathIsFile
+        .mockResolvedValueOnce(true)
+        .mockResolvedValueOnce(false);
+      fs.localPathExists
+        .mockResolvedValueOnce(true)
+        .mockResolvedValueOnce(true);
+
+      const res = await postUpgradeCommands.postUpgradeCommandsExecutor(
+        commands,
+        config,
+      );
+
+      expect(res.updatedArtifacts).toHaveLength(0);
+    });
   });
 });
diff --git a/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts b/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts
index d934ae4248..83b3763a0c 100644
--- a/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts
+++ b/lib/workers/repository/update/branch/execute-post-upgrade-commands.ts
@@ -46,7 +46,9 @@ export async function postUpgradeCommandsExecutor(
     const fileFilters = upgrade.postUpgradeTasks?.fileFilters ?? ['**/*'];
     if (is.nonEmptyArray(commands)) {
       // Persist updated files in file system so any executed commands can see them
-      for (const file of config.updatedPackageFiles!.concat(updatedArtifacts)) {
+      const previouslyModifiedFiles =
+        config.updatedPackageFiles!.concat(updatedArtifacts);
+      for (const file of previouslyModifiedFiles) {
         const canWriteFile = await localPathIsFile(file.path);
         if (file.type === 'addition' && !file.isSymlink && canWriteFile) {
           let contents: Buffer | null;
@@ -122,6 +124,25 @@ export async function postUpgradeCommandsExecutor(
         `Checking ${addedOrModifiedFiles.length} added or modified files for post-upgrade changes`,
       );
 
+      // Check for files which were previously deleted but have been re-added without modification
+      const previouslyDeletedFiles = updatedArtifacts.filter(
+        (ua) => ua.type === 'deletion',
+      );
+      for (const previouslyDeletedFile of previouslyDeletedFiles) {
+        if (!addedOrModifiedFiles.includes(previouslyDeletedFile.path)) {
+          logger.debug(
+            { file: previouslyDeletedFile.path },
+            'Previously deleted file has been restored without modification',
+          );
+          updatedArtifacts = updatedArtifacts.filter(
+            (ua) =>
+              !(
+                ua.type === 'deletion' && ua.path === previouslyDeletedFile.path
+              ),
+          );
+        }
+      }
+
       for (const relativePath of addedOrModifiedFiles) {
         let fileMatched = false;
         for (const pattern of fileFilters) {
-- 
GitLab