Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/large-windows-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openfn/cli': patch
---

Fix loading of workflow.yaml files
6 changes: 5 additions & 1 deletion packages/cli/src/util/load-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ const loadPlan = async (
if (options.path && /ya?ml$/.test(options.path)) {
const content = await fs.readFile(path.resolve(options.path), 'utf-8');
const workflow = yamlToJson(content);
// Temporarily support workflow.yaml files without a top workflow key
// This was released in july 2025 but it's actually wrong!
// we'll support it for a while for back-compat, but this need removing in 2027
const plan = workflow.workflow ? workflow : { workflow };
options.baseDir = dirname(options.path);
return loadXPlan({ workflow }, options, logger);
return loadXPlan(plan, options, logger);
}

// Run a workflow from a project, with a path and workflow name
Expand Down
62 changes: 55 additions & 7 deletions packages/cli/test/util/load-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import { createMockLogger } from '@openfn/logger';
import type { Job } from '@openfn/lexicon';

import loadPlan from '../../src/util/load-plan';
import {
collectionsEndpoint,
collectionsVersion,
Opts,
} from '../../src/options';
import { Opts } from '../../src/options';

const logger = createMockLogger(undefined, { level: 'debug' });

Expand Down Expand Up @@ -47,8 +43,6 @@ test.afterEach(() => {
mock.restore();
});

// TODO: add some tests for handling yaml stuff

test.serial('expression: load a plan from an expression.js', async (t) => {
const opts = {
expressionPath: 'test/job.js',
Expand Down Expand Up @@ -429,3 +423,57 @@ test.serial('xplan: append collections', async (t) => {
collections_token: opts.apiKey,
});
});

// This is basically an invalid workflow yaml that was accidentally supported
// Preserving for back compat for now
test.serial(
'xplan: load a workflow.yaml (without top workflow key)',
async (t) => {
mock({
'test/wf.yaml': `
name: wf
steps:
- id: a
adaptors: []
expression: x()
`,
});
const opts = {
path: 'test/wf.yaml', // TODO should this work with workflow path as well?
expandAdaptors: true,
plan: {},
};

const plan = await loadPlan(opts, logger);

t.truthy(plan);
// Note that options are lost in this design!
t.deepEqual(plan, { workflow: sampleXPlan.workflow, options: {} });
}
);

test.serial('xplan: load a proper workflow.yaml', async (t) => {
mock({
'test/wf.yaml': `
workflow:
name: wf
steps:
- id: a
adaptors: []
expression: x()
options:
start: a
`,
});
const opts = {
path: 'test/wf.yaml', // TODO should this work with workflow path as well?
expandAdaptors: true,
plan: {},
};

const plan = await loadPlan(opts, logger);

t.truthy(plan);
// Note that options are lost in this design!
t.deepEqual(plan, sampleXPlan);
});
12 changes: 8 additions & 4 deletions packages/project/src/Workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ class Workflow {
index: any;

name?: string;
start?: string;
id: string;
openfn?: l.WorkflowMeta;
options: any; // TODO
options: l.WorkflowOptions;

constructor(workflow: l.Workflow) {
constructor(workflow: l.Workflow, options: l.WorkflowOptions = {}) {
this.index = {
steps: {}, // steps by id
edges: {}, // edges by from-id id
Expand All @@ -30,7 +31,7 @@ class Workflow {
// history needs to be on workflow object.
this.workflow.history = workflow.history?.length ? workflow.history : [];

const { id, name, openfn, steps, history, ...options } = workflow;
const { id, name, openfn } = workflow;
if (!(id || name)) {
throw new Error('A Workflow MUST have a name or id');
}
Expand Down Expand Up @@ -159,7 +160,10 @@ class Workflow {
}

toJSON(): Object {
return clone(this.workflow);
return {
...clone(this.workflow),
options: this.options,
};
}

getUUIDMap(): Record<string, string> {
Expand Down
9 changes: 8 additions & 1 deletion packages/project/src/parse/from-app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Project } from '../Project';
import renameKeys from '../util/rename-keys';
import slugify from '../util/slugify';
import ensureJson from '../util/ensure-json';
import Workflow from '../Workflow';

export type fromAppStateConfig = Partial<l.WorkspaceConfig> & {
format?: 'yaml' | 'json';
Expand Down Expand Up @@ -96,6 +97,7 @@ export const mapWorkflow = (workflow: Provisioner.Workflow) => {
name: workflow.name,
steps: [],
history: workflow.version_history ?? [],

openfn: renameKeys(remoteProps, { id: 'uuid' }),
};
if (workflow.name) {
Expand Down Expand Up @@ -161,5 +163,10 @@ export const mapWorkflow = (workflow: Provisioner.Workflow) => {
mapped.steps.push(s);
});

return mapped;
// TODO do we need to load other options from the state file?
const options = {
start: 'trigger',
};

return new Workflow(mapped, options);
};
7 changes: 5 additions & 2 deletions packages/project/src/serialize/to-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export const extractWorkflow = (project: Project, workflowId: string) => {
name: workflow.name,
// Note: if no options are defined, options will serialize to an empty object
// Not crazy about this - maybe we should do something better? Or do we like the consistency?
options: workflow.options,
steps: workflow.steps.map((step) => {
const { openfn, expression, next, ...mapped } = step;
if (expression) {
Expand All @@ -65,7 +64,11 @@ export const extractWorkflow = (project: Project, workflowId: string) => {
return mapped;
}),
};
return handleOutput(wf, path, format!);
return handleOutput(
{ workflow: wf, options: workflow.options },
path,
format!
);
};

// extracts an expression.js from a workflow in project
Expand Down
4 changes: 3 additions & 1 deletion packages/project/test/fixtures/sample-v2-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const json: SerializedProject = {
],
name: 'Workflow',
id: 'workflow',
options: { start: 'trigger' },
openfn: { uuid: 1 },
history: [],
},
Expand Down Expand Up @@ -65,6 +66,7 @@ workflows:
name: Workflow
id: workflow
openfn:
uuid: 1
uuid: 1
history: []
start: trigger
`;
1 change: 1 addition & 0 deletions packages/project/test/parse/from-app-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ test('should create a Project from prov state with a workflow', (t) => {
id: 'my-workflow',
name: 'My Workflow',
history: [],
start: 'trigger',
steps: [
{
id: 'trigger',
Expand Down
2 changes: 2 additions & 0 deletions packages/project/test/parse/from-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ test('import from a v2 project as JSON', async (t) => {
uuid: 1,
},
history: [],
start: 'trigger',
steps: [
{
name: 'b',
Expand Down Expand Up @@ -141,6 +142,7 @@ test('import from a v2 project as YAML', async (t) => {
uuid: 1,
},
history: [],
start: 'trigger',
steps: [
{
name: 'b',
Expand Down
Loading