Skip to content

Commit 0337621

Browse files
fix: replace adaptors in plan with resolved adaptors at install (#857)
* fix: replace adaptors in plan to what they resolved to This mostly effects adaptors specified with aliases like common@latest which will be replaced to the what they resolved to like [email protected] * test: override plan test * test: check for resolved with number to be sure linking stage passed * test: a workflow using different versions of the same adaptor * version: [email protected] --------- Co-authored-by: Joe Clark <[email protected]>
1 parent a1a071b commit 0337621

File tree

8 files changed

+224
-2
lines changed

8 files changed

+224
-2
lines changed

integration-tests/cli/test/autoinstall.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ test.serial(
4949
t.regex(stdout, /Installing packages.../);
5050
t.regex(stdout, /Will install @openfn\/language-common version/);
5151
t.regex(stdout, /Installed @openfn\/language-common@/);
52+
t.regex(
53+
stdout,
54+
/Resolved adaptor @openfn\/language-common to version \d+(\.\d+)*/
55+
);
5256
}
5357
);
5458

@@ -68,6 +72,10 @@ test.serial(
6872
stdout,
6973
/Skipping @openfn\/language-common@(.+) as already installed/
7074
);
75+
t.regex(
76+
stdout,
77+
/Resolved adaptor @openfn\/language-common to version \d+(\.\d+)*/
78+
);
7179
}
7280
);
7381

@@ -78,7 +86,6 @@ test.serial(
7886
const { stdout, err } = await run(t.title);
7987

8088
// t.falsy(err); // TODO I think this is a broken adaptor build?
81-
8289
t.regex(stdout, /Auto-installing language adaptors/);
8390
t.regex(stdout, /Looked up latest version of @openfn\/language-testing/);
8491
t.regex(stdout, /Installing packages.../);
@@ -92,6 +99,12 @@ test.serial(
9299
stdout,
93100
new RegExp(`Installed @openfn\/language-testing@${TEST_LATEST}`)
94101
);
102+
t.regex(
103+
stdout,
104+
new RegExp(
105+
`Resolved adaptor @openfn/language-testing to version ${TEST_LATEST}`
106+
)
107+
);
95108
}
96109
);
97110

integration-tests/cli/test/execute-workflow.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,25 @@ test.serial(
199199
});
200200
}
201201
);
202+
203+
test.serial(
204+
`openfn ${jobsPath}/different-adaptor-versions.json -l debug`,
205+
async (t) => {
206+
const { err, stdout } = await run(t.title);
207+
t.falsy(err);
208+
209+
t.regex(
210+
stdout,
211+
/Resolved adaptor @openfn\/language-common to version 2.1.0/
212+
);
213+
t.regex(
214+
stdout,
215+
/Resolved adaptor @openfn\/language-common to version 2.0.3/
216+
);
217+
218+
const out = getJSON();
219+
t.deepEqual(out, {
220+
z: 1,
221+
});
222+
}
223+
);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"options": {},
3+
"workflow": {
4+
"name": "Steps with different adaptor versions",
5+
"steps": [
6+
{
7+
"id": "lesser",
8+
"adaptor": "[email protected]",
9+
"state": {
10+
"x": 1
11+
},
12+
"expression": "fn(state=> ({y: state.x}))",
13+
"next": {
14+
"latest-again": true
15+
}
16+
},
17+
{
18+
"id": "latest-again",
19+
"adaptor": "[email protected]",
20+
"expression": "fn(state=> ({z: state.y}))"
21+
}
22+
]
23+
}
24+
}

packages/cli/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @openfn/cli
22

3+
## 1.10.2
4+
5+
### Patch Changes
6+
7+
- Properly resolve adaptor versions when they are explicitly set to @latest
8+
39
## 1.10.1
410

