From fe3306350efd6fbe603a7c80079aa59e7f3cc8b1 Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Wed, 8 Apr 2026 15:51:04 +0000 Subject: [PATCH 01/10] feat: update getActiveProject to getCheckedOutProject --- packages/cli/src/projects/checkout.ts | 2 +- packages/cli/src/projects/deploy.ts | 2 +- packages/cli/src/projects/merge.ts | 2 +- packages/cli/src/projects/version.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index 5ae52a2f7..b7bc02506 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -46,7 +46,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { // TODO: try to retain the endpoint for the projects const { project: _, ...config } = workspace.getConfig() as any; - const currentProject = workspace.getActiveProject(); + const currentProject = await workspace.getCheckedOutProject(); // get the project let switchProject; diff --git a/packages/cli/src/projects/deploy.ts b/packages/cli/src/projects/deploy.ts index 8549d481e..2d63992b6 100644 --- a/packages/cli/src/projects/deploy.ts +++ b/packages/cli/src/projects/deploy.ts @@ -251,7 +251,7 @@ export async function handler(options: DeployOptions, logger: Logger) { // We need track alias in openfn.yaml to make this easier (and tracked in from fs) const ws = new Workspace(options.workspace || '.'); - const active = ws.getActiveProject(); + const active = await ws.getCheckedOutProject(); const alias = options.alias ?? active?.alias; const localProject = await Project.from('fs', { diff --git a/packages/cli/src/projects/merge.ts b/packages/cli/src/projects/merge.ts index 6b471e39a..e8333c0fd 100644 --- a/packages/cli/src/projects/merge.ts +++ b/packages/cli/src/projects/merge.ts @@ -71,7 +71,7 @@ export const handler = async (options: MergeOptions, logger: Logger) => { logger.debug('Loading target project from path', basePath); targetProject = await Project.from('path', basePath); } else { - targetProject = workspace.getActiveProject()!; + targetProject = await workspace.getCheckedOutProject()!; if (!targetProject) { logger.error(`No project currently checked out`); return; diff --git a/packages/cli/src/projects/version.ts b/packages/cli/src/projects/version.ts index dfe0c90bf..94b20a893 100644 --- a/packages/cli/src/projects/version.ts +++ b/packages/cli/src/projects/version.ts @@ -32,7 +32,7 @@ export const handler = async (options: VersionOptions, logger: Logger) => { const output = new Map(); - const activeProject = workspace.getActiveProject(); + const activeProject = await workspace.getCheckedOutProject(); if (options.workflow) { const workflow = activeProject?.getWorkflow(options.workflow); if (!workflow) { From 22daada0a8c1c0da8051547ac111c510de20767b Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Wed, 8 Apr 2026 16:01:53 +0000 Subject: [PATCH 02/10] feat: back to active project --- packages/cli/src/projects/merge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/projects/merge.ts b/packages/cli/src/projects/merge.ts index e8333c0fd..6b471e39a 100644 --- a/packages/cli/src/projects/merge.ts +++ b/packages/cli/src/projects/merge.ts @@ -71,7 +71,7 @@ export const handler = async (options: MergeOptions, logger: Logger) => { logger.debug('Loading target project from path', basePath); targetProject = await Project.from('path', basePath); } else { - targetProject = await workspace.getCheckedOutProject()!; + targetProject = workspace.getActiveProject()!; if (!targetProject) { logger.error(`No project currently checked out`); return; From 803dde01af8d68bab888d2b3a474bb73996b09ca Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Wed, 8 Apr 2026 16:38:22 +0000 Subject: [PATCH 03/10] chore: rename getActiveProject to getTrackedProject --- .claude/command-refactor.md | 18 ++++++++++++++++-- packages/cli/src/projects/list.ts | 2 +- packages/cli/src/projects/merge.ts | 2 +- packages/project/src/Workspace.ts | 2 +- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.claude/command-refactor.md b/.claude/command-refactor.md index ea6930782..9359ee505 100644 --- a/.claude/command-refactor.md +++ b/.claude/command-refactor.md @@ -13,6 +13,7 @@ Use `src/projects/list.ts` as the reference pattern for all refactored commands. Create a new file in `src/projects/.ts` that consolidates all the existing command files. **Key elements:** + - Import `yargs`, `ensure`, `build`, `Logger`, and options from `../options` - Define a `Options` type that picks required fields from `Opts` - Create an `options` array with the required options (e.g., `[o.workflow, o.workspace, o.workflowMappings]`) @@ -24,6 +25,7 @@ Create a new file in `src/projects/.ts` that consolidates all the - Export a named `handler` function that takes `(options: Options, logger: Logger)` **Example structure:** + ```typescript import yargs from 'yargs'; import { Workspace } from '@openfn/project'; @@ -88,23 +90,26 @@ export const projectsCommand = { Make three changes: **a) Remove the old import** (if it exists): + ```typescript // Remove: import commandName from './/handler'; ``` **b) Add to CommandList type:** + ```typescript export type CommandList = | 'apollo' // ... | 'project-list' | 'project-version' - | 'project-' // Add this line + | 'project-' // Add this line | 'test' | 'version'; ``` **c) Add to handlers object:** + ```typescript const handlers = { // ... @@ -121,6 +126,7 @@ If the old command was referenced elsewhere in the handlers object (like `projec ### 5. Delete the Old Command Folder Remove the old command directory: + ```bash rm -rf packages/cli/src/ ``` @@ -142,6 +148,7 @@ import checkoutCommand from './checkout/command'; ``` The command will be registered with yargs: + ```typescript .command(projectsCommand) .command(mergeCommand) // Top-level alias @@ -159,18 +166,21 @@ This is different from the `install`/`repo install` pattern, where both entries ## Common Patterns ### Options to Use + - Always include `o.workspace` for project-related commands - Use `o.workflow` for workflow-specific operations - Include `o.json` if the command supports JSON output - Include `o.log` for commands that need detailed logging ### Handler Pattern + - Use `new Workspace(options.workspace)` to access the workspace - Check `workspace.valid` before proceeding -- Use `workspace.getActiveProject()` to get the current project +- Use `workspace.getTrackedProject()` to get the current project - Use appropriate logger methods: `logger.info()`, `logger.error()`, `logger.success()` ### Testing Pattern + If the old command has tests, they need to be refactored: 1. **Create new test file**: Move from `test//handler.test.ts` to `test/projects/.test.ts` @@ -181,6 +191,7 @@ If the old command has tests, they need to be refactored: 4. **Delete old test folder**: Remove `test/` directory **Example changes:** + ```typescript // Before: import mergeHandler from '../../src/merge/handler'; @@ -194,6 +205,7 @@ await mergeHandler({ command: 'project-merge', ... }, logger); ## Checklist ### Basic Refactoring + - [ ] Create new file in `src/projects/.ts` - [ ] Define `Options` type - [ ] Export default command object with `ensure('project-', options)` @@ -206,6 +218,7 @@ await mergeHandler({ command: 'project-merge', ... }, logger); - [ ] Delete old command folder ### Testing (if applicable) + - [ ] Create new test file in `test/projects/.test.ts` - [ ] Update import to use `{ handler } from '../../src/projects/'` - [ ] Update all `command: ''` to `command: 'project-'` in test cases @@ -213,6 +226,7 @@ await mergeHandler({ command: 'project-merge', ... }, logger); - [ ] Run tests to verify they pass ### Additional Steps for Top-Level Aliasing + - [ ] Import command directly in `src/cli.ts` (e.g., `import mergeCommand from './projects/merge'`) - [ ] Register the imported command with `.command(mergeCommand)` - [ ] Verify NO duplicate entries in `src/commands.ts` - only `project-` should exist, not `` diff --git a/packages/cli/src/projects/list.ts b/packages/cli/src/projects/list.ts index dc1e84848..06d091c59 100644 --- a/packages/cli/src/projects/list.ts +++ b/packages/cli/src/projects/list.ts @@ -39,7 +39,7 @@ export const handler = async (options: ProjectListOptions, logger: Logger) => { logger.always(`Available openfn projects\n\n${workspace .list() - .map((p) => describeProject(p, p === workspace.getActiveProject())) + .map((p) => describeProject(p, p === workspace.getTrackedProject())) .join('\n\n')} `); }; diff --git a/packages/cli/src/projects/merge.ts b/packages/cli/src/projects/merge.ts index 6b471e39a..7c9837c7d 100644 --- a/packages/cli/src/projects/merge.ts +++ b/packages/cli/src/projects/merge.ts @@ -71,7 +71,7 @@ export const handler = async (options: MergeOptions, logger: Logger) => { logger.debug('Loading target project from path', basePath); targetProject = await Project.from('path', basePath); } else { - targetProject = workspace.getActiveProject()!; + targetProject = workspace.getTrackedProject()!; if (!targetProject) { logger.error(`No project currently checked out`); return; diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index 618bdbcf2..fead165b5 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -116,7 +116,7 @@ export class Workspace { return this.projectPaths.get(id); } - getActiveProject() { + getTrackedProject() { return ( this.projects.find((p) => p.openfn?.uuid === this.activeProject?.uuid) ?? this.projects.find((p) => p.id === this.activeProject?.id) From 7c71b7071a3c2cc6e676866f80b457106071249e Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Wed, 8 Apr 2026 17:11:20 +0000 Subject: [PATCH 04/10] feat: deploy should use getTrackedProject --- packages/cli/src/projects/deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/projects/deploy.ts b/packages/cli/src/projects/deploy.ts index 2d63992b6..c355f2773 100644 --- a/packages/cli/src/projects/deploy.ts +++ b/packages/cli/src/projects/deploy.ts @@ -251,7 +251,7 @@ export async function handler(options: DeployOptions, logger: Logger) { // We need track alias in openfn.yaml to make this easier (and tracked in from fs) const ws = new Workspace(options.workspace || '.'); - const active = await ws.getCheckedOutProject(); + const active = ws.getTrackedProject(); const alias = options.alias ?? active?.alias; const localProject = await Project.from('fs', { From b11fcb72f927494dced4e65cbc16eafc47059136 Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Thu, 9 Apr 2026 11:57:57 +0000 Subject: [PATCH 05/10] chore: getCheckedout return undefined like getActiveprojects --- packages/project/src/Workspace.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index fead165b5..0f7eb847b 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -123,8 +123,14 @@ export class Workspace { ); } - getCheckedOutProject() { - return Project.from('fs', { root: this.root, config: this.config }); + async getCheckedOutProject() { + return await Project.from('fs', { + root: this.root, + config: this.config, + }).catch((e) => { + if (e.code === 'ENOENT') return undefined; + throw e; + }); } getCredentialMap() { From 748c11d43291172634a89d5bb8d6afccc163e9ac Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Thu, 9 Apr 2026 12:03:17 +0000 Subject: [PATCH 06/10] fix: types --- packages/cli/src/util/load-plan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/util/load-plan.ts b/packages/cli/src/util/load-plan.ts index 203a7acb9..67180457d 100644 --- a/packages/cli/src/util/load-plan.ts +++ b/packages/cli/src/util/load-plan.ts @@ -62,7 +62,7 @@ const loadPlan = async ( }; options.credentials ??= workspace.getConfig().credentials; - options.collectionsEndpoint ??= proj.openfn?.endpoint; + options.collectionsEndpoint ??= proj?.openfn?.endpoint; // Set the cache path to be relative to the workflow options.cachePath ??= workspace.workflowsPath + `/${name}/${CACHE_DIR}`; } From 7490da9f91600e26eab79740e645d1320170a5b8 Mon Sep 17 00:00:00 2001 From: "Farhan Y." Date: Thu, 9 Apr 2026 15:01:21 +0000 Subject: [PATCH 07/10] feat: update removed & renamed workflows on checkout (#1358) --- .changeset/olive-rats-kick.md | 5 + packages/cli/src/projects/checkout.ts | 7 +- packages/cli/src/projects/util.ts | 14 +- packages/cli/test/projects/checkout.test.ts | 255 ++++++++++++++++++++ 4 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 .changeset/olive-rats-kick.md diff --git a/.changeset/olive-rats-kick.md b/.changeset/olive-rats-kick.md new file mode 100644 index 000000000..30ab43f81 --- /dev/null +++ b/.changeset/olive-rats-kick.md @@ -0,0 +1,5 @@ +--- +'@openfn/cli': minor +--- + +Remove renamed/deleted workflows or steps during checkout diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index b7bc02506..bf27cc121 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -46,8 +46,6 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { // TODO: try to retain the endpoint for the projects const { project: _, ...config } = workspace.getConfig() as any; - const currentProject = await workspace.getCheckedOutProject(); - // get the project let switchProject; if (/\.(yaml|json)$/.test(projectIdentifier)) { @@ -68,8 +66,9 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { } // get the current state of the checked out project + let localProject: Project | undefined; try { - const localProject = await Project.from('fs', { + localProject = await Project.from('fs', { root: options.workspace || '.', }); logger?.success(`Loaded local project ${localProject.alias}`); @@ -108,7 +107,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { if (options.clean) { await rimraf(workspace.workflowsPath); } else { - await tidyWorkflowDir(currentProject!, switchProject); + await tidyWorkflowDir(localProject, switchProject, false, workspacePath); } // write the forked from map diff --git a/packages/cli/src/projects/util.ts b/packages/cli/src/projects/util.ts index 43f847add..7a9185585 100644 --- a/packages/cli/src/projects/util.ts +++ b/packages/cli/src/projects/util.ts @@ -1,4 +1,5 @@ import path from 'node:path'; +import fs from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; import { Provisioner } from '@openfn/lexicon/lightning'; import { fetch } from 'undici'; @@ -214,7 +215,8 @@ class DeployError extends Error { export async function tidyWorkflowDir( currentProject: Project | undefined, incomingProject: Project | undefined, - dryRun = false + dryRun = false, + dirPath = '.' ) { if (!currentProject || !incomingProject) { return []; @@ -232,7 +234,15 @@ export async function tidyWorkflowDir( } if (!dryRun) { - await rimraf(toRemove); + const abs = (p: string) => path.join(dirPath, p); + await rimraf(toRemove.map(abs)); + + const dirs = new Set(toRemove.map((f) => path.dirname(f))); + for (const dir of Array.from(dirs)) { + try { + fs.rmdirSync(abs(dir)); // throws when not empty + } catch {} + } } // Return and sort for testing diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index 8f7f74add..29c53065d 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -499,3 +499,258 @@ workspace: ], }); }); + +test.serial( + 'checkout: removes old workflow directory when workflow is renamed on server', + async (t) => { + mock({ + '/ws3/workflows/old-workflow': { + 'old-workflow.yaml': jsonToYaml({ + id: 'old-workflow', + name: 'Old Workflow', + start: 'webhook', + options: {}, + steps: [ + { + id: 'webhook', + type: 'webhook', + enabled: true, + next: { + 'run-job': { disabled: false, condition: 'always' }, + }, + }, + { + id: 'run-job', + name: 'Run Job', + adaptor: '@openfn/language-http@latest', + expression: './run-job.js', + }, + ], + }), + 'run-job.js': 'fn(state => state)', + }, + '/ws3/openfn.yaml': jsonToYaml({ + project: { id: 'main-project' }, + workspace: { + formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, + }, + }), + '/ws3/.projects/main-project@server.yaml': jsonToYaml({ + id: '', + name: 'Main Project', + workflows: [ + { + name: 'New Workflow', + id: 'old-workflow', + version_history: ['v1'], + jobs: [ + { + name: 'Run Job', + body: 'fn(state => state)', + adaptor: '@openfn/language-http@latest', + id: 'run-job-uuid', + }, + ], + triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }], + edges: [ + { + id: 'edge-id', + target_job_id: 'run-job-uuid', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + ], + }), + }); + + await checkoutHandler( + { + command: 'project-checkout', + project: 'main-project', + workspace: '/ws3', + }, + logger + ); + + t.false(fs.existsSync('/ws3/workflows/old-workflow')); + t.true(fs.existsSync('/ws3/workflows/new-workflow')); + } +); + +test.serial( + 'checkout: removes old step file when step is renamed on server', + async (t) => { + mock({ + '/ws4/workflows/my-workflow': { + 'my-workflow.yaml': jsonToYaml({ + id: 'my-workflow', + name: 'My Workflow', + start: 'webhook', + options: {}, + steps: [ + { + id: 'webhook', + type: 'webhook', + enabled: true, + next: { + 'old-step': { disabled: false, condition: 'always' }, + }, + }, + { + id: 'old-step', + name: 'Old Step', + adaptor: '@openfn/language-http@latest', + expression: './old-step.js', + }, + ], + }), + 'old-step.js': 'fn(state => state)', + }, + '/ws4/openfn.yaml': jsonToYaml({ + project: { id: 'main-project' }, + workspace: { + formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, + }, + }), + '/ws4/.projects/main-project@server.yaml': jsonToYaml({ + id: '', + name: 'Main Project', + workflows: [ + { + name: 'My Workflow', + id: 'my-wf-uuid', + version_history: ['v1'], + jobs: [ + { + name: 'New Step', + body: 'fn(state => state)', + adaptor: '@openfn/language-http@latest', + id: 'new-step-uuid', + }, + ], + triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }], + edges: [ + { + id: 'edge-id', + target_job_id: 'new-step-uuid', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + ], + }), + }); + + await checkoutHandler( + { + command: 'project-checkout', + project: 'main-project', + workspace: '/ws4', + }, + logger + ); + + t.false(fs.existsSync('/ws4/workflows/my-workflow/old-step.js')); + t.true(fs.existsSync('/ws4/workflows/my-workflow/new-step.js')); + t.true(fs.existsSync('/ws4/workflows/my-workflow')); + } +); + +test.serial( + 'checkout: removes workflow directory when workflow is deleted on server', + async (t) => { + mock({ + '/ws5/workflows/workflow-a': { + 'workflow-a.yaml': jsonToYaml({ + id: 'workflow-a', + name: 'Workflow A', + start: 'webhook', + options: {}, + steps: [ + { + id: 'webhook', + type: 'webhook', + enabled: true, + next: { 'run-job': { disabled: false, condition: 'always' } }, + }, + { + id: 'run-job', + name: 'Run Job', + adaptor: '@openfn/language-http@latest', + expression: './run-job.js', + }, + ], + }), + 'run-job.js': 'fn(state => state)', + }, + '/ws5/workflows/workflow-b': { + 'workflow-b.yaml': jsonToYaml({ + id: 'workflow-b', + name: 'Workflow B', + start: 'webhook', + options: {}, + steps: [ + { + id: 'webhook', + type: 'webhook', + enabled: true, + next: {}, + }, + ], + }), + }, + '/ws5/openfn.yaml': jsonToYaml({ + project: { id: 'main-project' }, + workspace: { + formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, + }, + }), + '/ws5/.projects/main-project@server.yaml': jsonToYaml({ + id: '', + name: 'Main Project', + workflows: [ + { + name: 'Workflow A', + id: 'wf-a-uuid', + version_history: ['v1'], + jobs: [ + { + name: 'Run Job', + body: 'fn(state => state)', + adaptor: '@openfn/language-http@latest', + id: 'run-job-uuid', + }, + ], + triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }], + edges: [ + { + id: 'edge-id', + target_job_id: 'run-job-uuid', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + ], + }), + }); + + await checkoutHandler( + { + command: 'project-checkout', + project: 'main-project', + workspace: '/ws5', + }, + logger + ); + + t.false(fs.existsSync('/ws5/workflows/workflow-b')); + t.true(fs.existsSync('/ws5/workflows/workflow-a')); + } +); From 5f19966f100fc91ed860cdc89f0cc1c53612bfbf Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Thu, 9 Apr 2026 16:10:39 +0000 Subject: [PATCH 08/10] tests: remove unwanted fields --- packages/cli/test/projects/checkout.test.ts | 172 ++------------------ 1 file changed, 16 insertions(+), 156 deletions(-) diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index 29c53065d..2960b9f0a 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -507,34 +507,11 @@ test.serial( '/ws3/workflows/old-workflow': { 'old-workflow.yaml': jsonToYaml({ id: 'old-workflow', - name: 'Old Workflow', - start: 'webhook', - options: {}, - steps: [ - { - id: 'webhook', - type: 'webhook', - enabled: true, - next: { - 'run-job': { disabled: false, condition: 'always' }, - }, - }, - { - id: 'run-job', - name: 'Run Job', - adaptor: '@openfn/language-http@latest', - expression: './run-job.js', - }, - ], + steps: [{ id: 'run-job', name: 'Run Job', adaptor: '@openfn/language-http@latest', expression: './run-job.js' }], }), 'run-job.js': 'fn(state => state)', }, - '/ws3/openfn.yaml': jsonToYaml({ - project: { id: 'main-project' }, - workspace: { - formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, - }, - }), + '/ws3/openfn.yaml': jsonToYaml({ project: { id: 'main-project' } }), '/ws3/.projects/main-project@server.yaml': jsonToYaml({ id: '', name: 'Main Project', @@ -542,36 +519,16 @@ test.serial( { name: 'New Workflow', id: 'old-workflow', - version_history: ['v1'], - jobs: [ - { - name: 'Run Job', - body: 'fn(state => state)', - adaptor: '@openfn/language-http@latest', - id: 'run-job-uuid', - }, - ], + jobs: [{ name: 'Run Job', body: 'fn(state => state)', adaptor: '@openfn/language-http@latest', id: 'run-job-uuid' }], triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }], - edges: [ - { - id: 'edge-id', - target_job_id: 'run-job-uuid', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], + edges: [{ id: 'edge-id', source_trigger_id: 'trigger-id', target_job_id: 'run-job-uuid', condition_type: 'always', enabled: true }], }, ], }), }); await checkoutHandler( - { - command: 'project-checkout', - project: 'main-project', - workspace: '/ws3', - }, + { command: 'project-checkout', project: 'main-project', workspace: '/ws3' }, logger ); @@ -587,34 +544,11 @@ test.serial( '/ws4/workflows/my-workflow': { 'my-workflow.yaml': jsonToYaml({ id: 'my-workflow', - name: 'My Workflow', - start: 'webhook', - options: {}, - steps: [ - { - id: 'webhook', - type: 'webhook', - enabled: true, - next: { - 'old-step': { disabled: false, condition: 'always' }, - }, - }, - { - id: 'old-step', - name: 'Old Step', - adaptor: '@openfn/language-http@latest', - expression: './old-step.js', - }, - ], + steps: [{ id: 'old-step', name: 'Old Step', adaptor: '@openfn/language-http@latest', expression: './old-step.js' }], }), 'old-step.js': 'fn(state => state)', }, - '/ws4/openfn.yaml': jsonToYaml({ - project: { id: 'main-project' }, - workspace: { - formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, - }, - }), + '/ws4/openfn.yaml': jsonToYaml({ project: { id: 'main-project' } }), '/ws4/.projects/main-project@server.yaml': jsonToYaml({ id: '', name: 'Main Project', @@ -622,36 +556,16 @@ test.serial( { name: 'My Workflow', id: 'my-wf-uuid', - version_history: ['v1'], - jobs: [ - { - name: 'New Step', - body: 'fn(state => state)', - adaptor: '@openfn/language-http@latest', - id: 'new-step-uuid', - }, - ], + jobs: [{ name: 'New Step', body: 'fn(state => state)', adaptor: '@openfn/language-http@latest', id: 'new-step-uuid' }], triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }], - edges: [ - { - id: 'edge-id', - target_job_id: 'new-step-uuid', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], + edges: [{ id: 'edge-id', source_trigger_id: 'trigger-id', target_job_id: 'new-step-uuid', condition_type: 'always', enabled: true }], }, ], }), }); await checkoutHandler( - { - command: 'project-checkout', - project: 'main-project', - workspace: '/ws4', - }, + { command: 'project-checkout', project: 'main-project', workspace: '/ws4' }, logger ); @@ -668,48 +582,14 @@ test.serial( '/ws5/workflows/workflow-a': { 'workflow-a.yaml': jsonToYaml({ id: 'workflow-a', - name: 'Workflow A', - start: 'webhook', - options: {}, - steps: [ - { - id: 'webhook', - type: 'webhook', - enabled: true, - next: { 'run-job': { disabled: false, condition: 'always' } }, - }, - { - id: 'run-job', - name: 'Run Job', - adaptor: '@openfn/language-http@latest', - expression: './run-job.js', - }, - ], + steps: [{ id: 'run-job', name: 'Run Job', adaptor: '@openfn/language-http@latest', expression: './run-job.js' }], }), 'run-job.js': 'fn(state => state)', }, '/ws5/workflows/workflow-b': { - 'workflow-b.yaml': jsonToYaml({ - id: 'workflow-b', - name: 'Workflow B', - start: 'webhook', - options: {}, - steps: [ - { - id: 'webhook', - type: 'webhook', - enabled: true, - next: {}, - }, - ], - }), + 'workflow-b.yaml': jsonToYaml({ id: 'workflow-b', steps: [] }), }, - '/ws5/openfn.yaml': jsonToYaml({ - project: { id: 'main-project' }, - workspace: { - formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, - }, - }), + '/ws5/openfn.yaml': jsonToYaml({ project: { id: 'main-project' } }), '/ws5/.projects/main-project@server.yaml': jsonToYaml({ id: '', name: 'Main Project', @@ -717,36 +597,16 @@ test.serial( { name: 'Workflow A', id: 'wf-a-uuid', - version_history: ['v1'], - jobs: [ - { - name: 'Run Job', - body: 'fn(state => state)', - adaptor: '@openfn/language-http@latest', - id: 'run-job-uuid', - }, - ], + jobs: [{ name: 'Run Job', body: 'fn(state => state)', adaptor: '@openfn/language-http@latest', id: 'run-job-uuid' }], triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }], - edges: [ - { - id: 'edge-id', - target_job_id: 'run-job-uuid', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], + edges: [{ id: 'edge-id', source_trigger_id: 'trigger-id', target_job_id: 'run-job-uuid', condition_type: 'always', enabled: true }], }, ], }), }); await checkoutHandler( - { - command: 'project-checkout', - project: 'main-project', - workspace: '/ws5', - }, + { command: 'project-checkout', project: 'main-project', workspace: '/ws5' }, logger ); From 0a3ea55cfa031f52d423eebe3d9087c62a9e976f Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Fri, 10 Apr 2026 06:18:56 +0000 Subject: [PATCH 09/10] feat: use currentProject --- packages/cli/src/projects/checkout.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index bf27cc121..c81351a85 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -46,6 +46,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { // TODO: try to retain the endpoint for the projects const { project: _, ...config } = workspace.getConfig() as any; + const currentProject = await workspace.getCheckedOutProject(); // get the project let switchProject; if (/\.(yaml|json)$/.test(projectIdentifier)) { @@ -66,9 +67,8 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { } // get the current state of the checked out project - let localProject: Project | undefined; try { - localProject = await Project.from('fs', { + const localProject = await Project.from('fs', { root: options.workspace || '.', }); logger?.success(`Loaded local project ${localProject.alias}`); @@ -107,7 +107,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { if (options.clean) { await rimraf(workspace.workflowsPath); } else { - await tidyWorkflowDir(localProject, switchProject, false, workspacePath); + await tidyWorkflowDir(currentProject, switchProject, false, workspacePath); } // write the forked from map From 0b797e5b4ebb4865b6d1decf650a8c4ec9f1ca9c Mon Sep 17 00:00:00 2001 From: Farhan Yahaya Date: Fri, 10 Apr 2026 06:35:07 +0000 Subject: [PATCH 10/10] tests: remove fields --- packages/cli/test/projects/checkout.test.ts | 47 ++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index 2960b9f0a..2208de789 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -507,7 +507,7 @@ test.serial( '/ws3/workflows/old-workflow': { 'old-workflow.yaml': jsonToYaml({ id: 'old-workflow', - steps: [{ id: 'run-job', name: 'Run Job', adaptor: '@openfn/language-http@latest', expression: './run-job.js' }], + steps: [{ id: 'run-job', expression: './run-job.js' }], }), 'run-job.js': 'fn(state => state)', }, @@ -518,17 +518,20 @@ test.serial( workflows: [ { name: 'New Workflow', - id: 'old-workflow', - jobs: [{ name: 'Run Job', body: 'fn(state => state)', adaptor: '@openfn/language-http@latest', id: 'run-job-uuid' }], - triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }], - edges: [{ id: 'edge-id', source_trigger_id: 'trigger-id', target_job_id: 'run-job-uuid', condition_type: 'always', enabled: true }], + jobs: [{ name: 'Run Job', body: 'fn(s => s)' }], + triggers: [], + edges: [], }, ], }), }); await checkoutHandler( - { command: 'project-checkout', project: 'main-project', workspace: '/ws3' }, + { + command: 'project-checkout', + project: 'main-project', + workspace: '/ws3', + }, logger ); @@ -544,7 +547,7 @@ test.serial( '/ws4/workflows/my-workflow': { 'my-workflow.yaml': jsonToYaml({ id: 'my-workflow', - steps: [{ id: 'old-step', name: 'Old Step', adaptor: '@openfn/language-http@latest', expression: './old-step.js' }], + steps: [{ id: 'old-step', expression: './old-step.js' }], }), 'old-step.js': 'fn(state => state)', }, @@ -555,17 +558,20 @@ test.serial( workflows: [ { name: 'My Workflow', - id: 'my-wf-uuid', - jobs: [{ name: 'New Step', body: 'fn(state => state)', adaptor: '@openfn/language-http@latest', id: 'new-step-uuid' }], - triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }], - edges: [{ id: 'edge-id', source_trigger_id: 'trigger-id', target_job_id: 'new-step-uuid', condition_type: 'always', enabled: true }], + jobs: [{ name: 'New Step', body: 'fn(s => s)' }], + triggers: [], + edges: [], }, - ], + ], }), }); await checkoutHandler( - { command: 'project-checkout', project: 'main-project', workspace: '/ws4' }, + { + command: 'project-checkout', + project: 'main-project', + workspace: '/ws4', + }, logger ); @@ -582,7 +588,7 @@ test.serial( '/ws5/workflows/workflow-a': { 'workflow-a.yaml': jsonToYaml({ id: 'workflow-a', - steps: [{ id: 'run-job', name: 'Run Job', adaptor: '@openfn/language-http@latest', expression: './run-job.js' }], + steps: [{ id: 'run-job', expression: './run-job.js' }], }), 'run-job.js': 'fn(state => state)', }, @@ -596,17 +602,20 @@ test.serial( workflows: [ { name: 'Workflow A', - id: 'wf-a-uuid', - jobs: [{ name: 'Run Job', body: 'fn(state => state)', adaptor: '@openfn/language-http@latest', id: 'run-job-uuid' }], - triggers: [{ type: 'webhook', enabled: true, id: 'trigger-id' }], - edges: [{ id: 'edge-id', source_trigger_id: 'trigger-id', target_job_id: 'run-job-uuid', condition_type: 'always', enabled: true }], + jobs: [{ name: 'Run Job', body: 'fn(s => s)' }], + triggers: [], + edges: [], }, ], }), }); await checkoutHandler( - { command: 'project-checkout', project: 'main-project', workspace: '/ws5' }, + { + command: 'project-checkout', + project: 'main-project', + workspace: '/ws5', + }, logger );