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

jreleaser / jreleaser / #510

27 Jul 2025 12:31PM UTC coverage: 45.783% (-3.6%) from 49.39%
#510

push

github

aalmiray
feat(packagers): Stage distribution publication in a fixed directory

Closes #1943

12 of 25 new or added lines in 4 files covered. (48.0%)

2208 existing lines in 190 files now uncovered.

23924 of 52255 relevant lines covered (45.78%)

0.46 hits per line

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

78.87
/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✔
UNCOV
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
                break;
1✔
121
            case SUCCESS:
122
                hooks.addAll((Collection<ScriptHook>) filter(scriptHooks.getSuccess(), event));
1✔
123
                break;
1✔
124
            case FAILURE:
125
                hooks.addAll((Collection<ScriptHook>) filter(scriptHooks.getFailure(), event));
1✔
126
                break;
127
        }
128

129
        if (!hooks.isEmpty()) {
1✔
130
            context.getLogger().info(RB.$("hooks.script.execution"), event.getType().name().toLowerCase(Locale.ENGLISH), hooks.size());
1✔
131
        }
132

133
        context.getLogger().setPrefix("hooks");
1✔
134
        context.getLogger().increaseIndent();
1✔
135

136
        try {
137
            for (ScriptHook hook : hooks) {
1✔
138
                if (!hook.getMatrix().isEmpty()) {
1✔
139
                    for (Map<String, String> matrixRow : hook.getMatrix().resolve()) {
1✔
140
                        if (matrixRow.containsKey(KEY_PLATFORM)) {
1✔
141
                            String srcPlatform = matrixRow.get(KEY_PLATFORM);
1✔
142
                            if (!context.isPlatformSelected(srcPlatform)) {
1✔
143
                                continue;
×
144
                            }
145
                        }
146

147
                        TemplateContext additionalContext = Matrix.asTemplateContext(matrixRow);
1✔
148
                        Map<String, String> localEnv = new LinkedHashMap<>(rootEnv);
1✔
149
                        localEnv.putAll(scriptHooks.getEnvironment());
1✔
150
                        localEnv = resolveEnvironment(localEnv, additionalContext);
1✔
151
                        Path scriptFile = null;
1✔
152

153
                        try {
154
                            scriptFile = createScriptFile(context, hook, additionalContext, event);
1✔
155
                        } catch (IOException e) {
×
156
                            throw new JReleaserException(RB.$("ERROR_script_hook_create_error"), e);
×
157
                        }
1✔
158

159
                        String resolvedCmd = hook.getShell().expression().replace("{{script}}", scriptFile.toAbsolutePath().toString());
1✔
160
                        executeCommandLine(localEnv, additionalContext, hook, resolvedCmd, resolvedCmd, "ERROR_script_hook_unexpected_error");
1✔
161
                    }
1✔
162
                } else {
163
                    Map<String, String> localEnv = new LinkedHashMap<>(rootEnv);
1✔
164
                    localEnv.putAll(scriptHooks.getEnvironment());
1✔
165
                    localEnv = resolveEnvironment(localEnv);
1✔
166
                    Path scriptFile = null;
1✔
167

168
                    try {
169
                        scriptFile = createScriptFile(context, hook, null, event);
1✔
170
                    } catch (IOException e) {
×
171
                        throw new JReleaserException(RB.$("ERROR_script_hook_create_error"), e);
×
172
                    }
1✔
173

174
                    String resolvedCmd = hook.getShell().expression().replace("{{script}}", scriptFile.toAbsolutePath().toString());
1✔
175
                    executeCommandLine(localEnv, null, hook, resolvedCmd, resolvedCmd, "ERROR_script_hook_unexpected_error");
1✔
176
                }
177
            }
1✔
178
        } finally {
179
            context.getLogger().decreaseIndent();
1✔
180
            context.getLogger().restorePrefix();
1✔
181
        }
182
    }
1✔
183

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

188
        if (hook.getShell() == org.jreleaser.model.api.hooks.ScriptHook.Shell.PWSH ||
1✔
189
            hook.getShell() == org.jreleaser.model.api.hooks.ScriptHook.Shell.POWERSHELL) {
1✔
190
            scriptContents = "$ErrorActionPreference = 'stop'" + lineSeparator() + scriptContents;
×
191
            scriptContents += lineSeparator() + "if ((Test-Path -LiteralPath variable:\\LASTEXITCODE)) { exit $LASTEXITCODE }";
×
192
        }
193

194
        Files.write(scriptFile, scriptContents.getBytes(UTF_8), WRITE);
1✔
195
        return scriptFile;
1✔
196
    }
197

