Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
720c0d2
allow workflow to be executed directly through workspace
josephjclark Jan 9, 2026
2fde3f2
types
josephjclark Jan 9, 2026
f5af862
remove default job.js
josephjclark Jan 9, 2026
3b7d454
changeset
josephjclark Jan 9, 2026
181813c
integration test
josephjclark Jan 9, 2026
57b0571
another integration test
josephjclark Jan 9, 2026
aab2b99
support credentials on workspace config
josephjclark Jan 9, 2026
80b4ab5
remove mock
josephjclark Jan 9, 2026
85cde89
project: better handling of start in workflow.yaml
josephjclark Jan 11, 2026
5104b0b
apply credentials map
josephjclark Jan 11, 2026
7620d61
add integration test for execute + credetial map
josephjclark Jan 11, 2026
41b3972
fix test
josephjclark Jan 11, 2026
6228c82
remove unused test
josephjclark Jan 12, 2026
7184a5a
format
josephjclark Jan 12, 2026
8c3fd82
go deep on input tests
josephjclark Jan 12, 2026
e0c8f09
throw if workflow not found
josephjclark Jan 12, 2026
2653577
project: fix an issue loading alias for a v1 project
josephjclark Jan 12, 2026
0394ecf
update v1 integration test
josephjclark Jan 12, 2026
dd1af0e
fix integration test
josephjclark Jan 12, 2026
2abc4a5
better credential map handling
josephjclark Jan 12, 2026
a6ab169
fix test
josephjclark Jan 12, 2026
b032fda
ensure collections and credential map can both be set
josephjclark Jan 12, 2026
4a748c5
changesets
josephjclark Jan 12, 2026
bb36848
test tweak
josephjclark Jan 12, 2026
d8ce473
more integration test tweaks
josephjclark Jan 12, 2026
309526a
fix integration tests yet again
josephjclark Jan 13, 2026
8b88291
change v2 test order
josephjclark Jan 13, 2026
0b100ab
let -> const
josephjclark Jan 13, 2026
66dbe05
update v2 test
josephjclark Jan 13, 2026
61edf1e
yet another test fix
josephjclark Jan 13, 2026
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
17 changes: 17 additions & 0 deletions .changeset/all-cloths-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@openfn/cli': minor
---

When running `execute` inside a Workspace (a folder with an `openfn.yaml` file), allow workflows to be run directly. Ie:

```bash
openfn process-patients
```

Instead of:

```
openfn ./workflows/process-patients/process-patients.yaml
```

When running through a Workspace, credential maps and collections endpoints are automatically applied for you.
5 changes: 5 additions & 0 deletions .changeset/curly-laws-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openfn/cli': patch
---

When executing jobs, the CLI no longer defaults the path to job.js
5 changes: 5 additions & 0 deletions .changeset/pretty-stars-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openfn/project': minor
---

Allow workflows to be run directly through a project
35 changes: 26 additions & 9 deletions integration-tests/cli/test/project-v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { rm, mkdir, writeFile, readFile } from 'node:fs/promises';
import path from 'node:path';
import run from '../src/run';

const TMP_DIR = path.resolve('tmp/project-v1');

// These tests use the legacy v1 yaml structure

const mainYaml = `
Expand Down Expand Up @@ -35,7 +37,7 @@ workflows:
jobs:
- name: Transform data
body: |
// TODO
fn(() => ({ x: 1}))
adaptor: "@openfn/language-common@latest"
id: b8b780f3-98dd-4244-880b-e534d8f24547
project_credential_id: null
Expand Down Expand Up @@ -95,16 +97,16 @@ workflows:
source_trigger_id: 7bb476cc-0292-4573-89d0-b13417bc648e
condition_type: always
`;
const projectsPath = path.resolve('tmp/project');
const projectsPath = path.resolve(TMP_DIR);

test.before(async () => {
// await rm('tmp/project', { recursive: true });
await mkdir('tmp/project/.projects', { recursive: true });
// await rm(TMP_DIR, { recursive: true });
await mkdir(`${TMP_DIR}/.projects`, { recursive: true });

await writeFile('tmp/project/openfn.yaml', '');
await writeFile('tmp/project/.projects/main@app.openfn.org.yaml', mainYaml);
await writeFile(`${TMP_DIR}/openfn.yaml`, '');
await writeFile(`${TMP_DIR}/.projects/main@app.openfn.org.yaml`, mainYaml);
await writeFile(
'tmp/project/.projects/staging@app.openfn.org.yaml',
`${TMP_DIR}/.projects/staging@app.openfn.org.yaml`,
stagingYaml
);
});
Expand Down Expand Up @@ -151,7 +153,22 @@ steps:
path.resolve(projectsPath, 'workflows/my-workflow/transform-data.js'),
'utf8'
);
t.is(expr.trim(), '// TODO');
t.is(expr.trim(), 'fn(() => ({ x: 1}))');
});

