From 0ad1e630d137a0f05a91845c096d4805126f4591 Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Wed, 30 Sep 2026 14:39:59 +0200 Subject: [PATCH 01/10] feat: deactivate git-server on migration --- src/otomi-stack.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index d0493fdb..e38e6443 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -700,10 +700,19 @@ export default class OtomiStack { const { repoUrl, branch } = rootStack.gitConfig const isDifferentRepo = repoUrl !== params.repoUrl || branch !== params.branch if (isDifferentRepo) { + // Do not migrate on credential or identity change only if (remoteHasContent) { throw new BadRequestError(`Branch ${params.branch} in repository is not empty`) } - // Do not migrate only on credential or identity change + if (!params.repoUrl.includes('git-server.git-server.svc.cluster.local')) { + // Deactivate the git-server app before pushing commit to new repo + const filePath = getResourceFilePath('AplApp', 'git-server') + const aplApp = this.fileStore.get(filePath) + if (aplApp?.spec?.enabled) { + set(aplApp, 'spec.enabled', false) + await this.saveAppToggle(aplApp) + } + } await this.commitAndPushMigration(params) } await this.storeGitConfig(params) From c96e39234814c02cf21820a9b0aa46b396f0e5ac Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Wed, 30 Sep 2026 15:38:20 +0200 Subject: [PATCH 02/10] fix: enhanced test for migration --- src/otomi-stack.test.ts | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/otomi-stack.test.ts b/src/otomi-stack.test.ts index 7597fc33..072d2030 100644 --- a/src/otomi-stack.test.ts +++ b/src/otomi-stack.test.ts @@ -1461,6 +1461,8 @@ describe('APL code repositories tests', () => { describe('OtomiStack.migrateGitSettings', () => { let stack: OtomiStack + const mockFilestoreGet = jest.fn().mockReturnValue({ spec: { enabled: true } }) + const mockSaveApp = jest.fn() const mockCommit = jest.fn().mockResolvedValue(undefined) const mockPushToNewRemote = jest.fn().mockResolvedValue(undefined) const mockRootPull = jest.fn().mockResolvedValue(undefined) @@ -1485,7 +1487,8 @@ describe('OtomiStack.migrateGitSettings', () => { jest.spyOn(stack as any, 'extractAndStoreSettingsSecrets').mockResolvedValue(undefined) jest.spyOn(require('src/utils'), 'getValuesSchema').mockResolvedValue({ properties: {} }) jest.spyOn(stack as any, 'saveSettings').mockResolvedValue(undefined) - ;(stack as any).fileStore = { set: jest.fn() } + ;(stack as any).fileStore = { set: jest.fn(), get: mockFilestoreGet } + ;(stack as any).saveAppToggle = mockSaveApp ;(stack as any).git = { commit: mockCommit, pushToNewRemote: mockPushToNewRemote, @@ -1516,23 +1519,8 @@ describe('OtomiStack.migrateGitSettings', () => { false, ) - expect(mockCommit).toHaveBeenCalled() - expect(mockPushToNewRemote).toHaveBeenCalled() - expect(mockRefreshGitClient).toHaveBeenCalled() - }) - - it('commits and pushes to new remote when branch changes and remote is empty', async () => { - await stack.migrateGitSettings( - { - repoUrl: 'https://old.example.com/repo.git', - username: 'user', - password: 'pass', - email: 'old@example.com', - branch: 'new-branch', - }, - false, - ) - + expect(mockFilestoreGet).toHaveBeenCalled() + expect(mockSaveApp).toHaveBeenCalledWith({ spec: { enabled: false } }) expect(mockCommit).toHaveBeenCalled() expect(mockPushToNewRemote).toHaveBeenCalled() expect(mockRefreshGitClient).toHaveBeenCalled() @@ -1552,6 +1540,8 @@ describe('OtomiStack.migrateGitSettings', () => { ), ).rejects.toThrow(new BadRequestError('Branch main in repository is not empty')) + expect(mockFilestoreGet).not.toHaveBeenCalled() + expect(mockSaveApp).not.toHaveBeenCalled() expect(mockCommit).not.toHaveBeenCalled() expect(mockPushToNewRemote).not.toHaveBeenCalled() expect(mockRefreshGitClient).not.toHaveBeenCalled() @@ -1569,6 +1559,8 @@ describe('OtomiStack.migrateGitSettings', () => { false, ) + expect(mockFilestoreGet).not.toHaveBeenCalled() + expect(mockSaveApp).not.toHaveBeenCalled() expect(mockCommit).not.toHaveBeenCalled() expect(mockPushToNewRemote).not.toHaveBeenCalled() expect(mockRefreshGitClient).toHaveBeenCalled() From 1c13edf5b05a20005ae272713a92656a86977907 Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Wed, 30 Sep 2026 16:08:46 +0200 Subject: [PATCH 03/10] fix: restore branch test, fix side-effect --- src/otomi-stack.test.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/otomi-stack.test.ts b/src/otomi-stack.test.ts index 072d2030..408c5d88 100644 --- a/src/otomi-stack.test.ts +++ b/src/otomi-stack.test.ts @@ -1461,7 +1461,13 @@ describe('APL code repositories tests', () => { describe('OtomiStack.migrateGitSettings', () => { let stack: OtomiStack - const mockFilestoreGet = jest.fn().mockReturnValue({ spec: { enabled: true } }) + const mockFilestoreGet = jest.fn().mockImplementation(() => { + return { + spec: { + enabled: true, + }, + } + }) const mockSaveApp = jest.fn() const mockCommit = jest.fn().mockResolvedValue(undefined) const mockPushToNewRemote = jest.fn().mockResolvedValue(undefined) @@ -1526,6 +1532,25 @@ describe('OtomiStack.migrateGitSettings', () => { expect(mockRefreshGitClient).toHaveBeenCalled() }) + it('commits and pushes to new remote when branch changes and remote is empty', async () => { + await stack.migrateGitSettings( + { + repoUrl: 'https://old.example.com/repo.git', + username: 'user', + password: 'pass', + email: 'old@example.com', + branch: 'new-branch', + }, + false, + ) + + expect(mockFilestoreGet).toHaveBeenCalled() + expect(mockSaveApp).toHaveBeenCalledWith({ spec: { enabled: false } }) + expect(mockCommit).toHaveBeenCalled() + expect(mockPushToNewRemote).toHaveBeenCalled() + expect(mockRefreshGitClient).toHaveBeenCalled() + }) + it('throws BadRequestError when repoUrl changes and remote already has content', async () => { await expect( stack.migrateGitSettings( From 51f9fe1badcef59a25de1a6802817662d04caa26 Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Wed, 30 Sep 2026 16:14:15 +0200 Subject: [PATCH 04/10] fix: check on false explicitly --- src/otomi-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index e38e6443..d8f61253 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -708,7 +708,7 @@ export default class OtomiStack { // Deactivate the git-server app before pushing commit to new repo const filePath = getResourceFilePath('AplApp', 'git-server') const aplApp = this.fileStore.get(filePath) - if (aplApp?.spec?.enabled) { + if (aplApp && aplApp.spec?.enabled !== false) { set(aplApp, 'spec.enabled', false) await this.saveAppToggle(aplApp) } From 622d5abd562a7a81854032e274765c840337696a Mon Sep 17 00:00:00 2001 From: merll Date: Wed, 30 Sep 2026 16:17:56 +0200 Subject: [PATCH 05/10] fix: check on hostname more safely Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/otomi-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index d8f61253..90ccb530 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -704,7 +704,7 @@ export default class OtomiStack { if (remoteHasContent) { throw new BadRequestError(`Branch ${params.branch} in repository is not empty`) } - if (!params.repoUrl.includes('git-server.git-server.svc.cluster.local')) { + if (new URL(params.repoUrl).hostname !== new URL(GIT_DEFAULT_CONFIG.repoUrl).hostname) { // Deactivate the git-server app before pushing commit to new repo const filePath = getResourceFilePath('AplApp', 'git-server') const aplApp = this.fileStore.get(filePath) From 68dd77e7fd3e85fc305b16e2fdaf091a25f9f77c Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Thu, 1 Oct 2026 10:14:14 +0200 Subject: [PATCH 06/10] fix: do not write status to the file store --- src/otomi-stack.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 90ccb530..f8dfd3ab 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -1120,7 +1120,7 @@ export default class OtomiStack { async saveCatalog(data: AplPlatformObject): Promise { debug(`Saving catalog: ${data.metadata.name}`) - const content = toPlatformObject(data.kind, data.metadata.name, data.spec) + const content = buildPlatformObject(data.kind, data.metadata.name, data.spec) const filePath = this.fileStore.setPlatformResource(content) await this.git.writeFile(filePath, content) @@ -2720,7 +2720,8 @@ export default class OtomiStack { } async saveAppToggle(app: AplObject): Promise { - const filePath = this.fileStore.setPlatformResource(app as AplPlatformObject) + const platformObject = buildPlatformObject('AplApp', app.metadata.name, app.spec) + const filePath = this.fileStore.setPlatformResource(platformObject) await this.git.writeFile(filePath, app) } From fbc646cf3b0c73ca44b496dcdb34d4728031c6fa Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Thu, 1 Oct 2026 10:20:32 +0200 Subject: [PATCH 07/10] fix: write rebuilt object --- src/otomi-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index f8dfd3ab..8415b918 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -2722,7 +2722,7 @@ export default class OtomiStack { async saveAppToggle(app: AplObject): Promise { const platformObject = buildPlatformObject('AplApp', app.metadata.name, app.spec) const filePath = this.fileStore.setPlatformResource(platformObject) - await this.git.writeFile(filePath, app) + await this.git.writeFile(filePath, platformObject) } async saveAdminApp(app: App): Promise { From c177314c9a68dc83b0177e98b0e0706bfb09f127 Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Thu, 1 Oct 2026 10:44:41 +0200 Subject: [PATCH 08/10] fix: return object with status --- src/otomi-stack.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 8415b918..fb51dc03 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -1120,9 +1120,9 @@ export default class OtomiStack { async saveCatalog(data: AplPlatformObject): Promise { debug(`Saving catalog: ${data.metadata.name}`) - const content = buildPlatformObject(data.kind, data.metadata.name, data.spec) - const filePath = this.fileStore.setPlatformResource(content) - await this.git.writeFile(filePath, content) + const content = toPlatformObject(data.kind, data.metadata.name, data.spec) + const filePath = this.fileStore.setPlatformResource(data) + await this.git.writeFile(filePath, data) return { filePath, content } } From 5c3606efdf17ca8aa0453dd56680cf2509ed4ef1 Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Thu, 1 Oct 2026 10:47:14 +0200 Subject: [PATCH 09/10] fix: ai clearly overthinking this, restoring to initial change --- src/otomi-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index fb51dc03..938c87bf 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -704,7 +704,7 @@ export default class OtomiStack { if (remoteHasContent) { throw new BadRequestError(`Branch ${params.branch} in repository is not empty`) } - if (new URL(params.repoUrl).hostname !== new URL(GIT_DEFAULT_CONFIG.repoUrl).hostname) { + if (!params.repoUrl.includes('git-server.git-server.svc.cluster.local')) { // Deactivate the git-server app before pushing commit to new repo const filePath = getResourceFilePath('AplApp', 'git-server') const aplApp = this.fileStore.get(filePath) From 31d48a10ff21f5705a6f0fdd9a7324ca7cf9b75a Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Thu, 1 Oct 2026 11:23:20 +0200 Subject: [PATCH 10/10] revert: unrelated changes --- src/otomi-stack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 938c87bf..481f6180 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -1121,8 +1121,8 @@ export default class OtomiStack { debug(`Saving catalog: ${data.metadata.name}`) const content = toPlatformObject(data.kind, data.metadata.name, data.spec) - const filePath = this.fileStore.setPlatformResource(data) - await this.git.writeFile(filePath, data) + const filePath = this.fileStore.setPlatformResource(content) + await this.git.writeFile(filePath, content) return { filePath, content } }