198
    private void executeCommandHooks(ExecutionEvent event, Map<String, String> rootEnv) {
199
        CommandHooks commandHooks = context.getModel().getHooks().getCommand();
1✔
200
        if (!commandHooks.isEnabled() || evaluateCondition(commandHooks.getCondition())) {
1✔
201
            return;
×
202
        }
203

204
        final List<CommandHook> hooks = new ArrayList<>();
1✔
205

206
        switch (event.getType()) {
1✔
207
            case BEFORE:
208
                hooks.addAll((Collection<CommandHook>) filter(commandHooks.getBefore(), event));
1✔
209
                break;
1✔
210
            case SUCCESS:
211
                hooks.addAll((Collection<CommandHook>) filter(commandHooks.getSuccess(), event));
1✔
212
                break;
1✔
213
            case FAILURE:
214
                hooks.addAll((Collection<CommandHook>) filter(commandHooks.getFailure(), event));
1✔
215
                break;
216
        }
217

218
        if (!hooks.isEmpty()) {
1✔
219
            context.getLogger().info(RB.$("hooks.command.execution"), event.getType().name().toLowerCase(Locale.ENGLISH), hooks.size());
1✔
220
        }
221

222
        context.getLogger().setPrefix("hooks");
1✔
223
        context.getLogger().increaseIndent();
1✔
224

225
        try {
226
            for (CommandHook hook : hooks) {
1✔
227
                if (!hook.getMatrix().isEmpty()) {
1✔
228
                    for (Map<String, String> matrixRow : hook.getMatrix().resolve()) {
×
229
                        if (matrixRow.containsKey(KEY_PLATFORM)) {
×
230
                            String srcPlatform = matrixRow.get(KEY_PLATFORM);
×
231
                            if (!context.isPlatformSelected(srcPlatform)) {
×
232
                                continue;
×
233
                            }
234
                        }
235

236
                        TemplateContext additionalContext = Matrix.asTemplateContext(matrixRow);
×
237
                        Map<String, String> localEnv = new LinkedHashMap<>(rootEnv);
×
238
                        localEnv.putAll(commandHooks.getEnvironment());
×
239
                        localEnv = resolveEnvironment(localEnv, additionalContext);
×
240
                        String resolvedCmd = hook.getResolvedCmd(context, additionalContext, event);
×
241
                        executeCommandLine(localEnv, additionalContext, hook, hook.getCmd(), resolvedCmd, "ERROR_command_hook_unexpected_error");
×
242
                    }
×
243
                } else {
244
                    Map<String, String> localEnv = new LinkedHashMap<>(rootEnv);
1✔
245
                    localEnv.putAll(commandHooks.getEnvironment());
1✔
246
                    localEnv = resolveEnvironment(localEnv);
1✔
247
                    String resolvedCmd = hook.getResolvedCmd(context, null, event);
1✔
248
                    executeCommandLine(localEnv, null, hook, hook.getCmd(), resolvedCmd, "ERROR_command_hook_unexpected_error");
1✔
249
                }
250
            }
1✔
251
        } finally {
252
            context.getLogger().decreaseIndent();
1✔
253
            context.getLogger().restorePrefix();
1✔
254
        }
255
    }
1✔
256

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

260
        Map<String, String> hookEnv = new LinkedHashMap<>(localEnv);
1✔
261
        hookEnv.putAll(hook.getEnvironment());
1✔
262
        hookEnv = resolveEnvironment(hookEnv, additionalContext);
1✔
263

264
        try {
265
            commandLine = parseCommand(resolvedCmd);
1✔
266
        } catch (IllegalStateException e) {
×
267
            throw new JReleaserException(RB.$("ERROR_command_hook_parser_error", cmd), e);
×
268
        }
1✔
269

270
        try {
271
            Command command = new Command(commandLine);
1✔
272
            processOutput(executeCommand(context.getBasedir(), command, hookEnv, hook.isVerbose()));
1✔
273
        } catch (CommandException e) {
1✔
274
            if (!hook.isContinueOnError()) {
1✔
275
                throw new JReleaserException(RB.$(errorKey), e);
1✔
276
            } else {
277
                if (null != e.getCause()) {
×
278
                    context.getLogger().warn(e.getCause().getMessage());
×
279
                } else {
280
                    context.getLogger().warn(e.getMessage());
×
281
                }
282
                context.getLogger().trace(RB.$(errorKey), e);
×
283
            }
284
        }
1✔
285
    }
1✔
286

287
    private void processOutput(Command.Result result) {
288
        if (!result.getOut().contains(JRELEASER_OUTPUT)) return;
1✔
289
        for (String line : result.getOut().split(lineSeparator())) {
×
290
            if (!line.startsWith(JRELEASER_OUTPUT)) continue;
×
291
            line = line.substring(JRELEASER_OUTPUT.length());
×
292
            int p = line.indexOf("=");
×
293
            String key = line.substring(0, p);
×
294
            String value = line.substring(p + 1);
×
295
            context.getModel().getEnvironment().getProperties().put(key, value);
×
296
        }
297
    }