// note: order of tests is important here
test.serial('execute a workflow from the checked out project', async (t) => {
// cheeky bonus test of checkout by alias
await run(`openfn checkout main -w ${projectsPath}`);

// execute a workflow
const { stdout } = await run(
`openfn my-workflow -o ${TMP_DIR}/output.json --log debug --workspace ${projectsPath}`
);

const output = await readFile(`${TMP_DIR}/output.json`, 'utf8');
const finalState = JSON.parse(output);
t.deepEqual(finalState, { x: 1 });
});

// requires the prior test to run
Expand All @@ -164,7 +181,7 @@ test.serial('merge a project', async (t) => {

// assert the initial step code
const initial = await readStep();
t.is(initial, '// TODO');
t.is(initial, 'fn(() => ({ x: 1}))');

// Run the merge
await run(`openfn merge hello-world-staging -w ${projectsPath} --force`);
Expand Down
184 changes: 163 additions & 21 deletions integration-tests/cli/test/project-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import test from 'ava';
import { rm, mkdir, writeFile, readFile } from 'node:fs/promises';
import path from 'node:path';
import run from '../src/run';
import createLightningServer from '@openfn/lightning-mock';

const TMP_DIR = path.resolve('tmp/project-v2');

const mainYaml = `
id: sandboxing-simple
Expand All @@ -26,6 +29,7 @@ options:
retention_policy: retain_all
workflows:
- name: Hello Workflow
start: trigger
steps:
- id: trigger
type: webhook
Expand All @@ -40,8 +44,8 @@ workflows:
uuid: add150e9-8616-48ca-844e-8aaa489c7a10
- id: transform-data
name: Transform data
expression: |-
// TODO
expression: |
fn(() => ({ x: 1}))
adaptor: "@openfn/language-dhis2@8.0.4"
openfn:
uuid: a9f64216-7974-469d-8415-d6d9baf2f92e
Expand Down Expand Up @@ -81,6 +85,7 @@ options:
retention_policy: retain_all
workflows:
- name: Hello Workflow
start: trigger
steps:
- id: trigger
type: webhook
Expand All @@ -95,7 +100,7 @@ workflows:
uuid: f34146b5-de43-4b05-ac00-3b4f327e62ec
- id: transform-data
name: Transform data
expression: |-
expression: |
fn()
adaptor: "@openfn/language-dhis2@8.0.4"
openfn:
Expand All @@ -112,40 +117,40 @@ workflows:
id: hello-workflow
history: []
`;
const projectsPath = path.resolve('tmp/project');

test.before(async () => {
await rm('tmp/project', { recursive: true });
await mkdir('tmp/project/.projects', { recursive: true });
// await rm(TMP_DIR, { recursive: true });
await mkdir(`${TMP_DIR}/.projects`, { recursive: true });

await writeFile('tmp/project/openfn.yaml', '');
await writeFile('tmp/project/.projects/main@app.openfn.org.yaml', mainYaml);
await writeFile(`${TMP_DIR}/openfn.yaml`, '');
await writeFile(`${TMP_DIR}/.projects/main@app.openfn.org.yaml`, mainYaml);
await writeFile(
'tmp/project/.projects/staging@app.openfn.org.yaml',
`${TMP_DIR}/.projects/staging@app.openfn.org.yaml`,
stagingYaml
);
});

test.serial('list available projects', async (t) => {
const { stdout } = await run(`openfn projects -w ${projectsPath}`);
const { stdout } = await run(`openfn projects -w ${TMP_DIR}`);
t.regex(stdout, /sandboxing-simple/);
t.regex(stdout, /a272a529-716a-4de7-a01c-a082916c6d23/);
t.regex(stdout, /staging/);
t.regex(stdout, /bc6629fb-7dc8-4b28-93af-901e2bd58dc4/);
});

test.serial('Checkout a project', async (t) => {
await run(`openfn checkout staging -w ${projectsPath}`);
await run(`openfn checkout staging -w ${TMP_DIR}`);

// check workflow.yaml
const workflowYaml = await readFile(
path.resolve(projectsPath, 'workflows/hello-workflow/hello-workflow.yaml'),
path.resolve(TMP_DIR, 'workflows/hello-workflow/hello-workflow.yaml'),
'utf8'
);
t.is(
workflowYaml,
`id: hello-workflow
name: Hello Workflow
start: trigger
options: {}
steps:
- id: trigger
Expand All @@ -162,30 +167,167 @@ steps:
);

const expr = await readFile(
path.resolve(projectsPath, 'workflows/hello-workflow/transform-data.js'),
path.resolve(TMP_DIR, 'workflows/hello-workflow/transform-data.js'),
'utf8'
);
t.is(expr.trim(), 'fn()');
});

// requires the prior test to run
test.serial('execute a workflow from the checked out project', async (t) => {
// cheeky bonus test of checkout by alias
await run(`openfn checkout main -w ${TMP_DIR}`);

// execute a workflow
await run(
`openfn hello-workflow -o ${TMP_DIR}/output.json --workspace ${TMP_DIR}`
);

const output = await readFile(`${TMP_DIR}/output.json`, 'utf8');
const finalState = JSON.parse(output);
t.deepEqual(finalState, { x: 1 });
});

test.serial(
'execute a workflow from the checked out project with a credential map',
async (t) => {
await run(`openfn checkout main --log debug -w ${TMP_DIR}`);

// Modify the checked out workflow code
await writeFile(
`${TMP_DIR}/workflows/hello-workflow/transform-data.js`,
`fn(s => ({ user: s.configuration.username }))`
);

// Modify the checked out workflow to add a credential
await writeFile(
`${TMP_DIR}/workflows/hello-workflow/hello-workflow.yaml`,
`id: hello-workflow
name: Hello Workflow
start: trigger
options: {}
steps:
- id: trigger
type: webhook
next:
transform-data:
disabled: false
condition: true
- id: transform-data
name: Transform data
configuration: abcd
adaptor: "@openfn/language-common@3.2.1"
expression: ./transform-data.js
`
);

// add the credential map to the yaml
await writeFile(`${TMP_DIR}/openfn.yaml`, `credentials: creds.yaml`);

// write the credential map
await writeFile(
`${TMP_DIR}/creds.yaml`,
`abcd:
username: pparker`
);

// finally execute the workflow
const { stdout } = await run(
`openfn hello-workflow -o ${TMP_DIR}/output.json --log debug --workspace ${TMP_DIR}`
);

const output = await readFile(`${TMP_DIR}/output.json`, 'utf8');
const finalState = JSON.parse(output);
t.deepEqual(finalState, { user: 'pparker' });
}
);

test.serial(
'execute a workflow from the checked out project with credentials and collections',
async (t) => {
const server = await createLightningServer({ port: 1234 });
server.collections.createCollection('stuff');
// Important: the collection value MUST be as string
server.collections.upsert('stuff', 'x', JSON.stringify({ id: 'x' }));

await run(`openfn checkout main --log debug -w ${TMP_DIR}`);

// Modify the checked out workflow code
await writeFile(
`${TMP_DIR}/workflows/hello-workflow/transform-data.js`,
`
fn(s => ({ ...s, user: s.configuration.username }));
collections.get('stuff', 'x')`
);

// Modify the checked out workflow to add a credential
await writeFile(
`${TMP_DIR}/workflows/hello-workflow/hello-workflow.yaml`,
`id: hello-workflow
name: Hello Workflow
start: trigger
options: {}
steps:
- id: trigger
type: webhook
next:
transform-data:
disabled: false
condition: true
- id: transform-data
name: Transform data
configuration: 'abcd'
adaptor: "@openfn/language-common@3.2.1"
expression: ./transform-data.js
`
);

// add the credential map to the yaml
await writeFile(
`${TMP_DIR}/openfn.yaml`,
`
project:
endpoint: http://localhost:1234
workspace:
credentials: creds.yaml`
);

// write the credential map
await writeFile(
`${TMP_DIR}/creds.yaml`,
`abcd:
username: pparker`
);

const { stdout } = await run(
`openfn hello-workflow -o ${TMP_DIR}/output.json --log debug --workspace ${TMP_DIR}`
);

const output = await readFile(`${TMP_DIR}/output.json`, 'utf8');
const finalState = JSON.parse(output);

t.deepEqual(finalState, {
data: { id: 'x' },
user: 'pparker',
});
server.destroy();
}
);

test.serial('merge a project', async (t) => {
await run(`openfn checkout main -w ${TMP_DIR}`);

const readStep = () =>
readFile(
path.resolve(projectsPath, 'workflows/hello-workflow/transform-data.js'),
path.resolve(TMP_DIR, 'workflows/hello-workflow/transform-data.js'),
'utf8'
).then((str) => str.trim());

await run(`openfn checkout sandboxing-simple -w ${projectsPath}`);

// assert the initial step code
const initial = await readStep();
t.is(initial, '// TODO');
t.is(initial, 'fn(() => ({ x: 1}))');

// Run the merge
const { stdout } = await run(
`openfn merge staging -w ${projectsPath} --force`
);
const { stdout } = await run(`openfn merge staging -w ${TMP_DIR} --force`);

// Check the step is updated
const merged = await readStep();
Expand Down
Loading