import { MANUAL_TRIGGER_NODE_NAME } from '../../../config/constants'; import { expect, test } from '../../../fixtures/base'; import type { n8nPage } from '../../../pages/n8nPage'; import { type GitRepoHelper, setupGitRepo } from '../../../utils/source-control-helper'; test.use({ capability: 'source-control' }); async function expectPushSuccess(n8n: n8nPage) { expect( await n8n.notifications.waitForNotificationAndClose('Pushed successfully', { timeout: 30000 }), ).toBe(true); } async function expectNoChangesToCommit(n8n: n8nPage) { expect( await n8n.notifications.waitForNotificationAndClose('No changes to commit', { timeout: 10000 }), ).toBe(true); } // Skipped: These tests are flaky. Re-enable when PAY-4365 is resolved. // https://linear.app/n8n/issue/PAY-4365/bug-source-control-operations-fail-in-multi-main-deployment test.describe( 'Push resources to Git @capability:source-control', { annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }], }, () => { test.fixme(); let gitRepo: GitRepoHelper; test.beforeEach(async ({ n8n, services }) => { await n8n.api.enableFeature('sourceControl'); await n8n.api.enableFeature('variables'); gitRepo = await setupGitRepo(n8n, services.gitea); }); test('should push a new workflow', async ({ n8n }) => { // create workflow const workflow = await n8n.api.workflows.createWorkflow({ name: 'Test Workflow', nodes: [], connections: {}, active: false, }); await n8n.navigate.toWorkflow(workflow.id); // check modal await n8n.sideBar.getSourceControlPushButton().click(); await expect(n8n.sourceControlPushModal.container).toBeVisible(); const isWorkflowsTabSelected = await n8n.sourceControlPushModal.isWorkflowsTabSelected(); expect(isWorkflowsTabSelected).toBe(true); // check selected workflow await expect(n8n.sourceControlPushModal.getFileInModal('Test Workflow')).toBeVisible(); const workflowCheckbox = n8n.sourceControlPushModal.getFileCheckboxByName('Test Workflow'); await expect(workflowCheckbox).toBeChecked(); // push and wait for commit to be indexed await gitRepo.pushAndWait(n8n, 'Add test workflow with manual trigger'); await expectPushSuccess(n8n); // check no changes to commit await n8n.sideBar.getSourceControlPushButton().click(); await expectNoChangesToCommit(n8n); }); test('should push all resource types together', async ({ n8n }) => { // variables and tags await n8n.api.variables.createTestVariable(); await n8n.api.tags.create('test-tag'); // projects and folders const project = await n8n.api.projects.createProject('Test Project'); const folderA = await n8n.api.projects.createFolder(project.id, 'Folder A'); const folderB = await n8n.api.projects.createFolder(project.id, 'Folder B', folderA.id); // workflows await n8n.api.workflows.createInProject(project.id, { name: 'Workflow in Folder A', folder: folderA.id, }); await n8n.api.workflows.createInProject(project.id, { name: 'Workflow in Folder B', folder: folderB.id, }); await n8n.api.workflows.createInProject(project.id, { name: 'Root Workflow', }); // credentials await n8n.api.credentials.createCredential({ name: 'Credential in Project 1', type: 'notionApi', data: { apiKey: '1234567890' }, projectId: project.id, }); // Open push modal await n8n.navigate.toHome(); await expect(n8n.workflows.cards.getWorkflow('Workflow in Folder A')).toBeVisible(); await expect(n8n.workflows.cards.getWorkflow('Workflow in Folder B')).toBeVisible(); await expect(n8n.workflows.cards.getWorkflow('Root Workflow')).toBeVisible(); await n8n.sideBar.getSourceControlPushButton().click(); await expect(n8n.sourceControlPushModal.container).toBeVisible(); // check notice const notice = n8n.sourceControlPushModal.getNotice(); await expect(notice).toBeVisible(); expect(await notice.textContent()).toContain('Variables'); expect(await notice.textContent()).toContain('Projects'); expect(await notice.textContent()).toContain('Folders'); expect(await notice.textContent()).toContain('Tags'); // check workflows await n8n.sourceControlPushModal.selectWorkflowsTab(); await expect(n8n.sourceControlPushModal.getFileInModal('Workflow in Folder A')).toBeVisible(); await expect(n8n.sourceControlPushModal.getFileInModal('Workflow in Folder B')).toBeVisible(); await expect(n8n.sourceControlPushModal.getFileInModal('Root Workflow')).toBeVisible(); await n8n.sourceControlPushModal.selectAllFilesInModal(); // check credentials await n8n.sourceControlPushModal.selectCredentialsTab(); await expect( n8n.sourceControlPushModal.getFileInModal('Credential in Project 1'), ).toBeVisible(); await n8n.sourceControlPushModal.selectAllFilesInModal(); // Push all resources await n8n.sourceControlPushModal.push('Add all resource types'); await expectPushSuccess(n8n); }); test('should push modifications and deletions', async ({ n8n }) => { // create resources const project = await n8n.api.projects.createProject('Test Project'); const workflow = await n8n.api.workflows.createInProject(project.id, { name: 'Workflow to Modify', }); const credential = await n8n.api.credentials.createCredential({ name: 'Credential to Delete', type: 'notionApi', data: { apiKey: '1234567890' }, projectId: project.id, }); // Push it to Git first await n8n.navigate.toHome(); await expect(n8n.workflows.cards.getWorkflow('Workflow to Modify')).toBeVisible(); await n8n.sideBar.getSourceControlPushButton().click(); await expect(n8n.sourceControlPushModal.container).toBeVisible(); await n8n.sourceControlPushModal.selectWorkflowsTab(); await n8n.sourceControlPushModal.selectAllFilesInModal(); await gitRepo.pushAndWait(n8n, 'new resources'); await expectPushSuccess(n8n); // modify and delete resources await n8n.navigate.toWorkflow(workflow.id); await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME); await n8n.canvas.waitForSaveWorkflowCompleted(); await n8n.api.credentials.deleteCredential(credential.id); await n8n.sideBar.getSourceControlPushButton().click(); await expect(n8n.sourceControlPushModal.container).toBeVisible(); await n8n.sourceControlPushModal.selectWorkflowsTab(); await expect(n8n.sourceControlPushModal.getFileInModal('Workflow to Modify')).toBeVisible(); await expect( n8n.sourceControlPushModal.getStatusBadge('Workflow to Modify', 'Modified'), ).toBeVisible(); await n8n.sourceControlPushModal.selectCredentialsTab(); await expect(n8n.sourceControlPushModal.getFileInModal('Credential to Delete')).toBeVisible(); await expect( n8n.sourceControlPushModal.getStatusBadge('Credential to Delete', 'Deleted'), ).toBeVisible(); // push and wait for commit await gitRepo.pushAndWait(n8n, 'Modify workflow and delete credential'); await expectPushSuccess(n8n); // check no changes to commit await n8n.sideBar.getSourceControlPushButton().click(); await expectNoChangesToCommit(n8n); }); test('should push selected resources', async ({ n8n }) => { // Create multiple workflows const workflowA = await n8n.api.workflows.createWorkflow({ name: 'Workflow A', nodes: [], connections: {}, active: false, }); await n8n.api.workflows.createWorkflow({ name: 'Workflow B', nodes: [], connections: {}, active: false, }); await n8n.api.workflows.createWorkflow({ name: 'Workflow C', nodes: [], connections: {}, active: false, }); // check resources await n8n.navigate.toHome(); await expect(n8n.workflows.cards.getWorkflow('Workflow A')).toBeVisible(); await expect(n8n.workflows.cards.getWorkflow('Workflow B')).toBeVisible(); await expect(n8n.workflows.cards.getWorkflow('Workflow C')).toBeVisible(); await n8n.sideBar.getSourceControlPushButton().click(); await expect(n8n.sourceControlPushModal.container).toBeVisible(); await expect(n8n.sourceControlPushModal.getFileCheckboxByName('Workflow A')).toBeVisible(); await expect(n8n.sourceControlPushModal.getFileCheckboxByName('Workflow B')).toBeVisible(); await expect(n8n.sourceControlPushModal.getFileCheckboxByName('Workflow C')).toBeVisible(); // select await n8n.sourceControlPushModal.selectFile('Workflow A'); await n8n.sourceControlPushModal.selectFile('Workflow C'); await expect(n8n.sourceControlPushModal.getFileCheckboxByName('Workflow A')).toBeChecked(); await expect( n8n.sourceControlPushModal.getFileCheckboxByName('Workflow B'), ).not.toBeChecked(); await expect(n8n.sourceControlPushModal.getFileCheckboxByName('Workflow C')).toBeChecked(); // push and wait for commit await gitRepo.pushAndWait(n8n, 'Push workflows A and C'); await expectPushSuccess(n8n); // modify workflow A await n8n.navigate.toWorkflow(workflowA.id); await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME); await n8n.canvas.waitForSaveWorkflowCompleted(); // check resources await n8n.navigate.toHome(); await n8n.sideBar.getSourceControlPushButton().click(); await expect(n8n.sourceControlPushModal.container).toBeVisible(); await expect(n8n.sourceControlPushModal.getFileCheckboxByName('Workflow A')).toBeVisible(); await expect(n8n.sourceControlPushModal.getFileCheckboxByName('Workflow B')).toBeVisible(); await expect(n8n.sourceControlPushModal.getFileCheckboxByName('Workflow C')).toBeHidden(); await n8n.sourceControlPushModal.selectFile('Workflow A'); await n8n.sourceControlPushModal.selectFile('Workflow B'); // push and wait for commit await gitRepo.pushAndWait(n8n, 'Push workflow A and B'); await expectPushSuccess(n8n); // Verify no more changes await n8n.sideBar.getSourceControlPushButton().click(); await expectNoChangesToCommit(n8n); }); }, );