• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jreleaser / jreleaser / #513

27 Jul 2025 10:36PM UTC coverage: 44.82% (-0.3%) from 45.153%
#513

push

github

aalmiray
feat(hooks): Add named groups to command and script hooks

Closes #1947

55 of 478 new or added lines in 14 files covered. (11.51%)

18 existing lines in 6 files now uncovered.

23664 of 52798 relevant lines covered (44.82%)

0.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.71
/core/jreleaser-engine/src/main/java/org/jreleaser/engine/hooks/HookExecutor.java
1
/*
2
 * SPDX-License-Identifier: Apache-2.0
3
 *
4
 * Copyright 2020-2025 The JReleaser authors.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     https://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
package org.jreleaser.engine.hooks;
19

20
import org.jreleaser.bundle.RB;
21
import org.jreleaser.model.JReleaserException;
22
import org.jreleaser.model.api.hooks.ExecutionEvent;
23
import org.jreleaser.model.internal.JReleaserContext;
24
import org.jreleaser.model.internal.common.Matrix;
25
import org.jreleaser.model.internal.hooks.CommandHook;
26
import org.jreleaser.model.internal.hooks.CommandHooks;
27
import org.jreleaser.model.internal.hooks.Hook;
28
import org.jreleaser.model.internal.hooks.Hooks;
29
import org.jreleaser.model.internal.hooks.ScriptHook;
30
import org.jreleaser.model.internal.hooks.ScriptHooks;
31
import org.jreleaser.mustache.TemplateContext;
32
import org.jreleaser.sdk.command.Command;
33
import org.jreleaser.sdk.command.CommandException;
34
import org.jreleaser.sdk.command.CommandExecutor;
35
import org.jreleaser.util.PlatformUtils;
36

37
import java.io.IOException;
38
import java.nio.file.Files;
39
import java.nio.file.Path;
40
import java.util.ArrayList;
41
import java.util.Collection;
42
import java.util.LinkedHashMap;
43
import java.util.List;
44
import java.util.Locale;
45
import java.util.Map;
46
import java.util.StringTokenizer;
47

48
import static java.lang.System.lineSeparator;
49
import static java.nio.charset.StandardCharsets.UTF_8;
50
import static java.nio.file.StandardOpenOption.WRITE;
51
import static org.jreleaser.model.Constants.KEY_PLATFORM;
52
import static org.jreleaser.mustache.Templates.resolveTemplate;
53
import static org.jreleaser.util.StringUtils.isFalse;
54
import static org.jreleaser.util.StringUtils.isNotBlank;
55

56
/**
57
 * @author Andres Almiray
58
 * @since 1.2.0
59
 */
