Skip to content

Commit c6611cb

Browse files
committed
feat: pass custom environment variables to shell command
1 parent 9716b2c commit c6611cb

File tree

6 files changed

+305
-13
lines changed

6 files changed

+305
-13
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ The plugin can be configured in the [**semantic-release** configuration file](ht
3636
"@semantic-release/exec",
3737
{
3838
"verifyConditionsCmd": "./verify.sh",
39+
"prepareCmd": {
40+
"cmd": "./prepare.sh ${nextRelease.version} ${nextRelease.gitTag}",
41+
"env": {
42+
"NEXT_RELEASE_NOTES": "${nextRelease.notes}"
43+
}
44+
},
3945
"publishCmd": "./publish.sh ${nextRelease.version} ${branch.name} ${commits.length} ${Date.now()}"
4046
}
4147
]
@@ -68,7 +74,14 @@ With this example:
6874
| `shell` | The shell to use to run the command. See [execa#shell](https://github.com/sindresorhus/execa#shell). |
6975
| `execCwd` | The path to use as current working directory when executing the shell commands. This path is relative to the path from which **semantic-release** is running. For example if **semantic-release** runs from `/my-project` and `execCwd` is set to `buildScripts` then the shell command will be executed from `/my-project/buildScripts` |
7076

71-
Each shell command is generated with [Lodash template](https://lodash.com/docs#template). All the [`context` object keys](https://github.com/semantic-release/semantic-release/blob/master/docs/developer-guide/plugin.md#context) passed to semantic-release plugins are available as template options.
77+
Each shell command can be a string or an object with the following properties:
78+
79+
| Property | Description |
80+
| -------- | ------------------------------------------------------|
81+
| `cmd` | The shell command to execute. |
82+
| `env` | An object to pass as additional environment variables |
83+
84+
Each shell command and environment variable value are generated with [Lodash template](https://lodash.com/docs#template). All the [`context` object keys](https://github.com/semantic-release/semantic-release/blob/master/docs/developer-guide/plugin.md#context) passed to semantic-release plugins are available as template options.
7285

7386
## verifyConditionsCmd
7487

lib/exec.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,46 @@
11
import { resolve } from "path";
2-
import { template } from "lodash-es";
2+
import { template, isNil, isPlainObject, isString } from "lodash-es";
33
import { execa } from "execa";
44

55
export default async function exec(
66
cmdProp,
77
{ shell, execCwd, ...config },
88
{ cwd, env, stdout, stderr, logger, ...context },
99
) {
10+
1011
const cmd = config[cmdProp] ? cmdProp : "cmd";
11-
const script = template(config[cmd])({ config, ...context });
12+
13+
const cmdParsed = parseCommand(config[cmd])
14+
const script = template(cmdParsed.cmd)({ config, ...context });
15+
16+
const envInterpolated = {
17+
...env,
18+
...Object.entries(cmdParsed.env).reduce((acc, [key, value]) => {
19+
acc[key] = template(value)({ config, ...context });
20+
return acc;
21+
}, {})
22+
}
1223

1324
logger.log("Call script %s", script);
1425

26+
1527
const result = execa(script, {
1628
shell: shell || true,
1729
cwd: execCwd ? resolve(cwd, execCwd) : cwd,
18-
env,
30+
env: envInterpolated,
1931
});
2032

2133
result.stdout.pipe(stdout, { end: false });
2234
result.stderr.pipe(stderr, { end: false });
2335

2436
return (await result).stdout.trim();
2537
}
38+
39+
function parseCommand(cmd) {
40+
if (isString(cmd)) {
41+
return { cmd, env: {} };
42+
}
43+
else if (isPlainObject(cmd) && !isNil(cmd.cmd)) {
44+
return { cmd: cmd.cmd, env: cmd.env || {} };
45+
}
46+
}

lib/verify-config.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { isNil, isString } from "lodash-es";
1+
import { isNil, isPlainObject, isString } from "lodash-es";
22
import AggregateError from "aggregate-error";
33
import getError from "./get-error.js";
44

55
const isNonEmptyString = (value) => isString(value) && value.trim();
66
const isOptional = (validator) => (value) => isNil(value) || validator(value);
7+
const isCmdWithEnv = (value) => isPlainObject(value) && isNonEmptyString(value.cmd) && (isOptional(isObjectOfStrings)(value.env));
8+
const isStringOrCmdWithEnv = (value) => isNonEmptyString(value) || isCmdWithEnv(value);
9+
const isObjectOfStrings = (value) => isPlainObject(value) && Object.values(value).every(element => isString(element));
710

811
const VALIDATORS = {
9-
cmd: isNonEmptyString,
12+
cmd: isStringOrCmdWithEnv,
1013
shell: isOptional((shell) => shell === true || isNonEmptyString(shell)),
1114
execCwd: isOptional(isNonEmptyString),
1215
};
@@ -30,12 +33,12 @@ export default function verifyConfig(
3033
VALIDATORS[option](value)
3134
? errors
3235
: [
33-
...errors,
34-
getError(`EINVALID${option.toUpperCase()}`, {
35-
[option]: value,
36-
cmdProp,
37-
}),
38-
],
36+
...errors,
37+
getError(`EINVALID${option.toUpperCase()}`, {
38+
[option]: value,
39+
cmdProp,
40+
}),
41+
],
3942
[],
4043
);
4144

test/exec.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,28 @@ test("Generate command with template", async (t) => {
5151
t.is(result, "confValue 1.0.0");
5252
});
5353

54+
test("Generate command with env variables", async (t) => {
55+
const pluginConfig = {
56+
publishCmd: {
57+
cmd: `./test/fixtures/echo-args.sh \${config.conf} \${lastRelease.version}`,
58+
env: {
59+
NEXT_RELEASE_NOTES: "${nextRelease.notes}",
60+
},
61+
},
62+
conf: "confValue",
63+
};
64+
const context = {
65+
stdout: t.context.stdout,
66+
stderr: t.context.stderr,
67+
lastRelease: { version: "1.0.0" },
68+
nextRelease: { notes: "notes &\"'" },
69+
logger: t.context.logger
70+
};
71+
72+
const result = await exec("publishCmd", pluginConfig, context);
73+
t.is(result, "confValue 1.0.0 notes &\"'");
74+
});
75+
5476
test('Execute the script with the specified "shell"', async (t) => {
5577
const context = {
5678
stdout: t.context.stdout,

test/fixtures/echo-args.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#!/bin/bash
2-
echo "$@"
2+
echo "$@" "$NEXT_RELEASE_NOTES"

0 commit comments

Comments
 (0)