feat: Environments release using source control (#6653)
* initial telemetry setup and adjusted pull return * quicksave before merge * feat: add conflicting workflow list to pull modal * feat: update source control pull modal * fix: fix linting issue * feat: add Enter keydown event for submitting source control push modal (no-changelog) feat: add Enter keydown event for submitting source control push modal * quicksave * user workflow table for export * improve telemetry data * pull api telemetry * fix lint * Copy tweaks. * remove authorName and authorEmail and pick from user * rename owners.json to workflow_owners.json * ignore credential conflicts on pull * feat: several push/pull flow changes and design update * pull and push return same data format * fix: add One last step toast for successful pull * feat: add up to date pull toast * fix: add proper Learn more link for push and pull modals * do not await tracking being sent * fix import * fix await * add more sourcecontrolfile status * Minor copy tweak for "More info". * Minor copy tweak for "More info". * ignore variable_stub conflicts on pull * ignore whitespace differences * do not show remote workflows that are not yet created * fix telemetry * fix toast when pulling deleted wf * lint fix * refactor and make some imports dynamic * fix variable edit validation * fix telemetry response * improve telemetry * fix unintenional delete commit * fix status unknown issue * fix up to date toast * do not export active state and reapply versionid * use update instead of upsert * fix: show all workflows when clicking push to git * feat: update Up to date pull translation * fix: update read only env checks * do not update versionid of only active flag changes * feat: prevent access to new workflow and templates import when read only env * feat: send only active state and version if workflow state is not dirty * fix: Detect when only active state has changed and prevent generation a new version ID * feat: improve readonly env messages * make getPreferences public * fix telemetry issue * fix: add partial workflow update based on dirty state when changing active state * update unit tests * fix: remove unsaved changes check in readOnlyEnv * fix: disable push to git button when read onyl env * fix: update readonly toast duration * fix: fix pinning and title input in protected mode * initial commit (NOT working) * working push * cleanup and implement pull * fix getstatus * update import to new method * var and tag diffs are no conflicts * only show pull conflict for workflows * refactor and ignore faulty credentials * add sanitycheck for missing git folder * prefer fetch over pull and limit depth to 1 * back to pull... * fix setting branch on initial connect * fix test * remove clean workfolder * refactor: Remove some unnecessary code * Fixed links to docs. * fix getstatus query params * lint fix * dialog to show local and remote name on conflict * only show remote name on conflict * fix credential expression export * fix: Broken test * dont show toast on pull with empty var/tags and refactor * apply frontend changes from old branch * fix tag with same name import * fix buttons shown for non instance owners * prepare local storage key for removal * refactor: Change wording on pushing and pulling * refactor: Change menu item * test: Fix broken test * Update packages/cli/src/environments/sourceControl/types/sourceControlPushWorkFolder.ts Co-authored-by: Iván Ovejero <ivov.src@gmail.com> --------- Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
committed by
GitHub
parent
bcfc5e717b
commit
fc7aa8bd66
@@ -1,25 +1,61 @@
|
||||
import { Container } from 'typedi';
|
||||
import Container from 'typedi';
|
||||
import { License } from '@/License';
|
||||
import { generateKeyPairSync } from 'crypto';
|
||||
import sshpk from 'sshpk';
|
||||
import type { KeyPair } from './types/keyPair';
|
||||
import { constants as fsConstants, mkdirSync, accessSync } from 'fs';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import { License } from '@/License';
|
||||
import type { KeyPair } from './types/keyPair';
|
||||
import { SOURCE_CONTROL_GIT_KEY_COMMENT } from './constants';
|
||||
import {
|
||||
SOURCE_CONTROL_GIT_KEY_COMMENT,
|
||||
SOURCE_CONTROL_TAGS_EXPORT_FILE,
|
||||
SOURCE_CONTROL_VARIABLES_EXPORT_FILE,
|
||||
} from './constants';
|
||||
import type { SourceControlledFile } from './types/sourceControlledFile';
|
||||
import path from 'path';
|
||||
|
||||
export function sourceControlFoldersExistCheck(folders: string[]) {
|
||||
export function stringContainsExpression(testString: string): boolean {
|
||||
return /^=.*\{\{.*\}\}/.test(testString);
|
||||
}
|
||||
|
||||
export function getWorkflowExportPath(workflowId: string, workflowExportFolder: string): string {
|
||||
return path.join(workflowExportFolder, `${workflowId}.json`);
|
||||
}
|
||||
|
||||
export function getCredentialExportPath(
|
||||
credentialId: string,
|
||||
credentialExportFolder: string,
|
||||
): string {
|
||||
return path.join(credentialExportFolder, `${credentialId}.json`);
|
||||
}
|
||||
|
||||
export function getVariablesPath(gitFolder: string): string {
|
||||
return path.join(gitFolder, SOURCE_CONTROL_VARIABLES_EXPORT_FILE);
|
||||
}
|
||||
|
||||
export function getTagsPath(gitFolder: string): string {
|
||||
return path.join(gitFolder, SOURCE_CONTROL_TAGS_EXPORT_FILE);
|
||||
}
|
||||
|
||||
export function sourceControlFoldersExistCheck(
|
||||
folders: string[],
|
||||
createIfNotExists = true,
|
||||
): boolean {
|
||||
// running these file access function synchronously to avoid race conditions
|
||||
let existed = true;
|
||||
folders.forEach((folder) => {
|
||||
try {
|
||||
accessSync(folder, fsConstants.F_OK);
|
||||
} catch {
|
||||
try {
|
||||
mkdirSync(folder);
|
||||
} catch (error) {
|
||||
LoggerProxy.error((error as Error).message);
|
||||
existed = false;
|
||||
if (createIfNotExists) {
|
||||
try {
|
||||
mkdirSync(folder, { recursive: true });
|
||||
} catch (error) {
|
||||
LoggerProxy.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return existed;
|
||||
}
|
||||
|
||||
export function isSourceControlLicensed() {
|
||||
@@ -27,7 +63,8 @@ export function isSourceControlLicensed() {
|
||||
return license.isSourceControlLicensed();
|
||||
}
|
||||
|
||||
export function generateSshKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') {
|
||||
export async function generateSshKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') {
|
||||
const sshpk = await import('sshpk');
|
||||
const keyPair: KeyPair = {
|
||||
publicKey: '',
|
||||
privateKey: '',
|
||||
@@ -65,3 +102,76 @@ export function generateSshKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') {
|
||||
publicKey: keyPair.publicKey,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRepoType(repoUrl: string): 'github' | 'gitlab' | 'other' {
|
||||
if (repoUrl.includes('github.com')) {
|
||||
return 'github';
|
||||
} else if (repoUrl.includes('gitlab.com')) {
|
||||
return 'gitlab';
|
||||
}
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function filterSourceControlledFilesUniqueIds(files: SourceControlledFile[]) {
|
||||
return (
|
||||
files.filter((file, index, self) => {
|
||||
return self.findIndex((f) => f.id === file.id) === index;
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
export function getTrackingInformationFromPullResult(result: SourceControlledFile[]): {
|
||||
cred_conflicts: number;
|
||||
workflow_conflicts: number;
|
||||
workflow_updates: number;
|
||||
} {
|
||||
const uniques = filterSourceControlledFilesUniqueIds(result);
|
||||
return {
|
||||
cred_conflicts: uniques.filter(
|
||||
(file) =>
|
||||
file.type === 'credential' && file.status === 'modified' && file.location === 'local',
|
||||
).length,
|
||||
workflow_conflicts: uniques.filter(
|
||||
(file) => file.type === 'workflow' && file.status === 'modified' && file.location === 'local',
|
||||
).length,
|
||||
workflow_updates: uniques.filter((file) => file.type === 'workflow').length,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTrackingInformationFromPrePushResult(result: SourceControlledFile[]): {
|
||||
workflows_eligible: number;
|
||||
workflows_eligible_with_conflicts: number;
|
||||
creds_eligible: number;
|
||||
creds_eligible_with_conflicts: number;
|
||||
variables_eligible: number;
|
||||
} {
|
||||
const uniques = filterSourceControlledFilesUniqueIds(result);
|
||||
return {
|
||||
workflows_eligible: uniques.filter((file) => file.type === 'workflow').length,
|
||||
workflows_eligible_with_conflicts: uniques.filter(
|
||||
(file) => file.type === 'workflow' && file.conflict,
|
||||
).length,
|
||||
creds_eligible: uniques.filter((file) => file.type === 'credential').length,
|
||||
creds_eligible_with_conflicts: uniques.filter(
|
||||
(file) => file.type === 'credential' && file.conflict,
|
||||
).length,
|
||||
variables_eligible: uniques.filter((file) => file.type === 'variables').length,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTrackingInformationFromPostPushResult(result: SourceControlledFile[]): {
|
||||
workflows_eligible: number;
|
||||
workflows_pushed: number;
|
||||
creds_pushed: number;
|
||||
variables_pushed: number;
|
||||
} {
|
||||
const uniques = filterSourceControlledFilesUniqueIds(result);
|
||||
return {
|
||||
workflows_pushed: uniques.filter((file) => file.pushed && file.type === 'workflow').length ?? 0,
|
||||
workflows_eligible: uniques.filter((file) => file.type === 'workflow').length ?? 0,
|
||||
creds_pushed:
|
||||
uniques.filter((file) => file.pushed && file.file.startsWith('credential_stubs')).length ?? 0,
|
||||
variables_pushed:
|
||||
uniques.filter((file) => file.pushed && file.file.startsWith('variable_stubs')).length ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user