511
### Patch Changes

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openfn/cli",
3-
"version": "1.10.1",
3+
"version": "1.10.2",
44
"description": "CLI devtools for the openfn toolchain.",
55
"engines": {
66
"node": ">=18",

packages/cli/src/execute/handler.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { clearCache } from '../util/cache';
1717
import fuzzyMatchStep from '../util/fuzzy-match-step';
1818
import abort from '../util/abort';
1919
import validatePlan from '../util/validate-plan';
20+
import overridePlanAdaptors from '../util/override-plan-adaptors';
2021

2122
const matchStep = (
2223
plan: ExecutionPlan,
@@ -55,6 +56,7 @@ const executeHandler = async (options: ExecuteOptions, logger: Logger) => {
5556
await clearCache(plan, options, logger);
5657
}
5758

59+
const moduleResolutions: Record<string, string> = {};
5860
const { repoDir, monorepoPath, autoinstall } = options;
5961
if (autoinstall) {
6062
if (monorepoPath) {
@@ -67,6 +69,14 @@ const executeHandler = async (options: ExecuteOptions, logger: Logger) => {
6769
{ packages: autoInstallTargets, repoDir },
6870
logger
6971
);
72+
73+
// create a module resolution dictionary.
74+
// this is to map aliases like @latest & @next to what they resolved into
75+
if (autoInstallTargets.length === options.adaptors.length) {
76+
for (let i = 0; i < autoInstallTargets.length; i++) {
77+
moduleResolutions[autoInstallTargets[i]] = options.adaptors[i];
78+
}
79+
}
7080
}
7181
}
7282
}
@@ -104,6 +114,9 @@ const executeHandler = async (options: ExecuteOptions, logger: Logger) => {
104114
}
105115
const state = await loadState(plan, options, logger, customStart);
106116

117+
// replacing adaptors in the original plan to what they resolved to.
118+
plan = overridePlanAdaptors(plan, moduleResolutions);
119+
107120
if (options.compile) {
108121
plan = await compile(plan, options, logger);
109122
} else {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ExecutionPlan, Job, Step } from '@openfn/lexicon';
2+
3+
function overridePlanAdaptors(
4+
plan: ExecutionPlan,
5+
resolutions: Record<string, string>
6+
): ExecutionPlan {
7+
const hasRes = Object.entries(resolutions).some(
8+
([key, value]) => key !== value
9+
);
10+
11+
// there's nothing to override when resolutions have the same values
12+
if (!hasRes) return plan;
13+
return {
14+
...plan,
15+
workflow: {
16+
...plan.workflow,
17+
steps: plan.workflow.steps.map((step) => {
18+
if (isJob(step))
19+
return {
20+
...step,
21+
adaptors: step.adaptors?.map((a) => resolutions[a] || a),
22+
};
23+
else return step;
24+
}),
25+
},
26+
};
27+
}
28+
29+
export function isJob(step: Step): step is Job {
30+
// @ts-ignore
31+
return step && typeof step.expression === 'string';
32+
}
33+
34+
export default overridePlanAdaptors;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { ExecutionPlan } from '@openfn/lexicon';
2+
import test from 'ava';
3+
import overridePlanAdaptors, {
4+
isJob,
5+
} from '../../src/util/override-plan-adaptors';
6+
7+
// validates adaptors in a plan against an array.
8+
function getAdaptors(plan: ExecutionPlan) {
9+
// check for a given id whether the
10+
const steps = plan.workflow.steps;
11+
12+
const adaptors = steps.map((step) => {
13+
if (isJob(step)) return step.adaptors;
14+
return undefined;
15+
});
16+
return adaptors;
17+
}
18+
19+
test('replace adaptors in plan using provided resolutions', (t) => {
20+
const plan: ExecutionPlan = {
21+
workflow: {
22+
steps: [
23+
{
24+
id: 'one',
25+
expression: 'fn(state=> state)',
26+
adaptors: ['@openfn/language-http@latest'],
27+
},
28+
{
29+
id: 'two',
30+
expression: 'fn(state=> state)',
31+
adaptors: ['@openfn/language-common@next'],
32+
},
33+
{
34+
expression: 'fn(state=> state)',
35+
adaptors: ['@openfn/[email protected]'],
36+
},
37+
],
38+
},
39+
};
40+
41+
const resolutions = {
42+
'@openfn/language-http@latest': '@openfn/[email protected]',
43+
'@openfn/language-common@next': '@openfn/[email protected]',
44+
};
45+
const finalPlan = overridePlanAdaptors(plan, resolutions);
46+
47+
t.deepEqual(getAdaptors(finalPlan), [
48+
['@openfn/[email protected]'],
49+
['@openfn/[email protected]'],
50+
['@openfn/[email protected]'],
51+
]);
52+
});
53+
54+
test("ignore override when there's nothing to override", (t) => {
55+
const plan: ExecutionPlan = {
56+
workflow: {
57+
steps: [
58+
{
59+
id: 'one',
60+
expression: 'fn(state=> state)',
61+
adaptors: ['@openfn/language-http@latest'],
62+
},
63+
{
64+
id: 'two',
65+
expression: 'fn(state=> state)',
66+
adaptors: ['@openfn/language-common@next'],
67+
},
68+
{
69+
expression: 'fn(state=> state)',
70+
adaptors: ['@openfn/[email protected]'],
71+
},
72+
],
73+
},
74+
};
75+
76+
const resolutions = {
77+
'@openfn/[email protected]': '@openfn/[email protected]',
78+
'@openfn/[email protected]': '@openfn/[email protected]',
79+
};
80+
81+
const finalPlan = overridePlanAdaptors(plan, resolutions);
82+
t.is(plan, finalPlan);
83+
});
84+
85+
test('replace adaptors on only job steps', (t) => {
86+
const plan: ExecutionPlan = {
87+
workflow: {
88+
steps: [
89+
{
90+
id: 'x',
91+
next: 'two',
92+
},
93+
{
94+
id: 'two',
95+
expression: 'fn(state=> state)',
96+
adaptors: ['@openfn/language-common@next'],
97+
},
98+
],
99+
},
100+
};
101+
const resolutions = {
102+
'@openfn/language-common@next': '@openfn/[email protected]',
103+
};
104+
105+
const finalPlan = overridePlanAdaptors(plan, resolutions);
106+
t.deepEqual(getAdaptors(finalPlan), [
107+
undefined,
108+
['@openfn/[email protected]'],
109+
]);
110+
});

0 commit comments

Comments
 (0)