×
298

299
    private Collection<? extends Hook> filter(List<? extends Hook> hooks, ExecutionEvent event) {
300
        List<Hook> tmp = new ArrayList<>();
1✔
301

302
        for (Hook hook : hooks) {
1✔
303
            if (!hook.isEnabled() || evaluateCondition(hook.getCondition())) {
1✔
304
                continue;
×
305
            }
306

307
            if (!hook.getFilter().getResolvedIncludes().isEmpty()) {
1✔
308
                if (hook.getFilter().getResolvedIncludes().contains(event.getName()) && filterByPlatform(hook)) {
1✔
309
                    tmp.add(hook);
1✔
310
                }
311
            } else if (filterByPlatform(hook)) {
1✔
312
                tmp.add(hook);
1✔
313
            }
314

315
            if (hook.getFilter().getResolvedExcludes().contains(event.getName())) {
1✔
316
                tmp.remove(hook);
×
317
            }
318
        }
1✔
319

320
        return tmp;
1✔
321
    }
322

323
    private boolean filterByPlatform(Hook hook) {
324
        if (hook.getPlatforms().isEmpty()) return true;
1✔
325

326
        boolean success = true;
1✔
327
        for (String platform : hook.getPlatforms()) {
1✔
328
            boolean exclude = false;
1✔
329
            if (platform.startsWith("!")) {
1✔
330
                exclude = true;
1✔
331
                platform = platform.substring(1);
1✔
332
            }
333

334
            success &= exclude != PlatformUtils.isCompatible(PlatformUtils.getCurrentFull(), platform);
1✔
335
        }
1✔
336

337
        return success;
1✔
338
    }
339

340
    private Command.Result executeCommand(Path directory, Command command, Map<String, String> env, boolean verbose) throws CommandException {
341
        Command.Result result = new CommandExecutor(context.getLogger(), verbose ? CommandExecutor.Output.VERBOSE : CommandExecutor.Output.DEBUG)
1✔
342
            .environment(env)
1✔
343
            .executeCommand(directory, command);
1✔
344
        if (result.getExitValue() != 0) {
1✔
345
            throw new CommandException(RB.$("ERROR_command_execution_exit_value", result.getExitValue()));
1✔
346
        }
347
        return result;
1✔
348
    }
349

350
    // adjusted from org.apache.tools.ant.types.Commandline#translateCommandLine
351
    public static List<String> parseCommand(String str) {
352
        final int normal = 0;
1✔
353
        final int inQuote = 1;
1✔
354
        final int inDoubleQuote = 2;
1✔
355
        int state = normal;
1✔
356
        final StringTokenizer tok = new StringTokenizer(str, "\"' ", true);
1✔
357
        final ArrayList<String> result = new ArrayList<>();
1✔
358
        final StringBuilder current = new StringBuilder();
1✔
359
        boolean lastTokenHasBeenQuoted = false;
1✔
360

361
        while (tok.hasMoreTokens()) {
1✔
362
            String nextTok = tok.nextToken();
1✔
363
            switch (state) {
1✔
364
                case inQuote:
365
                    if ("'".equals(nextTok)) {
×
366
                        lastTokenHasBeenQuoted = true;
×
367
                        state = normal;
×
368
                    } else {
369
                        current.append(nextTok);
×
370
                    }
371
                    break;
×
372
                case inDoubleQuote:
373
                    if ("\"".equals(nextTok)) {
1✔
374
                        lastTokenHasBeenQuoted = true;
1✔
375
                        state = normal;
1✔
376
                    } else {
377
                        current.append(nextTok);
1✔
378
                    }
379
                    break;
1✔
380
                default:
381
                    if ("'".equals(nextTok)) {
1✔
382
                        state = inQuote;
×
383
                    } else if ("\"".equals(nextTok)) {
1✔
384
                        state = inDoubleQuote;
1✔
385
                    } else if (" ".equals(nextTok)) {
1✔
386
                        if (lastTokenHasBeenQuoted || current.length() > 0) {
1✔
387
                            result.add(current.toString());
1✔
388
                            current.setLength(0);
1✔
389
                        }
390
                    } else {
391
                        current.append(nextTok);
1✔
392
                    }
393
                    lastTokenHasBeenQuoted = false;
1✔
394
                    break;
395
            }
396
        }
1✔
397

398
        if (lastTokenHasBeenQuoted || current.length() > 0) {
1✔
399
            result.add(current.toString());
1✔
400
        }
401

402
        if (state == inQuote || state == inDoubleQuote) {
1✔
403
            throw new IllegalStateException(RB.$("ERROR_unbalanced_quotes", str));
×
404
        }
405

406
        return result;
1✔
407
    }
408
}
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