From 23e56dc389a5599aa645e3a965d329bbb85d8f71 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Tue, 21 Oct 2025 18:17:21 +0100 Subject: [PATCH 01/18] trying to enable old json formats --- packages/cli/src/pull/beta.ts | 34 ++++++++++++-------- packages/project/src/Project.ts | 6 ++++ packages/project/src/Workspace.ts | 9 ++++++ packages/project/src/parse/from-app-state.ts | 1 + 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/pull/beta.ts b/packages/cli/src/pull/beta.ts index bc63df2a1..1a88cf939 100644 --- a/packages/cli/src/pull/beta.ts +++ b/packages/cli/src/pull/beta.ts @@ -3,7 +3,7 @@ import { confirm } from '@inquirer/prompts'; import path from 'path'; import fs from 'node:fs/promises'; import { DeployConfig, getProject } from '@openfn/deploy'; -import Project from '@openfn/project'; +import Project, { Workspace } from '@openfn/project'; import type { Logger } from '../util/logger'; import { rimraf } from 'rimraf'; import { Opts } from '../options'; @@ -37,39 +37,44 @@ export type PullOptionsBeta = Required< export async function handler(options: PullOptionsBeta, logger: Logger) { const { OPENFN_API_KEY, OPENFN_ENDPOINT } = process.env; - const config: Partial = { + const cfg: Partial = { apiKey: options.apiKey, endpoint: options.endpoint, }; if (!options.apiKey && OPENFN_API_KEY) { logger.info('Using OPENFN_API_KEY environment variable'); - config.apiKey = OPENFN_API_KEY; + cfg.apiKey = OPENFN_API_KEY; } if (!options.endpoint && OPENFN_ENDPOINT) { logger.info('Using OPENFN_ENDPOINT environment variable'); - config.endpoint = OPENFN_ENDPOINT; + cfg.endpoint = OPENFN_ENDPOINT; } + // TODO `path` or `output` ? + // I don't think I want to model this as output. deploy is really + // designed to run from the working folder + // could be projectPath or repoPath too + const outputRoot = path.resolve(options.path || '.'); + + // TODO is outputRoot the right dir for this? + const workspace = new Workspace(outputRoot); + const config = workspace.getConfig(); + // download the state.json from lightning - const { data } = await getProject(config as DeployConfig, options.projectId); + const { data } = await getProject(cfg as DeployConfig, options.projectId); // TODO if the user doesn't specify an env name, prompt for one const name = options.env || 'project'; const project = Project.from('state', data, { - endpoint: config.endpoint, + repo: config, + endpoint: cfg.endpoint, env: name, fetched_at: new Date().toISOString(), }); - // TODO `path` or `output` ? - // I don't think I want to model this as output. deploy is really - // designed to run from the working folder - // could be projectPath or repoPath too - const outputRoot = path.resolve(options.path || '.'); - const projectFileName = project.getIdentifier(); await fs.mkdir(`${outputRoot}/.projects`, { recursive: true }); @@ -77,7 +82,7 @@ export async function handler(options: PullOptionsBeta, logger: Logger) { const workflowsRoot = path.resolve( outputRoot, - project.repo?.workflowRoot ?? 'workflows' + project.repo.dirs?.workflows ?? 'workflows' ); // Prompt before deleting // TODO this is actually the wrong path @@ -96,7 +101,8 @@ export async function handler(options: PullOptionsBeta, logger: Logger) { await rimraf(workflowsRoot); const state = project?.serialize('state'); - if (project.repo?.formats.project === 'yaml') { + + if (project.repo.formats?.project === 'yaml') { await fs.writeFile(`${stateOutputPath}.yaml`, state); } else { await fs.writeFile( diff --git a/packages/project/src/Project.ts b/packages/project/src/Project.ts index bf725df05..0218b757b 100644 --- a/packages/project/src/Project.ts +++ b/packages/project/src/Project.ts @@ -22,6 +22,8 @@ const maybeCreateWorkflow = (wf: any) => export interface OpenfnConfig { name: string; + + /** @deprecated */ workflowRoot: string; dirs: { workflows: string; @@ -68,6 +70,7 @@ type RepoOptions = { // TODO maybe use an npm for this, or create util +// TODO this need to be controlled by the workspace const setConfigDefaults = (config: OpenfnConfig = {}) => ({ ...config, workflowRoot: config.workflowRoot ?? 'workflows', @@ -109,6 +112,7 @@ export class Project { // workspace-wide configuration options // these should be shared across projects // and saved to an openfn.yaml file + // TODO should this just be the Workspace now? repo?: Required; // load a project from a state file (project.json) @@ -160,6 +164,8 @@ export class Project { // maybe this second arg is config - like env, branch rules, serialisation rules // stuff that's external to the actual project and managed by the repo constructor(data: l.Project, repoConfig: RepoOptions = {}) { + // TODO replace with workspace + // If no workspace is provided, maybe create a default empty one in the working dir this.repo = setConfigDefaults(repoConfig); this.name = data.name; this.description = data.description; diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index ba1204f23..c08961b0c 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -15,6 +15,7 @@ export class Workspace { private projects: Project[] = []; private projectPaths = new Map(); private isValid: boolean = false; + constructor(workspacePath: string) { const openfnYamlPath = path.join(workspacePath, OPENFN_YAML_FILE); // dealing with openfn.yaml @@ -50,6 +51,12 @@ export class Workspace { } } + // This will load a project within this workspace + // uses Project.from + // Rather than doing new Workspace + Project.from(), + // you can do it in a single call + loadProject() {} + list() { return this.projects; } @@ -66,6 +73,8 @@ export class Workspace { return this.projects.find((p) => p.name === this.config?.name); } + // TODO this needs to return default values + // We should always rely on the workspace to load these values getConfig(): Partial { return this.config; } diff --git a/packages/project/src/parse/from-app-state.ts b/packages/project/src/parse/from-app-state.ts index 13e1dde7c..f624b74a0 100644 --- a/packages/project/src/parse/from-app-state.ts +++ b/packages/project/src/parse/from-app-state.ts @@ -14,6 +14,7 @@ type FromAppStateConfig = { format?: 'json' | 'yaml'; // Allow workspace config to be passed + // TODO can we just pass a Workspace? repo: OpenfnConfig; }; From e4584abfa43e6874772c7de443d61aaaff22d14b Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 11:45:55 +0100 Subject: [PATCH 02/18] refactor project.repo to project.config --- packages/cli/src/pull/beta.ts | 4 +- packages/project/src/Project.ts | 56 ++++---------- packages/project/src/Workspace.ts | 6 +- .../project/src/serialize/to-app-state.ts | 2 +- packages/project/src/serialize/to-fs.ts | 11 ++- packages/project/src/serialize/to-json.ts | 2 +- packages/project/src/util/config.ts | 74 +++++++++++++++++++ packages/project/test/parse/from-fs.test.ts | 13 ++-- packages/project/test/parse/from-path.test.ts | 2 +- packages/project/test/project.test.ts | 2 +- packages/project/test/serialize/to-fs.test.ts | 16 ++-- 11 files changed, 120 insertions(+), 68 deletions(-) create mode 100644 packages/project/src/util/config.ts diff --git a/packages/cli/src/pull/beta.ts b/packages/cli/src/pull/beta.ts index 1a88cf939..118338e38 100644 --- a/packages/cli/src/pull/beta.ts +++ b/packages/cli/src/pull/beta.ts @@ -82,7 +82,7 @@ export async function handler(options: PullOptionsBeta, logger: Logger) { const workflowsRoot = path.resolve( outputRoot, - project.repo.dirs?.workflows ?? 'workflows' + project.config.dirs.workflows ?? 'workflows' ); // Prompt before deleting // TODO this is actually the wrong path @@ -102,7 +102,7 @@ export async function handler(options: PullOptionsBeta, logger: Logger) { const state = project?.serialize('state'); - if (project.repo.formats?.project === 'yaml') { + if (project.config.formats.project === 'yaml') { await fs.writeFile(`${stateOutputPath}.yaml`, state); } else { await fs.writeFile( diff --git a/packages/project/src/Project.ts b/packages/project/src/Project.ts index 0218b757b..8f9e34ee1 100644 --- a/packages/project/src/Project.ts +++ b/packages/project/src/Project.ts @@ -9,40 +9,17 @@ import getIdentifier from './util/get-identifier'; import slugify from './util/slugify'; import { getUuidForEdge, getUuidForStep } from './util/uuid'; import { merge, MergeProjectOptions } from './merge/merge-project'; +import { Workspace } from './Workspace'; +import { buildConfig, WorkspaceConfig } from './util/config'; type MergeOptions = { force?: boolean; workflows?: string[]; // which workflows to include }; -type FileFormats = 'yaml' | 'json'; - const maybeCreateWorkflow = (wf: any) => wf instanceof Workflow ? wf : new Workflow(wf); -export interface OpenfnConfig { - name: string; - - /** @deprecated */ - workflowRoot: string; - dirs: { - workflows: string; - projects: string; - }; - formats: { - openfn: FileFormats; - project: FileFormats; - workflow: FileFormats; - }; - project: { - projectId: string; - endpoint: string; - env: string; - inserted_at: string; - updated_at: string; - }; -} - // TODO -------------- // I think this needs renaming to config // and it's part of the workspace technically @@ -71,16 +48,6 @@ type RepoOptions = { // TODO maybe use an npm for this, or create util // TODO this need to be controlled by the workspace -const setConfigDefaults = (config: OpenfnConfig = {}) => ({ - ...config, - workflowRoot: config.workflowRoot ?? 'workflows', - formats: { - // TODO change these maybe - openfn: config.formats?.openfn ?? 'yaml', - project: config.formats?.project ?? 'yaml', - workflow: config.formats?.workflow ?? 'yaml', - }, -}); // A single openfn project // could be an app project or a checked out fs @@ -109,11 +76,9 @@ export class Project { // this contains meta about the connected openfn project openfn?: l.ProjectConfig; - // workspace-wide configuration options - // these should be shared across projects - // and saved to an openfn.yaml file - // TODO should this just be the Workspace now? - repo?: Required; + workspace?: Workspace; + + config: WorkspaceConfig; // load a project from a state file (project.json) // or from a path (the file system) @@ -163,10 +128,11 @@ export class Project { // uh maybe // maybe this second arg is config - like env, branch rules, serialisation rules // stuff that's external to the actual project and managed by the repo + + // TODO maybe the constructor is (data, Workspace) constructor(data: l.Project, repoConfig: RepoOptions = {}) { - // TODO replace with workspace - // If no workspace is provided, maybe create a default empty one in the working dir - this.repo = setConfigDefaults(repoConfig); + this.setConfig(repoConfig); + this.name = data.name; this.description = data.description; this.openfn = data.openfn; @@ -177,6 +143,10 @@ export class Project { this.meta = data.meta; } + setConfig(config: Partial) { + this.config = buildConfig(config); + } + serialize(type: 'json' | 'yaml' | 'fs' | 'state' = 'json', options?: any) { if (type in serializers) { // @ts-ignore diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index c08961b0c..c2bfa768d 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -2,6 +2,7 @@ import { OpenfnConfig, Project } from './Project'; import pathExists from './util/path-exists'; import { yamlToJson } from './util/yaml'; +import { buildConfig } from './util/config'; import path from 'path'; import fs from 'fs'; import fromAppState from './parse/from-app-state'; @@ -11,7 +12,8 @@ const OPENFN_YAML_FILE = 'openfn.yaml'; const PROJECT_EXTENSIONS = ['.yaml', '.yml']; export class Workspace { - private config?: OpenfnConfig; + config?: OpenfnConfig; + private projects: Project[] = []; private projectPaths = new Map(); private isValid: boolean = false; @@ -22,7 +24,7 @@ export class Workspace { if (pathExists(openfnYamlPath, 'file')) { this.isValid = true; const data = fs.readFileSync(openfnYamlPath, 'utf-8'); - this.config = yamlToJson(data); + this.config = buildConfig(yamlToJson(data)); } const projectsPath = path.join( workspacePath, diff --git a/packages/project/src/serialize/to-app-state.ts b/packages/project/src/serialize/to-app-state.ts index 3cade56bf..62495dc76 100644 --- a/packages/project/src/serialize/to-app-state.ts +++ b/packages/project/src/serialize/to-app-state.ts @@ -26,7 +26,7 @@ export default function (project: Project, options: Options = {}) { const shouldReturnYaml = options.format === 'yaml' || - (!options.format && project.repo.formats.project === 'yaml'); + (!options.format && project.config.formats.project === 'yaml'); if (shouldReturnYaml) { return jsonToYaml(state); diff --git a/packages/project/src/serialize/to-fs.ts b/packages/project/src/serialize/to-fs.ts index 8a2cf6f0a..7cdd48ca7 100644 --- a/packages/project/src/serialize/to-fs.ts +++ b/packages/project/src/serialize/to-fs.ts @@ -30,14 +30,15 @@ export default function (project: Project) { // extracts a workflow.json|yaml from a project export const extractWorkflow = (project: Project, workflowId: string) => { - const format = project.repo.formats.workflow; + const format = project.config.formats.workflow; const workflow = project.getWorkflow(workflowId); if (!workflow) { throw new Error(`workflow not found: ${workflowId}`); } - const root = project.repo?.workflowRoot ?? 'workflows/'; + const root = + project.config.dirs.workflow ?? project.config.workflowRoot ?? 'workflows/'; const path = nodepath.join(root, workflow.id, workflow.id); @@ -80,11 +81,13 @@ export const extractStep = (project: Project, workflowId, stepId) => { }; // extracts contents for openfn.yaml|json +// TODO this is nwo WorkspaceConfig +// Need to support new and old formats export const extractRepoConfig = (project) => { - const format = project.repo.formats.openfn; + const format = project.config.formats.openfn; const config = { name: project.name, - ...project.repo, + ...project.config, project: project.openfn ?? {}, }; diff --git a/packages/project/src/serialize/to-json.ts b/packages/project/src/serialize/to-json.ts index e8adc3015..958d34c9c 100644 --- a/packages/project/src/serialize/to-json.ts +++ b/packages/project/src/serialize/to-json.ts @@ -9,7 +9,7 @@ export default function (project: Project) { // Do we just serialize all public fields? name: project.name, description: project.description, - repo: project.repo, + config: project.config, meta: project.meta, workflows: project.workflows, collections: project.collections, diff --git a/packages/project/src/util/config.ts b/packages/project/src/util/config.ts new file mode 100644 index 000000000..c33f3deac --- /dev/null +++ b/packages/project/src/util/config.ts @@ -0,0 +1,74 @@ +// Initiaise and default Workspace (and Project) config + +type FileFormats = 'yaml' | 'json'; + +// This is the old workspace config file, up to 0.6 +export interface WorkspaceFileLegacy { + workflowRoot: string; + dirs: { + workflows: string; + projects: string; + }; + formats: { + openfn: FileFormats; + project: FileFormats; + workflow: FileFormats; + }; + + // TODO this isn't actually config - this is other stuff + name: string; + project: { + projectId: string; + endpoint: string; + env: string; + inserted_at: string; + updated_at: string; + }; +} + +// Structure of the new openfn.yaml file +export interface WorkspaceFile { + workspace: WorkspaceConfig; + project: ProjectMeta; +} + +export interface WorkspaceConfig { + dirs: { + workflows: string; + projects: string; + }; + formats: { + openfn: FileFormats; + project: FileFormats; + workflow: FileFormats; + }; +} + +// TODO this is not implemented yet +export interface ProjectMeta { + is: string; + name: string; + uuid: string; + endpoint: string; + env: string; + inserted_at: string; + updated_at: string; +} + +export const buildConfig = (config: WorkspaceConfig = {}) => ({ + ...config, + dirs: { + projects: '.projects', // TODO change to projects + workflows: 'workflows', + }, + formats: { + openfn: config.formats?.openfn ?? 'yaml', + project: config.formats?.project ?? 'yaml', + workflow: config.formats?.workflow ?? 'yaml', + }, +}); + +// +export const extractConfig = (source: Project | Workspace) => {}; + +export const loadCOnfig = (contents: string) => {}; diff --git a/packages/project/test/parse/from-fs.test.ts b/packages/project/test/parse/from-fs.test.ts index 98ce209d7..f521c1e16 100644 --- a/packages/project/test/parse/from-fs.test.ts +++ b/packages/project/test/parse/from-fs.test.ts @@ -130,27 +130,28 @@ y: 2`, '/p3/wfs/my-workflow/job.js': `fn(s => s)`, }); -test('should load the openfn repo config from json', async (t) => { +test('should load workspace config from json', async (t) => { const project = await parseProject({ root: '/p1' }); - t.deepEqual(project.repo, { + t.deepEqual(project.config, { workflowRoot: 'workflows', + dirs: { projects: '.projects', workflows: 'workflows' }, formats: { openfn: 'json', project: 'json', workflow: 'json' }, }); }); -test('should load custom repro props and include default', async (t) => { +test('should load custom config props and include default', async (t) => { const project = await parseProject({ root: '/p3' }); - t.deepEqual(project.repo, { + t.deepEqual(project.config, { x: 1, y: 2, - workflowRoot: 'workflows', + dirs: { projects: '.projects', workflows: 'workflows' }, formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, }); }); -test('should load the openfn project config from json', async (t) => { +test('should load the workspace config from json', async (t) => { const project = await parseProject({ root: '/p1' }); t.deepEqual(project.openfn, { diff --git a/packages/project/test/parse/from-path.test.ts b/packages/project/test/parse/from-path.test.ts index e0a5abfd1..b8c69f74a 100644 --- a/packages/project/test/parse/from-path.test.ts +++ b/packages/project/test/parse/from-path.test.ts @@ -51,5 +51,5 @@ test.serial('should use workspace config', async (t) => { t.is(project.name, proj.name); t.deepEqual(project.openfn.uuid, proj.openfn.uuid); - t.is(project.repo.x, config.x); + t.is(project.config.x, config.x); }); diff --git a/packages/project/test/project.test.ts b/packages/project/test/project.test.ts index ac5475322..dd405516c 100644 --- a/packages/project/test/project.test.ts +++ b/packages/project/test/project.test.ts @@ -104,7 +104,7 @@ test('should convert a state file to a project and back again', (t) => { t.is(project.name, state.name); // TODO: this hack is needed right now to serialize the state as json - project.repo.formats.project = 'json'; + project.config.formats.project = 'json'; const newState = project.serialize('state'); t.deepEqual(newState, state); diff --git a/packages/project/test/serialize/to-fs.test.ts b/packages/project/test/serialize/to-fs.test.ts index 4c40c2e1b..97ce16813 100644 --- a/packages/project/test/serialize/to-fs.test.ts +++ b/packages/project/test/serialize/to-fs.test.ts @@ -215,7 +215,6 @@ test('extractConfig: create a default openfn.json', (t) => { }, ], }, - // TODO still a little uncomfortable about this structure { formats: { openfn: 'json', // note that we have to set this @@ -226,7 +225,10 @@ test('extractConfig: create a default openfn.json', (t) => { t.is(path, 'openfn.json'); t.deepEqual(JSON.parse(content), { - workflowRoot: 'workflows', + dirs: { + projects: '.projects', + workflows: 'workflows', + }, formats: { openfn: 'json', workflow: 'yaml', @@ -262,7 +264,9 @@ test('extractConfig: create a default openfn.yaml', (t) => { t.is( content, `name: My Project -workflowRoot: workflows +dirs: + projects: .projects + workflows: workflows formats: openfn: yaml project: yaml @@ -294,12 +298,10 @@ test('extractConfig: include empty project config for local projects', (t) => { ); const { path, content } = extractRepoConfig(project); - t.log(path); - t.log(content); t.is(path, 'openfn.json'); t.deepEqual(JSON.parse(content), { - workflowRoot: 'workflows', + dirs: { projects: '.projects', workflows: 'workflows' }, formats: { openfn: 'json', workflow: 'yaml', @@ -340,8 +342,8 @@ test('toFs: extract a project with 1 workflow and 1 step', (t) => { // (this should be validated in more detail by each step) const config = JSON.parse(files['openfn.json']); t.deepEqual(config, { - workflowRoot: 'workflows', formats: { openfn: 'json', project: 'yaml', workflow: 'json' }, + dirs: { projects: '.projects', workflows: 'workflows' }, project: {}, }); From 5e919d5c796aba3a17489a120688cacc7a0b1c0a Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 11:46:20 +0100 Subject: [PATCH 03/18] changeset --- .changeset/fuzzy-colts-invite.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fuzzy-colts-invite.md diff --git a/.changeset/fuzzy-colts-invite.md b/.changeset/fuzzy-colts-invite.md new file mode 100644 index 000000000..61467069e --- /dev/null +++ b/.changeset/fuzzy-colts-invite.md @@ -0,0 +1,5 @@ +--- +'@openfn/project': patch +--- + +Refactor project.repo to project.config From 1c1ae21bc976946eed7409d143804c5fa5a8b594 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 12:03:22 +0100 Subject: [PATCH 04/18] test and refactor in workspace --- packages/project/src/Workspace.ts | 16 +++++++++--- packages/project/test/workspace.test.ts | 33 ++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index c2bfa768d..fe0ef85a3 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -2,7 +2,7 @@ import { OpenfnConfig, Project } from './Project'; import pathExists from './util/path-exists'; import { yamlToJson } from './util/yaml'; -import { buildConfig } from './util/config'; +import { buildConfig, ProjectMeta } from './util/config'; import path from 'path'; import fs from 'fs'; import fromAppState from './parse/from-app-state'; @@ -18,13 +18,17 @@ export class Workspace { private projectPaths = new Map(); private isValid: boolean = false; + private projectMeta: ProjectMeta; + constructor(workspacePath: string) { const openfnYamlPath = path.join(workspacePath, OPENFN_YAML_FILE); // dealing with openfn.yaml if (pathExists(openfnYamlPath, 'file')) { this.isValid = true; const data = fs.readFileSync(openfnYamlPath, 'utf-8'); - this.config = buildConfig(yamlToJson(data)); + const { name, project, ...c } = yamlToJson(data); + this.config = buildConfig(c); + this.projectMeta = project; } const projectsPath = path.join( workspacePath, @@ -53,6 +57,7 @@ export class Workspace { } } + // TODO // This will load a project within this workspace // uses Project.from // Rather than doing new Workspace + Project.from(), @@ -63,6 +68,7 @@ export class Workspace { return this.projects; } + // TODO clear up name/id confusion get(id: string) { return this.projects.find((p) => p.name === id); } @@ -72,7 +78,8 @@ export class Workspace { } getActiveProject() { - return this.projects.find((p) => p.name === this.config?.name); + // TODO should use id, not name + return this.projects.find((p) => p.name === this.projectMeta?.name); } // TODO this needs to return default values @@ -82,7 +89,8 @@ export class Workspace { } get activeProjectId() { - return this.config?.name; + // TODO should return activeProject.id + return this.projectMeta?.name; } get valid() { diff --git a/packages/project/test/workspace.test.ts b/packages/project/test/workspace.test.ts index 5e7c016b5..e1bdc2747 100644 --- a/packages/project/test/workspace.test.ts +++ b/packages/project/test/workspace.test.ts @@ -2,15 +2,22 @@ import mock from 'mock-fs'; import { jsonToYaml, Workspace } from '../src'; import test from 'ava'; +// TODO need a test on the legacy and new yaml formats here mock({ '/ws/openfn.yaml': jsonToYaml({ name: 'some-project-name', - workflowRoot: 'workflows', + project: { + uuid: '1234', + name: 'some-project-name', + }, formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml', + custom: true, // Note tha this will be excluded }, + // deliberately exclude dirs + custom: true, }), '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml({ id: 'some-id', @@ -105,3 +112,27 @@ test('workspace-get: get projects in the workspace', (t) => { ['simple-workflow', 'another-workflow'] ); }); + +test('load config', (t) => { + const ws = new Workspace('/ws'); + t.deepEqual(ws.config, { + formats: { + openfn: 'yaml', + project: 'yaml', + workflow: 'yaml', + }, + dirs: { + workflows: 'workflows', + projects: '.projects', + }, + custom: true, + }); +}); + +test('load project meta', (t) => { + const ws = new Workspace('/ws'); + t.deepEqual(ws.projectMeta, { + uuid: '1234', + name: 'some-project-name', + }); +}); From 0b4313573b0482fb0f502820c45134e4f693b5e5 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 12:05:47 +0100 Subject: [PATCH 05/18] make project meta public --- packages/project/src/Workspace.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index fe0ef85a3..c67123b21 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -13,13 +13,12 @@ const PROJECT_EXTENSIONS = ['.yaml', '.yml']; export class Workspace { config?: OpenfnConfig; + projectMeta: ProjectMeta; private projects: Project[] = []; private projectPaths = new Map(); private isValid: boolean = false; - private projectMeta: ProjectMeta; - constructor(workspacePath: string) { const openfnYamlPath = path.join(workspacePath, OPENFN_YAML_FILE); // dealing with openfn.yaml From 518ec0bbe0154101e6453a930b859ec6dd4cc47c Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 13:23:28 +0100 Subject: [PATCH 06/18] new helper to load workspace file --- packages/project/src/Workspace.ts | 6 +- packages/project/src/parse/from-fs.ts | 51 ++++----- packages/project/src/util/config.ts | 60 ++++++++++- packages/project/test/parse/from-fs.test.ts | 7 +- packages/project/test/util/config.test.ts | 111 ++++++++++++++++++++ 5 files changed, 195 insertions(+), 40 deletions(-) create mode 100644 packages/project/test/util/config.test.ts diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index c67123b21..bd16320a4 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -2,7 +2,7 @@ import { OpenfnConfig, Project } from './Project'; import pathExists from './util/path-exists'; import { yamlToJson } from './util/yaml'; -import { buildConfig, ProjectMeta } from './util/config'; +import { buildConfig, loadWorkspaceFile, ProjectMeta } from './util/config'; import path from 'path'; import fs from 'fs'; import fromAppState from './parse/from-app-state'; @@ -25,8 +25,8 @@ export class Workspace { if (pathExists(openfnYamlPath, 'file')) { this.isValid = true; const data = fs.readFileSync(openfnYamlPath, 'utf-8'); - const { name, project, ...c } = yamlToJson(data); - this.config = buildConfig(c); + const { project, workspace } = loadWorkspaceFile(data); + this.config = buildConfig(workspace); this.projectMeta = project; } const projectsPath = path.join( diff --git a/packages/project/src/parse/from-fs.ts b/packages/project/src/parse/from-fs.ts index f7fd6fc74..f040bd148 100644 --- a/packages/project/src/parse/from-fs.ts +++ b/packages/project/src/parse/from-fs.ts @@ -6,6 +6,12 @@ import * as l from '@openfn/lexicon'; import { Project } from '../Project'; import getIdentifier from '../util/get-identifier'; import { yamlToJson } from '../util/yaml'; +import { + buildConfig, + WorkspaceConfig, + WorkspaceFile, + loadWorkspaceFile, +} from '../util/config'; import slugify from '../util/slugify'; import fromAppState from './from-app-state'; @@ -14,20 +20,17 @@ export type FromFsConfig = { }; // Parse a single project from a root folder -// focus on this first // root must be absolute? export const parseProject = async (options: FromFsConfig = {}) => { const { root } = options; - const proj = {}; - let config; // TODO need a type for the shape of this file + let context: WorkspaceFile; try { - // TODO any flex on the openfn.json file name? const file = await fs.readFile( path.resolve(path.join(root, 'openfn.yaml')), 'utf8' ); - config = yamlToJson(file); + context = loadWorkspaceFile(file); } catch (e) { // Not found - try and parse as JSON try { @@ -35,21 +38,22 @@ export const parseProject = async (options: FromFsConfig = {}) => { path.join(root || '.', 'openfn.json'), 'utf8' ); - config = JSON.parse(file); + context = loadWorkspaceFile(file, 'json'); } catch (e) { console.log(e); // TODO better error handling throw e; } } + const config = buildConfig(context.workspace); // Now we need to look for the corresponding state file // Need to load UUIDs and other app settings from this // If we load it as a Project, uuid tracking is way easier let state: Project; const identifier = getIdentifier({ - endpoint: config.project?.endpoint, - env: config.project?.env, + endpoint: context.project?.endpoint, + env: context.project?.env, }); try { const format = @@ -66,11 +70,12 @@ export const parseProject = async (options: FromFsConfig = {}) => { console.warn(`Failed to find state file for ${identifier}`); // console.warn(e); } - // find the openfn settings - const { project: openfn, ...repo } = config; - proj.openfn = openfn; - proj.config = repo; + const proj = { + openfn: context.project, + config: config, + workflows: [], + }; // now find all the workflows // this will find all json files in the workflows folder @@ -129,7 +134,7 @@ export const parseProject = async (options: FromFsConfig = {}) => { } } - workflows.push(wf); + proj.workflows.push(wf); } } catch (e) { console.log(e); @@ -138,26 +143,8 @@ export const parseProject = async (options: FromFsConfig = {}) => { continue; } } - // now for each workflow, read in the expression files - proj.workflows = workflows; - - // TODO do the workflow folder and the workflows file need to be same name? - - // proj.openfn = { - // projectId: id, - // endpoint: config.endpoint, - // inserted_at, - // updated_at, - // }; - - // // TODO maybe this for local metadata, stuff that isn't synced? - // proj.meta = { - // fetched_at: config.fetchedAt, - // }; - - // proj.workflows = state.workflows.map(mapWorkflow); - return new Project(proj as l.Project, repo); + return new Project(proj as l.Project, context.workspace); }; // Parse the filesystem for all projects diff --git a/packages/project/src/util/config.ts b/packages/project/src/util/config.ts index c33f3deac..89cc3dc0c 100644 --- a/packages/project/src/util/config.ts +++ b/packages/project/src/util/config.ts @@ -1,8 +1,15 @@ -// Initiaise and default Workspace (and Project) config +import { yamlToJson } from './yaml'; +import { chain, pickBy, isNil } from 'lodash-es'; + +// Initialize and default Workspace (and Project) config type FileFormats = 'yaml' | 'json'; // This is the old workspace config file, up to 0.6 +// TODO would like a better name than "Workspace File" +// Can't use config, it means something else (and not all of it is config!) +// State is good but overloaded +// Settings? Context? export interface WorkspaceFileLegacy { workflowRoot: string; dirs: { @@ -68,7 +75,54 @@ export const buildConfig = (config: WorkspaceConfig = {}) => ({ }, }); -// +// TODO +// Generate a config file from a project export const extractConfig = (source: Project | Workspace) => {}; -export const loadCOnfig = (contents: string) => {}; +export const loadWorkspaceFile = ( + contents: string, + format: 'yaml' | 'json' = 'yaml' +) => { + let project, workspace; + let json; + if (format === 'yaml') { + json = yamlToJson(contents); + } else if (typeof contents === 'string') { + json = JSON.parse(contents); + } + + const legacy = !json.workspace && !json.projects; + if (legacy) { + project = json.project; + + // prettier-ignore + const { + formats, + dirs, + project: _ /* ignore!*/, + name, + ...rest + } = json; + + workspace = pickBy( + { + ...rest, + formats, + dirs, + }, + (value) => !isNil(value) + ); + } else { + project = json.project ?? {}; + workspace = json.workspace ?? {}; + } + + return { project, workspace }; +}; + +// TODO +// find the workspace file in a specific dir +// throws if it can't find one +export const findWorkspaceFile = (dir: string) => { + return { content: '', type: '' }; +}; diff --git a/packages/project/test/parse/from-fs.test.ts b/packages/project/test/parse/from-fs.test.ts index f521c1e16..9df74ed0c 100644 --- a/packages/project/test/parse/from-fs.test.ts +++ b/packages/project/test/parse/from-fs.test.ts @@ -117,8 +117,11 @@ mock({ // p3 uses custom yaml '/p3/openfn.yaml': ` -x: 1 -y: 2`, +workspace: + x: 1 + y: 2 +project: +`, '/p3/wfs/my-workflow/my-workflow.yaml': ` id: my-workflow name: My Workflow diff --git a/packages/project/test/util/config.test.ts b/packages/project/test/util/config.test.ts new file mode 100644 index 000000000..7c5ab21a9 --- /dev/null +++ b/packages/project/test/util/config.test.ts @@ -0,0 +1,111 @@ +import test from 'ava'; +import { loadWorkspaceFile } from '../../src/util/config'; + +test('load config as yaml', (t) => { + const yaml = ` +workspace: + formats: + openfn: yaml + project: json + workflow: yaml + x: 1 +project: + name: joe-sandbox-testing + uuid: d3367267-ef25-41cf-91b2-43d18f831f3c + endpoint: https://app.staging.openfn.org + env: dev + inserted_at: 2025-10-21T17:10:57Z + updated_at: 2025-10-21T17:10:57Z +`; + const result = loadWorkspaceFile(yaml); + + t.deepEqual(result.workspace, { + formats: { + openfn: 'yaml', + project: 'json', + workflow: 'yaml', + }, + x: 1, + }); + t.deepEqual(result.project, { + name: 'joe-sandbox-testing', + uuid: 'd3367267-ef25-41cf-91b2-43d18f831f3c', + endpoint: 'https://app.staging.openfn.org', + env: 'dev', + inserted_at: '2025-10-21T17:10:57Z', + updated_at: '2025-10-21T17:10:57Z', + }); +}); + +test.todo('load config as json'); + +// Note that the legacy format is the old 0.6 version of openfn.yaml +test('legacy: load config as yaml', (t) => { + const yaml = ` +name: joe-sandbox-testing +workflowRoot: workflows +formats: + openfn: yaml + project: json + workflow: yaml +project: + uuid: d3367267-ef25-41cf-91b2-43d18f831f3c + endpoint: https://app.staging.openfn.org + env: dev + inserted_at: 2025-10-21T17:10:57Z + updated_at: 2025-10-21T17:10:57Z +`; + const result = loadWorkspaceFile(yaml); + + t.deepEqual(result.workspace, { + workflowRoot: 'workflows', + formats: { + openfn: 'yaml', + project: 'json', + workflow: 'yaml', + }, + }); + t.deepEqual(result.project, { + uuid: 'd3367267-ef25-41cf-91b2-43d18f831f3c', + endpoint: 'https://app.staging.openfn.org', + env: 'dev', + inserted_at: '2025-10-21T17:10:57Z', + updated_at: '2025-10-21T17:10:57Z', + }); +}); + +test('legacy: load config as json', (t) => { + const json = JSON.stringify({ + name: 'joe-sandbox-testing', + workflowRoot: 'workflows', + formats: { + openfn: 'yaml', + project: 'json', + workflow: 'yaml', + }, + project: { + uuid: 'd3367267-ef25-41cf-91b2-43d18f831f3c', + endpoint: 'https://app.staging.openfn.org', + env: 'dev', + inserted_at: '2025-10-21T17:10:57Z', + updated_at: '2025-10-21T17:10:57Z', + }, + }); + const result = loadWorkspaceFile(json, 'json'); + + t.deepEqual(result.workspace, { + workflowRoot: 'workflows', + formats: { + openfn: 'yaml', + project: 'json', + workflow: 'yaml', + }, + }); + t.deepEqual(result.project, { + uuid: 'd3367267-ef25-41cf-91b2-43d18f831f3c', + endpoint: 'https://app.staging.openfn.org', + env: 'dev', + inserted_at: '2025-10-21T17:10:57Z', + updated_at: '2025-10-21T17:10:57Z', + }); +}); From 2eeb12a1286c38116c8c0b45527c30dae99c08ca Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 13:46:13 +0100 Subject: [PATCH 07/18] standardise workflow.yaml loading --- packages/project/src/Workspace.ts | 39 ++++++++++--------- packages/project/src/parse/from-fs.ts | 25 ++---------- packages/project/src/util/config.ts | 33 ++++++++++++---- packages/project/test/util/config.test.ts | 23 ++++++++++- .../test/util/version-workflow.test.ts | 10 ++--- 5 files changed, 76 insertions(+), 54 deletions(-) diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index bd16320a4..1e8cee993 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -1,14 +1,16 @@ -// when given a file path from cli it'll create a workspace object +import path from 'node:path'; +import fs from 'node:fs'; + import { OpenfnConfig, Project } from './Project'; +import fromAppState from './parse/from-app-state'; import pathExists from './util/path-exists'; import { yamlToJson } from './util/yaml'; -import { buildConfig, loadWorkspaceFile, ProjectMeta } from './util/config'; -import path from 'path'; -import fs from 'fs'; -import fromAppState from './parse/from-app-state'; +import { + buildConfig, + loadWorkspaceFile, + findWorkspaceFile, +} from './util/config'; -const PROJECTS_DIRECTORY = '.projects'; -const OPENFN_YAML_FILE = 'openfn.yaml'; const PROJECT_EXTENSIONS = ['.yaml', '.yml']; export class Workspace { @@ -20,19 +22,20 @@ export class Workspace { private isValid: boolean = false; constructor(workspacePath: string) { - const openfnYamlPath = path.join(workspacePath, OPENFN_YAML_FILE); - // dealing with openfn.yaml - if (pathExists(openfnYamlPath, 'file')) { + let context; + try { + const { type, content } = findWorkspaceFile(workspacePath); + context = loadWorkspaceFile(content, type); this.isValid = true; - const data = fs.readFileSync(openfnYamlPath, 'utf-8'); - const { project, workspace } = loadWorkspaceFile(data); - this.config = buildConfig(workspace); - this.projectMeta = project; + } catch (e) { + // invalid workspace + return; } - const projectsPath = path.join( - workspacePath, - this.config?.dirs?.projects ?? PROJECTS_DIRECTORY - ); + + this.config = buildConfig(context.workspace); + this.projectMeta = context.project; + + const projectsPath = path.join(workspacePath, this.config.dirs.projects); // dealing with projects if (this.isValid && pathExists(projectsPath, 'directory')) { diff --git a/packages/project/src/parse/from-fs.ts b/packages/project/src/parse/from-fs.ts index f040bd148..e408197ea 100644 --- a/packages/project/src/parse/from-fs.ts +++ b/packages/project/src/parse/from-fs.ts @@ -11,6 +11,7 @@ import { WorkspaceConfig, WorkspaceFile, loadWorkspaceFile, + findWorkspaceFile, } from '../util/config'; import slugify from '../util/slugify'; import fromAppState from './from-app-state'; @@ -20,31 +21,11 @@ export type FromFsConfig = { }; // Parse a single project from a root folder -// root must be absolute? export const parseProject = async (options: FromFsConfig = {}) => { const { root } = options; - let context: WorkspaceFile; - try { - const file = await fs.readFile( - path.resolve(path.join(root, 'openfn.yaml')), - 'utf8' - ); - context = loadWorkspaceFile(file); - } catch (e) { - // Not found - try and parse as JSON - try { - const file = await fs.readFile( - path.join(root || '.', 'openfn.json'), - 'utf8' - ); - context = loadWorkspaceFile(file, 'json'); - } catch (e) { - console.log(e); - // TODO better error handling - throw e; - } - } + const { type, content } = findWorkspaceFile(root); + const context = loadWorkspaceFile(content, type); const config = buildConfig(context.workspace); // Now we need to look for the corresponding state file diff --git a/packages/project/src/util/config.ts b/packages/project/src/util/config.ts index 89cc3dc0c..5cc8bd7b0 100644 --- a/packages/project/src/util/config.ts +++ b/packages/project/src/util/config.ts @@ -1,5 +1,7 @@ -import { yamlToJson } from './yaml'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; import { chain, pickBy, isNil } from 'lodash-es'; +import { yamlToJson } from './yaml'; // Initialize and default Workspace (and Project) config @@ -80,11 +82,11 @@ export const buildConfig = (config: WorkspaceConfig = {}) => ({ export const extractConfig = (source: Project | Workspace) => {}; export const loadWorkspaceFile = ( - contents: string, + contents: string | WorkspaceFile | WorkspaceFileLegacy, format: 'yaml' | 'json' = 'yaml' ) => { let project, workspace; - let json; + let json = contents; if (format === 'yaml') { json = yamlToJson(contents); } else if (typeof contents === 'string') { @@ -120,9 +122,24 @@ export const loadWorkspaceFile = ( return { project, workspace }; }; -// TODO -// find the workspace file in a specific dir -// throws if it can't find one -export const findWorkspaceFile = (dir: string) => { - return { content: '', type: '' }; +export const findWorkspaceFile = (dir: string = '.') => { + let content, type; + try { + type = 'yaml'; + content = readFileSync(path.resolve(path.join(dir, 'openfn.yaml')), 'utf8'); + } catch (e) { + // Not found - try and parse as JSON + try { + type = 'json'; + const file = readFileSync(path.join(dir, 'openfn.json'), 'utf8'); + if (file) { + content = JSON.parse(file); + } + } catch (e) { + console.log(e); + // TODO better error handling + throw e; + } + } + return { content, type }; }; diff --git a/packages/project/test/util/config.test.ts b/packages/project/test/util/config.test.ts index 7c5ab21a9..fecf9c060 100644 --- a/packages/project/test/util/config.test.ts +++ b/packages/project/test/util/config.test.ts @@ -1,5 +1,10 @@ import test from 'ava'; -import { loadWorkspaceFile } from '../../src/util/config'; +import { findWorkspaceFile, loadWorkspaceFile } from '../../src/util/config'; +import mock from 'mock-fs'; + +test.afterEach(() => { + mock.restore(); +}); test('load config as yaml', (t) => { const yaml = ` @@ -109,3 +114,19 @@ test('legacy: load config as json', (t) => { updated_at: '2025-10-21T17:10:57Z', }); }); + +test('find openfn.yaml', (t) => { + mock({ '/tmp/openfn.yaml': 'x: 1' }); + + const result = findWorkspaceFile('/tmp'); + t.is(result.type, 'yaml'); + t.is(result.content, 'x: 1'); +}); + +test('find openfn.json', (t) => { + mock({ '/tmp/openfn.json': '{ "x": 1 }' }); + + const result = findWorkspaceFile('/tmp'); + t.is(result.type, 'json'); + t.deepEqual(result.content, { x: 1 }); +}); diff --git a/packages/project/test/util/version-workflow.test.ts b/packages/project/test/util/version-workflow.test.ts index 4b75ea774..1977d3839 100644 --- a/packages/project/test/util/version-workflow.test.ts +++ b/packages/project/test/util/version-workflow.test.ts @@ -38,10 +38,10 @@ test('unique hash but different steps order', (t) => { // different order of nodes (b & c changed position) but should generate the same hash // validate second step is actually different - t.is(workflow1.steps[1].name, 'b') - t.is(workflow2.steps[1].name, 'c') + t.is(workflow1.steps[1].name, 'b'); + t.is(workflow2.steps[1].name, 'c'); // assert that hashes are the same - t.is(generateHash(workflow1), generateHash(workflow2)) + t.is(generateHash(workflow1), generateHash(workflow2)); }); /** @@ -63,7 +63,7 @@ test('hash changes when workflow name changes', (t) => { b-c ` ); - const wf2= generateWorkflow( + const wf2 = generateWorkflow( ` @name wf-2 @id workflow-id @@ -98,7 +98,7 @@ test.skip('hash changes when credentials field changes', (t) => { }); test("hash changes when a step's adaptor changes", (t) => { - const wf1 = generateWorkflow( + const wf1 = generateWorkflow( ` @name wf-1 @id workflow-id From 17fe1bc623725f319c4fc954f6f4ad666743942b Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 14:25:45 +0100 Subject: [PATCH 08/18] update new format for openfn.yaml --- packages/project/src/serialize/to-fs.ts | 19 +-- packages/project/src/util/config.ts | 28 +++- packages/project/test/serialize/to-fs.test.ts | 135 +----------------- packages/project/test/util/config.test.ts | 42 +++++- 4 files changed, 73 insertions(+), 151 deletions(-) diff --git a/packages/project/src/serialize/to-fs.ts b/packages/project/src/serialize/to-fs.ts index 7cdd48ca7..ebd472f91 100644 --- a/packages/project/src/serialize/to-fs.ts +++ b/packages/project/src/serialize/to-fs.ts @@ -3,13 +3,14 @@ import nodepath from 'path'; import { Project } from '../Project'; import { jsonToYaml } from '../util/yaml'; +import { extractConfig } from '../util/config'; const stringify = (json) => JSON.stringify(json, null, 2); export default function (project: Project) { const files: Record = {}; - const { path, content } = extractRepoConfig(project); + const { path, content } = extractConfig(project); files[path] = content; for (const wf of project.workflows) { @@ -80,25 +81,11 @@ export const extractStep = (project: Project, workflowId, stepId) => { } }; -// extracts contents for openfn.yaml|json -// TODO this is nwo WorkspaceConfig -// Need to support new and old formats -export const extractRepoConfig = (project) => { - const format = project.config.formats.openfn; - const config = { - name: project.name, - ...project.config, - project: project.openfn ?? {}, - }; - - return handleOutput(config, 'openfn', format); -}; - const handleOutput = (data, filePath, format) => { const path = `${filePath}.${format}`; let content; if (format === 'json') { - content = stringify(data, null, 2); + content = stringify(data); } else if (format === 'yaml') { content = jsonToYaml(data); } else { diff --git a/packages/project/src/util/config.ts b/packages/project/src/util/config.ts index 5cc8bd7b0..44f488bdb 100644 --- a/packages/project/src/util/config.ts +++ b/packages/project/src/util/config.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { chain, pickBy, isNil } from 'lodash-es'; -import { yamlToJson } from './yaml'; +import { yamlToJson, jsonToYaml } from './yaml'; // Initialize and default Workspace (and Project) config @@ -77,9 +77,29 @@ export const buildConfig = (config: WorkspaceConfig = {}) => ({ }, }); -// TODO -// Generate a config file from a project -export const extractConfig = (source: Project | Workspace) => {}; +// Generate a workspace config (openfn.yaml) file for a project +export const extractConfig = (source: Project) => { + const project = { + ...(source.openfn || {}), + }; + const workspace = { + ...source.config, + }; + + const content = { project, workspace }; + + const format = workspace.formats.openfn; + if (format === 'yaml') { + return { + path: 'openfn.yaml', + content: jsonToYaml(content), + }; + } + return { + path: 'openfn.json', + content: JSON.stringify(content, null, 2), + }; +}; export const loadWorkspaceFile = ( contents: string | WorkspaceFile | WorkspaceFileLegacy, diff --git a/packages/project/test/serialize/to-fs.test.ts b/packages/project/test/serialize/to-fs.test.ts index 97ce16813..a455338aa 100644 --- a/packages/project/test/serialize/to-fs.test.ts +++ b/packages/project/test/serialize/to-fs.test.ts @@ -4,9 +4,9 @@ import { Project } from '../../src/Project'; import toFs, { extractWorkflow, extractStep, - extractRepoConfig, mapWorkflow, } from '../../src/serialize/to-fs'; +import { extractConfig } from '../../src/util/config'; const stringify = (json) => JSON.stringify(json, null, 2); @@ -184,133 +184,6 @@ test('extractWorkflow: single simple workflow with custom root', (t) => { t.is(path, 'openfn/wfs/my-workflow/my-workflow.json'); }); -test('extractStep: extract a step', (t) => { - const project = new Project({ - workflows: [ - { - id: 'my-workflow', - steps: [step], - }, - ], - }); - - const { path, content } = extractStep(project, 'my-workflow', 'step'); - - t.is(path, 'workflows/my-workflow/step.js'); - t.is(content, step.expression); -}); - -test('extractConfig: create a default openfn.json', (t) => { - const project = new Project( - { - openfn: { - env: 'main', - id: '123', - endpoint: 'app.openfn.org', - }, - workflows: [ - { - id: 'my-workflow', - steps: [step], - }, - ], - }, - { - formats: { - openfn: 'json', // note that we have to set this - }, - } - ); - const { path, content } = extractRepoConfig(project); - - t.is(path, 'openfn.json'); - t.deepEqual(JSON.parse(content), { - dirs: { - projects: '.projects', - workflows: 'workflows', - }, - formats: { - openfn: 'json', - workflow: 'yaml', - project: 'yaml', - }, - project: { - id: '123', - endpoint: 'app.openfn.org', - env: 'main', - }, - }); -}); - -test('extractConfig: create a default openfn.yaml', (t) => { - const project = new Project({ - name: 'My Project', - openfn: { - env: 'main', - id: '123', - endpoint: 'app.openfn.org', - }, - workflows: [ - { - id: 'my-workflow', - steps: [step], - }, - ], - }); - - const { path, content } = extractRepoConfig(project); - - t.is(path, 'openfn.yaml'); - t.is( - content, - `name: My Project -dirs: - projects: .projects - workflows: workflows -formats: - openfn: yaml - project: yaml - workflow: yaml -project: - env: main - id: "123" - endpoint: app.openfn.org -` - ); -}); - -test('extractConfig: include empty project config for local projects', (t) => { - const project = new Project( - { - // no openfn obj! - workflows: [ - { - id: 'my-workflow', - steps: [step], - }, - ], - }, - { - formats: { - openfn: 'json', // for easier testing - }, - } - ); - - const { path, content } = extractRepoConfig(project); - - t.is(path, 'openfn.json'); - t.deepEqual(JSON.parse(content), { - dirs: { projects: '.projects', workflows: 'workflows' }, - formats: { - openfn: 'json', - workflow: 'yaml', - project: 'yaml', - }, - project: {}, - }); -}); - test('toFs: extract a project with 1 workflow and 1 step', (t) => { const project = new Project( { @@ -342,8 +215,10 @@ test('toFs: extract a project with 1 workflow and 1 step', (t) => { // (this should be validated in more detail by each step) const config = JSON.parse(files['openfn.json']); t.deepEqual(config, { - formats: { openfn: 'json', project: 'yaml', workflow: 'json' }, - dirs: { projects: '.projects', workflows: 'workflows' }, + workspace: { + formats: { openfn: 'json', project: 'yaml', workflow: 'json' }, + dirs: { projects: '.projects', workflows: 'workflows' }, + }, project: {}, }); diff --git a/packages/project/test/util/config.test.ts b/packages/project/test/util/config.test.ts index fecf9c060..1f582a100 100644 --- a/packages/project/test/util/config.test.ts +++ b/packages/project/test/util/config.test.ts @@ -1,6 +1,11 @@ import test from 'ava'; -import { findWorkspaceFile, loadWorkspaceFile } from '../../src/util/config'; +import { + extractConfig, + findWorkspaceFile, + loadWorkspaceFile, +} from '../../src/util/config'; import mock from 'mock-fs'; +import Project from '../../src'; test.afterEach(() => { mock.restore(); @@ -130,3 +135,38 @@ test('find openfn.json', (t) => { t.is(result.type, 'json'); t.deepEqual(result.content, { x: 1 }); }); + +test('generate openfn.yaml', (t) => { + const proj = new Project( + { + id: 'p1', + name: 'My Project', + openfn: { + uuid: 1234, + }, + }, + { + formats: { + openfn: 'yaml', + }, + } + ); + const result = extractConfig(proj); + t.is(result.path, 'openfn.yaml'), + t.deepEqual( + result.content, + `project: + uuid: 1234 +workspace: + formats: + openfn: yaml + project: yaml + workflow: yaml + dirs: + projects: .projects + workflows: workflows +` + ); +}); + +test.todo('generate openfn.json'); From b511a8cac7ae118b45f54a60b2ed16d044338740 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 14:31:15 +0100 Subject: [PATCH 09/18] fix types --- packages/project/src/Workspace.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index 1e8cee993..0f037fbfb 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -1,7 +1,8 @@ import path from 'node:path'; import fs from 'node:fs'; -import { OpenfnConfig, Project } from './Project'; +import { Project } from './Project'; +import type { WorkspaceConfig } from './util/config'; import fromAppState from './parse/from-app-state'; import pathExists from './util/path-exists'; import { yamlToJson } from './util/yaml'; @@ -14,7 +15,7 @@ import { const PROJECT_EXTENSIONS = ['.yaml', '.yml']; export class Workspace { - config?: OpenfnConfig; + config?: WorkspaceConfig; projectMeta: ProjectMeta; private projects: Project[] = []; From 12502a6e1c6103fcb5c189ce0c8d29813bea9124 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 14:40:39 +0100 Subject: [PATCH 10/18] suppress error logging --- packages/project/src/util/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/project/src/util/config.ts b/packages/project/src/util/config.ts index 44f488bdb..e86234d80 100644 --- a/packages/project/src/util/config.ts +++ b/packages/project/src/util/config.ts @@ -156,7 +156,7 @@ export const findWorkspaceFile = (dir: string = '.') => { content = JSON.parse(file); } } catch (e) { - console.log(e); + // console.log(e); // TODO better error handling throw e; } From c09b04e7ea194ef7fced5cc60514a37d43431b8e Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 14:47:57 +0100 Subject: [PATCH 11/18] types --- packages/project/src/Project.ts | 5 ++--- packages/project/src/parse/from-app-state.ts | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/project/src/Project.ts b/packages/project/src/Project.ts index 8f9e34ee1..36096c1fb 100644 --- a/packages/project/src/Project.ts +++ b/packages/project/src/Project.ts @@ -1,7 +1,6 @@ -import * as l from '@openfn/lexicon'; import Workflow from './Workflow'; import * as serializers from './serialize'; -import fromAppState from './parse/from-app-state'; +import fromAppState, { FromAppStateConfig } from './parse/from-app-state'; import fromPath from './parse/from-path'; // TODO this naming clearly isn't right import { parseProject as fromFs, FromFsConfig } from './parse/from-fs'; @@ -102,7 +101,7 @@ export class Project { static from( type: 'state' | 'path' | 'fs', data: any, - options: Partial = {} + options: FromAppStateConfig = {} ): Project { if (type === 'state') { return fromAppState(data, options); diff --git a/packages/project/src/parse/from-app-state.ts b/packages/project/src/parse/from-app-state.ts index f624b74a0..97496624d 100644 --- a/packages/project/src/parse/from-app-state.ts +++ b/packages/project/src/parse/from-app-state.ts @@ -2,12 +2,13 @@ import * as l from '@openfn/lexicon'; import { Provisioner } from '@openfn/lexicon/lightning'; -import { OpenfnConfig, Project } from '../Project'; +import { Project } from '../Project'; import { yamlToJson } from '../util/yaml'; import renameKeys from '../util/rename-keys'; +import { WorkspaceConfig } from '../util/config'; // Extra metadata used to init the project -type FromAppStateConfig = { +export type FromAppStateConfig = { endpoint: string; env?: string; fetchedAt?: string; @@ -15,7 +16,7 @@ type FromAppStateConfig = { // Allow workspace config to be passed // TODO can we just pass a Workspace? - repo: OpenfnConfig; + config: WorkspaceConfig; }; function slugify(text) { @@ -66,7 +67,7 @@ export default (state: Provisioner.Project, config: FromAppStateConfig) => { proj.workflows = state.workflows.map(mapWorkflow); - return new Project(proj as l.Project, config?.repo); + return new Project(proj as l.Project, config?.config); }; const mapTriggerEdgeCondition = (edge: Provisioner.Edge) => { From 7160782a64f992371d7e663c5e8e9f465a583c78 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 14:48:44 +0100 Subject: [PATCH 12/18] cli: fix config --- packages/cli/src/pull/beta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/pull/beta.ts b/packages/cli/src/pull/beta.ts index 118338e38..63df6c977 100644 --- a/packages/cli/src/pull/beta.ts +++ b/packages/cli/src/pull/beta.ts @@ -69,7 +69,7 @@ export async function handler(options: PullOptionsBeta, logger: Logger) { const name = options.env || 'project'; const project = Project.from('state', data, { - repo: config, + config, endpoint: cfg.endpoint, env: name, fetched_at: new Date().toISOString(), From 21ac5cc6c2166fc5263ff3814d4a5e8ee0842885 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 15:00:52 +0100 Subject: [PATCH 13/18] fix tests --- .changeset/fine-mammals-juggle.md | 5 +++++ packages/project/src/Project.ts | 4 ++-- packages/project/src/Workspace.ts | 2 +- packages/project/src/parse/from-path.ts | 9 ++++----- packages/project/test/parse/from-path.test.ts | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 .changeset/fine-mammals-juggle.md diff --git a/.changeset/fine-mammals-juggle.md b/.changeset/fine-mammals-juggle.md new file mode 100644 index 000000000..bc28baeab --- /dev/null +++ b/.changeset/fine-mammals-juggle.md @@ -0,0 +1,5 @@ +--- +'@openfn/cli': patch +--- + +Update Project dependency diff --git a/packages/project/src/Project.ts b/packages/project/src/Project.ts index 36096c1fb..f96de8280 100644 --- a/packages/project/src/Project.ts +++ b/packages/project/src/Project.ts @@ -1,7 +1,7 @@ import Workflow from './Workflow'; import * as serializers from './serialize'; import fromAppState, { FromAppStateConfig } from './parse/from-app-state'; -import fromPath from './parse/from-path'; +import fromPath, { FromPathConfig } from './parse/from-path'; // TODO this naming clearly isn't right import { parseProject as fromFs, FromFsConfig } from './parse/from-fs'; import getIdentifier from './util/get-identifier'; @@ -96,7 +96,7 @@ export class Project { static from( type: 'path', data: string, - options?: { config?: Partial } + options?: { config?: FromPathConfig } ): Project; static from( type: 'state' | 'path' | 'fs', diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index 0f037fbfb..88daba895 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -87,7 +87,7 @@ export class Workspace { // TODO this needs to return default values // We should always rely on the workspace to load these values - getConfig(): Partial { + getConfig(): Partial { return this.config; } diff --git a/packages/project/src/parse/from-path.ts b/packages/project/src/parse/from-path.ts index 185c17a35..a300bfb4f 100644 --- a/packages/project/src/parse/from-path.ts +++ b/packages/project/src/parse/from-path.ts @@ -3,11 +3,10 @@ import { readFile } from 'node:fs/promises'; import fromAppState from './from-app-state'; import { yamlToJson } from '../util/yaml'; -import { OpenfnConfig } from '../Project'; +import { WorkspaceConfig } from '../util/config'; -type FromPathConfig = { - // repo config options - repo: OpenfnConfig; +export type FromPathConfig = { + config: WorkspaceConfig; }; // Load a project from a file path @@ -21,7 +20,7 @@ export default async (path: string, options: FromPathConfig = {}) => { const config = { format: null, - repo: options.repo ?? options.config, // TMP + config: options.config, }; let state; if (ext === '.json') { diff --git a/packages/project/test/parse/from-path.test.ts b/packages/project/test/parse/from-path.test.ts index b8c69f74a..d79911c81 100644 --- a/packages/project/test/parse/from-path.test.ts +++ b/packages/project/test/parse/from-path.test.ts @@ -46,7 +46,7 @@ test.serial('should use workspace config', async (t) => { x: 1234, }; const project = await fromPath('/p1/main@openfn.org.yaml', { - repo: config, + config, }); t.is(project.name, proj.name); From 4f986674f0bd8e0349c031c071f0ac6c04af0099 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 15:33:45 +0100 Subject: [PATCH 14/18] track project name in legacy config files --- packages/project/src/util/config.ts | 5 ++++- packages/project/test/parse/from-fs.test.ts | 2 +- packages/project/test/util/config.test.ts | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/project/src/util/config.ts b/packages/project/src/util/config.ts index e86234d80..86f6cc02e 100644 --- a/packages/project/src/util/config.ts +++ b/packages/project/src/util/config.ts @@ -115,7 +115,10 @@ export const loadWorkspaceFile = ( const legacy = !json.workspace && !json.projects; if (legacy) { - project = json.project; + project = json.project ?? {}; + if (json.name) { + project.name = json.name; + } // prettier-ignore const { diff --git a/packages/project/test/parse/from-fs.test.ts b/packages/project/test/parse/from-fs.test.ts index 9df74ed0c..07f12dca8 100644 --- a/packages/project/test/parse/from-fs.test.ts +++ b/packages/project/test/parse/from-fs.test.ts @@ -158,10 +158,10 @@ test('should load the workspace config from json', async (t) => { const project = await parseProject({ root: '/p1' }); t.deepEqual(project.openfn, { + name: 'My Project', id: 'e16c5f09-f0cb-4ba7-a4c2-73fcb2f29d00', env: 'staging', endpoint: 'https://app.openfn.org', - name: 'My Project', description: '...', }); }); diff --git a/packages/project/test/util/config.test.ts b/packages/project/test/util/config.test.ts index 1f582a100..91e2c39f4 100644 --- a/packages/project/test/util/config.test.ts +++ b/packages/project/test/util/config.test.ts @@ -76,6 +76,7 @@ project: }, }); t.deepEqual(result.project, { + name: 'joe-sandbox-testing', uuid: 'd3367267-ef25-41cf-91b2-43d18f831f3c', endpoint: 'https://app.staging.openfn.org', env: 'dev', @@ -112,6 +113,7 @@ test('legacy: load config as json', (t) => { }, }); t.deepEqual(result.project, { + name: 'joe-sandbox-testing', uuid: 'd3367267-ef25-41cf-91b2-43d18f831f3c', endpoint: 'https://app.staging.openfn.org', env: 'dev', From cf572a6350a55a1fa33623253d41b549bf404e62 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 15:48:48 +0100 Subject: [PATCH 15/18] track project name from app state --- packages/cli/src/checkout/handler.ts | 2 +- packages/cli/test/checkout/handler.test.ts | 26 +++++++++++++------- packages/project/src/parse/from-app-state.ts | 1 + 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/checkout/handler.ts b/packages/cli/src/checkout/handler.ts index 1ae8cf855..746801354 100644 --- a/packages/cli/src/checkout/handler.ts +++ b/packages/cli/src/checkout/handler.ts @@ -15,7 +15,7 @@ const checkoutHandler = async (options: CheckoutOptions, logger: Logger) => { // get the config // TODO: try to retain the endpoint for the projects - const { project: _, ...config } = workspace.getConfig() ?? {}; + const { project: _, ...config } = workspace.getConfig() as any; // get the project let switchProject; diff --git a/packages/cli/test/checkout/handler.test.ts b/packages/cli/test/checkout/handler.test.ts index cb5c4b61f..ecfde24af 100644 --- a/packages/cli/test/checkout/handler.test.ts +++ b/packages/cli/test/checkout/handler.test.ts @@ -164,7 +164,7 @@ test.serial('checkout: invalid project id', (t) => { test.serial('checkout: to a different valid project', async (t) => { // before checkout. some-project-name is active and expanded const bcheckout = new Workspace('/ws'); - t.is(bcheckout.getConfig()?.name, 'some-project-name'); + t.is(bcheckout.projectMeta.name, 'some-project-name'); t.is(bcheckout.getActiveProject()?.name, 'some-project-name'); await checkoutHandler( @@ -176,7 +176,7 @@ test.serial('checkout: to a different valid project', async (t) => { // after checkout. main-project-id is active and expanded const acheckout = new Workspace('/ws'); - t.is(acheckout.getConfig()?.name, 'main-project-id'); + t.is(acheckout.projectMeta.name, 'main-project-id'); t.is(acheckout.getActiveProject()?.name, 'main-project-id'); // check if files where well expanded @@ -189,11 +189,15 @@ test.serial('checkout: to a different valid project', async (t) => { test.serial('checkout: same id as active', async (t) => { // before checkout. some-project-name is active and expanded const bcheckout = new Workspace('/ws'); - t.is(bcheckout.getConfig()?.name, 'some-project-name'); + t.is(bcheckout.projectMeta.name, 'some-project-name'); t.is(bcheckout.getActiveProject()?.name, 'some-project-name'); await checkoutHandler( - { command: 'checkout', projectName: 'some-project-name', projectPath: '/ws' }, + { + command: 'checkout', + projectName: 'some-project-name', + projectPath: '/ws', + }, logger ); const { message } = logger._parse(logger._last); @@ -201,7 +205,7 @@ test.serial('checkout: same id as active', async (t) => { // after checkout. main-project-id is active and expanded const acheckout = new Workspace('/ws'); - t.is(acheckout.getConfig()?.name, 'some-project-name'); + t.is(acheckout.projectMeta.name, 'some-project-name'); t.is(acheckout.getActiveProject()?.name, 'some-project-name'); // check if files where well expanded @@ -214,7 +218,7 @@ test.serial('checkout: same id as active', async (t) => { test.serial('checkout: switching to and back between projects', async (t) => { // before checkout. some-project-name is active and expanded const bcheckout = new Workspace('/ws'); - t.is(bcheckout.getConfig()?.name, 'some-project-name'); + t.is(bcheckout.projectMeta.name, 'some-project-name'); t.is(bcheckout.getActiveProject()?.name, 'some-project-name'); // 1. switch from some-project-name to main-project-id @@ -227,7 +231,7 @@ test.serial('checkout: switching to and back between projects', async (t) => { // after checkout. main-project-id is active and expanded const acheckout = new Workspace('/ws'); - t.is(acheckout.getConfig()?.name, 'main-project-id'); + t.is(acheckout.projectMeta.name, 'main-project-id'); t.is(acheckout.getActiveProject()?.name, 'main-project-id'); // check if files where well expanded @@ -238,7 +242,11 @@ test.serial('checkout: switching to and back between projects', async (t) => { // 2. switch back from main-project-id to some-project-name await checkoutHandler( - { command: 'checkout', projectName: 'some-project-name', projectPath: '/ws' }, + { + command: 'checkout', + projectName: 'some-project-name', + projectPath: '/ws', + }, logger ); const { message: lastMsg } = logger._parse(logger._last); @@ -246,7 +254,7 @@ test.serial('checkout: switching to and back between projects', async (t) => { // after checkout. main-project-id is active and expanded const fcheckout = new Workspace('/ws'); - t.is(fcheckout.getConfig()?.name, 'some-project-name'); + t.is(fcheckout.projectMeta.name, 'some-project-name'); t.is(fcheckout.getActiveProject()?.name, 'some-project-name'); // check if files where well expanded diff --git a/packages/project/src/parse/from-app-state.ts b/packages/project/src/parse/from-app-state.ts index 97496624d..d4edb58ff 100644 --- a/packages/project/src/parse/from-app-state.ts +++ b/packages/project/src/parse/from-app-state.ts @@ -54,6 +54,7 @@ export default (state: Provisioner.Project, config: FromAppStateConfig) => { proj.openfn = { uuid: id, + name: name, endpoint: config.endpoint, env: config.env, inserted_at, From b0c90901335bc63915ce4096649ec446f3e2cf9f Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 15:55:42 +0100 Subject: [PATCH 16/18] update test --- packages/project/test/parse/from-app-state.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/project/test/parse/from-app-state.test.ts b/packages/project/test/parse/from-app-state.test.ts index 16fda2237..3e1cbce09 100644 --- a/packages/project/test/parse/from-app-state.test.ts +++ b/packages/project/test/parse/from-app-state.test.ts @@ -75,6 +75,7 @@ test('should create a Project from prov state with app project metadata', (t) => t.deepEqual(project.openfn, { env: 'test', uuid: state.id, + name: 'My Workflow', endpoint: config.endpoint, inserted_at: state.inserted_at, updated_at: state.updated_at, From 6ea477f4568b05b55453b7ccecfae2115e625259 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 16:04:03 +0100 Subject: [PATCH 17/18] fix more tests --- packages/cli/test/merge/handler.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/test/merge/handler.test.ts b/packages/cli/test/merge/handler.test.ts index 21d96b258..fe9a183b6 100644 --- a/packages/cli/test/merge/handler.test.ts +++ b/packages/cli/test/merge/handler.test.ts @@ -113,7 +113,7 @@ test('merging into the same project', async (t) => { test('merging a different project into checked-out', async (t) => { // state of main projects workflow before sandbox is merged in const bworkspace = new Workspace('/ws'); - t.is(bworkspace.getConfig()?.name, 'main-project-id'); + t.is(bworkspace.projectMeta.name, 'main-project-id'); t.is(bworkspace.getActiveProject()?.name, 'main-project-id'); const bprojects = bworkspace.list(); t.is(bprojects[0].workflows[0].steps.length, 2); @@ -133,13 +133,13 @@ test('merging a different project into checked-out', async (t) => { // state of main projects workflow before sandbox is merged in const workspace = new Workspace('/ws'); - t.is(workspace.getConfig()?.name, 'main-project-id'); + t.is(workspace.projectMeta.name, 'main-project-id'); t.is(workspace.getActiveProject()?.name, 'main-project-id'); const projects = workspace.list(); t.is(projects[0].workflows[0].steps.length, 3); t.is(projects[0].workflows[0].steps[1].name, 'Job X'); t.is(projects[0].workflows[0].steps[1].openfn?.uuid, 'job-a'); // id got retained - t.is(projects[0].workflows[0].steps[2].name, 'Job Y'); + t.is(projects[0].workflows[0].steps[2].name, 'Job Y'); t.is(projects[0].workflows[0].steps[2].openfn?.uuid, 'job-y'); // id not retained - new nod const { message, level } = logger._parse(logger._last); From 87b8d184177032d7990da54909f1001cff80677c Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 22 Oct 2025 16:55:00 +0100 Subject: [PATCH 18/18] fix empty workspace files --- packages/project/src/Workspace.ts | 2 ++ packages/project/src/util/config.ts | 7 +++++-- packages/project/test/util/config.test.ts | 8 ++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index 88daba895..0b196bf0c 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -26,9 +26,11 @@ export class Workspace { let context; try { const { type, content } = findWorkspaceFile(workspacePath); + console.log(content); context = loadWorkspaceFile(content, type); this.isValid = true; } catch (e) { + console.log(e); // invalid workspace return; } diff --git a/packages/project/src/util/config.ts b/packages/project/src/util/config.ts index 86f6cc02e..df80ac59b 100644 --- a/packages/project/src/util/config.ts +++ b/packages/project/src/util/config.ts @@ -108,7 +108,7 @@ export const loadWorkspaceFile = ( let project, workspace; let json = contents; if (format === 'yaml') { - json = yamlToJson(contents); + json = yamlToJson(contents) ?? {}; } else if (typeof contents === 'string') { json = JSON.parse(contents); } @@ -146,10 +146,13 @@ export const loadWorkspaceFile = ( }; export const findWorkspaceFile = (dir: string = '.') => { + console.log({ dir }); let content, type; try { type = 'yaml'; + console.log(path.resolve(path.join(dir, 'openfn.yaml'))); content = readFileSync(path.resolve(path.join(dir, 'openfn.yaml')), 'utf8'); + console.log({ content }); } catch (e) { // Not found - try and parse as JSON try { @@ -159,7 +162,7 @@ export const findWorkspaceFile = (dir: string = '.') => { content = JSON.parse(file); } } catch (e) { - // console.log(e); + console.log(e); // TODO better error handling throw e; } diff --git a/packages/project/test/util/config.test.ts b/packages/project/test/util/config.test.ts index 91e2c39f4..b463d52d0 100644 --- a/packages/project/test/util/config.test.ts +++ b/packages/project/test/util/config.test.ts @@ -122,6 +122,14 @@ test('legacy: load config as json', (t) => { }); }); +test('legacy: load empty config', (t) => { + const yaml = ``; + const result = loadWorkspaceFile(yaml); + + t.deepEqual(result.workspace, {}); + t.deepEqual(result.project, {}); +}); + test('find openfn.yaml', (t) => { mock({ '/tmp/openfn.yaml': 'x: 1' });