60
public final class HookExecutor {
61
    private static final String JRELEASER_OUTPUT = "JRELEASER_OUTPUT:";
62
    private final JReleaserContext context;
63

64
    public HookExecutor(JReleaserContext context) {
1✔
65
        this.context = context;
1✔
66
    }
1✔
67

68
    public void execute(String step, Runnable runnable) {
69
        executeHooks(ExecutionEvent.before(step));
1✔
70

71
        try {
72
            runnable.run();
1✔
73
        } catch (RuntimeException e) {
1✔
74
            executeHooks(ExecutionEvent.failure(step, e));
1✔
75
            throw e;
1✔
76
        }
1✔
77
        executeHooks(ExecutionEvent.success(step));
1✔
78
    }
1✔
79

80
    public void executeHooks(ExecutionEvent event) {
81
        Hooks hooks = context.getModel().getHooks();
1✔
82
        if (!hooks.isEnabled() || evaluateCondition(hooks.getCondition())) {
1✔
83
            return;
×
84
        }
85

86
        Map<String, String> rootEnv = resolveEnvironment(hooks.getEnvironment());
1✔
87
        executeScriptHooks(event, rootEnv);
1✔
88
        executeCommandHooks(event, rootEnv);
1✔
89
    }
1✔
90

91
    private boolean evaluateCondition(String condition) {
92
        return isNotBlank(condition) && isFalse(context.eval(condition));
1✔
93
    }
94

95
    private Map<String, String> resolveEnvironment(Map<String, String> src) {
96
        return resolveEnvironment(src, null);
1✔
97
    }
98

99
    private Map<String, String> resolveEnvironment(Map<String, String> src, TemplateContext additionalContext) {
100
        Map<String, String> env = new LinkedHashMap<>();
1✔
101
        TemplateContext props = context.props().setAll(additionalContext);
1✔
102
        src.forEach((k, v) -> {
1✔
103
            String value = resolveTemplate(v, props);
1✔
104
            if (isNotBlank(value)) env.put(k, value);
1✔
105
        });
1✔
106
        return env;
1✔
107
    }
108

109
    private void executeScriptHooks(ExecutionEvent event, Map<String, String> rootEnv) {
110
        ScriptHooks scriptHooks = context.getModel().getHooks().getScript();
1✔
111
        if (!scriptHooks.isEnabled() || evaluateCondition(scriptHooks.getCondition())) {
1✔
112
            return;
×
113
        }
114

115
        final List<ScriptHook> hooks = new ArrayList<>();
1✔
116

117
        switch (event.getType()) {
1✔
118
            case BEFORE:
119
                hooks.addAll((Collection<ScriptHook>) filter(scriptHooks.getBefore(), event));
1✔
120
                scriptHooks.getGroups().values().forEach(group -> {
1✔
NEW
121
                    if (!group.isEnabled() || evaluateCondition(group.getCondition())) {
×
NEW
122
                        return;
×
123
                    }
NEW
124
                    hooks.addAll((Collection<ScriptHook>) filter(group.getBefore(), event));
×
NEW
125
                });
×
126
                break;
1✔
127
            case SUCCESS:
128
                hooks.addAll((Collection<ScriptHook>) filter(scriptHooks.getSuccess(), event));
1✔
129
                scriptHooks.getGroups().values().forEach(group -> {
1✔
NEW
130
                    if (!group.isEnabled() || evaluateCondition(group.getCondition())) {
×
NEW
131
                        return;
×
132
                    }
NEW
133
                    hooks.addAll((Collection<ScriptHook>) filter(group.getSuccess(), event));
×
NEW
134
                });
×
135
                break;
1✔
136
            case FAILURE:
137
                hooks.addAll((Collection<ScriptHook>) filter(scriptHooks.getFailure(), event));
1✔
138
                scriptHooks.getGroups().values().forEach(group -> {
1✔
NEW
139
                    if (!group.isEnabled() || evaluateCondition(group.getCondition())) {
×
NEW
140
                        return;
×
141
                    }
NEW
142
                    hooks.addAll((Collection<ScriptHook>) filter(group.getFailure(), event));
×
NEW
143
                });
×
144
                break;
145
        }
146

147
        if (!hooks.isEmpty()) {
1✔
148
            context.getLogger().info(RB.$("hooks.script.execution"), event.getType().name().toLowerCase(Locale.ENGLISH), hooks.size());
1✔
149
        }
150

151
        context.getLogger().setPrefix("hooks");
1✔
152
        context.getLogger().increaseIndent();
1✔
153

154
        try {
155
            for (ScriptHook hook : hooks) {
1✔
156
                String prefix = "hooks";
1✔
157
                if (isNotBlank(hook.getName())) {
1✔
NEW
158
                    prefix += "." + hook.getName();
×
159
                }
160
                context.getLogger().replacePrefix(prefix);
1✔
161

162
                if (!hook.getMatrix().isEmpty()) {
1✔
163
                    for (Map<String, String> matrixRow : hook.getMatrix().resolve()) {
1✔
164
                        if (matrixRow.containsKey(KEY_PLATFORM)) {
1✔
165
                            String srcPlatform = matrixRow.get(KEY_PLATFORM);
1✔
166
                            if (!context.isPlatformSelected(srcPlatform)) {
1✔
167
                                continue;
×
168
                            }
169
                        }
170

171
                        TemplateContext additionalContext = Matrix.asTemplateContext(matrixRow);
1✔
172
                        Map<String, String> localEnv = new LinkedHashMap<>(rootEnv);
1✔
173
                        localEnv.putAll(scriptHooks.getEnvironment());
1✔
174
                        localEnv = resolveEnvironment(localEnv, additionalContext);
1✔
175
                        Path scriptFile = null;
1✔
176

177
                        try {
178
                            scriptFile = createScriptFile(context, hook, additionalContext, event);
1✔
179
                        } catch (IOException e) {
×
180
                            throw new JReleaserException(RB.$("ERROR_script_hook_create_error"), e);
×
181
                        }
1✔
182

183
                        String resolvedCmd = hook.getShell().expression().replace("{{script}}", scriptFile.toAbsolutePath().toString());
1✔
184
                        executeCommandLine(localEnv, additionalContext, hook, resolvedCmd, resolvedCmd, "ERROR_script_hook_unexpected_error");
1✔
185
                    }
1✔
186
                } else {
187
                    Map<String, String> localEnv = new LinkedHashMap<>(rootEnv);
1✔
188
                    localEnv.putAll(scriptHooks.getEnvironment());
1✔
189
                    localEnv = resolveEnvironment(localEnv);
1✔
190
                    Path scriptFile = null;
1✔
191

192
                    try {
193
                        scriptFile = createScriptFile(context, hook, null, event);
1✔
194
                    } catch (IOException e) {
×
195
                        throw new JReleaserException(RB.$("ERROR_script_hook_create_error"), e);
×
196
                    }
1✔
197

198
                    String resolvedCmd = hook.getShell().expression().replace("{{script}}", scriptFile.toAbsolutePath().toString());
1✔
199
                    executeCommandLine(localEnv, null, hook, resolvedCmd, resolvedCmd, "ERROR_script_hook_unexpected_error");
1✔
200
                }
201
            }
1✔
202
        } finally {
203
            context.getLogger().decreaseIndent();
1✔
204
            context.getLogger().restorePrefix();
1✔
205
        }
206
    }
1✔
207

208
    private Path createScriptFile(JReleaserContext context, ScriptHook hook, TemplateContext additionalContext, ExecutionEvent event) throws IOException {
209
        String scriptContents = hook.getResolvedRun(context, additionalContext, event);
1✔
210
        Path scriptFile = Files.createTempFile("jreleaser", hook.getShell().extension());
1✔
211

212
        if (hook.getShell() == org.jreleaser.model.api.hooks.ScriptHook.Shell.PWSH ||
1✔
213
            hook.getShell() == org.jreleaser.model.api.hooks.ScriptHook.Shell.POWERSHELL) {
1✔
214
            scriptContents = "$ErrorActionPreference = 'stop'" + lineSeparator() + scriptContents;
×
215
            scriptContents += lineSeparator() + "if ((Test-Path -LiteralPath variable:\\LASTEXITCODE)) { exit $LASTEXITCODE }";
×
216
        }
217

218
        Files.write(scriptFile, scriptContents.getBytes(UTF_8), WRITE);
1✔
219
        return scriptFile;
1✔
220
    }
221

222
    private void executeCommandHooks(ExecutionEvent event, Map<String, String> rootEnv) {
223
        CommandHooks commandHooks = context.getModel().getHooks().getCommand();
1✔
224
        if (!commandHooks.isEnabled() || evaluateCondition(commandHooks.getCondition())) {
1✔
225
            return;
×
226
        }
227

228
        final List<CommandHook> hooks = new ArrayList<>();
1✔
229

230
        switch (event.getType()) {
1✔
231
            case BEFORE:
232
                hooks.addAll((Collection<CommandHook>) filter(commandHooks.getBefore(), event));
1✔
233
                commandHooks.getGroups().values().forEach(group -> {
1✔
NEW
234
                    if (!group.isEnabled() || evaluateCondition(group.getCondition())) {
×
NEW
235
                        return;
×
236
                    }
NEW
237
                    hooks.addAll((Collection<CommandHook>) filter(group.getBefore(), event));
×
NEW
238
                });
×
239
                break;
1✔
240
            case SUCCESS:
241
                hooks.addAll((Collection<CommandHook>) filter(commandHooks.getSuccess(), event));
1✔
242
                commandHooks.getGroups().values().forEach(group -> {
1✔
NEW
243
                    if (!group.isEnabled() || evaluateCondition(group.getCondition())) {
×
NEW
244
                        return;
×
245
                    }
NEW
246
                    hooks.addAll((Collection<CommandHook>) filter(group.getSuccess(), event));
×
NEW
247
                });
×
248
                break;
1✔
249
            case FAILURE:
250
                hooks.addAll((Collection<CommandHook>) filter(commandHooks.getFailure(), event));
1✔
251
                commandHooks.getGroups().values().forEach(group -> {
1✔
NEW
252
                    if (!group.isEnabled() || evaluateCondition(group.getCondition())) {
×
NEW
253
                        return;
×
254
                    }
NEW
255
                    hooks.addAll((Collection<CommandHook>) filter(group.getFailure(), event));
×
NEW
256
                });
×
257
                break;
258
        }
259

260
        if (!hooks.isEmpty()) {
1✔
261
            context.getLogger().info(RB.$("hooks.command.execution"), event.getType().name().toLowerCase(Locale.ENGLISH), hooks.size());
1✔
262
        }
263

264
        context.getLogger().setPrefix("hooks");
1✔
265
        context.getLogger().increaseIndent();
1✔
266

267
        try {
268
            for (CommandHook hook : hooks) {
1✔
269
                String prefix = "hooks";
1✔
270
                if (isNotBlank(hook.getName())) {
1✔
NEW
271
                    prefix += "." + hook.getName();
×
272
                }
273
                context.getLogger().replacePrefix(prefix);
1✔
274

275
                if (!hook.getMatrix().isEmpty()) {
1✔
276
                    for (Map<String, String> matrixRow : hook.getMatrix().resolve()) {
×
277
                        if (matrixRow.containsKey(KEY_PLATFORM)) {
×
278
                            String srcPlatform = matrixRow.get(KEY_PLATFORM);
×
279
                            if (!context.isPlatformSelected(srcPlatform)) {
×
280
                                continue;
×
281
                            }
282
                        }
283

284
                        TemplateContext additionalContext = Matrix.asTemplateContext(matrixRow);
×
285
                        Map<String, String> localEnv = new LinkedHashMap<>(rootEnv);
×
286
                        localEnv.putAll(commandHooks.getEnvironment());
×
287
                        localEnv = resolveEnvironment(localEnv, additionalContext);
×
288
                        String resolvedCmd = hook.getResolvedCmd(context, additionalContext, event);
×
289
                        executeCommandLine(localEnv, additionalContext, hook, hook.getCmd(), resolvedCmd, "ERROR_command_hook_unexpected_error");
×
290
                    }
×
291
                } else {
292
                    Map<String, String> localEnv = new LinkedHashMap<>(rootEnv);
1✔
293
                    localEnv.putAll(commandHooks.getEnvironment());
1✔
294
                    localEnv = resolveEnvironment(localEnv);
1✔
295
                    String resolvedCmd = hook.getResolvedCmd(context, null, event);
1✔
296
                    executeCommandLine(localEnv, null, hook, hook.getCmd(), resolvedCmd, "ERROR_command_hook_unexpected_error");
1✔
297
                }
298
            }
1✔
299
        } finally {
300
            context.getLogger().decreaseIndent();
1✔
301
            context.getLogger().restorePrefix();
1✔
302
        }
303
    }
1✔
304

305
    private void executeCommandLine(Map<String, String> localEnv, TemplateContext additionalContext, Hook hook, String cmd, String resolvedCmd, String errorKey) {
306
        List<String> commandLine = null;
1✔
307

308
        Map<String, String> hookEnv = new LinkedHashMap<>(localEnv);
1✔
309
        hookEnv.putAll(hook.getEnvironment());
1✔
310
        hookEnv = resolveEnvironment(hookEnv, additionalContext);
1✔
311

312
        try {
313
            commandLine = parseCommand(resolvedCmd);
1✔
314
        } catch (IllegalStateException e) {
×
315
            throw new JReleaserException(RB.$("ERROR_command_hook_parser_error", cmd), e);
×
316
        }
1✔
317

318
        try {
319
            Command command = new Command(commandLine);
1✔
320
            processOutput(executeCommand(context.getBasedir(), command, hookEnv, hook.isVerbose()));
1✔
321
        } catch (CommandException e) {
1✔
322
            if (!hook.isContinueOnError()) {
1✔
323
                throw new JReleaserException(RB.$(errorKey), e);
1✔
324
            } else {
325
                if (null != e.getCause()) {
×
326
                    context.getLogger().warn(e.getCause().getMessage());
×
327
                } else {
328
                    context.getLogger().warn(e.getMessage());
×
329
                }
330
                context.getLogger().trace(RB.$(errorKey), e);
×
331
            }
332
        }
1✔
333
    }
1✔
334

335
    private void processOutput(Command.Result result) {
336
        if (!result.getOut().contains(JRELEASER_OUTPUT)) return;
1✔
337
        for (String line : result.getOut().split(lineSeparator())) {
×
338
            if (!line.startsWith(JRELEASER_OUTPUT)) continue;
×
339
            line = line.substring(JRELEASER_OUTPUT.length());
×
340
            int p = line.indexOf("=");
×
341
            String key = line.substring(0, p);
×
342
            String value = line.substring(p + 1);
×
343
            context.getModel().getEnvironment().getProperties().put(key, value);
×
344
        }
345
    }
×
346

347
    private Collection<? extends Hook> filter(List<? extends Hook> hooks, ExecutionEvent event) {
348
        List<Hook> tmp = new ArrayList<>();
1✔
349

350
        for (Hook hook : hooks) {
1✔
351
            if (!hook.isEnabled() || evaluateCondition(hook.getCondition())) {
1✔
352
                continue;
×
353
            }
354

355
            if (!hook.getFilter().getResolvedIncludes().isEmpty()) {
1✔
356
                if (hook.getFilter().getResolvedIncludes().contains(event.getName()) && filterByPlatform(hook)) {
1✔
357
                    tmp.add(hook);
1✔
358
                }
359
            } else if (filterByPlatform(hook)) {
1✔
360
                tmp.add(hook);
1✔
361
            }
362

363
            if (hook.getFilter().getResolvedExcludes().contains(event.getName())) {
1✔
364
                tmp.remove(hook);
×
365
            }
366
        }
1✔
367

368
        return tmp;
1✔
369
    }
370

371
    private boolean filterByPlatform(Hook hook) {
372
        if (hook.getPlatforms().isEmpty()) return true;
1✔
373

374
        boolean success = true;
1✔
375
        for (String platform : hook.getPlatforms()) {
1✔
376
            boolean exclude = false;
1✔
377
            if (platform.startsWith("!")) {
1✔
378
                exclude = true;
1✔
379
                platform = platform.substring(1);
1✔
380
            }
381

382
            success &= exclude != PlatformUtils.isCompatible(PlatformUtils.getCurrentFull(), platform);
1✔
383
        }
1✔
384

385
        return success;
1✔
386
    }
387

388
    private Command.Result executeCommand(Path directory, Command command, Map<String, String> env, boolean verbose) throws CommandException {
389
        Command.Result result = new CommandExecutor(context.getLogger(), verbose ? CommandExecutor.Output.VERBOSE : CommandExecutor.Output.DEBUG)
1✔
390
            .environment(env)
1✔
391
            .executeCommand(directory, command);
1✔
392
        if (result.getExitValue() != 0) {
1✔
393
            throw new CommandException(RB.$("ERROR_command_execution_exit_value", result.getExitValue()));
1✔
394
        }
395
        return result;
1✔
396
    }
397

398
    // adjusted from org.apache.tools.ant.types.Commandline#translateCommandLine
399
    public static List<String> parseCommand(String str) {
400
        final int normal = 0;
1✔
401
        final int inQuote = 1;
1✔
402
        final int inDoubleQuote = 2;
1✔
403
        int state = normal;
1✔
404
        final StringTokenizer tok = new StringTokenizer(str, "\"' ", true);
1✔
405
        final ArrayList<String> result = new ArrayList<>();
1✔
406
        final StringBuilder current = new StringBuilder();
1✔
407
        boolean lastTokenHasBeenQuoted = false;
1✔
408

409
        while (tok.hasMoreTokens()) {
1✔
410
            String nextTok = tok.nextToken();
1✔
411
            switch (state) {
1✔
412
                case inQuote:
413
                    if ("'".equals(nextTok)) {
×
414
                        lastTokenHasBeenQuoted = true;
×
415
                        state = normal;
×
416
                    } else {
417
                        current.append(nextTok);
×
418
                    }
419
                    break;
×
420
                case inDoubleQuote:
421
                    if ("\"".equals(nextTok)) {
1✔
422
                        lastTokenHasBeenQuoted = true;
1✔
423
                        state = normal;
1✔
424
                    } else {
425
                        current.append(nextTok);
1✔
426
                    }
427
                    break;
1✔
428
                default:
429
                    if ("'".equals(nextTok)) {
1✔
430
                        state = inQuote;
×
431
                    } else if ("\"".equals(nextTok)) {
1✔
432
                        state = inDoubleQuote;
1✔
433
                    } else if (" ".equals(nextTok)) {
1✔
434
                        if (lastTokenHasBeenQuoted || current.length() > 0) {
1✔
435
                            result.add(current.toString());
1✔
436
                            current.setLength(0);
1✔
437
                        }
438
                    } else {
439
                        current.append(nextTok);
1✔
440
                    }
441
                    lastTokenHasBeenQuoted = false;
1✔
442
                    break;
443
            }
444
        }
1✔
445

446
        if (lastTokenHasBeenQuoted || current.length() > 0) {
1✔
447
            result.add(current.toString());
1✔
448
        }
449

450
        if (state == inQuote || state == inDoubleQuote) {
1✔
451
            throw new IllegalStateException(RB.$("ERROR_unbalanced_quotes", str));
×
452
        }
453

454
        return result;
1✔
455
    }
456
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc