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

jreleaser / jreleaser / #540

02 Oct 2025 04:42PM UTC coverage: 48.269%. Remained the same
#540

push

github

aalmiray
build: Update GH actions

25820 of 53492 relevant lines covered (48.27%)

0.48 hits per line

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

71.21
/sdks/jreleaser-java-sdk-commons/src/main/java/org/jreleaser/sdk/commons/AbstractMavenDeployer.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.sdk.commons;
19

20
import feign.form.FormData;
21
import org.apache.commons.io.IOUtils;
22
import org.jreleaser.bundle.RB;
23
import org.jreleaser.model.JReleaserException;
24
import org.jreleaser.model.api.signing.SigningException;
25
import org.jreleaser.model.internal.JReleaserContext;
26
import org.jreleaser.model.internal.deploy.maven.Maven;
27
import org.jreleaser.model.spi.deploy.DeployException;
28
import org.jreleaser.model.spi.deploy.maven.Deployable;
29
import org.jreleaser.model.spi.deploy.maven.MavenDeployer;
30
import org.jreleaser.model.spi.upload.UploadException;
31
import org.jreleaser.sdk.command.Command;
32
import org.jreleaser.sdk.command.CommandException;
33
import org.jreleaser.sdk.signing.SigningUtils;
34
import org.jreleaser.sdk.tool.PomChecker;
35
import org.jreleaser.sdk.tool.ToolException;
36
import org.jreleaser.util.Algorithm;
37
import org.jreleaser.util.ChecksumUtils;
38
import org.jreleaser.util.CollectionUtils;
39
import org.jreleaser.util.Errors;
40
import org.jreleaser.util.FileUtils;
41
import org.w3c.dom.Document;
42
import org.xml.sax.SAXException;
43

44
import javax.xml.parsers.DocumentBuilderFactory;
45
import javax.xml.parsers.ParserConfigurationException;
46
import javax.xml.xpath.XPathConstants;
47
import javax.xml.xpath.XPathExpressionException;
48
import javax.xml.xpath.XPathFactory;
49
import java.io.IOException;
50
import java.net.HttpURLConnection;
51
import java.net.MalformedURLException;
52
import java.net.URI;
53
import java.net.URISyntaxException;
54
import java.net.URL;
55
import java.nio.file.FileVisitResult;
56
import java.nio.file.Files;
57
import java.nio.file.Path;
58
import java.nio.file.SimpleFileVisitor;
59
import java.nio.file.attribute.BasicFileAttributes;
60
import java.nio.file.attribute.FileTime;
61
import java.time.Instant;
62
import java.time.LocalDateTime;
63
import java.time.ZoneId;
64
import java.util.ArrayList;
65
import java.util.Enumeration;
66
import java.util.LinkedHashMap;
67
import java.util.LinkedHashSet;
68
import java.util.List;
69
import java.util.Locale;
70
import java.util.Map;
71
import java.util.Optional;
72
import java.util.Set;
73
import java.util.TreeSet;
74
import java.util.function.Function;
75
import java.util.jar.JarEntry;
76
import java.util.jar.JarFile;
77
import java.util.stream.Collectors;
78

79
import static java.nio.charset.StandardCharsets.UTF_8;
80
import static java.nio.file.FileVisitResult.CONTINUE;
81
import static java.util.stream.Collectors.toSet;
82
import static org.jreleaser.model.spi.deploy.maven.Deployable.EXT_ASC;
83
import static org.jreleaser.model.spi.deploy.maven.Deployable.EXT_JAR;
84
import static org.jreleaser.model.spi.deploy.maven.Deployable.EXT_POM;
85
import static org.jreleaser.model.spi.deploy.maven.Deployable.EXT_WAR;
86
import static org.jreleaser.model.spi.deploy.maven.Deployable.MAVEN_METADATA_XML;
87
import static org.jreleaser.model.spi.deploy.maven.Deployable.PACKAGING_JAR;
88
import static org.jreleaser.model.spi.deploy.maven.Deployable.PACKAGING_WAR;
89
import static org.jreleaser.util.StringUtils.isNotBlank;
90

91
/**
92
 * @author Andres Almiray
93
 * @since 1.4.0
94
 */
95
public abstract class AbstractMavenDeployer<A extends org.jreleaser.model.api.deploy.maven.MavenDeployer,
96
    D extends org.jreleaser.model.internal.deploy.maven.MavenDeployer<A>> implements MavenDeployer<A, D> {
97
    private static final Algorithm[] ALGORITHMS = {
1✔
98
        Algorithm.MD5,
99
        Algorithm.SHA_1,
100
        Algorithm.SHA_256,
101
        Algorithm.SHA_512
102
    };
103

104
    private static final String BUILD_TAG = "-build";
105
    private static final Map<String, String> KEY_SERVERS = CollectionUtils.<String, String>map()
1✔
106
        .e("https://keys.openpgp.org", "https://keys.openpgp.org/search?q=%s")
1✔
107
        .e("https://keyserver.ubuntu.com", "https://keyserver.ubuntu.com/pks/lookup?search=%s&fingerprint=on&options=mr&op=index")
1✔
108
        .e("https://pgp.mit.edu", "https://pgp.mit.edu/pks/lookup?op=get&search=0x%s");
1✔
109

110
    protected final JReleaserContext context;
111

112
    protected AbstractMavenDeployer(JReleaserContext context) {
1✔
113
        this.context = context;
1✔
114
    }
1✔
115

116
    protected Set<Deployable> collectDeployableArtifacts() {
117
        Set<Deployable> deployables = new TreeSet<>();
1✔
118
        for (String stagingRepository : getDeployer().getStagingRepositories()) {
1✔
119
            collectDeployables(deployables, stagingRepository);
1✔
120
        }
1✔
121

122
        return deployables;
1✔
123
    }
124

125
    protected Set<Deployable> collectDeployables() {
126
        return collectDeployables(true);
1✔
127
    }
128

129
    protected Set<Deployable> collectDeployables(boolean checksum) {
130
        Set<Deployable> deployables = collectDeployableArtifacts().stream()
1✔
131
            .filter(deployable -> checksum || !deployable.isChecksum())
1✔
132
            .collect(toSet());
1✔
133

134
        Map<String, Deployable> deployablesMap = deployables.stream()
1✔
135
            .collect(Collectors.toMap(Deployable::getFullDeployPath, Function.identity()));
1✔
136

137
        Errors errors = checkMavenCentralRules(deployablesMap);
1✔
138
        if (errors.hasErrors()) {
1✔
139
            errors.logErrors(context.getLogger());
×
140
            throw new JReleaserException(RB.$("ERROR_deployer_maven_central_rules"));
×
141
        }
142

143
        signDeployables(deployablesMap, deployables);
1✔
144
        if (checksum) checksumDeployables(deployablesMap, deployables);
1✔
145

146
        return deployables;
1✔
147
    }
148

149
    public Set<Deployable> collectDeployables(Set<Deployable> deployables, String stagingRepository) {
150
        Path root = context.getBasedir().resolve(stagingRepository).normalize();
1✔
151

152
        if (!Files.exists(root)) {
1✔
153
            throw new JReleaserException(RB.$("validation_directory_not_exist",
×
154
                "maven." + getDeployer().getType() + "." + getDeployer().getName() + ".stagingRepository",
×
155
                context.relativizeToBasedir(root).toString()));
×
156
        }
157

158
        if (!root.toFile().isDirectory()) {
1✔
159
            throw new JReleaserException(RB.$("validation_is_not_a_directory",
×
160
                "maven." + getDeployer().getType() + "." + getDeployer().getName() + ".stagingRepository",
×
161
                context.relativizeToBasedir(root).toString()));
×
162
        }
163

164
        try {
165
            DeployableCollector collector = new DeployableCollector(root, context.getModel().getProject().isSnapshot());
1✔
166

167
            java.nio.file.Files.walkFileTree(root, collector);
1✔
168
            if (collector.failed) {
1✔
169
                throw new JReleaserException(RB.$("ERROR_deployer_stage_resolution"));
×
170
            }
171

172
            deployables.addAll(collector.deployables);
1✔
173
        } catch (IOException e) {
×
174
            throw new JReleaserException(RB.$("ERROR_deployer_unexpected_error_stage"), e);
×
175
        }
1✔
176

177
        return deployables;
1✔
178
    }
179

180
    private Errors checkMavenCentralRules(Map<String, Deployable> deployablesMap) {
181
        Errors errors = new Errors();
1✔
182

183
        context.getLogger().info(RB.$("deployers.maven.prerequisites"));
1✔
184

185
        Map<String, Deployable> buildPoms = new LinkedHashMap<>();
1✔
186

187
        // 1st check jar, sources, javadoc if applicable
188
        for (Deployable deployable : deployablesMap.values()) {
1✔
189
            if (!deployable.getFilename().endsWith(EXT_POM)) {
1✔
190
                continue;
1✔
191
            }
192

193
            String base = deployable.getFilename();
1✔
194
            base = base.substring(0, base.length() - 4);
1✔
195

196
            boolean buildPom = false;
1✔
197
            if (base.endsWith(BUILD_TAG)) {
1✔
198
                Deployable baseDeployable = deployable.deriveByFilename(base.substring(0, base.length() - BUILD_TAG.length()) + EXT_POM);
×
199
                if (deployablesMap.containsKey(baseDeployable.getFullDeployPath())) {
×
200
                    buildPom = true;
×
201
                    buildPoms.put(deployable.getFullDeployPath(), deployable);
×
202
                }
203
            }
204

205
            if (!buildPom) {
1✔
206
                if (requiresJar(deployable)) {
1✔
207
                    Deployable derived = deployable.deriveByFilename(PACKAGING_JAR, base + EXT_JAR);
1✔
208
                    if (!deployablesMap.containsKey(derived.getFullDeployPath())) {
1✔
209
                        errors.configuration(RB.$("validation_is_missing", derived.getFilename()));
×
210
                    }
211
                }
212

213
                if (!deployable.isRelocated() && deployable.requiresWar()) {
1✔
214
                    Deployable derived = deployable.deriveByFilename(PACKAGING_WAR, base + EXT_WAR);
×
215
                    if (!deployablesMap.containsKey(derived.getFullDeployPath())) {
×
216
                        errors.configuration(RB.$("validation_is_missing", derived.getFilename()));
×
217
                    }
218
                }
219

220
                if (!deployable.isRelocated() && requiresSourcesJar(deployable, base)) {
1✔
221
                    Deployable derived = deployable.deriveByFilename(PACKAGING_JAR, base + "-sources.jar");
1✔
222
                    if (!deployablesMap.containsKey(derived.getFullDeployPath())) {
1✔
223
                        errors.configuration(RB.$("validation_is_missing", derived.getFilename()));
×
224
                    }
225
                }
226

227
                if (!deployable.isRelocated() && requiresJavadocJar(deployable, base)) {
1✔
228
                    Deployable derived = deployable.deriveByFilename(PACKAGING_JAR, base + "-javadoc.jar");
1✔
229
                    if (!deployablesMap.containsKey(derived.getFullDeployPath())) {
1✔
230
                        errors.configuration(RB.$("validation_is_missing", derived.getFilename()));
×
231
                    }
232
                }
233
            }
234
        }
1✔
235

236
        if (!getDeployer().isVerifyPom()) {
1✔
237
            return errors;
1✔
238
        }
239

240
        context.getLogger().info(RB.$("deployers.maven.verify.poms"));
1✔
241

242
        Maven.Pomchecker pomcheckerModel = context.getModel().getDeploy().getMaven().getPomchecker();
1✔
243
        PomChecker pomChecker = new PomChecker(context.asImmutable(),
1✔
244
            pomcheckerModel.getVersion());
1✔
245
        try {
246
            if (!pomChecker.setup()) {
1✔
247
                context.getLogger().warn(RB.$("tool_unavailable", "pomchecker"));
×
248
                return errors;
×
249
            }
250
        } catch (ToolException e) {
×
251
            context.getLogger().warn(RB.$("tool_unavailable", "pomchecker"), e);
×
252
            return errors;
×
253
        }
1✔
254

255
        Set<String> paths = new LinkedHashSet<>();
1✔
256
        for (String stagingRepository : getDeployer().getStagingRepositories()) {
1✔
257
            paths.add(context.getBasedir().resolve(stagingRepository)
1✔
258
                .toAbsolutePath()
1✔
259
                .normalize()
1✔
260
                .toString());
1✔
261
        }
1✔
262

263
        // 2nd check pom
264
        for (Deployable deployable : deployablesMap.values()) {
1✔
265
            if (!deployable.getFilename().endsWith(EXT_POM) || buildPoms.containsKey(deployable.getFullDeployPath()) ||
1✔
266
                !requiresPomVerification(deployable)) {
1✔
267
                continue;
×
268
            }
269

270
            List<String> args = new ArrayList<>();
1✔
271
            args.add("check-maven-central");
1✔
272
            args.add("--quiet");
1✔
273
            if (context.getModel().getProject().isSnapshot() &&
1✔
274
                getDeployer().isSnapshotSupported()) {
×
275
                args.add("--no-release");
×
276
            }
277
            if (pomChecker.isVersionCompatibleWith("1.9.0")) {
1✔
278
                if (!pomcheckerModel.isFailOnWarning()) {
1✔
279
                    args.add("--no-fail-on-warning");
×
280
                }
281
                if (!pomcheckerModel.isFailOnError()) {
1✔
282
                    args.add("--no-fail-on-error");
×
283
                }
284
            }
285

286
            if (pomChecker.isVersionCompatibleWith("1.13.0")) {
1✔
287
                for (String path : paths) {
1✔
288
                    args.add("--repository");
1✔
289
                    args.add(path);
1✔
290
                }
1✔
291
            }
292

293
            if (pomcheckerModel.isStrict()) {
1✔
294
                args.add("--strict");
1✔
295
            } else {
296
                args.add("--no-strict");
×
297
            }
298

299
            args.add("--file");
1✔
300
            args.add(deployable.getLocalPath().toAbsolutePath().toString());
1✔
301

302
            context.getLogger().debug(RB.$("deployers.maven.verify.pom", deployable.getLocalPath()));
1✔
303

304
            Command.Result result = Command.Result.empty();
1✔
305
            try {
306
                result = pomChecker.check(context.getBasedir(), args);
1✔
307
            } catch (CommandException e) {
×
308
                handlePomcheckerResult(deployable.getLocalPath().getFileName().toString(), result, e, errors);
×
309
            }
1✔
310

311
            if (result.getExitValue() != 0) {
1✔
312
                handlePomcheckerResult(deployable.getLocalPath().getFileName().toString(), result, null, errors);
×
313
            }
314
        }
1✔
315

316
        return errors;
1✔
317
    }
318

319
    private void handlePomcheckerResult(String filename, Command.Result result, CommandException e, Errors errors) {
320
        String plumbing = result.getErr();
×
321
        String validation = result.getOut();
×
322

323
        // 1st check out -> validation issues
324
        if (isNotBlank(validation)) {
×
325
            errors.configuration(RB.$("ERROR_deployer_pomchecker_header", filename, validation));
×
326
        } else if (isNotBlank(plumbing)) {
×
327
            // 2nd check err -> plumbing issues
328
            errors.configuration(RB.$("ERROR_deployer_pomchecker_header", filename, plumbing));
×
329
        } else if (null != e) {
×
330
            // command failed and we've got no clue!
331
            errors.configuration(e.getMessage());
×
332
        }
333
    }
×
334

335
    private boolean requiresJar(Deployable deployable) {
336
        if (!deployable.requiresJar()) return false;
1✔
337

338
        Optional<org.jreleaser.model.internal.deploy.maven.MavenDeployer.ArtifactOverride> override = getDeployer().getArtifactOverrides().stream()
1✔
339
            .filter(a -> a.getGroupId().equals(deployable.getGroupId()) && a.getArtifactId().equals(deployable.getArtifactId()))
1✔
340
            .findFirst();
1✔
341

342
        if (override.isPresent() && (override.get().isJarSet())) return override.get().isJar();
1✔
343

344
        return !deployable.isRelocated();
1✔
345
    }
346

347
    private boolean requiresSourcesJar(Deployable deployable, String baseFilename) {
348
        if (!deployable.requiresSourcesJar()) return false;
1✔
349

350
        Optional<org.jreleaser.model.internal.deploy.maven.MavenDeployer.ArtifactOverride> override = getDeployer().getArtifactOverrides().stream()
1✔
351
            .filter(a -> a.getGroupId().equals(deployable.getGroupId()) && a.getArtifactId().equals(deployable.getArtifactId()))
1✔
352
            .findFirst();
1✔
353

354
        if (override.isPresent() && (override.get().isSourceJarSet())) return override.get().isSourceJar();
1✔
355

356
        if (!getDeployer().isSourceJarSet()) {
1✔
357
            return hasJavaClass(deployable, baseFilename);
1✔
358
        }
359

360
        return getDeployer().isSourceJar();
×
361
    }
362

363
    private boolean requiresJavadocJar(Deployable deployable, String baseFilename) {
364
        if (!deployable.requiresJavadocJar()) return false;
1✔
365

366
        Optional<org.jreleaser.model.internal.deploy.maven.MavenDeployer.ArtifactOverride> override = getDeployer().getArtifactOverrides().stream()
1✔
367
            .filter(a -> a.getGroupId().equals(deployable.getGroupId()) && a.getArtifactId().equals(deployable.getArtifactId()))
1✔
368
            .findFirst();
1✔
369

370
        if (override.isPresent() && (override.get().isJavadocJarSet())) return override.get().isJavadocJar();
1✔
371

372
        if (!getDeployer().isJavadocJarSet()) {
1✔
373
            return hasJavaClass(deployable, baseFilename);
1✔
374
        }
375

376
        return getDeployer().isJavadocJar();
×
377
    }
378

379
    private boolean hasJavaClass(Deployable deployable, String baseFilename) {
380
        Deployable jar = deployable.deriveByFilename(PACKAGING_JAR, baseFilename + EXT_JAR);
1✔
381
        JarFile jarFile = null;
1✔
382

383
        try {
384
            jarFile = new JarFile(jar.getLocalPath().toFile());
1✔
385
            Enumeration<JarEntry> entries = jarFile.entries();
1✔
386
            while (entries.hasMoreElements()) {
1✔
387
                JarEntry entry = entries.nextElement();
1✔
388
                if (entry.getName().endsWith(".class")) {
1✔
389
                    return true;
1✔
390
                }
391
            }
1✔
392
            return false;
×
393
        } catch (IOException e) {
×
394
            context.getLogger().warn(RB.$("ERROR_deployer_read_local_file", jar.getLocalPath()), e);
×
395
            return false;
×
396
        } finally {
397
            IOUtils.closeQuietly(jarFile);
1✔
398
        }
399
    }
400

401
    private boolean requiresPomVerification(Deployable deployable) {
402
        Optional<org.jreleaser.model.internal.deploy.maven.MavenDeployer.ArtifactOverride> override = getDeployer().getArtifactOverrides().stream()
1✔
403
            .filter(a -> a.getGroupId().equals(deployable.getGroupId()) && a.getArtifactId().equals(deployable.getArtifactId()))
1✔
404
            .findFirst();
1✔
405

406
        if (override.isPresent() && (override.get().isVerifyPomSet())) return override.get().isVerifyPom();
1✔
407

408
        return getDeployer().isVerifyPom();
1✔
409
    }
410

411
    private void signDeployables(Map<String, Deployable> deployablesMap, Set<Deployable> deployables) {
412
        if (!getDeployer().isSign()) {
1✔
413
            return;
1✔
414
        }
415

416
        verifyKeyIsValid();
1✔
417

418
        for (Deployable deployable : deployablesMap.values()) {
1✔
419
            if (deployable.isSignature() || deployable.isChecksum() || deployable.isMavenMetadata()) continue;
1✔
420

421
            Deployable signedDeployable = deployable.deriveByFilename(deployable.getFilename() + EXT_ASC);
1✔
422

423
            if (isNewer(signedDeployable, deployable)) {
1✔
424
                continue;
1✔
425
            }
426

427
            try {
428
                context.getLogger().setPrefix("sign");
1✔
429
                SigningUtils.sign(context.asImmutable(), deployable.getLocalPath());
1✔
430
                deployables.add(signedDeployable);
1✔
431
            } catch (SigningException e) {
×
432
                throw new JReleaserException(RB.$("ERROR_unexpected_error_signing_file", deployable.getFilename()), e);
×
433
            } finally {
434
                context.getLogger().restorePrefix();
1✔
435
            }
436
        }
1✔
437
    }
1✔
438

439
    private boolean isNewer(Deployable source, Deployable target) {
440
        Path sourcePath = source.getLocalPath();
1✔
441
        if (!Files.exists(sourcePath)) {
1✔
442
            return false;
1✔
443
        }
444

445
        Path targetPath = target.getLocalPath();
1✔
446

447
        FileTime sourceLastModifiedTime = null;
1✔
448
        FileTime targetLastModifiedTime = null;
1✔
449

450
        try {
451
            sourceLastModifiedTime = Files.getLastModifiedTime(sourcePath);
1✔
452
        } catch (IOException e) {
×
453
            throw new JReleaserException(RB.$("ERROR_unexpected_error_timestamp_file", sourcePath.getFileName()), e);
×
454
        }
1✔
455

456
        try {
457
            targetLastModifiedTime = Files.getLastModifiedTime(targetPath);
1✔
458
        } catch (IOException e) {
×
459
            throw new JReleaserException(RB.$("ERROR_unexpected_error_timestamp_file", targetPath.getFileName()), e);
×
460
        }
1✔
461

462
        return sourceLastModifiedTime.compareTo(targetLastModifiedTime) > 0;
1✔
463
    }
464

465
    private void verifyKeyIsValid() {
466
        Optional<String> publicKeyID;
467
        Optional<String> fingerprint;
468

469
        boolean verify = context.getModel().getSigning().isVerify();
1✔
470

471
        try {
472
            publicKeyID = SigningUtils.getPublicKeyID(context.asImmutable());
1✔
473
        } catch (SigningException e) {
×
474
            if (verify) {
×
475
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
476
            }
477
            context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
478
            return;
×
479
        }
1✔
480

481
        if (!publicKeyID.isPresent()) {
1✔
482
            if (verify) {
×
483
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
484
            }
485
            context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
486
            return;
×
487
        }
488

489
        try {
490
            fingerprint = SigningUtils.getFingerprint(context.asImmutable());
1✔
491
        } catch (SigningException e) {
×
492
            if (verify) {
×
493
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
494
            }
495
            context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
496
            return;
×
497
        }
1✔
498

499
        if (!fingerprint.isPresent()) {
1✔
500
            if (verify) {
×
501
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
502
            }
503
            context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
504
            return;
×
505
        }
506

507
        String keyID = publicKeyID.get().toUpperCase(Locale.ENGLISH);
1✔
508
        String fp = fingerprint.get().toUpperCase(Locale.ENGLISH);
1✔
509

510
        try {
511
            Optional<Instant> expirationDate = SigningUtils.getExpirationDateOfPublicKey(context.asImmutable());
1✔
512

513
            if (expirationDate.isPresent()) {
1✔
514
                Instant ed = expirationDate.get();
1✔
515
                if (Instant.EPOCH.equals(ed)) {
1✔
516
                    context.getLogger().warn(RB.$("signing.public.key.no.expiration.date", keyID));
×
517

518
                } else if (Instant.now().isAfter(ed)) {
1✔
519
                    LocalDateTime ldt = LocalDateTime.ofInstant(ed, ZoneId.systemDefault());
×
520
                    if (verify) {
×
521
                        throw new JReleaserException(RB.$("ERROR_public_key_expired", keyID, ldt));
×
522
                    } else {
523
                        context.getLogger().warn(RB.$("ERROR_public_key_expired", keyID, ldt));
×
524
                    }
525
                } else {
×
526
                    context.getLogger().info(RB.$("signing.public.key.expiration.date", keyID, LocalDateTime.ofInstant(ed, ZoneId.systemDefault())));
1✔
527
                }
528
            }
529
        } catch (SigningException e) {
×
530
            if (verify) {
×
531
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
532
            } else {
533
                context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
534
                return;
×
535
            }
536
        }
1✔
537

538
        boolean published = false;
1✔
539

540
        context.getLogger().info(RB.$("signing.check.published.key", keyID));
1✔
541
        for (Map.Entry<String, String> e : KEY_SERVERS.entrySet()) {
1✔
542
            try {
543
                boolean notfound = false;
1✔
544
                // 1st try with keyId
545
                URL url = new URI(String.format(e.getValue(), fp)).toURL();
1✔
546
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
1✔
547
                connection.setConnectTimeout(20_000);
1✔
548
                connection.setReadTimeout(40_000);
1✔
549
                if (connection.getResponseCode() < 400) {
1✔
550
                    context.getLogger().debug(" + " + e.getKey());
1✔
551
                    published = true;
1✔
552
                } else {
553
                    notfound = true;
1✔
554
                }
555

556
                if (!notfound) {
1✔
557
                    // 2nd try with fingerprint
558
                    url = new URI(String.format(e.getValue(), keyID)).toURL();
1✔
559
                    connection = (HttpURLConnection) url.openConnection();
1✔
560
                    connection.setConnectTimeout(20_000);
1✔
561
                    connection.setReadTimeout(40_000);
1✔
562
                    if (connection.getResponseCode() < 400) {
1✔
563
                        context.getLogger().debug(" + " + e.getKey());
1✔
564
                        published = true;
1✔
565
                        notfound = false;
1✔
566
                    }
567
                }
568

569
                if (!notfound) {
1✔
570
                    context.getLogger().debug(" x " + e.getKey());
1✔
571
                }
572
            } catch (MalformedURLException | URISyntaxException ignored) {
×
573
                // ignored
574
            } catch (IOException ex) {
×
575
                context.getLogger().debug(RB.$("ERROR_unexpected_error") + " " + ex.getMessage());
×
576
            }
1✔
577
        }
1✔
578

579
        if (published) {
1✔
580
            context.getLogger().info(RB.$("signing.key.published", keyID));
1✔
581
        } else {
582
            context.getLogger().warn(RB.$("signing.key.not.published", keyID));
×
583
        }
584
    }
1✔
585

586
    private void checksumDeployables(Map<String, Deployable> deployablesMap, Set<Deployable> deployables) {
587
        if (!getDeployer().isChecksums()) {
1✔
588
            return;
1✔
589
        }
590

591
        for (Deployable deployable : deployablesMap.values()) {
1✔
592
            if (deployable.isChecksum()) continue;
1✔
593

594
            if (deployable.getFilename().endsWith(EXT_ASC)) {
1✔
595
                // remove checksum for signature files
596
                for (Algorithm algorithm : ALGORITHMS) {
1✔
597
                    Deployable checksumDeployable = deployable.deriveByFilename(deployable.getFilename() + "." + algorithm.formatted());
1✔
598
                    deployables.remove(checksumDeployable);
1✔
599
                }
600
                continue;
1✔
601
            }
602

603
            try {
604
                byte[] data = Files.readAllBytes(deployable.getLocalPath());
1✔
605
                for (Algorithm algorithm : ALGORITHMS) {
1✔
606
                    Deployable checksumDeployable = deployable.deriveByFilename(deployable.getFilename() + "." + algorithm.formatted());
1✔
607

608
                    if (isNewer(checksumDeployable, deployable)) {
1✔
609
                        continue;
1✔
610
                    }
611

612
                    context.getLogger().debug(RB.$("checksum.calculating", algorithm.formatted(), deployable.getFilename()));
1✔
613
                    String checksum = ChecksumUtils.checksum(algorithm, data);
1✔
614
                    Files.write(checksumDeployable.getLocalPath(), checksum.getBytes(UTF_8));
1✔
615
                    deployables.add(checksumDeployable);
1✔
616
                }
617
            } catch (IOException e) {
×
618
                throw new JReleaserException(RB.$("ERROR_unexpected_error_calculate_checksum", deployable.getFilename()), e);
×
619
            }
1✔
620
        }
1✔
621
    }
1✔
622

623
    protected void deployPackages() throws DeployException {
624
        Set<Deployable> deployables = collectDeployables();
1✔
625
        if (deployables.isEmpty()) {
1✔
626
            context.getLogger().info(RB.$("artifacts.no.match"));
×
627
        }
628

629
        D deployer = getDeployer();
1✔
630
        String baseUrl = deployer.getResolvedUrl(context.fullProps());
1✔
631
        String token = deployer.getPassword();
1✔
632

633
        if (!baseUrl.endsWith("/")) {
1✔
634
            baseUrl += " ";
×
635
        }
636

637
        // delete existing packages (if any)
638
        deleteExistingPackages(baseUrl, token, deployables);
1✔
639

640
        for (Deployable deployable : deployables) {
1✔
641
            if (deployable.isSignature() || deployable.isChecksum()) continue;
1✔
642
            Path localPath = deployable.getLocalPath();
1✔
643
            context.getLogger().info(" - {}", deployable.getFilename());
1✔
644

645
            if (!context.isDryrun()) {
1✔
646
                try {
647
                    Map<String, String> headers = new LinkedHashMap<>();
×
648
                    headers.put("Authorization", "Bearer " + token);
×
649
                    FormData data = ClientUtils.toFormData(localPath);
×
650

651
                    String url = baseUrl + deployable.getFullDeployPath();
×
652
                    ClientUtils.putFile(context.getLogger(),
×
653
                        url,
654
                        deployer.getConnectTimeout(),
×
655
                        deployer.getReadTimeout(),
×
656
                        data,
657
                        headers);
658
                } catch (IOException | UploadException e) {
×
659
                    context.getLogger().trace(e);
×
660
                    throw new DeployException(RB.$("ERROR_unexpected_deploy",
×
661
                        context.getBasedir().relativize(localPath), e.getMessage()), e);
×
662
                }
×
663
            }
664
        }
1✔
665
    }
1✔
666

667
    protected void deleteExistingPackages(String baseUrl, String token, Set<Deployable> deployables) throws DeployException {
668
        // noop
669
    }
×
670

671
    private class DeployableCollector extends SimpleFileVisitor<Path> {
672
        private final Path root;
673
        private final Set<Deployable> deployables = new TreeSet<>();
1✔
674
        private final boolean projectIsSnapshot;
675
        private boolean failed;
676

677
        public DeployableCollector(Path root, boolean projectIsSnapshot) {
1✔
678
            this.root = root;
1✔
679
            this.projectIsSnapshot = projectIsSnapshot;
1✔
680
        }
1✔
681

682
        private void match(Path path) {
683
            if (FileUtils.isHidden(path)) return;
1✔
684

685
            String filename = path.getFileName().toString();
1✔
686

687
            if (filename.contains(MAVEN_METADATA_XML)) {
1✔
688
                if (projectIsSnapshot) {
1✔
689
                    addDeployable(path);
×
690
                }
691
            } else {
692
                addDeployable(path);
1✔
693
            }
694
        }
1✔
695

696
        private void addDeployable(Path path) {
697
            String stagingRepository = root.toAbsolutePath().toString();
1✔
698
            String stagingPath = path.getParent().toAbsolutePath().toString();
1✔
699
            PomResult pomResult = parsePom(path);
1✔
700
            deployables.add(new Deployable(
1✔
701
                projectIsSnapshot,
702
                stagingRepository,
703
                stagingPath.substring(stagingRepository.length()),
1✔
704
                pomResult.packaging,
1✔
705
                path.getFileName().toString(),
1✔
706
                pomResult.relocated
1✔
707
            ));
708
        }
1✔
709

710
        private PomResult parsePom(Path artifactPath) {
711
            // only inspect if artifactPath ends with .pom
712
            if (artifactPath.getFileName().toString().endsWith(EXT_JAR)) return new PomResult(PACKAGING_JAR);
1✔
713
            if (!artifactPath.getFileName().toString().endsWith(EXT_POM)) return new PomResult("");
1✔
714

715
            try {
716
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
1✔
717
                factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
1✔
718
                Document document = factory
1✔
719
                    .newDocumentBuilder()
1✔
720
                    .parse(artifactPath.toFile());
1✔
721
                String query = "/project/packaging";
1✔
722
                String packaging = (String) XPathFactory.newInstance()
1✔
723
                    .newXPath()
1✔
724
                    .compile(query)
1✔
725
                    .evaluate(document, XPathConstants.STRING);
1✔
726

727
                query = "/project/distributionManagement/relocation";
1✔
728
                Object relocation = XPathFactory.newInstance()
1✔
729
                    .newXPath()
1✔
730
                    .compile(query)
1✔
731
                    .evaluate(document, XPathConstants.NODE);
1✔
732

733
                return new PomResult(isNotBlank(packaging) ? packaging.trim() : PACKAGING_JAR,
1✔
734
                    relocation != null);
735
            } catch (ParserConfigurationException | IOException | SAXException | XPathExpressionException e) {
×
736
                throw new IllegalStateException(e);
×
737
            }
738
        }
739

740
        @Override
741
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
742
            match(file);
1✔
743
            return CONTINUE;
1✔
744
        }
745

746
        @Override
747
        public FileVisitResult visitFileFailed(Path file, IOException e) throws IOException {
748
            failed = true;
×
749
            context.getLogger().trace(e);
×
750
            context.getLogger().error(RB.$("ERROR_artifacts_unexpected_error_path"),
×
751
                root.toAbsolutePath().relativize(file.toAbsolutePath()), e);
×
752
            return CONTINUE;
×
753
        }
754

755
        private final class PomResult {
756
            private String packaging;
757
            private boolean relocated;
758

759
            private PomResult(String packaging) {
1✔
760
                this.packaging = packaging;
1✔
761
            }
1✔
762

763
            private PomResult(String packaging, boolean relocated) {
1✔
764
                this.packaging = packaging;
1✔
765
                this.relocated = relocated;
1✔
766
            }
1✔
767
        }
768
    }
769
}
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