From cd75b3b361fd3b2cf910b2e57867fc79bc177216 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Sat, 6 Jun 2026 12:14:48 +0100 Subject: [PATCH 01/15] improve error messages for invalid workspace --- packages/cli/src/projects/list.ts | 5 ++++- packages/cli/src/util/abort.ts | 14 +++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/projects/list.ts b/packages/cli/src/projects/list.ts index 06d091c59..932ad182e 100644 --- a/packages/cli/src/projects/list.ts +++ b/packages/cli/src/projects/list.ts @@ -7,6 +7,7 @@ import * as o from '../options'; import * as po from './options'; import type { Opts } from './options'; +import abort from '../util/abort'; export type ProjectListOptions = Pick; @@ -34,7 +35,9 @@ export const handler = async (options: ProjectListOptions, logger: Logger) => { // eg, this will happen if there's no openfn.yaml file // basically we need the workspace to return a reason // (again, I'm thinking of removing the validation entirely) - throw new Error('No OpenFn projects found'); + abort(logger, `No OpenFn projects found at ${options.workspace}`, { + fix: 'Run this command from a folder with an openfn.yaml file, or pass --workspace to set the workspace root', + }); } logger.always(`Available openfn projects\n\n${workspace diff --git a/packages/cli/src/util/abort.ts b/packages/cli/src/util/abort.ts index 83aa44950..aecc3464e 100644 --- a/packages/cli/src/util/abort.ts +++ b/packages/cli/src/util/abort.ts @@ -16,24 +16,24 @@ interface CLIFriendlyError extends Error { export default ( logger: Logger, reason: string, - error?: CLIFriendlyError, + error?: Partial, help?: string ) => { const e = new AbortError(reason); logger.break(); logger.error(reason); if (error) { - logger.error(error.message); - logger.break(); + if (error.message) { + logger.error(error.message); + logger.break(); + } if (error.details) { - logger.error('ERROR DETAILS:'); logger.error(error.details); logger.break(); } if (error.fix) { - logger.error('FIX HINT:'); - logger.error(error.fix); + logger.always(error.fix); logger.break(); } } @@ -41,7 +41,7 @@ export default ( logger.always(help); } logger.break(); - logger.error('Critical error: aborting command'); + // logger.error('Critical error: aborting command'); process.exitCode = 1; From dff60287a7b42d31e4f84158e3e6b96aa74ca1e6 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Sat, 6 Jun 2026 12:26:13 +0100 Subject: [PATCH 02/15] better diverence message --- packages/cli/src/projects/checkout.ts | 29 ++++++++++++------------- packages/cli/test/projects/list.test.ts | 4 ++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index 883feb128..a46bca84a 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -16,6 +16,7 @@ import { updateForkedFrom, } from './util'; import { createProjectCredentials } from './create-credentials'; +import abort from '../util/abort'; export type CheckoutOptions = Pick< Opts, @@ -71,34 +72,32 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { `Project with id ${projectIdentifier} not found in the workspace` ); } + logger?.info(`Checking out ${switchProject.alias}`); // get the current state of the checked out project try { const localProject = await Project.from('fs', { root: options.workspace || '.', }); - logger?.success(`Loaded local project ${localProject.alias}`); + logger?.info( + `Loaded currently checked out project ${localProject.alias} to check for divergence` + ); const changed = await findLocallyChangedWorkflows( workspace, localProject, 'assume-ok' ); if (changed.length && !options.force) { - logger?.break(); - logger?.warn( - 'WARNING: detected changes on your currently checked-out project' - ); - logger?.warn( - `Changes may be lost by checking out ${localProject.alias} right now` + const err = { + details: `Changes may be lost by checking out ${localProject.alias} right now`, + // TODO how can users save changes? Not really possible right now + fix: 'Pass --force or -f to override this warning and continue', + }; + abort( + logger!, + `${switchProject.alias} has diverged from ${localProject.alias}!`, + err ); - logger?.warn(`Pass --force or -f to override this warning and continue`); - // TODO log to run with force - // TODO need to implement a save function - const e = new Error( - `The currently checked out project has diverged! Changes may be lost` - ); - delete e.stack; - throw e; } } catch (e: any) { if (e.message.match('ENOENT')) { diff --git a/packages/cli/test/projects/list.test.ts b/packages/cli/test/projects/list.test.ts index 6b4b2d05e..0891e5038 100644 --- a/packages/cli/test/projects/list.test.ts +++ b/packages/cli/test/projects/list.test.ts @@ -135,7 +135,7 @@ test('throw for invalid workspace directory', async (t) => { await t.throwsAsync( () => list({ command: 'projects', workspace: '/invalid' }, logger), { - message: 'No OpenFn projects found', + message: 'No OpenFn projects found at /invalid', } ); // const { message } = logger._parse(logger._last); @@ -146,7 +146,7 @@ test('throw if dir is not a workspace', async (t) => { await t.throwsAsync( () => list({ command: 'projects', workspace: '/no-ws' }, logger), { - message: 'No OpenFn projects found', + message: 'No OpenFn projects found at /no-ws', } ); }); From 729302cda5fc5a1a5560e5f136543eef862ad228 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Sun, 7 Jun 2026 11:15:14 +0100 Subject: [PATCH 03/15] add failing test and notes --- packages/cli/src/projects/util.ts | 2 +- packages/cli/test/projects/checkout.test.ts | 164 ++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/projects/util.ts b/packages/cli/src/projects/util.ts index abe1f7e7b..f83758b31 100644 --- a/packages/cli/src/projects/util.ts +++ b/packages/cli/src/projects/util.ts @@ -266,7 +266,7 @@ export const findLocallyChangedWorkflows = async ( ) => { // Check openfn.yaml for the forked_from versions const { forked_from } = workspace.activeProject ?? {}; - + console.log({ forked_from }); // If there are no forked_from references, we have no baseline // so assume everything has changed if (!forked_from || Object.keys(forked_from).length === 0) { diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index 3a110c5bf..82221787e 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -722,3 +722,167 @@ test.serial( t.true(fs.existsSync('/ws5/workflows/workflow-a')); } ); + +const main = `id: sandboxing +name: sandboxing +schema_version: '4.0' +collections: [] +channels: [] +credentials: + - uuid: 8c675997-117b-4e8a-a65e-1ddea0d0e525 + name: name + owner: editor@openfn.org +openfn: + uuid: 44c0c920-5635-4984-ade2-b95fb24cbaf0 + endpoint: http://localhost:4000 + inserted_at: 2025-10-15T11:29:36Z + updated_at: 2026-03-17T11:59:53Z +options: + env: main + allow_support_access: false + requires_mfa: false + retention_policy: retain_all +workflows: + - name: A + steps: + - id: aaa + name: aaa + expression: // abc + adaptor: '@openfn/language-common@latest' + openfn: + uuid: 7b6a6de4-eed2-4204-8ac0-4da8fa64206c + next: + bbb: + disabled: false + condition: on_job_success + openfn: + uuid: 64f1b20f-bfdf-4626-87de-403008cfb05d + - id: bbb + name: bbb + expression: '2' + adaptor: '@openfn/language-common@3.3.1' + openfn: + uuid: 832f5560-69c5-4eae-89cc-823b93af82c8 + - id: webhook + type: webhook + enabled: true + webhook_reply: before_start + openfn: + uuid: 16ddedbb-1d70-44b7-8653-26f8dc802757 + next: + aaa: + disabled: false + condition: always + openfn: + uuid: eccb03ef-990d-4ca7-877b-5452bbc8f63b + history: + - app:0a97362c97b3 + - app:8eb248f07744 + openfn: + uuid: 4b2c13aa-2497-421a-9bb2-783309254130 + updated_at: 2026-05-14T10:25:36Z + inserted_at: 2026-05-14T10:25:10Z + lock_version: 6 + id: a + start: webhook +`; +const staging = `id: joe-2 +name: joe-2 +schema_version: '4.0' +cli: + forked_from: + a: cli:145ff1ae62e5 +collections: [] +channels: [] +credentials: [] +openfn: + uuid: 7c478de6-4c82-427d-aad2-875b1b9eccb8 + endpoint: http://localhost:4000 + alias: staging + inserted_at: 2026-05-26T16:27:05Z + updated_at: 2026-05-26T16:27:05Z +options: + allow_support_access: false + requires_mfa: false + retention_policy: retain_all +workflows: + - name: A + steps: + - id: aaa + name: aaa + expression: // 2 + adaptor: '@openfn/language-common@latest' + openfn: + uuid: 8227ae53-81f8-447f-bb93-213d5721f884 + next: + bbb: + disabled: false + condition: on_job_success + openfn: + uuid: 474d6861-bb47-4fad-953d-a7762751bae0 + - id: bbb + name: bbb + expression: '2' + adaptor: '@openfn/language-http@7.2.11' + openfn: + uuid: 862bec16-ef94-4438-b307-8594a70276fe + - id: webhook + type: webhook + enabled: false + webhook_reply: before_start + openfn: + uuid: d7dfdd68-ecb8-4adc-90cf-8a4ed8cc0235 + next: + aaa: + disabled: false + condition: always + openfn: + uuid: 067cab97-bef8-4d70-b484-5d013d27142b + history: + - cli:145ff1ae62e5 + openfn: + uuid: 9746c1d9-1499-4413-9edc-c23577e9308e + inserted_at: 2026-05-26T16:27:05Z + updated_at: 2026-05-26T16:27:05Z + lock_version: 1 + id: a + start: webhook +`; + +// is this "check out unrelated projects" ? +// which case its project a and project b, not main and staging +// unrelated projects should not throw a divergence warning if not changed +// note that there seem to be no divergence tests in this file +// so probably I need a basic divergence test too +test.serial.only('local issue', async (t) => { + mock({ + '/tmp/openfn.yaml': '', + '/tmp/.projects/main@server.yaml': main, + '/tmp/.projects/staging@server.yaml': staging, + }); + + // first checkout main to set up the file system + await checkoutHandler( + { + command: 'project-checkout', + project: 'main', + workspace: '/tmp', + }, + logger + ); + console.log('main ok'); + logger._reset(); + + // now checkout staging + // this throws, which reproduces my issue + await checkoutHandler( + { + command: 'project-checkout', + project: 'staging', + workspace: '/tmp', + }, + logger + ); +}); + +// TODO: unrelated projects should throw a divergence warning if changed From b23067c2713863ebbe992145d8f95a90fc71b5fb Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Sun, 7 Jun 2026 12:19:42 +0100 Subject: [PATCH 04/15] project: set the alias on a checked out project --- .changeset/yummy-balloons-search.md | 5 +++++ packages/project/src/Workspace.ts | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/yummy-balloons-search.md diff --git a/.changeset/yummy-balloons-search.md b/.changeset/yummy-balloons-search.md new file mode 100644 index 000000000..c6cac63f2 --- /dev/null +++ b/.changeset/yummy-balloons-search.md @@ -0,0 +1,5 @@ +--- +'@openfn/project': patch +--- + +Set the correct alias on the checked out project diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index 0f7eb847b..71ebb4ae9 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -123,10 +123,13 @@ export class Workspace { ); } - async getCheckedOutProject() { + async getCheckedOutProject(alias: string | null = null) { return await Project.from('fs', { root: this.root, config: this.config, + // The checked out project can't meaningfully be said to have an alias + // But we can force one if it makes sense from context + alias: alias, }).catch((e) => { if (e.code === 'ENOENT') return undefined; throw e; From c9cccd5675aa7bddccccfa814ee0bb5a4d39efdd Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Sun, 7 Jun 2026 12:20:17 +0100 Subject: [PATCH 05/15] fix divergence on checkout --- .changeset/late-needles-scream.md | 5 + packages/cli/src/projects/checkout.ts | 97 +++++++++----- packages/cli/src/projects/util.ts | 2 + packages/cli/test/projects/checkout.test.ts | 134 ++++++++++++++------ 4 files changed, 172 insertions(+), 66 deletions(-) create mode 100644 .changeset/late-needles-scream.md diff --git a/.changeset/late-needles-scream.md b/.changeset/late-needles-scream.md new file mode 100644 index 000000000..5b48afa21 --- /dev/null +++ b/.changeset/late-needles-scream.md @@ -0,0 +1,5 @@ +--- +'@openfn/cli': patch +--- + +Fix an issue on checkout where incorrect divergence warnings can be shown diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index a46bca84a..b59e34a52 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -1,5 +1,5 @@ import yargs from 'yargs'; -import Project, { Workspace } from '@openfn/project'; +import Project, { versionsEqual, Workspace } from '@openfn/project'; import path from 'path'; import fs from 'fs'; import { rimraf } from 'rimraf'; @@ -10,11 +10,7 @@ import * as o from '../options'; import * as po from './options'; import type { Opts } from './options'; -import { - findLocallyChangedWorkflows, - tidyWorkflowDir, - updateForkedFrom, -} from './util'; +import { tidyWorkflowDir, updateForkedFrom } from './util'; import { createProjectCredentials } from './create-credentials'; import abort from '../util/abort'; @@ -53,7 +49,9 @@ 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(); + const localProject = await workspace.getCheckedOutProject( + workspace.activeProject.alias + ); // get the project let switchProject; if (/\.(yaml|json)$/.test(projectIdentifier)) { @@ -76,28 +74,26 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { // get the current state of the checked out project try { - const localProject = await Project.from('fs', { - root: options.workspace || '.', - }); - logger?.info( - `Loaded currently checked out project ${localProject.alias} to check for divergence` - ); - const changed = await findLocallyChangedWorkflows( - workspace, - localProject, - 'assume-ok' - ); - if (changed.length && !options.force) { - const err = { - details: `Changes may be lost by checking out ${localProject.alias} right now`, - // TODO how can users save changes? Not really possible right now - fix: 'Pass --force or -f to override this warning and continue', - }; - abort( - logger!, - `${switchProject.alias} has diverged from ${localProject.alias}!`, - err + // If there's no project checked out, there's nothing to compare + if (localProject.workflows.length) { + logger?.info( + `Loaded currently checked out project ${localProject.alias} to check for untracked changes` ); + // TODO is alias robust here? Should we get by alias and domain? + const tracked = workspace.get(localProject.alias); + const changed = hasUntrackedChanges(localProject, tracked); + if (changed.length && !options.force) { + const err = { + details: `Changes may be lost by checking out ${localProject.alias} right now`, + // TODO how can users save changes? Not really possible right now + fix: 'Pass --force or -f to override this warning and continue', + }; + abort( + logger!, + `${switchProject.alias} has diverged from ${localProject.alias}!`, + err + ); + } } } catch (e: any) { if (e.message.match('ENOENT')) { @@ -112,7 +108,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { if (options.clean) { await rimraf(workspace.workflowsPath); } else { - await tidyWorkflowDir(currentProject, switchProject, false, workspacePath); + await tidyWorkflowDir(localProject, switchProject, false, workspacePath); } // write the forked from map @@ -136,3 +132,46 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { logger?.success(`Expanded project to ${workspacePath}`); }; + +// This function will tell us if the active/checked out project +// has any changes compared to the tracked state file +// It implies that changes will be lost on checkout +// (later, users can save a project to an arbitrary save file and so this may not be true) +const hasUntrackedChanges = ( + activeProject: Project, + tracked?: Project | null +): string[] => { + if (!tracked) { + // if there's no tracking we can't compare + // should we log a warning then? + return []; + } + + const changedWorkflows: string[] = []; + + // Check for changed and added workflows + for (const workflow of activeProject.workflows) { + const currentHash = workflow.getVersionHash(); + + const trackedWorkflow = tracked.getWorkflow(workflow.id); + if (!trackedWorkflow) { + // this is a new workflow added locally + changedWorkflows.push(workflow.id); + } + + const trackedHash = trackedWorkflow!.getVersionHash(); + if (!versionsEqual(currentHash, trackedHash)) { + changedWorkflows.push(workflow.id); + } + } + + // Check for removed workflows + for (const workflow of tracked.workflows) { + const localWorkflow = activeProject.getWorkflow(workflow.id); + if (!localWorkflow) { + changedWorkflows.push(workflow.id); + } + } + + return changedWorkflows; +}; diff --git a/packages/cli/src/projects/util.ts b/packages/cli/src/projects/util.ts index f83758b31..7b297be93 100644 --- a/packages/cli/src/projects/util.ts +++ b/packages/cli/src/projects/util.ts @@ -259,6 +259,8 @@ export const updateForkedFrom = (proj: Project) => { return proj; }; +// Compare a project to its version hashed when forked +// This tells us whether the project was edited since it was created export const findLocallyChangedWorkflows = async ( workspace: Workspace, project: Project, diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index 82221787e..f025ca501 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -723,8 +723,12 @@ test.serial( } ); -const main = `id: sandboxing -name: sandboxing +/** + * Using projects foo and bar here which come from a real issue + * Keeping those exact state files to keep diversity in the tests + */ +const foo = `id: foo +name: foo schema_version: '4.0' collections: [] channels: [] @@ -786,8 +790,8 @@ workflows: id: a start: webhook `; -const staging = `id: joe-2 -name: joe-2 +const bar = `id: bar +name: bar schema_version: '4.0' cli: forked_from: @@ -849,40 +853,96 @@ workflows: start: webhook `; -// is this "check out unrelated projects" ? -// which case its project a and project b, not main and staging -// unrelated projects should not throw a divergence warning if not changed -// note that there seem to be no divergence tests in this file -// so probably I need a basic divergence test too -test.serial.only('local issue', async (t) => { - mock({ - '/tmp/openfn.yaml': '', - '/tmp/.projects/main@server.yaml': main, - '/tmp/.projects/staging@server.yaml': staging, - }); +test.serial.only( + 'Checkout unrelated bar from unrelated project foo without divergence warning', + async (t) => { + mock({ + '/tmp/openfn.yaml': '', + '/tmp/.projects/main@server.yaml': foo, + '/tmp/.projects/staging@server.yaml': bar, + }); - // first checkout main to set up the file system - await checkoutHandler( - { - command: 'project-checkout', - project: 'main', - workspace: '/tmp', - }, - logger - ); - console.log('main ok'); - logger._reset(); + // first checkout foo to set up the file system + await checkoutHandler( + { + command: 'project-checkout', + project: 'foo', + workspace: '/tmp', + }, + logger + ); - // now checkout staging - // this throws, which reproduces my issue - await checkoutHandler( - { - command: 'project-checkout', - project: 'staging', - workspace: '/tmp', - }, - logger - ); -}); + // assert that staging was checked out ok + let openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'foo'); + + let expression = fs.readFileSync('/tmp/workflows/a/aaa.js', 'utf8'); + t.is(expression, '// abc'); + + // now checkout bar + await checkoutHandler( + { + command: 'project-checkout', + project: 'bar', + workspace: '/tmp', + }, + logger + ); + logger._reset(); + + // assert that main was checked out ok + openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'bar'); + + expression = fs.readFileSync('/tmp/workflows/a/aaa.js', 'utf8'); + t.is(expression, '// 2'); + } +); + +test.serial.only( + 'Checkout unrelated foo from unrelated project bar without divergence warning', + async (t) => { + mock({ + '/tmp/openfn.yaml': '', + '/tmp/.projects/main@server.yaml': foo, + '/tmp/.projects/staging@server.yaml': bar, + }); + + // first checkout bar to set up the file system + await checkoutHandler( + { + command: 'project-checkout', + project: 'bar', + workspace: '/tmp', + }, + logger + ); + logger._reset(); + + // assert that main was checked out ok + let openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'bar'); + + let expression = fs.readFileSync('/tmp/workflows/a/aaa.js', 'utf8'); + t.is(expression, '// 2'); + + // now checkout foo + await checkoutHandler( + { + command: 'project-checkout', + project: 'foo', + workspace: '/tmp', + }, + logger + ); + + // assert that staging was checked out ok + openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'foo'); + + expression = fs.readFileSync('/tmp/workflows/a/aaa.js', 'utf8'); + t.is(expression, '// abc'); + } +); // TODO: unrelated projects should throw a divergence warning if changed From 8a4377280cf1dafe7f922945be02c440c09905eb Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Sun, 7 Jun 2026 12:54:11 +0100 Subject: [PATCH 06/15] fixes for tests --- packages/cli/src/projects/checkout.ts | 5 +- packages/cli/test/projects/checkout.test.ts | 131 +++++++++++++------- packages/project/src/Project.ts | 8 +- 3 files changed, 95 insertions(+), 49 deletions(-) diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index b59e34a52..bd9256769 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -50,7 +50,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { const { project: _, ...config } = workspace.getConfig() as any; const localProject = await workspace.getCheckedOutProject( - workspace.activeProject.alias + workspace.activeProject.alias ?? null ); // get the project let switchProject; @@ -80,7 +80,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { `Loaded currently checked out project ${localProject.alias} to check for untracked changes` ); // TODO is alias robust here? Should we get by alias and domain? - const tracked = workspace.get(localProject.alias); + const tracked = workspace.get(localProject.alias ?? localProject.id); const changed = hasUntrackedChanges(localProject, tracked); if (changed.length && !options.force) { const err = { @@ -157,6 +157,7 @@ const hasUntrackedChanges = ( if (!trackedWorkflow) { // this is a new workflow added locally changedWorkflows.push(workflow.id); + continue; } const trackedHash = trackedWorkflow!.getVersionHash(); diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index f025ca501..c201af029 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -241,52 +241,55 @@ test.serial( } ); -test.serial('checkout: switching to and back between projects', async (t) => { - // before checkout. my-project is active and expanded - const bcheckout = new Workspace('/ws'); - t.is(bcheckout.activeProject!.id, 'my-project'); +test.serial.only( + 'checkout: switching to and back between projects', + async (t) => { + // before checkout. my-project is active and expanded + const bcheckout = new Workspace('/ws'); + t.is(bcheckout.activeProject!.id, 'my-project'); - // 1. switch from my-project to my-staging - await checkoutHandler( - { command: 'project-checkout', project: 'my-staging', workspace: '/ws' }, - logger - ); - const { message } = logger._parse(logger._last); - t.is(message, 'Expanded project to /ws'); + // 1. switch from my-project to my-staging + await checkoutHandler( + { command: 'project-checkout', project: 'my-staging', workspace: '/ws' }, + logger + ); + const { message } = logger._parse(logger._last); + t.is(message, 'Expanded project to /ws'); - // after checkout. my-staging is active and expanded - const acheckout = new Workspace('/ws'); - t.is(acheckout.activeProject!.id, 'my-staging'); + // after checkout. my-staging is active and expanded + const acheckout = new Workspace('/ws'); + t.is(acheckout.activeProject!.id, 'my-staging'); - // check if files where well expanded - t.deepEqual( - fs.readdirSync('/ws/workflows').sort(), - ['simple-workflow', 'another-workflow'].sort() - ); + // check if files where well expanded + t.deepEqual( + fs.readdirSync('/ws/workflows').sort(), + ['simple-workflow', 'another-workflow'].sort() + ); - // 2. switch back from my-project to my-project - await checkoutHandler( - { - command: 'project-checkout', - project: 'my-project', - workspace: '/ws', - clean: true, - }, - logger - ); - const { message: lastMsg } = logger._parse(logger._last); - t.is(lastMsg, 'Expanded project to /ws'); + // 2. switch back from my-project to my-project + await checkoutHandler( + { + command: 'project-checkout', + project: 'my-project', + workspace: '/ws', + clean: true, + }, + logger + ); + const { message: lastMsg } = logger._parse(logger._last); + t.is(lastMsg, 'Expanded project to /ws'); - // after checkout. my-project is active and expanded - const fcheckout = new Workspace('/ws'); - t.is(fcheckout.activeProject!.id, 'my-project'); + // after checkout. my-project is active and expanded + const fcheckout = new Workspace('/ws'); + t.is(fcheckout.activeProject!.id, 'my-project'); - // check if files where well expanded - t.deepEqual( - fs.readdirSync('/ws/workflows').sort(), - ['simple-workflow-main', 'another-workflow-main'].sort() - ); -}); + // check if files where well expanded + t.deepEqual( + fs.readdirSync('/ws/workflows').sort(), + ['simple-workflow-main', 'another-workflow-main'].sort() + ); + } +); test.serial('checkout: switch with id', async (t) => { const before = new Workspace('/ws'); @@ -853,7 +856,7 @@ workflows: start: webhook `; -test.serial.only( +test.serial( 'Checkout unrelated bar from unrelated project foo without divergence warning', async (t) => { mock({ @@ -899,7 +902,7 @@ test.serial.only( } ); -test.serial.only( +test.serial( 'Checkout unrelated foo from unrelated project bar without divergence warning', async (t) => { mock({ @@ -945,4 +948,46 @@ test.serial.only( } ); -// TODO: unrelated projects should throw a divergence warning if changed +test.serial( + 'If the checked out project has diverged from the tracked version, show a divergence warning on checkout', + async (t) => { + mock({ + '/tmp/openfn.yaml': '', + '/tmp/.projects/main@server.yaml': foo, + '/tmp/.projects/staging@server.yaml': bar, + }); + + await checkoutHandler( + { + command: 'project-checkout', + project: 'bar', + workspace: '/tmp', + }, + logger + ); + logger._reset(); + + // assert that main was checked out ok + let openfn = yamlToJson(fs.readFileSync('/tmp/openfn.yaml', 'utf8')); + t.is(openfn.project.id, 'bar'); + + // Now make a change - on checkout, this change will be lost (it is not saved anywhere) + fs.writeFileSync('/tmp/workflows/a/aaa.js', 'foobar'); + + // now try to checkout foo + await t.throwsAsync( + () => + checkoutHandler( + { + command: 'project-checkout', + project: 'foo', + workspace: '/tmp', + }, + logger + ), + { + message: 'main has diverged from staging!', + } + ); + } +); diff --git a/packages/project/src/Project.ts b/packages/project/src/Project.ts index 749600b26..c1a401db4 100644 --- a/packages/project/src/Project.ts +++ b/packages/project/src/Project.ts @@ -36,7 +36,7 @@ type UUIDMap = { type CLIMeta = { version?: number; - alias?: string; + alias?: string | null; forked_from?: Record; }; @@ -169,11 +169,11 @@ export class Project { } /** Local alias for the project. Comes from the file name. Not shared with Lightning. */ - get alias() { - return this.cli.alias ?? 'main'; + get alias(): string | null { + return this.cli.alias ?? null; } - set alias(value: string) { + set alias(value: string | null) { this.cli ??= {}; this.cli.alias = value; } From 95734c848f990a3b88904d5938ffaecc44a9e3d2 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 8 Jun 2026 09:10:53 +0100 Subject: [PATCH 07/15] fix tests and enable project alias to be null --- .changeset/hungry-lights-love.md | 5 ++ packages/cli/src/projects/checkout.ts | 10 ++- packages/cli/test/projects/checkout.test.ts | 89 ++++++++++--------- packages/lexicon/core.d.ts | 2 +- packages/project/src/Workspace.ts | 11 ++- packages/project/src/parse/from-fs.ts | 2 +- .../src/util/convert-lightning-plan.ts | 3 + 7 files changed, 72 insertions(+), 50 deletions(-) create mode 100644 .changeset/hungry-lights-love.md diff --git a/.changeset/hungry-lights-love.md b/.changeset/hungry-lights-love.md new file mode 100644 index 000000000..232d38d53 --- /dev/null +++ b/.changeset/hungry-lights-love.md @@ -0,0 +1,5 @@ +--- +'@openfn/lexicon': patch +--- + +Allow project.alias to be null diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index bd9256769..f5dabd998 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -50,8 +50,10 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { const { project: _, ...config } = workspace.getConfig() as any; const localProject = await workspace.getCheckedOutProject( - workspace.activeProject.alias ?? null + // TODO not sold on this assignment - I think my test case must be wrong + workspace.activeProject!.alias as any ); + // get the project let switchProject; if (/\.(yaml|json)$/.test(projectIdentifier)) { @@ -75,7 +77,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { // get the current state of the checked out project try { // If there's no project checked out, there's nothing to compare - if (localProject.workflows.length) { + if (localProject?.workflows.length) { logger?.info( `Loaded currently checked out project ${localProject.alias} to check for untracked changes` ); @@ -84,7 +86,9 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { const changed = hasUntrackedChanges(localProject, tracked); if (changed.length && !options.force) { const err = { - details: `Changes may be lost by checking out ${localProject.alias} right now`, + details: `Changes may be lost by checking out ${ + localProject.alias ?? localProject.id + } right now`, // TODO how can users save changes? Not really possible right now fix: 'Pass --force or -f to override this warning and continue', }; diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index c201af029..628bcad8c 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -241,55 +241,52 @@ test.serial( } ); -test.serial.only( - 'checkout: switching to and back between projects', - async (t) => { - // before checkout. my-project is active and expanded - const bcheckout = new Workspace('/ws'); - t.is(bcheckout.activeProject!.id, 'my-project'); +test.serial('checkout: switching to and back between projects', async (t) => { + // before checkout. my-project is active and expanded + const bcheckout = new Workspace('/ws'); + t.is(bcheckout.activeProject!.id, 'my-project'); - // 1. switch from my-project to my-staging - await checkoutHandler( - { command: 'project-checkout', project: 'my-staging', workspace: '/ws' }, - logger - ); - const { message } = logger._parse(logger._last); - t.is(message, 'Expanded project to /ws'); + // 1. switch from my-project to my-staging + await checkoutHandler( + { command: 'project-checkout', project: 'my-staging', workspace: '/ws' }, + logger + ); + const { message } = logger._parse(logger._last); + t.is(message, 'Expanded project to /ws'); - // after checkout. my-staging is active and expanded - const acheckout = new Workspace('/ws'); - t.is(acheckout.activeProject!.id, 'my-staging'); + // after checkout. my-staging is active and expanded + const acheckout = new Workspace('/ws'); + t.is(acheckout.activeProject!.id, 'my-staging'); - // check if files where well expanded - t.deepEqual( - fs.readdirSync('/ws/workflows').sort(), - ['simple-workflow', 'another-workflow'].sort() - ); + // check if files where well expanded + t.deepEqual( + fs.readdirSync('/ws/workflows').sort(), + ['simple-workflow', 'another-workflow'].sort() + ); - // 2. switch back from my-project to my-project - await checkoutHandler( - { - command: 'project-checkout', - project: 'my-project', - workspace: '/ws', - clean: true, - }, - logger - ); - const { message: lastMsg } = logger._parse(logger._last); - t.is(lastMsg, 'Expanded project to /ws'); + // 2. switch back from my-project to my-project + await checkoutHandler( + { + command: 'project-checkout', + project: 'my-project', + workspace: '/ws', + clean: true, + }, + logger + ); + const { message: lastMsg } = logger._parse(logger._last); + t.is(lastMsg, 'Expanded project to /ws'); - // after checkout. my-project is active and expanded - const fcheckout = new Workspace('/ws'); - t.is(fcheckout.activeProject!.id, 'my-project'); + // after checkout. my-project is active and expanded + const fcheckout = new Workspace('/ws'); + t.is(fcheckout.activeProject!.id, 'my-project'); - // check if files where well expanded - t.deepEqual( - fs.readdirSync('/ws/workflows').sort(), - ['simple-workflow-main', 'another-workflow-main'].sort() - ); - } -); + // check if files where well expanded + t.deepEqual( + fs.readdirSync('/ws/workflows').sort(), + ['simple-workflow-main', 'another-workflow-main'].sort() + ); +}); test.serial('checkout: switch with id', async (t) => { const before = new Workspace('/ws'); @@ -533,6 +530,8 @@ test.serial( command: 'project-checkout', project: 'main-project', workspace: '/ws3', + // the project on-disk has diverged from the statefile, so we need to force it through + force: true, }, logger ); @@ -573,6 +572,8 @@ test.serial( command: 'project-checkout', project: 'main-project', workspace: '/ws4', + // the project on-disk has diverged from the statefile, so we need to force it through + force: true, }, logger ); @@ -717,6 +718,8 @@ test.serial( command: 'project-checkout', project: 'main-project', workspace: '/ws5', + // the project on-disk has diverged from the statefile, so we need to force it through + force: true, }, logger ); diff --git a/packages/lexicon/core.d.ts b/packages/lexicon/core.d.ts index bdf6be58c..1158ffb69 100644 --- a/packages/lexicon/core.d.ts +++ b/packages/lexicon/core.d.ts @@ -94,7 +94,7 @@ export interface LocalMeta { This only affects how a state file ondisk is parsed */ version?: number; /** Shorthand identifier used by CLI commands */ - alias?: string; + alias?: string | null; [key: string]: any; } diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index 71ebb4ae9..f7e2bc3ec 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -48,6 +48,13 @@ export class Workspace { } } this.config = buildConfig(context.workspace); + + // TODO: work out the alias of the active project + // and make sure it's written + // tbh as activeProject is just the metadata in openfn.yaml, + // it's not super reliable + // Actually would it not be better to find the ACTUAL project and just + // reference that? this.activeProject = context.project; const projectsPath = path.join(workspacePath, this.config.dirs.projects); @@ -123,13 +130,13 @@ export class Workspace { ); } - async getCheckedOutProject(alias: string | null = null) { + async getCheckedOutProject(alias?: string | null) { return await Project.from('fs', { root: this.root, config: this.config, // The checked out project can't meaningfully be said to have an alias // But we can force one if it makes sense from context - alias: alias, + alias: alias ?? null, }).catch((e) => { if (e.code === 'ENOENT') return undefined; throw e; diff --git a/packages/project/src/parse/from-fs.ts b/packages/project/src/parse/from-fs.ts index 1382eecba..dd6bad57a 100644 --- a/packages/project/src/parse/from-fs.ts +++ b/packages/project/src/parse/from-fs.ts @@ -19,7 +19,7 @@ export type FromFsConfig = { root: string; config?: Partial; logger?: Logger; - alias?: string; + alias?: string | null; name?: string; }; diff --git a/packages/ws-worker/src/util/convert-lightning-plan.ts b/packages/ws-worker/src/util/convert-lightning-plan.ts index 4307e7dbe..b00153b46 100644 --- a/packages/ws-worker/src/util/convert-lightning-plan.ts +++ b/packages/ws-worker/src/util/convert-lightning-plan.ts @@ -71,6 +71,7 @@ export default ( for (const root of monorepoRoots) { const candidate = path.resolve(root, 'packages', shortName); if (fs.existsSync(path.join(candidate, 'package.json'))) { + console.log(' >>>> found ', candidate); return candidate; } } @@ -83,6 +84,7 @@ export default ( }; const appendLocalVersions = (job: Job) => { + console.log({ monorepoRoots }); if (monorepoRoots.length && job.adaptors!) { for (const adaptor of job.adaptors) { const { name, version } = getNameAndVersion(adaptor); @@ -95,6 +97,7 @@ export default ( path: localPath, version: 'local', }; + console.log(job); } } } From 8dcc9f9c1af3a94aeedc9813aa9cb2f998a84206 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 8 Jun 2026 10:04:51 +0100 Subject: [PATCH 08/15] update tests --- packages/cli/test/compile/compile.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/compile/compile.test.ts b/packages/cli/test/compile/compile.test.ts index 1265fb44a..8aaee6dc9 100644 --- a/packages/cli/test/compile/compile.test.ts +++ b/packages/cli/test/compile/compile.test.ts @@ -87,9 +87,10 @@ test.serial('throw an AbortError if a job is uncompilable', async (t) => { message: 'Failed to compile job', }); + console.log(logger._history); + t.assert(logger._find('error', /unexpected token/i)); t.assert(logger._find('always', /check the syntax of the job expression/i)); - t.assert(logger._find('error', /critical error: aborting command/i)); }); test.serial( @@ -111,7 +112,6 @@ test.serial( t.assert(logger._find('error', /unexpected token/i)); t.assert(logger._find('always', /check the syntax of the job expression/i)); - t.assert(logger._find('error', /critical error: aborting command/i)); } ); From ae5f6ac1457aee52501b63bfad0d4c39081b94a5 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 8 Jun 2026 10:25:15 +0100 Subject: [PATCH 09/15] update more tests --- integration-tests/cli/test/errors.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/integration-tests/cli/test/errors.test.ts b/integration-tests/cli/test/errors.test.ts index 666026c8b..49f1a0e78 100644 --- a/integration-tests/cli/test/errors.test.ts +++ b/integration-tests/cli/test/errors.test.ts @@ -22,7 +22,6 @@ test.serial('expression not found', async (t) => { const stdlogs = extractLogs(stdout); assertLog(t, stdlogs, /expression not found/i); assertLog(t, stdlogs, /failed to load the expression from blah.js/i); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial('workflow not found', async (t) => { @@ -33,7 +32,6 @@ test.serial('workflow not found', async (t) => { assertLog(t, stdlogs, /workflow not found/i); assertLog(t, stdlogs, /failed to load a workflow from blah.json/i); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial('job contains invalid js', async (t) => { @@ -45,7 +43,6 @@ test.serial('job contains invalid js', async (t) => { assertLog(t, stdlogs, /failed to compile job/i); assertLog(t, stdlogs, /unexpected token \(2:10\)/i); assertLog(t, stdlogs, /check the syntax of the job expression/i); - assertLog(t, stdlogs, /critical error: aborting command/i); }); // TODO this should really mention which job threw the error @@ -60,7 +57,6 @@ test.serial('workflow references a job with invalid js', async (t) => { assertLog(t, stdlogs, /failed to compile job/i); assertLog(t, stdlogs, /unexpected token \(2:10\)/i); assertLog(t, stdlogs, /check the syntax of the job expression/i); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial("can't find an expression referenced in a workflow", async (t) => { @@ -77,7 +73,6 @@ test.serial("can't find an expression referenced in a workflow", async (t) => { stdlogs, /This workflow references a file which cannot be found at does-not-exist.js/i ); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial("can't find config referenced in a workflow", async (t) => { @@ -98,7 +93,6 @@ test.serial("can't find config referenced in a workflow", async (t) => { stdlogs, /This workflow references a file which cannot be found at does-not-exist.js/i ); - assertLog(t, stdlogs, /critical error: aborting command/i); }); test.serial('circular workflow', async (t) => { From 20770b08c65bf1e5f8bef681b819c116cf37c51b Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 8 Jun 2026 12:04:03 +0100 Subject: [PATCH 10/15] relax alias lookup to fix tests --- packages/cli/src/projects/checkout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index f5dabd998..380187c5a 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -51,7 +51,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { const localProject = await workspace.getCheckedOutProject( // TODO not sold on this assignment - I think my test case must be wrong - workspace.activeProject!.alias as any + workspace.activeProject?.alias as any ); // get the project From f6e9d7b86656d592022cb827994866d55258b4ac Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 8 Jun 2026 15:40:02 +0100 Subject: [PATCH 11/15] when merging, force-checkout the result --- integration-tests/cli/test/project-v1.test.ts | 5 ++--- packages/cli/src/projects/checkout.ts | 19 ++++++++++--------- packages/cli/src/projects/merge.ts | 2 ++ packages/project/src/Workflow.ts | 1 - 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/integration-tests/cli/test/project-v1.test.ts b/integration-tests/cli/test/project-v1.test.ts index 7f60a315b..3865d6a54 100644 --- a/integration-tests/cli/test/project-v1.test.ts +++ b/integration-tests/cli/test/project-v1.test.ts @@ -193,10 +193,9 @@ test.serial('merge a project', async (t) => { t.is(initial, 'fn(() => ({ x: 1}))'); // Run the merge - await run( - `openfn merge hello-world-staging --workspace ${projectsPath} --force` + const { stdout } = await run( + `openfn merge hello-world-staging --workspace ${projectsPath} --force --log debug` ); - // Check the step is updated const merged = await readStep(); t.is(merged, "log('hello world')"); diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index 380187c5a..3874238d4 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -84,6 +84,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { // TODO is alias robust here? Should we get by alias and domain? const tracked = workspace.get(localProject.alias ?? localProject.id); const changed = hasUntrackedChanges(localProject, tracked); + logger?.debug(changed); if (changed.length && !options.force) { const err = { details: `Changes may be lost by checking out ${ @@ -144,29 +145,29 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => { const hasUntrackedChanges = ( activeProject: Project, tracked?: Project | null -): string[] => { +) => { if (!tracked) { // if there's no tracking we can't compare // should we log a warning then? return []; } - const changedWorkflows: string[] = []; + const changedWorkflows: Array<{ + id: string; + type: 'new' | 'changed' | 'removed'; + }> = []; // Check for changed and added workflows for (const workflow of activeProject.workflows) { - const currentHash = workflow.getVersionHash(); - const trackedWorkflow = tracked.getWorkflow(workflow.id); if (!trackedWorkflow) { // this is a new workflow added locally - changedWorkflows.push(workflow.id); + changedWorkflows.push({ id: workflow.id, type: 'new' }); continue; } - const trackedHash = trackedWorkflow!.getVersionHash(); - if (!versionsEqual(currentHash, trackedHash)) { - changedWorkflows.push(workflow.id); + if (!tracked.canMergeInto(activeProject)) { + changedWorkflows.push({ id: workflow.id, type: 'changed' }); } } @@ -174,7 +175,7 @@ const hasUntrackedChanges = ( for (const workflow of tracked.workflows) { const localWorkflow = activeProject.getWorkflow(workflow.id); if (!localWorkflow) { - changedWorkflows.push(workflow.id); + changedWorkflows.push({ id: workflow.id, type: 'removed' }); } } diff --git a/packages/cli/src/projects/merge.ts b/packages/cli/src/projects/merge.ts index 0f6b1fa7e..7e797a9f3 100644 --- a/packages/cli/src/projects/merge.ts +++ b/packages/cli/src/projects/merge.ts @@ -171,6 +171,8 @@ export const handler = async (options: MergeOptions, logger: Logger) => { workspace: workspacePath, project: options.outputPath ? finalPath : final.id, log: options.log, + // after the merge, we have to force the output to be checked out, ignoring divergence + force: true, }, logger ); diff --git a/packages/project/src/Workflow.ts b/packages/project/src/Workflow.ts index 8fcce5868..b4d42d237 100644 --- a/packages/project/src/Workflow.ts +++ b/packages/project/src/Workflow.ts @@ -208,7 +208,6 @@ class Workflow { this.workflow.history?.concat(this.getVersionHash()) ?? []; const targetHistory = target.workflow.history?.concat(target.getVersionHash()) ?? []; - const targetHead = targetHistory[targetHistory.length - 1]; return thisHistory.indexOf(targetHead) > -1; } From 8c17bb5033f9266b73b6896eee037e04d4498976 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 8 Jun 2026 15:44:01 +0100 Subject: [PATCH 12/15] remove comment --- packages/cli/src/projects/util.ts | 1 - packages/cli/test/compile/compile.test.ts | 2 -- packages/ws-worker/src/util/convert-lightning-plan.ts | 3 --- 3 files changed, 6 deletions(-) diff --git a/packages/cli/src/projects/util.ts b/packages/cli/src/projects/util.ts index 7b297be93..7f446fd22 100644 --- a/packages/cli/src/projects/util.ts +++ b/packages/cli/src/projects/util.ts @@ -268,7 +268,6 @@ export const findLocallyChangedWorkflows = async ( ) => { // Check openfn.yaml for the forked_from versions const { forked_from } = workspace.activeProject ?? {}; - console.log({ forked_from }); // If there are no forked_from references, we have no baseline // so assume everything has changed if (!forked_from || Object.keys(forked_from).length === 0) { diff --git a/packages/cli/test/compile/compile.test.ts b/packages/cli/test/compile/compile.test.ts index 8aaee6dc9..6acdb9bf8 100644 --- a/packages/cli/test/compile/compile.test.ts +++ b/packages/cli/test/compile/compile.test.ts @@ -87,8 +87,6 @@ test.serial('throw an AbortError if a job is uncompilable', async (t) => { message: 'Failed to compile job', }); - console.log(logger._history); - t.assert(logger._find('error', /unexpected token/i)); t.assert(logger._find('always', /check the syntax of the job expression/i)); }); diff --git a/packages/ws-worker/src/util/convert-lightning-plan.ts b/packages/ws-worker/src/util/convert-lightning-plan.ts index b00153b46..4307e7dbe 100644 --- a/packages/ws-worker/src/util/convert-lightning-plan.ts +++ b/packages/ws-worker/src/util/convert-lightning-plan.ts @@ -71,7 +71,6 @@ export default ( for (const root of monorepoRoots) { const candidate = path.resolve(root, 'packages', shortName); if (fs.existsSync(path.join(candidate, 'package.json'))) { - console.log(' >>>> found ', candidate); return candidate; } } @@ -84,7 +83,6 @@ export default ( }; const appendLocalVersions = (job: Job) => { - console.log({ monorepoRoots }); if (monorepoRoots.length && job.adaptors!) { for (const adaptor of job.adaptors) { const { name, version } = getNameAndVersion(adaptor); @@ -97,7 +95,6 @@ export default ( path: localPath, version: 'local', }; - console.log(job); } } } From 9c0594903c5637ee65c78e8ef271ffbc1f9f8922 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 8 Jun 2026 15:48:27 +0100 Subject: [PATCH 13/15] types --- packages/cli/src/projects/checkout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/projects/checkout.ts b/packages/cli/src/projects/checkout.ts index 3874238d4..f622b2a0e 100644 --- a/packages/cli/src/projects/checkout.ts +++ b/packages/cli/src/projects/checkout.ts @@ -1,5 +1,5 @@ import yargs from 'yargs'; -import Project, { versionsEqual, Workspace } from '@openfn/project'; +import Project, { Workspace } from '@openfn/project'; import path from 'path'; import fs from 'fs'; import { rimraf } from 'rimraf'; From 447015ccefafdcaa958b944d171053f734289053 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 8 Jun 2026 16:00:19 +0100 Subject: [PATCH 14/15] fix one more test --- integration-tests/cli/test/errors.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/integration-tests/cli/test/errors.test.ts b/integration-tests/cli/test/errors.test.ts index 49f1a0e78..315722079 100644 --- a/integration-tests/cli/test/errors.test.ts +++ b/integration-tests/cli/test/errors.test.ts @@ -135,7 +135,6 @@ test.serial('invalid end (ambiguous)', async (t) => { const stdlogs = extractLogs(stdout); assertLog(t, stdlogs, /Error: end pattern matched multiple steps/i); - assertLog(t, stdlogs, /aborting/i); }); // These test error outputs within valid workflows From d3d42c3604af5948ef18d2c2ec0195e697993c99 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Mon, 8 Jun 2026 17:09:06 +0100 Subject: [PATCH 15/15] update test --- integration-tests/cli/test/project-v2.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/integration-tests/cli/test/project-v2.test.ts b/integration-tests/cli/test/project-v2.test.ts index e3ec33a8e..8beaf99f8 100644 --- a/integration-tests/cli/test/project-v2.test.ts +++ b/integration-tests/cli/test/project-v2.test.ts @@ -174,12 +174,13 @@ steps: test.serial('execute a workflow from the checked out project', async (t) => { // cheeky bonus test of checkout by alias - await run(`openfn checkout main --workspace ${TMP_DIR}`); + await run(`openfn checkout main --workspace ${TMP_DIR} --force`); // execute a workflow - await run( + const { stdout } = await run( `openfn hello-workflow -o ${TMP_DIR}/output.json --workspace ${TMP_DIR}` ); + console.log(stdout); const output = await readFile(`${TMP_DIR}/output.json`, 'utf8'); const finalState = JSON.parse(output);