From e2cf18cc3f389f1d12a4a67f5e6bcf0a358dbe1c Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Thu, 28 May 2026 11:33:39 +0100 Subject: [PATCH 1/3] add --workflows flag --- packages/cli/src/projects/deploy.ts | 55 +++++++-- packages/cli/src/projects/merge.ts | 28 ++++- packages/cli/src/projects/options.ts | 15 +++ packages/cli/test/projects/deploy.test.ts | 88 +++++++++++++ packages/cli/test/projects/merge.test.ts | 129 ++++++++++++++++++++ packages/project/src/index.ts | 2 + packages/project/src/merge/merge-project.ts | 12 +- 7 files changed, 308 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/projects/deploy.ts b/packages/cli/src/projects/deploy.ts index 03f4e21eb..74b471b77 100644 --- a/packages/cli/src/projects/deploy.ts +++ b/packages/cli/src/projects/deploy.ts @@ -1,5 +1,9 @@ import yargs from 'yargs'; -import Project, { versionsEqual, Workspace } from '@openfn/project'; +import Project, { + MergeProjectOptions, + versionsEqual, + Workspace, +} from '@openfn/project'; import { writeFile } from 'node:fs/promises'; import path from 'node:path'; @@ -42,6 +46,7 @@ export type DeployOptions = Pick< name?: string; alias?: string; jsonDiff?: boolean; + workflows?: string[]; }; const options = [ @@ -53,6 +58,7 @@ const options = [ o2.name, o2.alias, o2.jsonDiff, + o2.workflows, // general options o.apiKey, @@ -170,14 +176,29 @@ const syncProjects = async ( // this will actually happen later } - const locallyChangedWorkflows = await findLocallyChangedWorkflows( - ws, - localProject - ); + let mergeCandidates: string[]; + if (options.workflows?.length) { + const missing = options.workflows.filter( + (id) => !localProject.workflows.some((w) => w.id === id) + ); + if (missing.length) { + throw new Error( + `The following workflows were not found in local project ${ + localProject.id + }: ${missing.join(', ')}` + ); + } + logger.info( + `--workflows passed: forcing deploy of ${options.workflows.join(', ')}` + ); + mergeCandidates = options.workflows; + } else { + mergeCandidates = await findLocallyChangedWorkflows(ws, localProject); + } // TODO: what if remote diff and the version checked disagree for some reason? - const diffs = locallyChangedWorkflows.length - ? remoteProject.diff(localProject, locallyChangedWorkflows) + const diffs = mergeCandidates.length + ? remoteProject.diff(localProject, mergeCandidates) : []; if (!diffs.length) { @@ -203,7 +224,7 @@ const syncProjects = async ( const divergentWorkflows = hasRemoteDiverged( localProject, remoteProject!, - locallyChangedWorkflows + mergeCandidates ); if (divergentWorkflows) { logger.warn( @@ -231,16 +252,24 @@ const syncProjects = async ( } logger.info('Merging changes into remote project'); - // TODO I would like to log which workflows are being updated - const merged = Project.merge(localProject, remoteProject!, { + const mergeOptions: MergeProjectOptions = { // If pushing the same project, we use a replace strategy // Otherwise, use the sandbox strategy to preserve UUIDs mode: localProject.uuid === remoteProject.uuid ? 'replace' : 'sandbox', force: true, - onlyUpdated: true, - }); + }; + if (options.workflows?.length) { + // If workflows is passed, force-include exactly the listed workflows via workflowMappings + mergeOptions.workflowMappings = Object.fromEntries( + options.workflows.map((id) => [id, id]) + ); + } else { + // Otherwise only merge locally updated workflows + mergeOptions.onlyUpdated = true; + } + const merged = Project.merge(localProject, remoteProject!, mergeOptions); - return { merged, remoteProject, locallyChangedWorkflows }; + return { merged, remoteProject, locallyChangedWorkflows: mergeCandidates }; }; export async function handler(options: DeployOptions, logger: Logger) { diff --git a/packages/cli/src/projects/merge.ts b/packages/cli/src/projects/merge.ts index 7c9837c7d..6c5a91155 100644 --- a/packages/cli/src/projects/merge.ts +++ b/packages/cli/src/projects/merge.ts @@ -17,11 +17,12 @@ export type MergeOptions = Required< 'command' | 'project' | 'workspace' | 'removeUnmapped' | 'workflowMappings' > > & - Pick & { base?: string }; + Pick & { base?: string }; const options = [ po.removeUnmapped, po.workflowMappings, + po.workflows, po.workspace, o.log, // custom output because we don't want defaults or anything @@ -109,6 +110,29 @@ export const handler = async (options: MergeOptions, logger: Logger) => { logger.error('The checked out project has no id'); return; } + + let workflowMappings = options.workflowMappings; + if (options.workflows?.length) { + if (workflowMappings && Object.keys(workflowMappings).length) { + logger.error( + '--workflows and --workflow-mappings are mutually exclusive' + ); + return; + } + const missing = options.workflows.filter( + (id) => !sourceProject.workflows.some((w) => w.id === id) + ); + if (missing.length) { + logger.error( + `The following workflows were not found in source project ${sourceProject.id}: ${missing.join(', ')}` + ); + return; + } + workflowMappings = Object.fromEntries( + options.workflows.map((id) => [id, id]) + ); + } + const finalPath = options.outputPath ?? workspace.getProjectPath(targetProject.id); if (!finalPath) { @@ -117,7 +141,7 @@ export const handler = async (options: MergeOptions, logger: Logger) => { } const final = Project.merge(sourceProject, targetProject, { removeUnmapped: options.removeUnmapped, - workflowMappings: options.workflowMappings, + workflowMappings, force: options.force, }); diff --git a/packages/cli/src/projects/options.ts b/packages/cli/src/projects/options.ts index ba5ecbfd4..92eac49cc 100644 --- a/packages/cli/src/projects/options.ts +++ b/packages/cli/src/projects/options.ts @@ -8,6 +8,7 @@ export type Opts = BaseOpts & { workspace?: string; removeUnmapped?: boolean | undefined; workflowMappings?: Record | undefined; + workflows?: string[]; project?: string; format?: 'yaml' | 'json' | 'state'; clean?: boolean; @@ -86,6 +87,20 @@ export const workflowMappings: CLIOption = { }, }; +export const workflows: CLIOption = { + name: 'workflows', + yargs: { + array: true, + description: + 'Restrict merge/deploy to the given workflow ids. Listed workflows are force-included from the source and will overwrite the target/remote even if unchanged locally. Mutually exclusive with --workflow-mappings.', + }, + ensure: (opts: any) => { + if (opts.workflows?.length) { + opts.workflows = Array.from(new Set(opts.workflows)); + } + }, +}; + // We declare a new output path here, overriding the default cli one, // because default rules are different export const outputPath: CLIOption = { diff --git a/packages/cli/test/projects/deploy.test.ts b/packages/cli/test/projects/deploy.test.ts index 2be2f28c2..a309d5f79 100644 --- a/packages/cli/test/projects/deploy.test.ts +++ b/packages/cli/test/projects/deploy.test.ts @@ -194,6 +194,94 @@ test.serial( } ); +test.serial( + '--workflows errors when an id is not in the local project', + async (t) => { + await setup(projectYaml); + + await t.throwsAsync( + () => + deploy( + { + endpoint: ENDPOINT, + apiKey: 'test-api-key', + workspace: '/ws', + confirm: false, + workflows: ['nope-not-a-real-workflow'], + } as any, + logger + ), + { message: /nope-not-a-real-workflow/ } + ); + } +); + +test.serial( + '--workflows force-pushes a locally-unchanged workflow over a diverged remote when --force is set', + async (t) => { + t.truthy(server.state.projects[UUID]); + await setup(projectYaml); + + // Local is unchanged. Modify remote so it diverges. + const modified = JSON.parse( + JSON.stringify(server.state.projects[UUID].workflows['my-workflow']) + ); + modified.jobs['transform-data'].body = 'each()'; + server.updateWorkflow(UUID, modified); + + // Without --workflows, change-detection would say "nothing to deploy". + // With --workflows + --force, we should revert the remote to local. + await deploy( + { + endpoint: ENDPOINT, + apiKey: 'test-api-key', + workspace: '/ws', + confirm: false, + workflows: ['my-workflow'], + force: true, + } as any, + logger + ); + + t.truthy(logger._find('success', /Updated project at/)); + + // The remote should have been overwritten with the local body + const transformData = + server.state.projects[UUID].workflows['my-workflow'].jobs[ + 'transform-data' + ]; + t.is(transformData.body, 'fn()'); + } +); + +test.serial( + '--workflows still errors on divergence without --force', + async (t) => { + await setup(projectYaml); + + const modified = JSON.parse( + JSON.stringify(server.state.projects[UUID].workflows['my-workflow']) + ); + modified.jobs['transform-data'].body = 'each()'; + server.updateWorkflow(UUID, modified); + + await t.throwsAsync( + () => + deploy( + { + endpoint: ENDPOINT, + apiKey: 'test-api-key', + workspace: '/ws', + confirm: false, + workflows: ['my-workflow'], + } as any, + logger + ), + { message: /PROJECTS_DIVERGED/ } + ); + } +); + test('printRichDiff: should report no changes for identical projects', (t) => { const wf = generateWorkflow('@id a trigger-x'); diff --git a/packages/cli/test/projects/merge.test.ts b/packages/cli/test/projects/merge.test.ts index e27aa59f6..3866f3198 100644 --- a/packages/cli/test/projects/merge.test.ts +++ b/packages/cli/test/projects/merge.test.ts @@ -321,3 +321,132 @@ test.serial('merge with custom base', async (t) => { t.is(merged.workflows[0].steps[1].name, 'Job X'); t.is(merged.workflows[0].steps[1].openfn?.uuid, 'job-a'); // id got retained }); + +// Multi-workflow fixtures used by --workflows tests +const buildWorkflow = (id: string, jobId: string, jobName: string) => ({ + name: id, + id, + jobs: [{ id: jobId, name: jobName }], + triggers: [{ type: 'cron', enabled: true, id: `${id}-trigger` }], + edges: [ + { + id: `${id}-edge`, + target_job_id: jobId, + enabled: true, + source_trigger_id: `${id}-trigger`, + condition_type: 'always', + }, + ], +}); + +const multiSandbox = { + id: '', + name: 'My Sandbox', + workflows: [ + buildWorkflow('workflow-1', 'job-x', 'Job X (from sandbox)'), + buildWorkflow('workflow-2', 'job-y', 'Job Y (from sandbox)'), + ], +}; + +const multiMain = { + id: '', + name: 'My Project', + workflows: [ + buildWorkflow('workflow-1', 'job-a', 'Job A (from main)'), + buildWorkflow('workflow-2', 'job-b', 'Job B (from main)'), + ], +}; + +const mockMultiWorkflowWorkspace = () => { + mock({ + '/ws/workflows': {}, + '/ws/openfn.yaml': jsonToYaml({ + project: { id: 'my-project', name: 'My Project' }, + workspace: { + dirs: { workflows: 'workflows' }, + formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, + }, + }), + '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml(multiSandbox), + '/ws/.projects/project@app.openfn.org.yaml': jsonToYaml(multiMain), + }); +}; + +test.serial( + '--workflows merges only the listed workflow, leaving other target workflows untouched', + async (t) => { + mockMultiWorkflowWorkspace(); + + await mergeHandler( + { + command: 'project-merge', + workspace: '/ws', + project: 'my-sandbox', + removeUnmapped: false, + workflowMappings: {}, + workflows: ['workflow-1'], + }, + logger + ); + + const merged = await Project.from( + 'path', + '/ws/.projects/project@app.openfn.org.yaml' + ); + + const wf1 = merged.workflows.find((w) => w.id === 'workflow-1')!; + const wf2 = merged.workflows.find((w) => w.id === 'workflow-2')!; + + // workflow-1 was merged from sandbox (Job X overlaid) + t.truthy(wf1.steps.find((s) => s.name === 'Job X (from sandbox)')); + // workflow-2 was NOT touched - still has Job B from main, not Job Y from sandbox + t.truthy(wf2.steps.find((s) => s.name === 'Job B (from main)')); + t.falsy(wf2.steps.find((s) => s.name === 'Job Y (from sandbox)')); + } +); + +test.serial( + '--workflows errors when an id is not in the source project', + async (t) => { + mockMultiWorkflowWorkspace(); + + await mergeHandler( + { + command: 'project-merge', + workspace: '/ws', + project: 'my-sandbox', + removeUnmapped: false, + workflowMappings: {}, + workflows: ['workflow-1', 'does-not-exist'], + }, + logger + ); + + const { message, level } = logger._parse(logger._last); + t.is(level, 'error'); + t.regex(message as string, /does-not-exist/); + } +); + +test.serial( + '--workflows and --workflow-mappings are mutually exclusive', + async (t) => { + mockMultiWorkflowWorkspace(); + + await mergeHandler( + { + command: 'project-merge', + workspace: '/ws', + project: 'my-sandbox', + removeUnmapped: false, + workflowMappings: { 'workflow-1': 'workflow-1' }, + workflows: ['workflow-1'], + }, + logger + ); + + const { message, level } = logger._parse(logger._last); + t.is(level, 'error'); + t.regex(message as string, /mutually exclusive/); + } +); diff --git a/packages/project/src/index.ts b/packages/project/src/index.ts index 431036dd7..f80d48072 100644 --- a/packages/project/src/index.ts +++ b/packages/project/src/index.ts @@ -23,3 +23,5 @@ export { } from './util/version'; export { mapWorkflow } from './parse/from-app-state'; + +export type { MergeProjectOptions } from './merge/merge-project'; diff --git a/packages/project/src/merge/merge-project.ts b/packages/project/src/merge/merge-project.ts index b689bb57f..a80159a6b 100644 --- a/packages/project/src/merge/merge-project.ts +++ b/packages/project/src/merge/merge-project.ts @@ -16,21 +16,21 @@ export const REPLACE_MERGE = 'replace'; export class UnsafeMergeError extends Error {} export type MergeProjectOptions = { - workflowMappings: Record; // - removeUnmapped: boolean; - force: boolean; + workflowMappings?: Record; // + removeUnmapped?: boolean; + force?: boolean; /** * If mode is sandbox, basically only content will be merged and all metadata/settings/options/config is ignored * If mode is replace, all properties on the source will override the target (including UUIDs, name) */ - mode: typeof SANDBOX_MERGE | typeof REPLACE_MERGE; + mode?: typeof SANDBOX_MERGE | typeof REPLACE_MERGE; /** * If true, only workflows that have changed in the source * will be merged. */ - onlyUpdated: boolean; + onlyUpdated?: boolean; }; const defaultOptions: MergeProjectOptions = { @@ -54,7 +54,7 @@ const defaultOptions: MergeProjectOptions = { export function merge( source: Project, target: Project, - opts?: Partial + opts?: MergeProjectOptions ) { const options = defaultsDeep( opts, From 79e3a3e70a68d6d714c4b1256a808953849f86b4 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Thu, 28 May 2026 15:57:36 +0100 Subject: [PATCH 2/3] rename --workflows to --workflow and alias --- packages/cli/src/projects/deploy.ts | 18 +++--- packages/cli/src/projects/merge.ts | 12 ++-- packages/cli/src/projects/options.ts | 15 ++--- packages/cli/test/projects/deploy.test.ts | 56 +++++++++++++++--- packages/cli/test/projects/fixtures.ts | 69 +++++++++++++++++++++++ packages/cli/test/projects/merge.test.ts | 12 ++-- 6 files changed, 145 insertions(+), 37 deletions(-) diff --git a/packages/cli/src/projects/deploy.ts b/packages/cli/src/projects/deploy.ts index 74b471b77..75cd97d5b 100644 --- a/packages/cli/src/projects/deploy.ts +++ b/packages/cli/src/projects/deploy.ts @@ -46,7 +46,7 @@ export type DeployOptions = Pick< name?: string; alias?: string; jsonDiff?: boolean; - workflows?: string[]; + workflow?: string[]; }; const options = [ @@ -58,7 +58,7 @@ const options = [ o2.name, o2.alias, o2.jsonDiff, - o2.workflows, + o2.workflow, // general options o.apiKey, @@ -177,8 +177,8 @@ const syncProjects = async ( } let mergeCandidates: string[]; - if (options.workflows?.length) { - const missing = options.workflows.filter( + if (options.workflow?.length) { + const missing = options.workflow.filter( (id) => !localProject.workflows.some((w) => w.id === id) ); if (missing.length) { @@ -189,9 +189,9 @@ const syncProjects = async ( ); } logger.info( - `--workflows passed: forcing deploy of ${options.workflows.join(', ')}` + `--workflow passed: forcing deploy of ${options.workflow.join(', ')}` ); - mergeCandidates = options.workflows; + mergeCandidates = options.workflow; } else { mergeCandidates = await findLocallyChangedWorkflows(ws, localProject); } @@ -258,10 +258,10 @@ const syncProjects = async ( mode: localProject.uuid === remoteProject.uuid ? 'replace' : 'sandbox', force: true, }; - if (options.workflows?.length) { - // If workflows is passed, force-include exactly the listed workflows via workflowMappings + if (options.workflow?.length) { + // If --workflow is passed, force-include exactly the listed workflows via workflowMappings mergeOptions.workflowMappings = Object.fromEntries( - options.workflows.map((id) => [id, id]) + options.workflow.map((id) => [id, id]) ); } else { // Otherwise only merge locally updated workflows diff --git a/packages/cli/src/projects/merge.ts b/packages/cli/src/projects/merge.ts index 6c5a91155..87663997d 100644 --- a/packages/cli/src/projects/merge.ts +++ b/packages/cli/src/projects/merge.ts @@ -17,12 +17,12 @@ export type MergeOptions = Required< 'command' | 'project' | 'workspace' | 'removeUnmapped' | 'workflowMappings' > > & - Pick & { base?: string }; + Pick & { base?: string }; const options = [ po.removeUnmapped, po.workflowMappings, - po.workflows, + po.workflow, po.workspace, o.log, // custom output because we don't want defaults or anything @@ -112,14 +112,14 @@ export const handler = async (options: MergeOptions, logger: Logger) => { } let workflowMappings = options.workflowMappings; - if (options.workflows?.length) { + if (options.workflow?.length) { if (workflowMappings && Object.keys(workflowMappings).length) { logger.error( - '--workflows and --workflow-mappings are mutually exclusive' + '--workflow and --workflow-mappings are mutually exclusive' ); return; } - const missing = options.workflows.filter( + const missing = options.workflow.filter( (id) => !sourceProject.workflows.some((w) => w.id === id) ); if (missing.length) { @@ -129,7 +129,7 @@ export const handler = async (options: MergeOptions, logger: Logger) => { return; } workflowMappings = Object.fromEntries( - options.workflows.map((id) => [id, id]) + options.workflow.map((id) => [id, id]) ); } diff --git a/packages/cli/src/projects/options.ts b/packages/cli/src/projects/options.ts index 92eac49cc..d38400574 100644 --- a/packages/cli/src/projects/options.ts +++ b/packages/cli/src/projects/options.ts @@ -8,7 +8,7 @@ export type Opts = BaseOpts & { workspace?: string; removeUnmapped?: boolean | undefined; workflowMappings?: Record | undefined; - workflows?: string[]; + workflow?: string[]; project?: string; format?: 'yaml' | 'json' | 'state'; clean?: boolean; @@ -87,17 +87,19 @@ export const workflowMappings: CLIOption = { }, }; -export const workflows: CLIOption = { - name: 'workflows', +export const workflow: CLIOption = { + name: 'workflow', yargs: { + alias: ['w'], array: true, description: - 'Restrict merge/deploy to the given workflow ids. Listed workflows are force-included from the source and will overwrite the target/remote even if unchanged locally. Mutually exclusive with --workflow-mappings.', + 'Restrict merge/deploy to the given workflow ids. Pass multiple times to include multiple workflows. Listed workflows are force-included from the source and will overwrite the target/remote even if unchanged locally. Mutually exclusive with --workflow-mappings.', }, ensure: (opts: any) => { - if (opts.workflows?.length) { - opts.workflows = Array.from(new Set(opts.workflows)); + if (opts.workflow?.length) { + opts.workflow = Array.from(new Set(opts.workflow)); } + delete opts.w; }, }; @@ -115,7 +117,6 @@ export const outputPath: CLIOption = { export const workspace: CLIOption = { name: 'workspace', yargs: { - alias: ['w'], description: 'Path to the project workspace (ie, path to openfn.yaml)', }, ensure: (opts: any) => { diff --git a/packages/cli/test/projects/deploy.test.ts b/packages/cli/test/projects/deploy.test.ts index a309d5f79..273b74b3b 100644 --- a/packages/cli/test/projects/deploy.test.ts +++ b/packages/cli/test/projects/deploy.test.ts @@ -11,7 +11,13 @@ import { hasRemoteDiverged, } from '../../src/projects/deploy'; import { printRichDiff } from '../../src/projects/diff'; -import { myProject_yaml, myProject_v1, UUID } from './fixtures'; +import { + myProject_yaml, + myProject_v1, + UUID, + two_workflows_yaml as twowfs, + TWO_WORKFLOWS_UUID, +} from './fixtures'; import { checkout } from '../../src/projects'; let server: any; @@ -21,6 +27,7 @@ const ENDPOINT = `http://localhost:${port}`; // quick fix to the fixture yaml, otherwise the deploy code kicks off const projectYaml = myProject_yaml.replace('https://app.openfn.org', ENDPOINT); +const two_workflows_yaml = twowfs.replace('https://app.openfn.org', ENDPOINT); const mockFs = (paths: Record) => { const pnpm = path.resolve('../../node_modules/.pnpm'); @@ -195,7 +202,37 @@ test.serial( ); test.serial( - '--workflows errors when an id is not in the local project', + 'Passing --workflow only updates the requested workflows', + async (t) => { + await server.addProject(two_workflows_yaml); + await setup(two_workflows_yaml); + + // Change both workflows locally + await writeFile('/ws/workflows/workflow-a/job-a.js', 'modifiedA()'); + await writeFile('/ws/workflows/workflow-b/job-b.js', 'modifiedB()'); + + await deploy( + { + endpoint: ENDPOINT, + apiKey: 'test-api-key', + workspace: '/ws', + confirm: false, + workflow: ['workflow-a'], + } as any, + logger + ); + + const remoteProject = server.state.projects[TWO_WORKFLOWS_UUID]; + t.is( + remoteProject.workflows['workflow-a'].jobs['job-a'].body, + 'modifiedA()' + ); + t.is(remoteProject.workflows['workflow-b'].jobs['job-b'].body, 'fn()'); + } +); + +test.serial( + '--workflow errors when an id is not in the local project', async (t) => { await setup(projectYaml); @@ -207,7 +244,7 @@ test.serial( apiKey: 'test-api-key', workspace: '/ws', confirm: false, - workflows: ['nope-not-a-real-workflow'], + workflow: ['nope-not-a-real-workflow'], } as any, logger ), @@ -216,8 +253,9 @@ test.serial( } ); +// TODO check this langauge and behaviour test.serial( - '--workflows force-pushes a locally-unchanged workflow over a diverged remote when --force is set', + '--workflow force-pushes a locally-unchanged workflow over a diverged remote when --force is set', async (t) => { t.truthy(server.state.projects[UUID]); await setup(projectYaml); @@ -229,15 +267,15 @@ test.serial( modified.jobs['transform-data'].body = 'each()'; server.updateWorkflow(UUID, modified); - // Without --workflows, change-detection would say "nothing to deploy". - // With --workflows + --force, we should revert the remote to local. + // Without --workflow, change-detection would say "nothing to deploy" + // With --workflow + --force, we should revert the remote to local await deploy( { endpoint: ENDPOINT, apiKey: 'test-api-key', workspace: '/ws', confirm: false, - workflows: ['my-workflow'], + workflow: ['my-workflow'], force: true, } as any, logger @@ -255,7 +293,7 @@ test.serial( ); test.serial( - '--workflows still errors on divergence without --force', + '--workflow still errors on divergence without --force', async (t) => { await setup(projectYaml); @@ -273,7 +311,7 @@ test.serial( apiKey: 'test-api-key', workspace: '/ws', confirm: false, - workflows: ['my-workflow'], + workflow: ['my-workflow'], } as any, logger ), diff --git a/packages/cli/test/projects/fixtures.ts b/packages/cli/test/projects/fixtures.ts index b67073a37..64f7e350f 100644 --- a/packages/cli/test/projects/fixtures.ts +++ b/packages/cli/test/projects/fixtures.ts @@ -103,3 +103,72 @@ workflows: lock_version: 1 id: my-workflow start: webhook`; + +export const TWO_WORKFLOWS_UUID = '4b09ddf1-35f4-4e40-9aa9-0d80c086dd9e'; + +export const two_workflows_yaml = `id: my-project +name: My Project +schema_version: '4.0' +description: '' +collections: [] +credentials: [] +openfn: + uuid: ${TWO_WORKFLOWS_UUID} + endpoint: https://app.openfn.org + inserted_at: 2025-04-23T11:15:59Z + updated_at: 2025-04-23T11:15:59Z +options: + allow_support_access: false + requires_mfa: false + retention_policy: retain_all +workflows: + - id: workflow-a + name: Workflow A + steps: + - id: job-a + name: Job A + expression: fn() + adaptor: '@openfn/language-common@latest' + openfn: + uuid: 3d4727b6-4052-4f58-a834-3a03e433ff1d + - id: trigger-a + type: webhook + enabled: true + openfn: + uuid: 1b1c1dd5-e8d9-432f-aeaf-4e09397cac98 + next: + job-a: + condition: always + openfn: + uuid: 1118353a-6015-40f9-8e57-51801a65bcfc + openfn: + uuid: 4584df01-cab4-4182-974d-6a75b13c7b97 + inserted_at: 2025-04-23T11:19:32Z + updated_at: 2025-04-23T11:19:32Z + lock_version: 1 + start: trigger-a + - id: workflow-b + name: Workflow B + steps: + - id: job-b + name: Job B + expression: fn() + adaptor: '@openfn/language-common@latest' + openfn: + uuid: 37e6e616-3840-4d71-b63c-a736ebc208b7 + - id: trigger-b + type: webhook + enabled: true + openfn: + uuid: d65ed915-7f39-428b-af57-57ed2ecf507e + next: + job-b: + condition: always + openfn: + uuid: 4b291d27-c055-40cd-b82d-210644338715 + openfn: + uuid: fc5eeff6-537b-4667-841b-4d17c70dfab9 + inserted_at: 2025-04-23T11:19:32Z + updated_at: 2025-04-23T11:19:32Z + lock_version: 1 + start: trigger-b`; diff --git a/packages/cli/test/projects/merge.test.ts b/packages/cli/test/projects/merge.test.ts index 3866f3198..90bcfbbd2 100644 --- a/packages/cli/test/projects/merge.test.ts +++ b/packages/cli/test/projects/merge.test.ts @@ -373,7 +373,7 @@ const mockMultiWorkflowWorkspace = () => { }; test.serial( - '--workflows merges only the listed workflow, leaving other target workflows untouched', + '--workflow merges only the listed workflow, leaving other target workflows untouched', async (t) => { mockMultiWorkflowWorkspace(); @@ -384,7 +384,7 @@ test.serial( project: 'my-sandbox', removeUnmapped: false, workflowMappings: {}, - workflows: ['workflow-1'], + workflow: ['workflow-1'], }, logger ); @@ -406,7 +406,7 @@ test.serial( ); test.serial( - '--workflows errors when an id is not in the source project', + '--workflow errors when an id is not in the source project', async (t) => { mockMultiWorkflowWorkspace(); @@ -417,7 +417,7 @@ test.serial( project: 'my-sandbox', removeUnmapped: false, workflowMappings: {}, - workflows: ['workflow-1', 'does-not-exist'], + workflow: ['workflow-1', 'does-not-exist'], }, logger ); @@ -429,7 +429,7 @@ test.serial( ); test.serial( - '--workflows and --workflow-mappings are mutually exclusive', + '--workflow and --workflow-mappings are mutually exclusive', async (t) => { mockMultiWorkflowWorkspace(); @@ -440,7 +440,7 @@ test.serial( project: 'my-sandbox', removeUnmapped: false, workflowMappings: { 'workflow-1': 'workflow-1' }, - workflows: ['workflow-1'], + workflow: ['workflow-1'], }, logger ); From baa7223c71c9cd7b7e005cef7a95df059f7da2d8 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Thu, 28 May 2026 15:59:29 +0100 Subject: [PATCH 3/3] changelog --- .changeset/few-cobras-pick.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/few-cobras-pick.md diff --git a/.changeset/few-cobras-pick.md b/.changeset/few-cobras-pick.md new file mode 100644 index 000000000..698db9380 --- /dev/null +++ b/.changeset/few-cobras-pick.md @@ -0,0 +1,7 @@ +--- +'@openfn/cli': minor +--- + +Allow users to specifiy which workflows to deploy or merge by passing `-w`. + +NOTE: the `-w` alias has been repurposed from `--workspace` to `--workflow`. This may affect your local development environment. If so, just expand `-w` to `--workspace`.